refactor: share cron and ollama test helpers

This commit is contained in:
Peter Steinberger 2026-03-13 19:46:08 +00:00
parent 8473a29da7
commit fff514c7f2
6 changed files with 51 additions and 127 deletions

View File

@ -1,31 +1,11 @@
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { jsonResponse, requestBodyText, requestUrl } from "../test-helpers/http.js";
import { import {
enrichOllamaModelsWithContext, enrichOllamaModelsWithContext,
resolveOllamaApiBase, resolveOllamaApiBase,
type OllamaTagModel, type OllamaTagModel,
} from "./ollama-models.js"; } from "./ollama-models.js";
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
function requestUrl(input: string | URL | Request): string {
if (typeof input === "string") {
return input;
}
if (input instanceof URL) {
return input.toString();
}
return input.url;
}
function requestBody(body: BodyInit | null | undefined): string {
return typeof body === "string" ? body : "{}";
}
describe("ollama-models", () => { describe("ollama-models", () => {
afterEach(() => { afterEach(() => {
vi.unstubAllGlobals(); vi.unstubAllGlobals();
@ -43,7 +23,7 @@ describe("ollama-models", () => {
if (!url.endsWith("/api/show")) { if (!url.endsWith("/api/show")) {
throw new Error(`Unexpected fetch: ${url}`); throw new Error(`Unexpected fetch: ${url}`);
} }
const body = JSON.parse(requestBody(init?.body)) as { name?: string }; const body = JSON.parse(requestBodyText(init?.body)) as { name?: string };
if (body.name === "llama3:8b") { if (body.name === "llama3:8b") {
return jsonResponse({ model_info: { "llama.context_length": 65536 } }); return jsonResponse({ model_info: { "llama.context_length": 65536 } });
} }

View File

@ -1,5 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import { jsonResponse, requestBodyText, requestUrl } from "../test-helpers/http.js";
import type { WizardPrompter } from "../wizard/prompts.js"; import type { WizardPrompter } from "../wizard/prompts.js";
import { import {
configureOllamaNonInteractive, configureOllamaNonInteractive,
@ -23,27 +24,6 @@ vi.mock("./oauth-env.js", () => ({
isRemoteEnvironment: isRemoteEnvironmentMock, isRemoteEnvironment: isRemoteEnvironmentMock,
})); }));
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
function requestUrl(input: string | URL | Request): string {
if (typeof input === "string") {
return input;
}
if (input instanceof URL) {
return input.toString();
}
return input.url;
}
function requestBody(body: BodyInit | null | undefined): string {
return typeof body === "string" ? body : "{}";
}
function createOllamaFetchMock(params: { function createOllamaFetchMock(params: {
tags?: string[]; tags?: string[];
show?: Record<string, number | undefined>; show?: Record<string, number | undefined>;
@ -61,7 +41,7 @@ function createOllamaFetchMock(params: {
return jsonResponse({ models: (params.tags ?? []).map((name) => ({ name })) }); return jsonResponse({ models: (params.tags ?? []).map((name) => ({ name })) });
} }
if (url.endsWith("/api/show")) { if (url.endsWith("/api/show")) {
const body = JSON.parse(requestBody(init?.body)) as { name?: string }; const body = JSON.parse(requestBodyText(init?.body)) as { name?: string };
const contextWindow = body.name ? params.show?.[body.name] : undefined; const contextWindow = body.name ? params.show?.[body.name] : undefined;
return contextWindow return contextWindow
? jsonResponse({ model_info: { "llama.context_length": contextWindow } }) ? jsonResponse({ model_info: { "llama.context_length": contextWindow } })
@ -359,7 +339,7 @@ describe("ollama setup", () => {
}); });
const pullRequest = fetchMock.mock.calls[1]?.[1]; const pullRequest = fetchMock.mock.calls[1]?.[1];
expect(JSON.parse(requestBody(pullRequest?.body))).toEqual({ name: "llama3.2:latest" }); expect(JSON.parse(requestBodyText(pullRequest?.body))).toEqual({ name: "llama3.2:latest" });
expect(result.agents?.defaults?.model).toEqual( expect(result.agents?.defaults?.model).toEqual(
expect.objectContaining({ primary: "ollama/llama3.2:latest" }), expect.objectContaining({ primary: "ollama/llama3.2:latest" }),
); );

View File

@ -1,6 +1,7 @@
import "./isolated-agent.mocks.js"; import "./isolated-agent.mocks.js";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
import { createCliDeps, mockAgentPayloads } from "./isolated-agent.delivery.test-helpers.js";
import { runCronIsolatedAgentTurn } from "./isolated-agent.js"; import { runCronIsolatedAgentTurn } from "./isolated-agent.js";
import { import {
makeCfg, makeCfg,
@ -9,27 +10,6 @@ import {
writeSessionStoreEntries, writeSessionStoreEntries,
} from "./isolated-agent.test-harness.js"; } from "./isolated-agent.test-harness.js";
function makeDeps() {
return {
sendMessageSlack: vi.fn(),
sendMessageWhatsApp: vi.fn(),
sendMessageTelegram: vi.fn(),
sendMessageDiscord: vi.fn(),
sendMessageSignal: vi.fn(),
sendMessageIMessage: vi.fn(),
};
}
function mockEmbeddedOk() {
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
payloads: [{ text: "ok" }],
meta: {
durationMs: 5,
agentMeta: { sessionId: "s", provider: "p", model: "m" },
},
});
}
function lastEmbeddedLane(): string | undefined { function lastEmbeddedLane(): string | undefined {
const calls = vi.mocked(runEmbeddedPiAgent).mock.calls; const calls = vi.mocked(runEmbeddedPiAgent).mock.calls;
expect(calls.length).toBeGreaterThan(0); expect(calls.length).toBeGreaterThan(0);
@ -45,11 +25,11 @@ async function runLaneCase(home: string, lane?: string) {
lastTo: "", lastTo: "",
}, },
}); });
mockEmbeddedOk(); mockAgentPayloads([{ text: "ok" }]);
await runCronIsolatedAgentTurn({ await runCronIsolatedAgentTurn({
cfg: makeCfg(home, storePath), cfg: makeCfg(home, storePath),
deps: makeDeps(), deps: createCliDeps(),
job: makeJob({ kind: "agentTurn", message: "do it", deliver: false }), job: makeJob({ kind: "agentTurn", message: "do it", deliver: false }),
message: "do it", message: "do it",
sessionKey: "cron:job-1", sessionKey: "cron:job-1",

View File

@ -2,6 +2,7 @@ import "./isolated-agent.mocks.js";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { loadModelCatalog } from "../agents/model-catalog.js"; import { loadModelCatalog } from "../agents/model-catalog.js";
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
import { createCliDeps, mockAgentPayloads } from "./isolated-agent.delivery.test-helpers.js";
import { runCronIsolatedAgentTurn } from "./isolated-agent.js"; import { runCronIsolatedAgentTurn } from "./isolated-agent.js";
import { import {
makeCfg, makeCfg,
@ -13,27 +14,6 @@ import type { CronJob } from "./types.js";
const withTempHome = withTempCronHome; const withTempHome = withTempCronHome;
function makeDeps() {
return {
sendMessageSlack: vi.fn(),
sendMessageWhatsApp: vi.fn(),
sendMessageTelegram: vi.fn(),
sendMessageDiscord: vi.fn(),
sendMessageSignal: vi.fn(),
sendMessageIMessage: vi.fn(),
};
}
function mockEmbeddedOk() {
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
payloads: [{ text: "ok" }],
meta: {
durationMs: 5,
agentMeta: { sessionId: "s", provider: "p", model: "m" },
},
});
}
/** /**
* Extract the provider and model from the last runEmbeddedPiAgent call. * Extract the provider and model from the last runEmbeddedPiAgent call.
*/ */
@ -62,7 +42,7 @@ async function runTurnCore(home: string, options: TurnOptions = {}) {
}, },
...options.storeEntries, ...options.storeEntries,
}); });
mockEmbeddedOk(); mockAgentPayloads([{ text: "ok" }]);
const jobPayload = options.jobPayload ?? { const jobPayload = options.jobPayload ?? {
kind: "agentTurn" as const, kind: "agentTurn" as const,
@ -72,7 +52,7 @@ async function runTurnCore(home: string, options: TurnOptions = {}) {
const res = await runCronIsolatedAgentTurn({ const res = await runCronIsolatedAgentTurn({
cfg: makeCfg(home, storePath, options.cfgOverrides), cfg: makeCfg(home, storePath, options.cfgOverrides),
deps: makeDeps(), deps: createCliDeps(),
job: makeJob(jobPayload), job: makeJob(jobPayload),
message: DEFAULT_MESSAGE, message: DEFAULT_MESSAGE,
sessionKey: options.sessionKey ?? "cron:job-1", sessionKey: options.sessionKey ?? "cron:job-1",
@ -310,7 +290,7 @@ describe("cron model formatting and precedence edge cases", () => {
// Step 2: No job model, session store says openai // Step 2: No job model, session store says openai
vi.mocked(runEmbeddedPiAgent).mockClear(); vi.mocked(runEmbeddedPiAgent).mockClear();
mockEmbeddedOk(); mockAgentPayloads([{ text: "ok" }]);
const step2 = await runTurn(home, { const step2 = await runTurn(home, {
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false }, jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
storeEntries: { storeEntries: {
@ -327,7 +307,7 @@ describe("cron model formatting and precedence edge cases", () => {
// Step 3: Job payload says anthropic, session store still says openai // Step 3: Job payload says anthropic, session store still says openai
vi.mocked(runEmbeddedPiAgent).mockClear(); vi.mocked(runEmbeddedPiAgent).mockClear();
mockEmbeddedOk(); mockAgentPayloads([{ text: "ok" }]);
const step3 = await runTurn(home, { const step3 = await runTurn(home, {
jobPayload: { jobPayload: {
kind: "agentTurn", kind: "agentTurn",
@ -365,7 +345,7 @@ describe("cron model formatting and precedence edge cases", () => {
// Run 2: no override — must revert to default anthropic // Run 2: no override — must revert to default anthropic
vi.mocked(runEmbeddedPiAgent).mockClear(); vi.mocked(runEmbeddedPiAgent).mockClear();
mockEmbeddedOk(); mockAgentPayloads([{ text: "ok" }]);
const r2 = await runTurn(home, { const r2 = await runTurn(home, {
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false }, jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
}); });

View File

@ -133,6 +133,16 @@ async function runTelegramDeliveryResult(bestEffort: boolean) {
return outcome; return outcome;
} }
function expectSuccessfulTelegramTextDelivery(params: {
res: Awaited<ReturnType<typeof runCronIsolatedAgentTurn>>;
deps: CliDeps;
}): void {
expect(params.res.status).toBe("ok");
expect(params.res.delivered).toBe(true);
expect(params.res.deliveryAttempted).toBe(true);
expect(runSubagentAnnounceFlow).not.toHaveBeenCalled();
}
async function runSignalDeliveryResult(bestEffort: boolean) { async function runSignalDeliveryResult(bestEffort: boolean) {
let outcome: let outcome:
| { | {
@ -379,33 +389,13 @@ describe("runCronIsolatedAgentTurn", () => {
}); });
it("delivers text directly when best-effort is disabled", async () => { it("delivers text directly when best-effort is disabled", async () => {
await withTempHome(async (home) => { const { res, deps } = await runTelegramDeliveryResult(false);
const storePath = await writeSessionStore(home, { lastProvider: "webchat", lastTo: "" }); expectSuccessfulTelegramTextDelivery({ res, deps });
const deps = createCliDeps();
mockAgentPayloads([{ text: "hello from cron" }]);
const res = await runTelegramAnnounceTurn({
home,
storePath,
deps,
delivery: {
mode: "announce",
channel: "telegram",
to: "123",
bestEffort: false,
},
});
expect(res.status).toBe("ok");
expect(res.delivered).toBe(true);
expect(res.deliveryAttempted).toBe(true);
expect(runSubagentAnnounceFlow).not.toHaveBeenCalled();
expectDirectTelegramDelivery(deps, { expectDirectTelegramDelivery(deps, {
chatId: "123", chatId: "123",
text: "hello from cron", text: "hello from cron",
}); });
}); });
});
it("returns error when text direct delivery fails and best-effort is disabled", async () => { it("returns error when text direct delivery fails and best-effort is disabled", async () => {
await withTelegramAnnounceFixture( await withTelegramAnnounceFixture(
@ -459,10 +449,7 @@ describe("runCronIsolatedAgentTurn", () => {
}, },
}); });
expect(res.status).toBe("ok"); expectSuccessfulTelegramTextDelivery({ res, deps });
expect(res.delivered).toBe(true);
expect(res.deliveryAttempted).toBe(true);
expect(runSubagentAnnounceFlow).not.toHaveBeenCalled();
expect(deps.sendMessageTelegram).toHaveBeenCalledTimes(2); expect(deps.sendMessageTelegram).toHaveBeenCalledTimes(2);
expect(deps.sendMessageTelegram).toHaveBeenLastCalledWith( expect(deps.sendMessageTelegram).toHaveBeenLastCalledWith(
"123", "123",
@ -490,10 +477,7 @@ describe("runCronIsolatedAgentTurn", () => {
it("delivers text directly when best-effort is enabled", async () => { it("delivers text directly when best-effort is enabled", async () => {
const { res, deps } = await runTelegramDeliveryResult(true); const { res, deps } = await runTelegramDeliveryResult(true);
expect(res.status).toBe("ok"); expectSuccessfulTelegramTextDelivery({ res, deps });
expect(res.delivered).toBe(true);
expect(res.deliveryAttempted).toBe(true);
expect(runSubagentAnnounceFlow).not.toHaveBeenCalled();
expectDirectTelegramDelivery(deps, { expectDirectTelegramDelivery(deps, {
chatId: "123", chatId: "123",
text: "hello from cron", text: "hello from cron",

20
src/test-helpers/http.ts Normal file
View File

@ -0,0 +1,20 @@
export function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
export function requestUrl(input: string | URL | Request): string {
if (typeof input === "string") {
return input;
}
if (input instanceof URL) {
return input.toString();
}
return input.url;
}
export function requestBodyText(body: BodyInit | null | undefined): string {
return typeof body === "string" ? body : "{}";
}