Files
pi-gui/test/agent-registry.test.js
T
alex b7fea83ed6 feat(bridge): establish local pi status bridge
Deliver the initial local bridge, Noctalia v4/v5 adapters, desktop client, service unit, tests, and implementation documentation for persistent Pi status and control.
2026-07-27 14:51:45 +02:00

241 lines
6.6 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { createAgentRegistry } from "../src/bridge/agent-registry.js";
async function createWorktrees() {
const root = await mkdtemp(join(tmpdir(), "pi-status-bridge-worktrees-"));
const home = join(root, "home");
const feature = join(root, "feature");
await Promise.all([mkdir(home), mkdir(feature)]);
return { root, home, feature, sessionRoot: join(root, "sessions") };
}
function createAdapterFactory() {
const calls = [];
return {
calls,
startAdapter: (options) => {
const adapter = {
sent: [],
extensionResponses: [],
stopped: false,
send(command) {
this.sent.push(command);
return Promise.resolve({
type: "response",
command: command.type,
success: true,
});
},
respondToExtension(requestId, response) {
this.extensionResponses.push({ requestId, response });
},
async stop() {
this.stopped = true;
},
};
calls.push({ options, adapter });
return adapter;
},
};
}
test("starts only the home agent and creates other worktree agents on explicit selection", async () => {
const worktrees = await createWorktrees();
const fixture = createAdapterFactory();
const registry = createAgentRegistry({
homeWorktree: worktrees.home,
sessionRoot: worktrees.sessionRoot,
startAdapter: fixture.startAdapter,
});
const home = await registry.start();
assert.equal(registry.listAgents().length, 1);
assert.equal(home.worktreePath, worktrees.home);
const feature = await registry.selectWorktree(worktrees.feature);
const selectedAgain = await registry.selectWorktree(
`${worktrees.feature}/../feature`,
);
assert.equal(feature.id, selectedAgain.id);
assert.notEqual(home.id, feature.id);
assert.deepEqual(
fixture.calls.map(({ options }) => options.cwd),
[worktrees.home, worktrees.feature],
);
assert.notEqual(home.sessionDir, feature.sessionDir);
await registry.stop();
assert.ok(fixture.calls.every(({ adapter }) => adapter.stopped));
});
test("catalogues managed directories and exposes only their sessions", async () => {
const worktrees = await createWorktrees();
const fixture = createAdapterFactory();
const registry = createAgentRegistry({
homeWorktree: worktrees.home,
sessionRoot: worktrees.sessionRoot,
startAdapter: fixture.startAdapter,
});
await registry.start();
const feature = await registry.selectWorktree(worktrees.feature);
const olderSession = join(feature.sessionDir, "older.jsonl");
const newestSession = join(feature.sessionDir, "newest.jsonl");
await writeFile(
olderSession,
[
JSON.stringify({
type: "session",
id: "older",
timestamp: "2025-01-01T00:00:00.000Z",
cwd: worktrees.feature,
}),
JSON.stringify({
type: "message",
message: { role: "user", content: "Older task" },
}),
].join("\n"),
);
await writeFile(
newestSession,
[
JSON.stringify({
type: "session",
id: "newest",
timestamp: "2025-01-02T00:00:00.000Z",
cwd: worktrees.feature,
}),
JSON.stringify({ type: "session_info", name: "Newest work" }),
JSON.stringify({
type: "message",
message: { role: "user", content: "Newest task" },
}),
].join("\n"),
);
await writeFile(
join(feature.sessionDir, "bridge-agent.json"),
`${JSON.stringify({ sessionPath: newestSession, worktreePath: worktrees.feature })}\n`,
);
const sessions = await registry.listSessions(feature.id);
assert.deepEqual(
sessions.map(({ id, name, isCurrent }) => ({ id, name, isCurrent })),
[
{ id: "newest", name: "Newest work", isCurrent: false },
{ id: "older", name: undefined, isCurrent: false },
],
);
await registry.route(feature.id, "switch_session", {
sessionPath: newestSession,
});
await registry.route(feature.id, "new_session");
assert.deepEqual(
fixture.calls[1].adapter.sent.map((command) => command.type),
["get_state", "switch_session", "get_state", "new_session", "get_state"],
);
await assert.rejects(
registry.route(feature.id, "switch_session", {
sessionPath: olderSession.replace("older", "../outside"),
}),
/ENOENT|belong/,
);
await registry.stop();
const restored = createAgentRegistry({
homeWorktree: worktrees.home,
sessionRoot: worktrees.sessionRoot,
startAdapter: createAdapterFactory().startAdapter,
});
await restored.start();
assert.deepEqual(await restored.listDirectories(), [
{ worktreePath: worktrees.feature, state: "inactive" },
{
worktreePath: worktrees.home,
state: "idle",
agentId: restored.listAgents()[0].id,
},
]);
await restored.stop();
});
test("routes commands by explicit agent ID and replays only events after the cursor", async () => {
const worktrees = await createWorktrees();
const fixture = createAdapterFactory();
const registry = createAgentRegistry({
homeWorktree: worktrees.home,
sessionRoot: worktrees.sessionRoot,
startAdapter: fixture.startAdapter,
});
const agent = await registry.start();
const [{ options, adapter }] = fixture.calls;
const response = await registry.route(agent.id, "prompt", {
message: "Stay in this worktree",
});
assert.deepEqual(response, {
type: "response",
command: "prompt",
success: true,
});
const stats = await registry.route(agent.id, "get_session_stats");
assert.deepEqual(stats, {
type: "response",
command: "get_session_stats",
success: true,
});
const commands = await registry.route(agent.id, "get_commands");
assert.deepEqual(commands, {
type: "response",
command: "get_commands",
success: true,
});
assert.deepEqual(adapter.sent, [
{ type: "get_state" },
{ type: "prompt", message: "Stay in this worktree" },
{ type: "get_session_stats" },
{ type: "get_commands" },
]);
options.onEvent({
type: "agent_state",
data: { state: "streaming" },
});
await registry.route(agent.id, "submit_prompt", {
message: "After this turn",
});
assert.deepEqual(adapter.sent.at(-1), {
type: "follow_up",
message: "After this turn",
});
options.onEvent({
type: "stream",
data: { event: { type: "message_update" } },
});
options.onEvent({ type: "queue", data: { event: { type: "queue_update" } } });
assert.deepEqual(
registry.eventsAfter(agent.id, 0).map(({ seq, type }) => ({ seq, type })),
[
{ seq: 1, type: "agent_state" },
{ seq: 2, type: "stream" },
{ seq: 3, type: "queue" },
],
);
assert.deepEqual(
registry.eventsAfter(agent.id, 1).map(({ seq, type }) => ({ seq, type })),
[
{ seq: 2, type: "stream" },
{ seq: 3, type: "queue" },
],
);
await assert.rejects(
registry.route("missing-agent", "abort"),
/unknown agent/,
);
await registry.stop();
});