mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 19:14:33 +00:00
test: dedupe sessions command tests and cover active filtering
This commit is contained in:
@@ -1,54 +1,19 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
makeRuntime,
|
||||||
|
mockSessionsConfig,
|
||||||
|
runSessionsJson,
|
||||||
|
writeStore,
|
||||||
|
} from "./sessions.test-helpers.js";
|
||||||
|
|
||||||
// Disable colors for deterministic snapshots.
|
// Disable colors for deterministic snapshots.
|
||||||
process.env.FORCE_COLOR = "0";
|
process.env.FORCE_COLOR = "0";
|
||||||
|
|
||||||
vi.mock("../config/config.js", async (importOriginal) => {
|
mockSessionsConfig();
|
||||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
loadConfig: () => ({
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: { primary: "pi:opus" },
|
|
||||||
models: { "pi:opus": {} },
|
|
||||||
contextTokens: 32000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
import { sessionsCommand } from "./sessions.js";
|
import { sessionsCommand } from "./sessions.js";
|
||||||
|
|
||||||
const makeRuntime = () => {
|
|
||||||
const logs: string[] = [];
|
|
||||||
return {
|
|
||||||
runtime: {
|
|
||||||
log: (msg: unknown) => logs.push(String(msg)),
|
|
||||||
error: (msg: unknown) => {
|
|
||||||
throw new Error(String(msg));
|
|
||||||
},
|
|
||||||
exit: (code: number) => {
|
|
||||||
throw new Error(`exit ${code}`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
logs,
|
|
||||||
} as const;
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeStore = (data: unknown) => {
|
|
||||||
const file = path.join(
|
|
||||||
os.tmpdir(),
|
|
||||||
`sessions-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
|
|
||||||
);
|
|
||||||
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
|
||||||
return file;
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("sessionsCommand", () => {
|
describe("sessionsCommand", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
@@ -126,18 +91,13 @@ describe("sessionsCommand", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { runtime, logs } = makeRuntime();
|
const payload = await runSessionsJson<{
|
||||||
await sessionsCommand({ store, json: true }, runtime);
|
|
||||||
|
|
||||||
fs.rmSync(store);
|
|
||||||
|
|
||||||
const payload = JSON.parse(logs[0] ?? "{}") as {
|
|
||||||
sessions?: Array<{
|
sessions?: Array<{
|
||||||
key: string;
|
key: string;
|
||||||
totalTokens: number | null;
|
totalTokens: number | null;
|
||||||
totalTokensFresh: boolean;
|
totalTokensFresh: boolean;
|
||||||
}>;
|
}>;
|
||||||
};
|
}>(sessionsCommand, store);
|
||||||
const main = payload.sessions?.find((row) => row.key === "main");
|
const main = payload.sessions?.find((row) => row.key === "main");
|
||||||
const group = payload.sessions?.find((row) => row.key === "discord:group:demo");
|
const group = payload.sessions?.find((row) => row.key === "discord:group:demo");
|
||||||
expect(main?.totalTokens).toBe(2000);
|
expect(main?.totalTokens).toBe(2000);
|
||||||
@@ -145,4 +105,47 @@ describe("sessionsCommand", () => {
|
|||||||
expect(group?.totalTokens).toBeNull();
|
expect(group?.totalTokens).toBeNull();
|
||||||
expect(group?.totalTokensFresh).toBe(false);
|
expect(group?.totalTokensFresh).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("applies --active filtering in JSON output", async () => {
|
||||||
|
const store = writeStore(
|
||||||
|
{
|
||||||
|
recent: {
|
||||||
|
sessionId: "recent",
|
||||||
|
updatedAt: Date.now() - 5 * 60_000,
|
||||||
|
model: "pi:opus",
|
||||||
|
},
|
||||||
|
stale: {
|
||||||
|
sessionId: "stale",
|
||||||
|
updatedAt: Date.now() - 45 * 60_000,
|
||||||
|
model: "pi:opus",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sessions-active",
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = await runSessionsJson<{
|
||||||
|
sessions?: Array<{
|
||||||
|
key: string;
|
||||||
|
}>;
|
||||||
|
}>(sessionsCommand, store, { active: "10" });
|
||||||
|
expect(payload.sessions?.map((row) => row.key)).toEqual(["recent"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid --active values", async () => {
|
||||||
|
const store = writeStore(
|
||||||
|
{
|
||||||
|
demo: {
|
||||||
|
sessionId: "demo",
|
||||||
|
updatedAt: Date.now() - 5 * 60_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sessions-active-invalid",
|
||||||
|
);
|
||||||
|
const { runtime, errors } = makeRuntime();
|
||||||
|
|
||||||
|
await expect(sessionsCommand({ store, active: "0" }, runtime)).rejects.toThrow("exit 1");
|
||||||
|
expect(errors[0]).toContain("--active must be a positive integer");
|
||||||
|
|
||||||
|
fs.rmSync(store);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,50 +1,35 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { mockSessionsConfig, runSessionsJson, writeStore } from "./sessions.test-helpers.js";
|
||||||
|
|
||||||
vi.mock("../config/config.js", async (importOriginal) => {
|
mockSessionsConfig();
|
||||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
loadConfig: () => ({
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: { primary: "pi:opus" },
|
|
||||||
models: { "pi:opus": {} },
|
|
||||||
contextTokens: 32000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
import { sessionsCommand } from "./sessions.js";
|
import { sessionsCommand } from "./sessions.js";
|
||||||
|
|
||||||
const makeRuntime = () => {
|
type SessionsJsonPayload = {
|
||||||
const logs: string[] = [];
|
sessions?: Array<{
|
||||||
return {
|
key: string;
|
||||||
runtime: {
|
model?: string | null;
|
||||||
log: (msg: unknown) => logs.push(String(msg)),
|
}>;
|
||||||
error: (msg: unknown) => {
|
|
||||||
throw new Error(String(msg));
|
|
||||||
},
|
|
||||||
exit: (code: number) => {
|
|
||||||
throw new Error(`exit ${code}`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
logs,
|
|
||||||
} as const;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const writeStore = (data: unknown) => {
|
async function resolveSubagentModel(
|
||||||
const file = path.join(
|
runtimeFields: Record<string, unknown>,
|
||||||
os.tmpdir(),
|
sessionId: string,
|
||||||
`sessions-model-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
|
): Promise<string | null | undefined> {
|
||||||
|
const store = writeStore(
|
||||||
|
{
|
||||||
|
"agent:research:subagent:demo": {
|
||||||
|
sessionId,
|
||||||
|
updatedAt: Date.now() - 2 * 60_000,
|
||||||
|
...runtimeFields,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sessions-model",
|
||||||
);
|
);
|
||||||
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
|
||||||
return file;
|
const payload = await runSessionsJson<SessionsJsonPayload>(sessionsCommand, store);
|
||||||
};
|
return payload.sessions?.find((row) => row.key === "agent:research:subagent:demo")?.model;
|
||||||
|
}
|
||||||
|
|
||||||
describe("sessionsCommand model resolution", () => {
|
describe("sessionsCommand model resolution", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -57,52 +42,22 @@ describe("sessionsCommand model resolution", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("prefers runtime model fields for subagent sessions in JSON output", async () => {
|
it("prefers runtime model fields for subagent sessions in JSON output", async () => {
|
||||||
const store = writeStore({
|
const model = await resolveSubagentModel(
|
||||||
"agent:research:subagent:demo": {
|
{
|
||||||
sessionId: "subagent-1",
|
|
||||||
updatedAt: Date.now() - 2 * 60_000,
|
|
||||||
modelProvider: "openai-codex",
|
modelProvider: "openai-codex",
|
||||||
model: "gpt-5.3-codex",
|
model: "gpt-5.3-codex",
|
||||||
modelOverride: "pi:opus",
|
modelOverride: "pi:opus",
|
||||||
},
|
},
|
||||||
});
|
"subagent-1",
|
||||||
|
);
|
||||||
const { runtime, logs } = makeRuntime();
|
expect(model).toBe("gpt-5.3-codex");
|
||||||
await sessionsCommand({ store, json: true }, runtime);
|
|
||||||
|
|
||||||
fs.rmSync(store);
|
|
||||||
|
|
||||||
const payload = JSON.parse(logs[0] ?? "{}") as {
|
|
||||||
sessions?: Array<{
|
|
||||||
key: string;
|
|
||||||
model?: string | null;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
const subagent = payload.sessions?.find((row) => row.key === "agent:research:subagent:demo");
|
|
||||||
expect(subagent?.model).toBe("gpt-5.3-codex");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to modelOverride when runtime model is missing", async () => {
|
it("falls back to modelOverride when runtime model is missing", async () => {
|
||||||
const store = writeStore({
|
const model = await resolveSubagentModel(
|
||||||
"agent:research:subagent:demo": {
|
{ modelOverride: "openai-codex/gpt-5.3-codex" },
|
||||||
sessionId: "subagent-2",
|
"subagent-2",
|
||||||
updatedAt: Date.now() - 2 * 60_000,
|
);
|
||||||
modelOverride: "openai-codex/gpt-5.3-codex",
|
expect(model).toBe("gpt-5.3-codex");
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { runtime, logs } = makeRuntime();
|
|
||||||
await sessionsCommand({ store, json: true }, runtime);
|
|
||||||
|
|
||||||
fs.rmSync(store);
|
|
||||||
|
|
||||||
const payload = JSON.parse(logs[0] ?? "{}") as {
|
|
||||||
sessions?: Array<{
|
|
||||||
key: string;
|
|
||||||
model?: string | null;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
const subagent = payload.sessions?.find((row) => row.key === "agent:research:subagent:demo");
|
|
||||||
expect(subagent?.model).toBe("gpt-5.3-codex");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
84
src/commands/sessions.test-helpers.ts
Normal file
84
src/commands/sessions.test-helpers.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
|
||||||
|
export function mockSessionsConfig() {
|
||||||
|
vi.mock("../config/config.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
loadConfig: () => ({
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: { primary: "pi:opus" },
|
||||||
|
models: { "pi:opus": {} },
|
||||||
|
contextTokens: 32000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeRuntime(params?: { throwOnError?: boolean }): {
|
||||||
|
runtime: RuntimeEnv;
|
||||||
|
logs: string[];
|
||||||
|
errors: string[];
|
||||||
|
} {
|
||||||
|
const logs: string[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
const throwOnError = params?.throwOnError ?? false;
|
||||||
|
return {
|
||||||
|
runtime: {
|
||||||
|
log: (msg: unknown) => logs.push(String(msg)),
|
||||||
|
error: (msg: unknown) => {
|
||||||
|
errors.push(String(msg));
|
||||||
|
if (throwOnError) {
|
||||||
|
throw new Error(String(msg));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exit: (code: number) => {
|
||||||
|
throw new Error(`exit ${code}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logs,
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeStore(data: unknown, prefix = "sessions"): string {
|
||||||
|
const file = path.join(
|
||||||
|
os.tmpdir(),
|
||||||
|
`${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
|
||||||
|
);
|
||||||
|
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runSessionsJson<T>(
|
||||||
|
run: (
|
||||||
|
opts: { json?: boolean; store?: string; active?: string },
|
||||||
|
runtime: RuntimeEnv,
|
||||||
|
) => Promise<void>,
|
||||||
|
store: string,
|
||||||
|
options?: {
|
||||||
|
active?: string;
|
||||||
|
},
|
||||||
|
): Promise<T> {
|
||||||
|
const { runtime, logs } = makeRuntime();
|
||||||
|
try {
|
||||||
|
await run(
|
||||||
|
{
|
||||||
|
store,
|
||||||
|
json: true,
|
||||||
|
active: options?.active,
|
||||||
|
},
|
||||||
|
runtime,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(store, { force: true });
|
||||||
|
}
|
||||||
|
return JSON.parse(logs[0] ?? "{}") as T;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user