fix: preserve pi and omp working status across reloads (#984)

This commit is contained in:
Dillon Mulroy 2026-07-03 14:47:31 -05:00 committed by GitHub
parent 6f148588dd
commit d5521e3d25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 142 additions and 0 deletions

View File

@ -4,6 +4,7 @@
test:
cargo nextest run --locked --status-level fail --final-status-level fail --failure-output final --success-output never
python3 -m unittest scripts.test_agent_detection_manifest_check scripts.test_changelog scripts.test_preview scripts.test_vendor_libghostty_vt scripts.test_vendor_portable_pty
just integration-assets-test
just plugin-marketplace-test
# Run one nextest filter, e.g. `just test-one codex_stale_working`
@ -18,6 +19,7 @@ lint:
# Run PR CI checks
ci filter='all()': lint
cargo nextest run --locked -E "{{filter}}" --status-level fail --final-status-level slow --failure-output final --success-output never
just integration-assets-test
just plugin-marketplace-test
# Run Windows target lint from Unix/macOS to catch cfg(windows) compile and clippy failures before CI
@ -45,6 +47,10 @@ build:
website-build:
cd website && bun install --frozen-lockfile && bun run build
# Test bundled agent integration assets
integration-assets-test:
bun test src/integration/assets/herdr-agent-state.test.ts
# Run plugin marketplace Worker tests
plugin-marketplace-test:
cd workers/plugin-marketplace && bun test

View File

@ -0,0 +1,132 @@
import { afterEach, expect, test } from "bun:test";
import { rm } from "node:fs/promises";
import { createServer, type Server } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
const originalEnvironment = {
HERDR_ENV: process.env.HERDR_ENV,
HERDR_PANE_ID: process.env.HERDR_PANE_ID,
HERDR_SOCKET_PATH: process.env.HERDR_SOCKET_PATH,
};
let server: Server | undefined;
let socketPath: string | undefined;
afterEach(async () => {
await new Promise<void>((resolve, reject) => {
if (!server) {
resolve();
return;
}
server.close((error) => (error ? reject(error) : resolve()));
});
server = undefined;
if (socketPath) {
await rm(socketPath, { force: true });
socketPath = undefined;
}
for (const [name, value] of Object.entries(originalEnvironment)) {
if (value === undefined) {
delete process.env[name];
} else {
process.env[name] = value;
}
}
});
const integrations = [
{ name: "Pi", modulePath: "./pi/herdr-agent-state.ts" },
{ name: "Oh My Pi", modulePath: "./omp/herdr-agent-state.ts" },
] as const;
for (const integration of integrations) {
test(`${integration.name} reload preserves working state when the agent is active`, async () => {
const recordingSocketPath = join(
tmpdir(),
`herdr-${integration.name.toLowerCase().replaceAll(" ", "-")}-${process.pid}.sock`,
);
socketPath = recordingSocketPath;
await rm(recordingSocketPath, { force: true });
const requests: unknown[] = [];
const recordingServer = createServer((socket) => {
let input = "";
socket.setEncoding("utf8");
socket.on("data", (chunk) => {
input += chunk;
const newline = input.indexOf("\n");
if (newline === -1) {
return;
}
requests.push(JSON.parse(input.slice(0, newline)));
socket.end("{}\n");
});
});
server = recordingServer;
await new Promise<void>((resolve, reject) => {
recordingServer.once("error", reject);
recordingServer.listen(recordingSocketPath, resolve);
});
process.env.HERDR_ENV = "1";
process.env.HERDR_SOCKET_PATH = recordingSocketPath;
process.env.HERDR_PANE_ID = "test:p1";
type Handler = (event: unknown, context: unknown) => unknown;
const handlers = new Map<string, Handler>();
const pi = {
on(event: string, handler: Handler) {
handlers.set(event, handler);
},
events: {
on() {
return () => {};
},
},
};
const { default: install } = await import(integration.modulePath);
install(pi);
const sessionStart = handlers.get("session_start");
expect(sessionStart).toBeDefined();
await sessionStart?.(
{ reason: "reload" },
{
hasUI: true,
isIdle: () => false,
sessionManager: {
getSessionFile: () => undefined,
getSessionId: () => undefined,
},
},
);
const reportedState = () => {
for (const request of requests) {
if (!isRecord(request) || request.method !== "pane.report_agent") {
continue;
}
const params = request.params;
if (isRecord(params) && typeof params.state === "string") {
return params.state;
}
}
return undefined;
};
const deadline = Date.now() + 1_000;
while (Date.now() < deadline && reportedState() === undefined) {
await Bun.sleep(5);
}
expect(reportedState()).toBe("working");
});
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}

View File

@ -375,6 +375,8 @@ export default function (pi) {
if (!activateRootSession(ctx)) {
return;
}
// A reload can replace this extension mid-run without emitting another agent_start.
agentActive = ctx?.isIdle?.() === false;
publishState(true);
});

View File

@ -332,6 +332,8 @@ export default function (pi) {
rootSession = true;
updateSessionRef(ctx);
void reportSession();
// A reload can replace this extension mid-run without emitting another agent_start.
agentActive = ctx?.isIdle?.() === false;
publishState(true);
});