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.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user