b7fea83ed6
Deliver the initial local bridge, Noctalia v4/v5 adapters, desktop client, service unit, tests, and implementation documentation for persistent Pi status and control.
97 lines
2.8 KiB
JavaScript
97 lines
2.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { EventEmitter } from "node:events";
|
|
import { PassThrough } from "node:stream";
|
|
import test from "node:test";
|
|
import { startPiRpcAdapter } from "../src/bridge/pi-rpc-adapter.js";
|
|
|
|
function createChild() {
|
|
const child = new EventEmitter();
|
|
child.stdin = new PassThrough();
|
|
child.stdout = new PassThrough();
|
|
child.stderr = new PassThrough();
|
|
child.kill = (signal) => {
|
|
child.emit("exit", null, signal);
|
|
return true;
|
|
};
|
|
return child;
|
|
}
|
|
|
|
test("normalizes streaming, tool, and queue events in child output order", async () => {
|
|
const child = createChild();
|
|
const events = [];
|
|
const adapter = startPiRpcAdapter({
|
|
cwd: "/workspace/home",
|
|
sessionDir: "/workspace/sessions",
|
|
spawnProcess: () => child,
|
|
onEvent: (event) => events.push(event),
|
|
});
|
|
|
|
const piEvents = [
|
|
{
|
|
type: "message_update",
|
|
message: { role: "assistant" },
|
|
assistantMessageEvent: { type: "text_delta", delta: "Hello" },
|
|
},
|
|
{
|
|
type: "tool_execution_start",
|
|
toolCallId: "tool-1",
|
|
toolName: "bash",
|
|
args: { command: "pwd" },
|
|
},
|
|
{
|
|
type: "tool_execution_end",
|
|
toolCallId: "todo-1",
|
|
toolName: "todo",
|
|
result: {
|
|
details: {
|
|
tasks: [{ id: 1, subject: "Ship pane", status: "in_progress" }],
|
|
},
|
|
},
|
|
isError: false,
|
|
},
|
|
{ type: "queue_update", steering: ["Refocus"], followUp: [] },
|
|
];
|
|
child.stdout.write(JSON.stringify(piEvents[0]).slice(0, 30));
|
|
child.stdout.write(
|
|
`${JSON.stringify(piEvents[0]).slice(30)}\n${JSON.stringify(piEvents[1])}\n${JSON.stringify(piEvents[2])}\n${JSON.stringify(piEvents[3])}\n`,
|
|
);
|
|
|
|
assert.deepEqual(events, [
|
|
{ seq: 1, type: "stream", data: { event: piEvents[0] } },
|
|
{ seq: 2, type: "tool", data: { event: piEvents[1] } },
|
|
{ seq: 3, type: "tool", data: { event: piEvents[2] } },
|
|
{ seq: 4, type: "queue", data: { event: piEvents[3] } },
|
|
]);
|
|
await adapter.stop();
|
|
});
|
|
|
|
test("marks Pi idle only after it settles and preserves extension errors as diagnostics", async () => {
|
|
const child = createChild();
|
|
const events = [];
|
|
const adapter = startPiRpcAdapter({
|
|
cwd: "/workspace/home",
|
|
sessionDir: "/workspace/sessions",
|
|
spawnProcess: () => child,
|
|
onEvent: (event) => events.push(event),
|
|
});
|
|
const piEvents = [
|
|
{ type: "agent_start" },
|
|
{ type: "agent_end", willRetry: false },
|
|
{ type: "agent_settled" },
|
|
{ type: "extension_error", error: "non-fatal extension failure" },
|
|
];
|
|
child.stdout.write(
|
|
`${piEvents.map((event) => JSON.stringify(event)).join("\n")}\n`,
|
|
);
|
|
assert.deepEqual(
|
|
events.map((event) => ({ type: event.type, state: event.data.state })),
|
|
[
|
|
{ type: "agent_state", state: "streaming" },
|
|
{ type: "transcript", state: undefined },
|
|
{ type: "agent_state", state: "idle" },
|
|
{ type: "transcript", state: undefined },
|
|
],
|
|
);
|
|
await adapter.stop();
|
|
});
|