mirror of https://github.com/openclaw/openclaw.git
refactor: share cron and ollama test helpers
This commit is contained in:
parent
8473a29da7
commit
fff514c7f2
|
|
@ -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 } });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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" }),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 : "{}";
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue