diff --git a/src/commands/sessions.e2e.test.ts b/src/commands/sessions.e2e.test.ts index 61f89889022..4c82b36b9c3 100644 --- a/src/commands/sessions.e2e.test.ts +++ b/src/commands/sessions.e2e.test.ts @@ -1,54 +1,19 @@ 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 { + makeRuntime, + mockSessionsConfig, + runSessionsJson, + writeStore, +} from "./sessions.test-helpers.js"; // Disable colors for deterministic snapshots. process.env.FORCE_COLOR = "0"; -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => ({ - agents: { - defaults: { - model: { primary: "pi:opus" }, - models: { "pi:opus": {} }, - contextTokens: 32000, - }, - }, - }), - }; -}); +mockSessionsConfig(); 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", () => { beforeEach(() => { vi.useFakeTimers(); @@ -126,18 +91,13 @@ describe("sessionsCommand", () => { }, }); - const { runtime, logs } = makeRuntime(); - await sessionsCommand({ store, json: true }, runtime); - - fs.rmSync(store); - - const payload = JSON.parse(logs[0] ?? "{}") as { + const payload = await runSessionsJson<{ sessions?: Array<{ key: string; totalTokens: number | null; totalTokensFresh: boolean; }>; - }; + }>(sessionsCommand, store); const main = payload.sessions?.find((row) => row.key === "main"); const group = payload.sessions?.find((row) => row.key === "discord:group:demo"); expect(main?.totalTokens).toBe(2000); @@ -145,4 +105,47 @@ describe("sessionsCommand", () => { expect(group?.totalTokens).toBeNull(); 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); + }); }); diff --git a/src/commands/sessions.model-resolution.test.ts b/src/commands/sessions.model-resolution.test.ts index 41bb3546881..4c0bac06759 100644 --- a/src/commands/sessions.model-resolution.test.ts +++ b/src/commands/sessions.model-resolution.test.ts @@ -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 { mockSessionsConfig, runSessionsJson, writeStore } from "./sessions.test-helpers.js"; -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => ({ - agents: { - defaults: { - model: { primary: "pi:opus" }, - models: { "pi:opus": {} }, - contextTokens: 32000, - }, - }, - }), - }; -}); +mockSessionsConfig(); 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; +type SessionsJsonPayload = { + sessions?: Array<{ + key: string; + model?: string | null; + }>; }; -const writeStore = (data: unknown) => { - const file = path.join( - os.tmpdir(), - `sessions-model-${Date.now()}-${Math.random().toString(16).slice(2)}.json`, +async function resolveSubagentModel( + runtimeFields: Record, + sessionId: string, +): Promise { + 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(sessionsCommand, store); + return payload.sessions?.find((row) => row.key === "agent:research:subagent:demo")?.model; +} describe("sessionsCommand model resolution", () => { beforeEach(() => { @@ -57,52 +42,22 @@ describe("sessionsCommand model resolution", () => { }); it("prefers runtime model fields for subagent sessions in JSON output", async () => { - const store = writeStore({ - "agent:research:subagent:demo": { - sessionId: "subagent-1", - updatedAt: Date.now() - 2 * 60_000, + const model = await resolveSubagentModel( + { modelProvider: "openai-codex", model: "gpt-5.3-codex", modelOverride: "pi:opus", }, - }); - - 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"); + "subagent-1", + ); + expect(model).toBe("gpt-5.3-codex"); }); it("falls back to modelOverride when runtime model is missing", async () => { - const store = writeStore({ - "agent:research:subagent:demo": { - sessionId: "subagent-2", - updatedAt: Date.now() - 2 * 60_000, - modelOverride: "openai-codex/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"); + const model = await resolveSubagentModel( + { modelOverride: "openai-codex/gpt-5.3-codex" }, + "subagent-2", + ); + expect(model).toBe("gpt-5.3-codex"); }); }); diff --git a/src/commands/sessions.test-helpers.ts b/src/commands/sessions.test-helpers.ts new file mode 100644 index 00000000000..bd6b981ae08 --- /dev/null +++ b/src/commands/sessions.test-helpers.ts @@ -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(); + 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( + run: ( + opts: { json?: boolean; store?: string; active?: string }, + runtime: RuntimeEnv, + ) => Promise, + store: string, + options?: { + active?: string; + }, +): Promise { + 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; +}