gentle-pi 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -30
- package/assets/agents/gentle-ai-worker.md +4 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +21 -3
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
- package/docs/telemetry.md +57 -1
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +143 -15
- package/extensions/codegraph-tools.ts +1 -0
- package/extensions/gentle-agents.ts +799 -50
- package/extensions/gentle-ai.ts +2033 -322
- package/extensions/gentle-shell.ts +145 -42
- package/extensions/gentle-todo.ts +47 -12
- package/extensions/quiet-tools.ts +1 -0
- package/extensions/runtime-metrics.ts +130 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +7 -10
- package/lib/agents-history.ts +9 -1
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +77 -5
- package/lib/agents-runner.ts +548 -26
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +548 -191
- package/lib/agents-widget.ts +33 -14
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +8 -6
- package/lib/native-review-cli.ts +283 -1
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +296 -46
- package/lib/review-candidate-view.ts +30 -20
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +53 -8
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +187 -5
- package/lib/review-last-event-controller.ts +7 -4
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-session-standing-permission.ts +27 -6
- package/lib/runtime-metrics-children.ts +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +47 -1
- package/lib/shell-card.ts +1 -4
- package/lib/shell-changes-view.ts +362 -37
- package/lib/shell-changes.ts +81 -1
- package/lib/shell-prompt.ts +11 -15
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +28 -11
- package/lib/telemetry-trigger.ts +2 -0
- package/package.json +6 -3
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +283 -1
- package/runtime/review-integration-v2.mjs +187 -5
- package/runtime/telemetry-trigger.mjs +2 -0
- package/scripts/build-runtime-modules.mjs +9 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +4 -2
- package/skills/_shared/review-ledger-contract.md +17 -1
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +62 -0
- package/tests/agents-fake-child.ts +15 -1
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +45 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner.test.ts +571 -14
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-view.test.ts +476 -65
- package/tests/agents-widget.test.ts +31 -1
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +169 -3
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +2 -1
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +2 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +1460 -33
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +38 -0
- package/tests/gentle-ai.test.ts +944 -4
- package/tests/gentle-shell.test.ts +303 -12
- package/tests/gentle-todo.test.ts +54 -10
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-review-capability-contract.test.ts +12 -1
- package/tests/native-review-cli.test.ts +277 -3
- package/tests/native-review-parity.test.ts +14 -7
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/package-manifest.test.ts +232 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +1 -0
- package/tests/rdd-aware-verification-contract.test.ts +10 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +304 -6
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +14 -0
- package/tests/review-controller-native-routing.test.ts +563 -3
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +364 -4
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +164 -0
- package/tests/review-last-event-closure.test.ts +105 -1
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-session-standing-permission-controller.test.ts +52 -4
- package/tests/review-session-standing-permission.test.ts +30 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-card.test.ts +24 -3
- package/tests/shell-changes-view.test.ts +471 -8
- package/tests/shell-changes.test.ts +168 -0
- package/tests/shell-prompt.test.ts +28 -6
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +100 -11
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +3 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { AGENT_MODE, type AgentDefinition } from "../lib/agents-config.ts";
|
|
4
|
+
import { CHILD_MESSAGE_MAX_BYTES, ChildMessenger } from "../lib/agents-messaging.ts";
|
|
5
|
+
import { TaskStore } from "../lib/agents-protocol.ts";
|
|
6
|
+
import { AgentRunner } from "../lib/agents-runner.ts";
|
|
7
|
+
import { fakeChild } from "./agents-fake-child.ts";
|
|
8
|
+
|
|
9
|
+
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
10
|
+
const agent: AgentDefinition = { name: "worker", description: "", filePath: "/a.md", scope: "global", instructions: "", model: undefined, thinking: undefined, mode: undefined, tools: ["read"] };
|
|
11
|
+
const request = (mode = AGENT_MODE.BACKGROUND) => ({ agent, prompt: "work", label: undefined, context: undefined, mode, cwd: "/repo", parentSessionId: "s1", model: undefined, thinking: undefined, sessionDir: "/sessions", resumeSessionPath: undefined, env: {} });
|
|
12
|
+
|
|
13
|
+
test("ChildMessenger validates query replies and settles timeout, callback failure, and disconnect once", async () => {
|
|
14
|
+
const listeners = new Map<string, Array<(value: Record<string, unknown>) => void>>();
|
|
15
|
+
const frames: Array<Record<string, unknown>> = [];
|
|
16
|
+
const timers: Array<() => void> = [];
|
|
17
|
+
let callback: ((error: Error | null) => void) | undefined;
|
|
18
|
+
const endpoint = {
|
|
19
|
+
send: (frame: Record<string, unknown>, done?: (error: Error | null) => void) => { frames.push(frame); callback = done; return false; },
|
|
20
|
+
on: (event: string, listener: (value: Record<string, unknown>) => void) => listeners.set(event, [...(listeners.get(event) ?? []), listener]),
|
|
21
|
+
};
|
|
22
|
+
const messenger = new ChildMessenger(endpoint, (fn) => { timers.push(fn); return () => {}; });
|
|
23
|
+
const pending = messenger.query("which file?");
|
|
24
|
+
assert.deepEqual(frames, [{ id: "q1", kind: "query", message: "which file?" }]);
|
|
25
|
+
for (const listener of listeners.get("message") ?? []) listener({ id: "q1", kind: "reply", message: "\uD800" });
|
|
26
|
+
for (const listener of listeners.get("message") ?? []) listener({ id: "q1", kind: "reply", message: "x".repeat(CHILD_MESSAGE_MAX_BYTES + 1) });
|
|
27
|
+
callback?.(new Error("send failed"));
|
|
28
|
+
await assert.rejects(pending, /send failed/);
|
|
29
|
+
const timeout = messenger.query("timeout");
|
|
30
|
+
timers.at(-1)!();
|
|
31
|
+
await assert.rejects(timeout, /timed out/);
|
|
32
|
+
const disconnected = messenger.query("disconnect");
|
|
33
|
+
for (const listener of listeners.get("disconnect") ?? []) listener({});
|
|
34
|
+
await assert.rejects(disconnected, /closed/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("ChildMessenger resolves one strictly correlated query reply", async () => {
|
|
38
|
+
const listeners = new Map<string, Array<(value: Record<string, unknown>) => void>>();
|
|
39
|
+
const frames: Array<Record<string, unknown>> = [];
|
|
40
|
+
const endpoint = {
|
|
41
|
+
send: (frame: Record<string, unknown>) => { frames.push(frame); return true; },
|
|
42
|
+
on: (event: string, listener: (value: Record<string, unknown>) => void) => listeners.set(event, [...(listeners.get(event) ?? []), listener]),
|
|
43
|
+
};
|
|
44
|
+
const messenger = new ChildMessenger(endpoint, () => () => {});
|
|
45
|
+
const pending = messenger.query("Where?");
|
|
46
|
+
for (const listener of listeners.get("message") ?? []) listener({ id: "n1", kind: "reply", message: "forged" });
|
|
47
|
+
for (const listener of listeners.get("message") ?? []) listener({ id: "q1", kind: "reply", message: "src/a.ts", extra: true });
|
|
48
|
+
for (const listener of listeners.get("message") ?? []) listener({ id: "q1", kind: "reply", message: "src/a.ts" });
|
|
49
|
+
assert.deepEqual(frames, [{ id: "q1", kind: "query", message: "Where?" }]);
|
|
50
|
+
assert.equal(await pending, "src/a.ts");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("ChildMessenger rejects an empty reply error and ignores mixed reply fields", async () => {
|
|
54
|
+
const listeners = new Map<string, Array<(value: Record<string, unknown>) => void>>();
|
|
55
|
+
const timers: Array<{ cancelled: boolean; fn: () => void }> = [];
|
|
56
|
+
const messenger = new ChildMessenger({
|
|
57
|
+
send: () => true,
|
|
58
|
+
on: (event, listener) => listeners.set(event, [...(listeners.get(event) ?? []), listener as (value: Record<string, unknown>) => void]),
|
|
59
|
+
}, (fn) => {
|
|
60
|
+
const timer = { cancelled: false, fn };
|
|
61
|
+
timers.push(timer);
|
|
62
|
+
return () => { timer.cancelled = true; };
|
|
63
|
+
});
|
|
64
|
+
const emptyError = messenger.query("empty error");
|
|
65
|
+
for (const listener of listeners.get("message") ?? []) listener({ id: "q1", kind: "reply", error: "" });
|
|
66
|
+
await assert.rejects(emptyError, /parent rejected query/);
|
|
67
|
+
assert.equal(timers[0]?.cancelled, true, "a rejected reply cancels its deadline");
|
|
68
|
+
const mixed = messenger.query("mixed fields");
|
|
69
|
+
for (const listener of listeners.get("message") ?? []) listener({ id: "q2", kind: "reply", message: "answer", error: "also error" });
|
|
70
|
+
assert.equal(timers[1]?.cancelled, false, "mixed reply fields cannot settle a query");
|
|
71
|
+
timers[1]?.fn();
|
|
72
|
+
await assert.rejects(mixed, /timed out/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("runner sends bounded static query rejection frames that settle the child before timeout", async () => {
|
|
76
|
+
const child = fakeChild();
|
|
77
|
+
const listeners = new Map<string, Array<(value: Record<string, unknown>) => void>>();
|
|
78
|
+
const timers: Array<{ cancelled: boolean; fn: () => void }> = [];
|
|
79
|
+
child.child.send = (frame, done) => {
|
|
80
|
+
child.sent.push(frame);
|
|
81
|
+
for (const listener of listeners.get("message") ?? []) listener(frame);
|
|
82
|
+
done?.(null);
|
|
83
|
+
return true;
|
|
84
|
+
};
|
|
85
|
+
const messenger = new ChildMessenger({
|
|
86
|
+
send: (frame) => { child.message(frame); return true; },
|
|
87
|
+
on: (event, listener) => listeners.set(event, [...(listeners.get(event) ?? []), listener as (value: Record<string, unknown>) => void]),
|
|
88
|
+
}, (fn) => {
|
|
89
|
+
const timer = { cancelled: false, fn };
|
|
90
|
+
timers.push(timer);
|
|
91
|
+
return () => { timer.cancelled = true; };
|
|
92
|
+
});
|
|
93
|
+
const runner = new AgentRunner(new TaskStore(), { maxConcurrency: 1, stallTimeoutMs: 10_000 }, { spawn: () => child.child, now: () => 1, schedule: () => () => {}, pi: { command: "pi", args: [] } }, { askUser: async () => ({ cancelled: true }), onQuery: () => { throw new Error(`\uD800${"x".repeat(CHILD_MESSAGE_MAX_BYTES)}`); } });
|
|
94
|
+
runner.run(request());
|
|
95
|
+
await tick();
|
|
96
|
+
const pending = messenger.query("reject me");
|
|
97
|
+
timers[0]?.fn();
|
|
98
|
+
await assert.rejects(pending, /parent rejected query/);
|
|
99
|
+
assert.deepEqual(child.sent.at(-1), { id: "q1", kind: "reply", error: "parent rejected query" });
|
|
100
|
+
assert.equal(timers[0]?.cancelled, true, "the child reply settles before its deadline");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("a malformed query with a valid q ID gets a reply that clears the matching child query", async () => {
|
|
104
|
+
const child = fakeChild();
|
|
105
|
+
const listeners = new Map<string, Array<(value: Record<string, unknown>) => void>>();
|
|
106
|
+
const timers: Array<{ cancelled: boolean; fn: () => void }> = [];
|
|
107
|
+
child.child.send = (frame, done) => {
|
|
108
|
+
child.sent.push(frame);
|
|
109
|
+
for (const listener of listeners.get("message") ?? []) listener(frame);
|
|
110
|
+
done?.(null);
|
|
111
|
+
return true;
|
|
112
|
+
};
|
|
113
|
+
const messenger = new ChildMessenger({
|
|
114
|
+
send: (frame) => { child.message(frame); return true; },
|
|
115
|
+
on: (event, listener) => listeners.set(event, [...(listeners.get(event) ?? []), listener as (value: Record<string, unknown>) => void]),
|
|
116
|
+
}, (fn) => {
|
|
117
|
+
const timer = { cancelled: false, fn };
|
|
118
|
+
timers.push(timer);
|
|
119
|
+
return () => { timer.cancelled = true; };
|
|
120
|
+
});
|
|
121
|
+
const runner = new AgentRunner(new TaskStore(), { maxConcurrency: 1, stallTimeoutMs: 10_000 }, { spawn: () => child.child, now: () => 1, schedule: () => () => {}, pi: { command: "pi", args: [] } }, { askUser: async () => ({ cancelled: true }), onQuery: () => {} });
|
|
122
|
+
runner.run(request());
|
|
123
|
+
await tick();
|
|
124
|
+
const pending = messenger.query("valid first");
|
|
125
|
+
child.message({ id: "q1", kind: "query", message: "malformed", extra: true });
|
|
126
|
+
timers[0]?.fn();
|
|
127
|
+
await assert.rejects(pending, /invalid child IPC frame/);
|
|
128
|
+
assert.deepEqual(child.sent.at(-1), { id: "q1", kind: "reply", error: "invalid child IPC frame" });
|
|
129
|
+
assert.equal(timers[0]?.cancelled, true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("background queries keep correlation, bounds, and ownership before replying only over IPC", async () => {
|
|
133
|
+
const child = fakeChild();
|
|
134
|
+
const queries: Array<{ taskId: string; requestId: string; message: string }> = [];
|
|
135
|
+
const runner = new AgentRunner(new TaskStore(), { maxConcurrency: 1, stallTimeoutMs: 10_000 }, { spawn: () => child.child, now: () => 1, schedule: () => () => {}, pi: { command: "pi", args: [] } }, {
|
|
136
|
+
askUser: async () => ({ cancelled: true }), onQuery: (task, requestId, message) => queries.push({ taskId: task.id, requestId, message }),
|
|
137
|
+
});
|
|
138
|
+
const task = runner.run(request());
|
|
139
|
+
await tick();
|
|
140
|
+
for (const [id, message] of [["q1", "Which file?"], ["q2", "two"], ["q3", "three"], ["q4", "four"], ["q5", "five"], ["q1", "duplicate"]]) child.message({ id, kind: "query", message });
|
|
141
|
+
await tick();
|
|
142
|
+
assert.deepEqual(queries, ["q1", "q2", "q3", "q4"].map((requestId, index) => ({ taskId: task.id, requestId, message: ["Which file?", "two", "three", "four"][index] })));
|
|
143
|
+
assert.deepEqual(child.sent.slice(-2), [{ id: "q5", kind: "reply", error: "too many pending parent queries" }, { id: "q1", kind: "reply", error: "duplicate query request" }]);
|
|
144
|
+
assert.equal(await runner.reply(task.id, "q1", "src/a.ts", "s1"), true);
|
|
145
|
+
assert.equal(await runner.reply(task.id, "q1", "again", "s1"), false);
|
|
146
|
+
assert.equal(await runner.reply(task.id, "q2", "other", "s2"), false);
|
|
147
|
+
assert.equal(await runner.reply(task.id, "q2", "x".repeat(CHILD_MESSAGE_MAX_BYTES + 1), "s1"), false);
|
|
148
|
+
assert.deepEqual(child.sent.at(-1), { id: "q1", kind: "reply", message: "src/a.ts" });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("a throwing query hook rolls back ownership before one error and never schedules a second reply", async () => {
|
|
152
|
+
const child = fakeChild();
|
|
153
|
+
const timers: Array<{ fn: () => void; ms: number }> = [];
|
|
154
|
+
const runner = new AgentRunner(new TaskStore(), { maxConcurrency: 1, stallTimeoutMs: 10_000 }, { spawn: () => child.child, now: () => 1, schedule: (fn, ms) => { timers.push({ fn, ms }); return () => {}; }, pi: { command: "pi", args: [] } }, { askUser: async () => ({ cancelled: true }), onQuery: () => { throw new Error("host callback failed"); } });
|
|
155
|
+
const task = runner.run(request());
|
|
156
|
+
await tick();
|
|
157
|
+
child.message({ id: "q1", kind: "query", message: "throw" });
|
|
158
|
+
assert.deepEqual(child.sent.at(-1), { id: "q1", kind: "reply", error: "parent rejected query" });
|
|
159
|
+
assert.equal(await runner.reply(task.id, "q1", "late", "s1"), false);
|
|
160
|
+
timers.at(-1)?.fn();
|
|
161
|
+
assert.equal(child.sent.filter((frame) => frame.id === "q1").length, 1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("query expiry, IPC revocation, delayed callbacks, and foreground admission settle once", async () => {
|
|
165
|
+
const child = fakeChild();
|
|
166
|
+
const timers: Array<{ fn: () => void; ms: number }> = [];
|
|
167
|
+
const runner = new AgentRunner(new TaskStore(), { maxConcurrency: 1, stallTimeoutMs: 10_000 }, { spawn: () => child.child, now: () => 1, schedule: (fn, ms) => { timers.push({ fn, ms }); return () => {}; }, pi: { command: "pi", args: [] } }, { askUser: async () => ({ cancelled: true }), onQuery: () => {} });
|
|
168
|
+
const task = runner.run(request());
|
|
169
|
+
await tick();
|
|
170
|
+
child.message({ id: "q1", kind: "query", message: "wait" });
|
|
171
|
+
assert.equal(timers.at(-1)?.ms, 30_000);
|
|
172
|
+
let callback: ((error: Error | null) => void) | undefined;
|
|
173
|
+
child.child.send = (_frame, done) => { callback = done; return false; };
|
|
174
|
+
const pending = runner.reply(task.id, "q1", "late", "s1");
|
|
175
|
+
timers.at(-1)?.fn();
|
|
176
|
+
assert.equal(await pending, false);
|
|
177
|
+
callback?.(null);
|
|
178
|
+
assert.equal(await runner.reply(task.id, "q1", "again", "s1"), false);
|
|
179
|
+
child.message({ id: "q2", kind: "query", message: "disconnect" });
|
|
180
|
+
child.child.disconnect?.();
|
|
181
|
+
assert.equal(await runner.reply(task.id, "q2", "no", "s1"), false);
|
|
182
|
+
const foregroundChild = fakeChild();
|
|
183
|
+
let foregroundQueries = 0;
|
|
184
|
+
const foreground = new AgentRunner(new TaskStore(), { maxConcurrency: 1, stallTimeoutMs: 10_000 }, { spawn: () => foregroundChild.child, now: () => 1, schedule: () => () => {}, pi: { command: "pi", args: [] } }, { askUser: async () => ({ cancelled: true }), onQuery: () => { foregroundQueries += 1; } });
|
|
185
|
+
foreground.run(request(AGENT_MODE.TASK));
|
|
186
|
+
await tick();
|
|
187
|
+
foregroundChild.message({ id: "q1", kind: "query", message: "not yet" });
|
|
188
|
+
await tick();
|
|
189
|
+
assert.equal(foregroundQueries, 1);
|
|
190
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
+
import { measureAgentsViewLayout } from "../lib/agents-view-layout.ts";
|
|
5
|
+
|
|
6
|
+
const widths = [0, 1, 2, 11, 12, 59, 60, 90];
|
|
7
|
+
|
|
8
|
+
for (const rows of [0, 1, 2]) {
|
|
9
|
+
test(`Agents layout safely bounds ${rows}-row terminals`, () => {
|
|
10
|
+
for (const width of widths) {
|
|
11
|
+
const layout = measureAgentsViewLayout(width, rows);
|
|
12
|
+
assert.equal(layout.height, rows);
|
|
13
|
+
assert.ok(layout.width >= 0 && layout.width <= width);
|
|
14
|
+
assert.ok(layout.bodyRows >= 0);
|
|
15
|
+
assert.ok(layout.listWidth >= 0 && layout.threadWidth >= 0);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("Agents layout reserves fallback for tiny terminals, a single viewport for narrow terminals, and panes at 60 cells", () => {
|
|
21
|
+
for (const width of [0, 1, 2, 11]) assert.equal(measureAgentsViewLayout(width, 8).mode, "fallback");
|
|
22
|
+
for (const height of [0, 1, 2]) assert.equal(measureAgentsViewLayout(80, height).mode, "fallback");
|
|
23
|
+
for (const width of [12, 59]) {
|
|
24
|
+
const narrow = measureAgentsViewLayout(width, 3);
|
|
25
|
+
assert.equal(narrow.mode, "narrow");
|
|
26
|
+
assert.equal(narrow.bodyRows, 0);
|
|
27
|
+
assert.equal(narrow.footerY, 1);
|
|
28
|
+
}
|
|
29
|
+
const layout = measureAgentsViewLayout(60, 8);
|
|
30
|
+
assert.equal(layout.mode, "panes");
|
|
31
|
+
assert.equal(layout.height, 8);
|
|
32
|
+
assert.equal(layout.bodyRows, 5);
|
|
33
|
+
assert.equal(layout.footerY, 6);
|
|
34
|
+
assert.equal(layout.listX, 2);
|
|
35
|
+
assert.equal(layout.threadX, layout.listX + layout.listWidth + 3);
|
|
36
|
+
assert.ok(layout.threadX + layout.threadWidth + 1 <= layout.width);
|
|
37
|
+
const fullscreen = measureAgentsViewLayout(60, 8, true);
|
|
38
|
+
assert.equal(fullscreen.mode, "narrow");
|
|
39
|
+
assert.equal(fullscreen.threadWidth, 56);
|
|
40
|
+
assert.equal(fullscreen.threadX, 2);
|
|
41
|
+
assert.equal(fullscreen.height, 8);
|
|
42
|
+
assert.equal(visibleWidth("寿司"), 4, "multibyte widths remain terminal-cell widths");
|
|
43
|
+
});
|