fix(quick-pane): preserve lifecycle and answer history
This commit is contained in:
@@ -162,7 +162,7 @@ test("sends abort without waiting behind a blocked prompt", async () => {
|
||||
await registry.stop();
|
||||
});
|
||||
|
||||
test("keeps a runtime healthy after a non-terminal adapter diagnostic", async () => {
|
||||
test("keeps a runtime healthy after an unknown non-terminal Pi notification", async () => {
|
||||
const worktrees = await createWorktrees();
|
||||
const fixture = createAdapterFactory();
|
||||
const registry = createAgentRegistry({
|
||||
@@ -173,7 +173,10 @@ test("keeps a runtime healthy after a non-terminal adapter diagnostic", async ()
|
||||
await registry.start();
|
||||
|
||||
fixture.calls[0].options.onError(
|
||||
{ code: "frame_too_large", message: "Pi RPC frame exceeds 32 bytes" },
|
||||
{
|
||||
code: "unsupported_event",
|
||||
message: "Pi RPC emitted unsupported event: future_pi_notification",
|
||||
},
|
||||
{ terminal: false },
|
||||
);
|
||||
|
||||
@@ -184,6 +187,31 @@ test("keeps a runtime healthy after a non-terminal adapter diagnostic", async ()
|
||||
await registry.stop();
|
||||
});
|
||||
|
||||
test("keeps explicit transport failures terminal", async () => {
|
||||
const worktrees = await createWorktrees();
|
||||
const fixture = createAdapterFactory();
|
||||
const registry = createAgentRegistry({
|
||||
homeWorktree: worktrees.home,
|
||||
sessionRoot: worktrees.sessionRoot,
|
||||
startAdapter: fixture.startAdapter,
|
||||
});
|
||||
await registry.start();
|
||||
|
||||
fixture.calls[0].options.onError({
|
||||
code: "invalid_json",
|
||||
message: "Pi RPC emitted invalid JSON",
|
||||
});
|
||||
|
||||
const runtime = registry.getWorkspace().directories[0].runtimes[0];
|
||||
assert.equal(runtime.state, "error");
|
||||
assert.equal(runtime.attention, true);
|
||||
assert.deepEqual(runtime.error, {
|
||||
code: "invalid_json",
|
||||
message: "Pi RPC emitted invalid JSON",
|
||||
});
|
||||
await registry.stop();
|
||||
});
|
||||
|
||||
test("stops agents created during shutdown and rejects new selections", async () => {
|
||||
const worktrees = await createWorktrees();
|
||||
const fixture = createAdapterFactory();
|
||||
|
||||
@@ -115,10 +115,7 @@ test("allows a per-command timeout override without serializing it to Pi", async
|
||||
type: "get_state",
|
||||
id: initialCommand.id,
|
||||
});
|
||||
await assert.rejects(
|
||||
adapter.send({ type: "get_session_stats" }),
|
||||
/timed out/,
|
||||
);
|
||||
await assert.rejects(adapter.send({ type: "get_session_stats" }), /timed out/);
|
||||
await adapter.stop();
|
||||
});
|
||||
|
||||
@@ -208,6 +205,49 @@ test("forwards extension responses without replacing Pi's request ID", async ()
|
||||
await adapter.stop();
|
||||
});
|
||||
|
||||
test("normalizes thinking changes and treats every unknown Pi notification as non-terminal", async () => {
|
||||
const fixture = createFixture();
|
||||
const events = [];
|
||||
const errors = [];
|
||||
const adapter = startPiRpcAdapter({
|
||||
cwd: "/workspace/home",
|
||||
sessionDir: "/workspace/sessions",
|
||||
spawnProcess: fixture.spawn,
|
||||
onEvent: (event) => events.push(event),
|
||||
onError: (error, metadata) => errors.push({ error, metadata }),
|
||||
});
|
||||
const thinkingChange = {
|
||||
type: "thinking_level_changed",
|
||||
level: "high",
|
||||
};
|
||||
fixture.child.stdout.write(`${JSON.stringify(thinkingChange)}\n`);
|
||||
fixture.child.stdout.write(
|
||||
`${JSON.stringify({ type: "future_pi_notification", detail: "new" })}\n`,
|
||||
);
|
||||
fixture.child.stdout.write(
|
||||
`${JSON.stringify({ type: "fatal_pi_notification", terminal: true })}\n`,
|
||||
);
|
||||
|
||||
assert.deepEqual(events, [
|
||||
{
|
||||
seq: 1,
|
||||
type: "transcript",
|
||||
data: { event: thinkingChange },
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(
|
||||
errors.map(({ error, metadata }) => ({
|
||||
code: error.code,
|
||||
terminal: metadata.terminal,
|
||||
})),
|
||||
[
|
||||
{ code: "unsupported_event", terminal: false },
|
||||
{ code: "unsupported_event", terminal: false },
|
||||
],
|
||||
);
|
||||
await adapter.stop();
|
||||
});
|
||||
|
||||
test("discards a fragmented oversized frame before processing the next frame", async () => {
|
||||
const fixture = createFixture();
|
||||
const errors = [];
|
||||
@@ -217,7 +257,7 @@ test("discards a fragmented oversized frame before processing the next frame", a
|
||||
sessionDir: "/workspace/sessions",
|
||||
spawnProcess: fixture.spawn,
|
||||
maxFrameBytes: 32,
|
||||
onError: (error) => errors.push(error),
|
||||
onError: (error, metadata) => errors.push({ error, metadata }),
|
||||
onEvent: (event) => events.push(event),
|
||||
});
|
||||
const oversized = JSON.stringify({
|
||||
@@ -230,8 +270,11 @@ test("discards a fragmented oversized frame before processing the next frame", a
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
errors.map((error) => error.code),
|
||||
["frame_too_large"],
|
||||
errors.map(({ error, metadata }) => ({
|
||||
code: error.code,
|
||||
terminal: metadata?.terminal !== false,
|
||||
})),
|
||||
[{ code: "frame_too_large", terminal: true }],
|
||||
);
|
||||
assert.deepEqual(events, [
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ test("Quick Pane polls snapshots, renders answers, and gates handoff", async ()
|
||||
);
|
||||
assert.match(source, /get_session_runtime_snapshot/);
|
||||
assert.match(source, /setInterval\(\(\) => void refresh\(\), 800\)/);
|
||||
assert.match(source, /\{answer && <pre>\{answer\}<\/pre>\}/);
|
||||
assert.match(source, /\{displayedAnswer && <pre>\{displayedAnswer\}<\/pre>\}/);
|
||||
assert.match(source, /const canContinue = !!escalation \|\| \(!!runtime && !!error\)/);
|
||||
assert.match(source, /\{canContinue && \(/);
|
||||
});
|
||||
@@ -40,7 +40,11 @@ test("main Settings uses shared catalog selectors with provider derived from mod
|
||||
assert.match(pane, /set_thinking_level/);
|
||||
assert.doesNotMatch(pane, /showSettings/);
|
||||
assert.doesNotMatch(pane, /Save Quick Pane settings/);
|
||||
assert.doesNotMatch(pane, /setRequest\(""\)/);
|
||||
assert.match(pane, /isStreaming === false/);
|
||||
assert.match(pane, /current === submitted\.request \? "" : current/);
|
||||
assert.match(pane, /\.slice\(0, 5\)/);
|
||||
assert.match(pane, /aria-label="Answer history"/);
|
||||
assert.doesNotMatch(pane, /localStorage|sessionStorage|save_ui_state/);
|
||||
assert.match(persistence, /quickPane: current\.quickPane/);
|
||||
});
|
||||
|
||||
@@ -75,7 +79,7 @@ test("Quick Pane routes extension responses through existing bridge command", as
|
||||
});
|
||||
|
||||
test("Quick Pane uses native dimensions and request-first compact layout", async () => {
|
||||
const [source, stylesheet, configSource] = await Promise.all([
|
||||
const [source, stylesheet, configSource, nativeSource, capabilitySource] = await Promise.all([
|
||||
readFile(
|
||||
new URL("../ui/src/quick-pane/QuickPane.tsx", import.meta.url),
|
||||
"utf8",
|
||||
@@ -85,8 +89,11 @@ test("Quick Pane uses native dimensions and request-first compact layout", async
|
||||
"utf8",
|
||||
),
|
||||
readFile(new URL("../ui/src-tauri/tauri.conf.json", import.meta.url), "utf8"),
|
||||
readFile(new URL("../ui/src-tauri/src/lib.rs", import.meta.url), "utf8"),
|
||||
readFile(new URL("../ui/src-tauri/capabilities/default.json", import.meta.url), "utf8"),
|
||||
]);
|
||||
const config = JSON.parse(configSource);
|
||||
const capability = JSON.parse(capabilitySource);
|
||||
const quickPane = config.app.windows.find((window) => window.label === "quick-pane");
|
||||
|
||||
assert.deepEqual(
|
||||
@@ -97,11 +104,18 @@ test("Quick Pane uses native dimensions and request-first compact layout", async
|
||||
},
|
||||
{ width: 640, height: 440, transparent: true },
|
||||
);
|
||||
assert.ok(capability.windows.includes("quick-pane"));
|
||||
assert.match(nativeSource, /WindowEvent::CloseRequested/);
|
||||
assert.match(nativeSource, /api\.prevent_close\(\)/);
|
||||
assert.match(nativeSource, /WebviewWindowBuilder::from_config\(app, config\)/);
|
||||
assert.match(nativeSource, /get_or_create_quick_pane/);
|
||||
assert.doesNotMatch(source, /onCloseRequested/);
|
||||
assert.match(source, /className="quick-pane-request"/);
|
||||
assert.match(source, /className="quick-pane-output"/);
|
||||
assert.match(source, /<footer className="quick-pane-controls">/);
|
||||
assert.match(source, /ref=\{inputRef\}/);
|
||||
assert.match(source, /focusInput\(\)/);
|
||||
assert.doesNotMatch(source, /if \(!event\.payload\)[\s\S]*?hide\(/);
|
||||
assert.match(
|
||||
source,
|
||||
/event\.key === "Enter" &&\s*!event\.shiftKey &&\s*!event\.nativeEvent\.isComposing/,
|
||||
|
||||
Reference in New Issue
Block a user