- /agent [filter] lists the run's agents (glyphs, elapsed, task) via the native selector and switchSession()es to the agent's own session file - run is restored in the agent session by deriving the run id from the session file path (<stateDir>/runs/<runId>/sessions/...) - main session file recorded in the run manifest so /back works even after a reload while viewing an agent chat - widget re-attach timer guard on session switch
80 lines
3.7 KiB
TypeScript
80 lines
3.7 KiB
TypeScript
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { RunStore } from "../src/run-store.js";
|
|
import { buildAgentChatOptions, runIdFromSessionPath } from "../src/supervisor.js";
|
|
import type { AgentRecord } from "../src/types.js";
|
|
|
|
const base: Omit<AgentRecord, "id" | "status" | "startedAt" | "finishedAt"> = {
|
|
runId: "run-test", parentId: "root", children: [], lineage: [], depth: 1,
|
|
task: "x", taskFingerprint: "x", expectedOutput: "", completionCriteria: "", specPath: "",
|
|
createdAt: new Date(Date.now() - 60_000).toISOString(),
|
|
};
|
|
const mk = (id: string, status: AgentRecord["status"], startedMin: number, finishedMin?: number): AgentRecord => {
|
|
const record: AgentRecord = {
|
|
...base, id, status,
|
|
startedAt: new Date(Date.now() - startedMin * 60_000).toISOString(),
|
|
};
|
|
if (finishedMin !== undefined) record.finishedAt = new Date(Date.now() - finishedMin * 60_000).toISOString();
|
|
return record;
|
|
};
|
|
|
|
describe("runIdFromSessionPath", () => {
|
|
const stateDir = "/proj/.hypothesis-machine";
|
|
it("derives the run id from an agent session file under the state dir", () => {
|
|
const sessionFile = resolve(stateDir, "runs/run-132965dd/sessions/agent-abc123.jsonl");
|
|
expect(runIdFromSessionPath(sessionFile, stateDir)).toBe("run-132965dd");
|
|
});
|
|
it("returns undefined for session files outside the state dir", () => {
|
|
expect(runIdFromSessionPath("/proj/.hypothesis-machine/other/file.jsonl", stateDir)).toBeUndefined();
|
|
expect(runIdFromSessionPath(resolve(stateDir, "runs/x.jsonl"), stateDir)).toBeUndefined();
|
|
expect(runIdFromSessionPath(undefined, stateDir)).toBeUndefined();
|
|
expect(runIdFromSessionPath("/home/emil/.pi/agent/sessions/main.jsonl", stateDir)).toBeUndefined();
|
|
});
|
|
it("rejects malformed run ids", () => {
|
|
expect(runIdFromSessionPath(resolve(stateDir, "runs/../evil/sessions/a.jsonl"), stateDir)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("buildAgentChatOptions", () => {
|
|
it("formats status glyphs, short names, elapsed time and task", () => {
|
|
const now = Date.now();
|
|
const options = buildAgentChatOptions([
|
|
mk("market-competitors-bb389dda", "running", 3),
|
|
mk("science-education-niches-293d399a", "completed", 55, 12),
|
|
mk("demand-b2b-b2g-57637d25", "failed", 40, 9),
|
|
], now);
|
|
expect(options).toHaveLength(3);
|
|
expect(options[0]).toContain("• market-competitors [running] 03:00");
|
|
expect(options[0]).toContain("x");
|
|
expect(options[1]).toContain("✓ science-education-niches [completed] 43:00");
|
|
expect(options[2]).toContain("✖ demand-b2b-b2g [failed] 31:00");
|
|
});
|
|
it("collapses whitespace in the task", () => {
|
|
const agent = mk("a-bb1", "waiting", 1);
|
|
agent.task = " Multi\nline task ";
|
|
expect(buildAgentChatOptions([agent], Date.now())[0]).toContain("— Multi line task");
|
|
});
|
|
});
|
|
|
|
describe("RunStore mainSessionFile", () => {
|
|
it("persists and reads back the main session file", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "hm-runstore-"));
|
|
try {
|
|
const store = new RunStore(dir);
|
|
const runId = "run-main";
|
|
const root: AgentRecord = { ...base, id: "root", parentId: null, depth: 0, runId, status: "completed" };
|
|
store.create(runId, "goal", root);
|
|
expect(store.mainSessionFileOf(runId)).toBeUndefined();
|
|
store.setMainSessionFile(runId, "/main/session.jsonl");
|
|
expect(store.mainSessionFileOf(runId)).toBe("/main/session.jsonl");
|
|
// survives reload
|
|
const reloaded = new RunStore(dir);
|
|
expect(reloaded.mainSessionFileOf(runId)).toBe("/main/session.jsonl");
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|