aboutsummaryrefslogtreecommitdiff
path: root/src/integration/assets/opencode/herdr-agent-state.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/integration/assets/opencode/herdr-agent-state.test.ts')
-rw-r--r--src/integration/assets/opencode/herdr-agent-state.test.ts142
1 files changed, 142 insertions, 0 deletions
diff --git a/src/integration/assets/opencode/herdr-agent-state.test.ts b/src/integration/assets/opencode/herdr-agent-state.test.ts
new file mode 100644
index 00000000..d8d6d14d
--- /dev/null
+++ b/src/integration/assets/opencode/herdr-agent-state.test.ts
@@ -0,0 +1,142 @@
+import { beforeEach, expect, mock, test } from "bun:test";
+
+const requests: unknown[] = [];
+const clients: FakeClient[] = [];
+const requestWaiters: Array<() => void> = [];
+let autoAcknowledge = true;
+let importCounter = 0;
+
+type FakeClient = {
+ emit: (event: string) => void;
+};
+
+mock.module("node:net", () => ({
+ default: {
+ createConnection(_path: string, onConnect: () => void) {
+ const handlers = new Map<string, () => void>();
+ const client = {
+ write(input: string) {
+ requests.push(JSON.parse(input.trim()));
+ requestWaiters.shift()?.();
+ if (autoAcknowledge) {
+ queueMicrotask(() => client.emit("data"));
+ }
+ },
+ setTimeout() {},
+ on(event: string, handler: () => void) {
+ handlers.set(event, handler);
+ },
+ destroy() {},
+ emit(event: string) {
+ handlers.get(event)?.();
+ },
+ };
+ clients.push(client);
+ queueMicrotask(onConnect);
+ return client;
+ },
+ },
+}));
+
+beforeEach(() => {
+ requests.length = 0;
+ clients.length = 0;
+ requestWaiters.length = 0;
+ autoAcknowledge = true;
+ process.env.HERDR_ENV = "1";
+ process.env.HERDR_SOCKET_PATH = "test.sock";
+ process.env.HERDR_PANE_ID = "test:p1";
+});
+
+async function loadPlugin() {
+ importCounter += 1;
+ const { HerdrAgentStatePlugin } = await import(`./herdr-agent-state.js?test=${importCounter}`);
+ return HerdrAgentStatePlugin();
+}
+
+function waitForNextRequest(): Promise<void> {
+ return new Promise((resolve) => requestWaiters.push(resolve));
+}
+
+test("serializes lifecycle reports", async () => {
+ autoAcknowledge = false;
+ const plugin = await loadPlugin();
+ const firstDispatched = waitForNextRequest();
+ const working = plugin.event({
+ event: {
+ type: "session.status",
+ properties: { sessionID: "root-session", status: { type: "busy" } },
+ },
+ });
+ await firstDispatched;
+
+ const secondDispatched = waitForNextRequest();
+ const idle = plugin.event({
+ event: {
+ type: "session.status",
+ properties: { sessionID: "root-session", status: { type: "idle" } },
+ },
+ });
+ expect(clients).toHaveLength(1);
+
+ clients[0]?.emit("data");
+ await secondDispatched;
+ expect(clients).toHaveLength(2);
+ clients[1]?.emit("data");
+ await Promise.all([working, idle]);
+
+ expect(requests.map(requestState)).toEqual(["working", "idle"]);
+ const sequences = requests.map(requestSeq);
+ expect(sequences[0]).toEqual(expect.any(Number));
+ expect(sequences[1]).toBe((sequences[0] as number) + 1);
+});
+
+test("suppresses redundant same-session updates", async () => {
+ const plugin = await loadPlugin();
+
+ await plugin.event({
+ event: {
+ type: "session.status",
+ properties: { sessionID: "root-session", status: { type: "busy" } },
+ },
+ });
+ await plugin.event({
+ event: { type: "session.updated", properties: { sessionID: "root-session" } },
+ });
+ await plugin.event({
+ event: { type: "session.updated", properties: { sessionID: "replacement-session" } },
+ });
+
+ expect(requests.map(requestMethod)).toEqual([
+ "pane.report_agent",
+ "pane.report_agent_session",
+ ]);
+ expect(requests.map(requestSessionID)).toEqual(["root-session", "replacement-session"]);
+});
+
+function requestMethod(request: unknown): unknown {
+ return isRecord(request) ? request.method : undefined;
+}
+
+function requestState(request: unknown): unknown {
+ return requestParam(request, "state");
+}
+
+function requestSeq(request: unknown): unknown {
+ return requestParam(request, "seq");
+}
+
+function requestSessionID(request: unknown): unknown {
+ return requestParam(request, "agent_session_id");
+}
+
+function requestParam(request: unknown, name: string): unknown {
+ if (!isRecord(request) || !isRecord(request.params)) {
+ return undefined;
+ }
+ return request.params[name];
+}
+
+function isRecord(value: unknown): value is Record<string, unknown> {
+ return typeof value === "object" && value !== null;
+}