Files
Emil 029ce743bb feat: /agent opens a subagent's full chat (OpenCode-style), /back returns
- /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
2026-08-01 00:18:47 +03:00

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 });
}
});
});