gentle-pi 2.4.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 +292 -25
- package/assets/agents/gentle-ai-worker.md +13 -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 +39 -11
- package/assets/orchestrator.md +5 -5
- 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/delegated-verification.md +25 -0
- package/docs/telemetry.md +94 -0
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +159 -25
- package/extensions/codegraph-tools.ts +95 -5
- package/extensions/gentle-agents.ts +1337 -0
- package/extensions/gentle-ai.ts +2916 -386
- package/extensions/gentle-shell.ts +650 -0
- package/extensions/gentle-todo.ts +234 -0
- package/extensions/quiet-tools.ts +2 -1
- 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 +315 -0
- package/lib/agents-history.ts +88 -0
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +501 -0
- package/lib/agents-runner.ts +1012 -0
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +914 -0
- package/lib/agents-widget.ts +241 -0
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +143 -25
- package/lib/native-choice-list.ts +194 -0
- package/lib/native-fullscreen-interaction.ts +47 -0
- package/lib/native-pointer-region.ts +164 -0
- package/lib/native-review-cli.ts +371 -13
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +427 -0
- package/lib/review-candidate-view.ts +150 -48
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +110 -0
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +243 -11
- package/lib/review-last-event-controller.ts +8 -4
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-repository.ts +2 -2
- package/lib/review-risk-assessment.ts +339 -0
- package/lib/review-session-standing-permission-ipc.ts +309 -0
- package/lib/review-session-standing-permission.ts +240 -0
- 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 +184 -0
- package/lib/shell-card.ts +133 -0
- package/lib/shell-changes-view.ts +530 -0
- package/lib/shell-changes.ts +290 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +115 -0
- 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 +297 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +153 -0
- package/package.json +8 -5
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +370 -12
- package/runtime/review-integration-v2.mjs +243 -11
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +154 -0
- package/scripts/build-runtime-modules.mjs +11 -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 +38 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +8 -2
- package/skills/_shared/review-ledger-contract.md +20 -2
- 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 +205 -0
- package/tests/agents-fake-child.ts +66 -0
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +198 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +959 -0
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +685 -0
- package/tests/agents-widget.test.ts +141 -0
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +325 -5
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +112 -2
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +110 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +2168 -0
- 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 +103 -0
- package/tests/gentle-ai.test.ts +971 -15
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +818 -0
- package/tests/gentle-todo.test.ts +226 -0
- 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-choice-list.test.ts +202 -0
- package/tests/native-fullscreen-interaction.test.ts +125 -0
- package/tests/native-pointer-region.test.ts +245 -0
- package/tests/native-review-capability-contract.test.ts +27 -1
- package/tests/native-review-cli.test.ts +317 -3
- package/tests/native-review-consent.test.ts +91 -0
- package/tests/native-review-parity-runtime.test.ts +8 -2
- package/tests/native-review-parity.test.ts +43 -29
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-budget.test.ts +69 -0
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/orchestrator-rdd-ownership.test.ts +9 -0
- package/tests/package-manifest.test.ts +243 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +97 -37
- package/tests/rdd-aware-verification-contract.test.ts +226 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +751 -7
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +17 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +884 -7
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +384 -8
- 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 +276 -0
- package/tests/review-last-event-closure.test.ts +112 -3
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-repository.test.ts +28 -1
- package/tests/review-risk-assessment.test.ts +626 -0
- package/tests/review-session-standing-permission-controller.test.ts +656 -0
- package/tests/review-session-standing-permission-ipc.test.ts +233 -0
- package/tests/review-session-standing-permission-runtime.test.ts +212 -0
- package/tests/review-session-standing-permission.test.ts +156 -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-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +139 -0
- package/tests/shell-changes-view.test.ts +609 -0
- package/tests/shell-changes.test.ts +350 -0
- package/tests/shell-prompt.test.ts +140 -0
- 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 +259 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +351 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test, { mock } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
KeybindingsManager, stripTerminalSequences, Text, TUI_KEYBINDINGS,
|
|
5
|
+
type Component, type TuiMouseEvent, visibleWidth,
|
|
6
|
+
} from "@earendil-works/pi-tui";
|
|
7
|
+
import { NativeChoiceList } from "../lib/native-choice-list.ts";
|
|
8
|
+
import {
|
|
9
|
+
NativePointerScope,
|
|
10
|
+
type NativePointerRegionCallbacks,
|
|
11
|
+
} from "../lib/native-pointer-region.ts";
|
|
12
|
+
import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
|
|
13
|
+
|
|
14
|
+
const selectedForeground = "\u001b[38;5;39m";
|
|
15
|
+
const hoverBackground = "\u001b[48;5;236m";
|
|
16
|
+
const theme = {
|
|
17
|
+
selectedPrefix: (text: string) => `${selectedForeground}${text}\u001b[39m`,
|
|
18
|
+
selectedText: (text: string) => `${selectedForeground}${text}\u001b[39m`,
|
|
19
|
+
description: (text: string) => `\u001b[38;5;244m${text}\u001b[39m`,
|
|
20
|
+
hoverBackground: (text: string) => `${hoverBackground}${text}\u001b[49m`,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function event(
|
|
24
|
+
type: TuiMouseEvent["type"], button: TuiMouseEvent["button"],
|
|
25
|
+
y: number, width: number, height: number, wheelDelta?: number,
|
|
26
|
+
): TuiMouseEvent {
|
|
27
|
+
return {
|
|
28
|
+
type, button, x: 0, y, screenX: 0, screenY: y, width, height,
|
|
29
|
+
shift: false, alt: false, ctrl: false, wheelDelta,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function layout(
|
|
34
|
+
list: NativeChoiceList<{ id: string; label: string; description: string }>, width: number, text: string,
|
|
35
|
+
) {
|
|
36
|
+
const lines = list.render(width);
|
|
37
|
+
assert.ok(lines.every((line) => visibleWidth(line) <= width), "ANSI-stripped lines fit the current render width");
|
|
38
|
+
const row = lines.findIndex((line) => stripTerminalSequences(line).includes(text));
|
|
39
|
+
assert.ok(row >= 0, "the current wrapped layout locates the requested row");
|
|
40
|
+
return { lines, row };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test("native choice list presents one hovered wrapped option without changing selection", () => {
|
|
44
|
+
const list = new NativeChoiceList([
|
|
45
|
+
{
|
|
46
|
+
id: "same", label: "Duplicate label",
|
|
47
|
+
description: "A wrapped description that spans more than one display line at this width.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "other", label: "Duplicate label",
|
|
51
|
+
description: "The second item has the same label but a distinct stable identity.",
|
|
52
|
+
},
|
|
53
|
+
], theme);
|
|
54
|
+
const narrow = layout(list, 40, "second item");
|
|
55
|
+
assert.equal(list.handleMouse(event("move", "none", narrow.row, 40, narrow.lines.length))?.render, true);
|
|
56
|
+
const narrowHover = list.render(40);
|
|
57
|
+
assert.ok(
|
|
58
|
+
narrowHover.every((line, index) => index >= narrow.row - 1 || !line.includes(hoverBackground)),
|
|
59
|
+
"only the hovered option rows have a background",
|
|
60
|
+
);
|
|
61
|
+
assert.ok(
|
|
62
|
+
narrowHover.some((line) => line.includes(selectedForeground) && !line.includes(hoverBackground)),
|
|
63
|
+
"keyboard selection has a distinct foreground",
|
|
64
|
+
);
|
|
65
|
+
assert.equal(list.handleMouse(event("move", "none", narrow.row, 40, narrow.lines.length))?.render, false);
|
|
66
|
+
assert.equal(list.getSelectedItem()?.id, "same");
|
|
67
|
+
|
|
68
|
+
const wider = layout(list, 56, "second item");
|
|
69
|
+
assert.doesNotMatch(
|
|
70
|
+
wider.lines.join("\n"),
|
|
71
|
+
new RegExp(hoverBackground.replace(/[\[\]]/g, "\\$&")), "resize clears hover",
|
|
72
|
+
);
|
|
73
|
+
assert.equal(list.handleMouse(event("move", "none", wider.row, 56, wider.lines.length))?.render, true);
|
|
74
|
+
const hovered = list.render(56);
|
|
75
|
+
assert.ok(hovered.some((line) => line.includes(hoverBackground)), "real SGR background marks the second option");
|
|
76
|
+
const firstRow = hovered.findIndex((line) => stripTerminalSequences(line).includes("Duplicate label"));
|
|
77
|
+
assert.ok(firstRow >= 0);
|
|
78
|
+
list.handleMouse(event("move", "none", firstRow, 56, hovered.length));
|
|
79
|
+
const selectedHover = list.render(56)[firstRow];
|
|
80
|
+
assert.ok(
|
|
81
|
+
selectedHover?.includes(selectedForeground) && selectedHover.includes(hoverBackground),
|
|
82
|
+
"selected and hovered styles combine",
|
|
83
|
+
);
|
|
84
|
+
list.invalidate();
|
|
85
|
+
assert.doesNotMatch(list.render(56).join("\n"), new RegExp(hoverBackground.replace(/[\[\]]/g, "\\$&")));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("native choice list ignores Kitty key releases", () => {
|
|
89
|
+
const list = new NativeChoiceList(
|
|
90
|
+
[{ id: "first", label: "First" }, { id: "second", label: "Second" }],
|
|
91
|
+
theme, new KeybindingsManager(TUI_KEYBINDINGS),
|
|
92
|
+
);
|
|
93
|
+
const selected: string[] = [];
|
|
94
|
+
let cancelled = 0;
|
|
95
|
+
list.onSelect = (item) => selected.push(item.id);
|
|
96
|
+
list.onCancel = () => { cancelled++; };
|
|
97
|
+
list.handleInput("\u001b[B");
|
|
98
|
+
list.handleInput("\r");
|
|
99
|
+
assert.deepEqual(selected, ["second"], "legacy arrow and Enter use semantic bindings");
|
|
100
|
+
list.setSelectedIndex(0);
|
|
101
|
+
selected.length = 0;
|
|
102
|
+
list.handleInput("\u001b[1;1:3B");
|
|
103
|
+
assert.equal(list.getSelectedItem()?.id, "first", "Kitty arrow release does not navigate");
|
|
104
|
+
list.handleInput("\u001b[1;1:1B");
|
|
105
|
+
assert.equal(list.getSelectedItem()?.id, "second", "Kitty arrow press navigates");
|
|
106
|
+
list.setSelectedIndex(0);
|
|
107
|
+
list.handleInput("\u001b[1;1:2B");
|
|
108
|
+
assert.equal(list.getSelectedItem()?.id, "second", "Kitty arrow repeat still navigates");
|
|
109
|
+
list.handleInput("\u001b[13;1:3u");
|
|
110
|
+
list.handleInput("\u001b[27;1:3u");
|
|
111
|
+
assert.deepEqual([selected, cancelled], [[], 0], "Kitty confirm and cancel releases do not activate callbacks");
|
|
112
|
+
list.handleInput("\u001b[13;1:1u");
|
|
113
|
+
list.handleInput("\u001b[27;1:1u");
|
|
114
|
+
assert.deepEqual([selected, cancelled], [["second"], 1], "Kitty confirm and cancel presses still activate");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("root observer clears hover and disabled controls ignore every input", () => {
|
|
118
|
+
const list = new NativeChoiceList([
|
|
119
|
+
{ id: "first", label: "First choice", description: "First detail." },
|
|
120
|
+
{ id: "second", label: "Second choice", description: "Second detail." },
|
|
121
|
+
], theme);
|
|
122
|
+
let renders = 0;
|
|
123
|
+
let selected = 0;
|
|
124
|
+
let cancelled = 0;
|
|
125
|
+
list.onSelect = () => { selected++; };
|
|
126
|
+
list.onCancel = () => { cancelled++; };
|
|
127
|
+
const root = createNativeFullscreenInteraction({
|
|
128
|
+
keyboardTarget: list, requestRender: () => { renders++; },
|
|
129
|
+
mouseObserver: list.createMouseObserver(() => { renders++; }),
|
|
130
|
+
});
|
|
131
|
+
root.addChild(new Text("Header", 0, 0));
|
|
132
|
+
root.addChild(list);
|
|
133
|
+
root.addChild(new Text("Footer", 0, 0));
|
|
134
|
+
const lines = root.render(48);
|
|
135
|
+
const firstRow = lines.findIndex((line) => stripTerminalSequences(line).includes("First"));
|
|
136
|
+
const secondRow = lines.findIndex((line) => stripTerminalSequences(line).includes("Second"));
|
|
137
|
+
assert.ok(firstRow >= 0 && secondRow >= 0);
|
|
138
|
+
root.handleMouse(event("move", "none", secondRow, 48, lines.length));
|
|
139
|
+
assert.match(root.render(48).join("\n"), new RegExp(hoverBackground.replace(/[\[\]]/g, "\\$&")));
|
|
140
|
+
root.handleMouse(event("move", "none", 0, 48, lines.length));
|
|
141
|
+
assert.doesNotMatch(root.render(48).join("\n"), new RegExp(hoverBackground.replace(/[\[\]]/g, "\\$&")));
|
|
142
|
+
assert.equal(renders, 1);
|
|
143
|
+
root.handleMouse(event("move", "none", 0, 48, lines.length));
|
|
144
|
+
assert.equal(renders, 1);
|
|
145
|
+
list.handleMouse(event("move", "none", secondRow, 48, lines.length));
|
|
146
|
+
list.setDisabled(true);
|
|
147
|
+
assert.doesNotMatch(
|
|
148
|
+
root.render(48).join("\n"),
|
|
149
|
+
new RegExp(hoverBackground.replace(/[\[\]]/g, "\\$&")), "disabling clears hover",
|
|
150
|
+
);
|
|
151
|
+
const before = list.getSelectedItem()?.id;
|
|
152
|
+
for (const [type, button, delta] of [
|
|
153
|
+
["move", "none"], ["press", "left"],
|
|
154
|
+
["click", "left"], ["wheel", "none", 1],
|
|
155
|
+
] as const) {
|
|
156
|
+
assert.equal(
|
|
157
|
+
root.handleMouse(event(type, button, firstRow, 48, lines.length, delta)), undefined,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
for (const input of ["\r", "\u001b"]) list.handleInput(input);
|
|
161
|
+
assert.deepEqual(
|
|
162
|
+
[list.getSelectedItem()?.id, selected, cancelled], [before, 0, 0],
|
|
163
|
+
"disabled mouse and keyboard events cannot mutate or activate",
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("empty native choice lists do not activate phantom selections", () => {
|
|
168
|
+
const list = new NativeChoiceList([], theme);
|
|
169
|
+
let selected = 0;
|
|
170
|
+
let cancelled = 0;
|
|
171
|
+
list.onSelect = () => { selected++; };
|
|
172
|
+
list.onCancel = () => { cancelled++; };
|
|
173
|
+
list.handleInput("\r");
|
|
174
|
+
list.handleInput("\u001b");
|
|
175
|
+
assert.equal(selected, 0);
|
|
176
|
+
assert.equal(cancelled, 1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("native choice list composes rows through the shared pointer scope", () => {
|
|
180
|
+
const originalWrap = NativePointerScope.prototype.wrap;
|
|
181
|
+
let wraps = 0;
|
|
182
|
+
const spy = mock.method(NativePointerScope.prototype, "wrap", function (
|
|
183
|
+
this: NativePointerScope, child: Component, callbacks: NativePointerRegionCallbacks,
|
|
184
|
+
) {
|
|
185
|
+
wraps++;
|
|
186
|
+
return originalWrap.call(this, child, callbacks);
|
|
187
|
+
});
|
|
188
|
+
try {
|
|
189
|
+
const list = new NativeChoiceList([
|
|
190
|
+
{ id: "first", label: "First" },
|
|
191
|
+
{ id: "second", label: "Second" },
|
|
192
|
+
], theme);
|
|
193
|
+
const lines = list.render(40);
|
|
194
|
+
assert.equal(wraps, 2, "each choice row uses the composable shared pointer primitive");
|
|
195
|
+
const second = lines.findIndex((line) => line.includes("Second"));
|
|
196
|
+
assert.ok(second >= 0);
|
|
197
|
+
assert.equal(list.handleMouse(event("move", "none", second, 40, lines.length))?.render, true);
|
|
198
|
+
assert.equal(list.getSelectedItem()?.id, "first", "hover remains distinct from keyboard selection");
|
|
199
|
+
} finally {
|
|
200
|
+
spy.mock.restore();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test, { mock } from "node:test";
|
|
3
|
+
import { Container, MouseRegion, type TuiMouseEvent, SelectList, Text } from "@earendil-works/pi-tui";
|
|
4
|
+
import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
|
|
5
|
+
|
|
6
|
+
const items = [
|
|
7
|
+
{ value: "first", label: "First option", description: "A description keeps the list layout realistic." },
|
|
8
|
+
{ value: "second", label: "Second option", description: "A second description verifies the target row." },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const theme = {
|
|
12
|
+
selectedPrefix: (text: string) => text,
|
|
13
|
+
selectedText: (text: string) => text,
|
|
14
|
+
description: (text: string) => text,
|
|
15
|
+
scrollInfo: (text: string) => text,
|
|
16
|
+
noMatch: (text: string) => text,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function mouse(
|
|
20
|
+
type: TuiMouseEvent["type"],
|
|
21
|
+
button: TuiMouseEvent["button"],
|
|
22
|
+
y: number,
|
|
23
|
+
height: number,
|
|
24
|
+
wheelDelta?: number,
|
|
25
|
+
): TuiMouseEvent {
|
|
26
|
+
return {
|
|
27
|
+
type,
|
|
28
|
+
button,
|
|
29
|
+
x: 0,
|
|
30
|
+
y,
|
|
31
|
+
screenX: 0,
|
|
32
|
+
screenY: y,
|
|
33
|
+
width: 80,
|
|
34
|
+
height,
|
|
35
|
+
shift: false,
|
|
36
|
+
alt: false,
|
|
37
|
+
ctrl: false,
|
|
38
|
+
wheelDelta,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test("native fullscreen interaction preserves observer order and native dispatch identity", () => {
|
|
43
|
+
const order: string[] = [];
|
|
44
|
+
const root = createNativeFullscreenInteraction({
|
|
45
|
+
keyboardTarget: new Text("keyboard", 0, 0),
|
|
46
|
+
requestRender() {},
|
|
47
|
+
mouseObserver: { beforeMouse: () => order.push("before"), afterMouse: () => order.push("after") },
|
|
48
|
+
});
|
|
49
|
+
root.addChild(new Text("before child", 0, 0));
|
|
50
|
+
root.addChild(new MouseRegion(new Text("native child", 0, 0), () => {
|
|
51
|
+
order.push("native child");
|
|
52
|
+
return { handled: true, focus: true };
|
|
53
|
+
}));
|
|
54
|
+
root.addChild(new Text("after child", 0, 0));
|
|
55
|
+
const lines = root.render(80);
|
|
56
|
+
const row = lines.findIndex((line) => line.includes("native child"));
|
|
57
|
+
assert.ok(row >= 0);
|
|
58
|
+
const nativeHandleMouse = Container.prototype.handleMouse;
|
|
59
|
+
let nativeResult: ReturnType<typeof nativeHandleMouse>;
|
|
60
|
+
const spy = mock.method(Container.prototype, "handleMouse", function (this: Container, event: TuiMouseEvent) {
|
|
61
|
+
nativeResult = nativeHandleMouse.call(this, event);
|
|
62
|
+
return nativeResult;
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
const result = root.handleMouse(mouse("press", "left", row, lines.length));
|
|
66
|
+
assert.deepEqual(order, ["before", "native child", "after"]);
|
|
67
|
+
assert.equal(result, nativeResult, "the observer returns the exact native Container result");
|
|
68
|
+
assert.equal(result?.focusTarget, root, "native focus ownership remains with the root");
|
|
69
|
+
} finally {
|
|
70
|
+
spy.mock.restore();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("native fullscreen interaction preserves Container geometry and SelectList behavior", () => {
|
|
75
|
+
const list = new SelectList(items, items.length, theme);
|
|
76
|
+
const selected: string[] = [];
|
|
77
|
+
let renders = 0;
|
|
78
|
+
list.onSelect = (item) => selected.push(item.value);
|
|
79
|
+
const root = createNativeFullscreenInteraction({
|
|
80
|
+
keyboardTarget: list,
|
|
81
|
+
requestRender: () => { renders++; },
|
|
82
|
+
});
|
|
83
|
+
root.addChild(new Text("A long header wraps before the SelectList and must not define hit rows.", 1, 0));
|
|
84
|
+
root.addChild(list);
|
|
85
|
+
|
|
86
|
+
assert.ok(root instanceof Container);
|
|
87
|
+
assert.ok(root.render(20).length > items.length, "the narrow render includes a wrapped header");
|
|
88
|
+
const lines = root.render(80);
|
|
89
|
+
const secondRow = lines.findIndex((line) => line.includes("Second option"));
|
|
90
|
+
assert.ok(secondRow >= 0, "the test locates the actual rendered SelectList row");
|
|
91
|
+
|
|
92
|
+
const wheel = root.handleMouse(mouse("wheel", "none", secondRow, lines.length, 1));
|
|
93
|
+
assert.equal(wheel?.handled, true);
|
|
94
|
+
assert.equal(wheel?.render, true);
|
|
95
|
+
assert.deepEqual(selected, [], "wheel never activates an option");
|
|
96
|
+
assert.equal(
|
|
97
|
+
root.handleMouse(mouse("wheel", "none", secondRow, lines.length, 1))?.render,
|
|
98
|
+
false,
|
|
99
|
+
"wheel clamps at the final item",
|
|
100
|
+
);
|
|
101
|
+
assert.equal(root.handleMouse(mouse("move", "none", secondRow, lines.length)), undefined);
|
|
102
|
+
assert.equal(list.getSelectedItem()?.value, "second", "move does not change native selection");
|
|
103
|
+
|
|
104
|
+
root.handleInput("\u001b[A");
|
|
105
|
+
assert.equal(renders, 1, "keyboard input forwards to the native control and requests a render");
|
|
106
|
+
assert.equal(list.getSelectedItem()?.value, "first", "keyboard selection remains independent of pointer movement");
|
|
107
|
+
assert.deepEqual(selected, [], "keyboard navigation does not activate an option");
|
|
108
|
+
|
|
109
|
+
const press = root.handleMouse(mouse("press", "left", secondRow, lines.length));
|
|
110
|
+
assert.equal(press?.handled, true);
|
|
111
|
+
assert.equal(press?.focus, true);
|
|
112
|
+
assert.equal(press?.focusTarget, root, "the root remains the keyboard focus owner");
|
|
113
|
+
assert.deepEqual(selected, [], "press never activates an option");
|
|
114
|
+
assert.equal(root.handleMouse(mouse("release", "left", secondRow, lines.length)), undefined);
|
|
115
|
+
assert.equal(root.handleMouse(mouse("click", "right", secondRow, lines.length)), undefined);
|
|
116
|
+
assert.equal(root.handleMouse(mouse("click", "left", lines.length, lines.length)), undefined);
|
|
117
|
+
|
|
118
|
+
root.handleInput("\r");
|
|
119
|
+
assert.equal(renders, 2, "Enter renders after activating the focused native item");
|
|
120
|
+
assert.deepEqual(selected, ["second"], "Enter activates the item selected by press");
|
|
121
|
+
|
|
122
|
+
const click = root.handleMouse(mouse("click", "left", secondRow, lines.length));
|
|
123
|
+
assert.equal(click?.handled, true);
|
|
124
|
+
assert.deepEqual(selected, ["second", "second"]);
|
|
125
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Box, Container, Text, type Component, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
5
|
+
import { createNativePointerScope } from "../lib/native-pointer-region.ts";
|
|
6
|
+
|
|
7
|
+
function mouse(
|
|
8
|
+
type: TuiMouseEvent["type"], button: TuiMouseEvent["button"], y: number, height: number,
|
|
9
|
+
): TuiMouseEvent {
|
|
10
|
+
return {
|
|
11
|
+
type, button, x: 0, y, screenX: 0, screenY: y, width: 40, height,
|
|
12
|
+
shift: false, alt: false, ctrl: false,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function dispatch(
|
|
17
|
+
root: Container,
|
|
18
|
+
observer: ReturnType<ReturnType<typeof createNativePointerScope>["createMouseObserver"]>,
|
|
19
|
+
event: TuiMouseEvent,
|
|
20
|
+
) {
|
|
21
|
+
observer.beforeMouse(event);
|
|
22
|
+
try {
|
|
23
|
+
return root.handleMouse(event);
|
|
24
|
+
} finally {
|
|
25
|
+
observer.afterMouse(event);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type PublicCustomFactory = Parameters<ExtensionContext["ui"]["custom"]>[0];
|
|
30
|
+
type PublicCustomOptions = NonNullable<Parameters<ExtensionContext["ui"]["custom"]>[1]>;
|
|
31
|
+
|
|
32
|
+
// Typed callback harness, not proof of physical terminal overlay UX.
|
|
33
|
+
async function mountOverlay(
|
|
34
|
+
factory: PublicCustomFactory,
|
|
35
|
+
options: PublicCustomOptions,
|
|
36
|
+
): Promise<Component & { dispose?(): void }> {
|
|
37
|
+
assert.equal(options.overlay, true, "the public custom factory receives overlay mode");
|
|
38
|
+
return factory(
|
|
39
|
+
{} as Parameters<PublicCustomFactory>[0],
|
|
40
|
+
{} as Parameters<PublicCustomFactory>[1],
|
|
41
|
+
{} as Parameters<PublicCustomFactory>[2],
|
|
42
|
+
() => {},
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
test("pointer regions give a public Text hover, press, and click behavior without implicit focus", () => {
|
|
47
|
+
const scope = createNativePointerScope();
|
|
48
|
+
const calls: string[] = [];
|
|
49
|
+
const region = scope.wrap(new Text("Open editor", 0, 0), {
|
|
50
|
+
onHover: () => {
|
|
51
|
+
calls.push("hover");
|
|
52
|
+
return { handled: true, render: true };
|
|
53
|
+
},
|
|
54
|
+
onPress: () => {
|
|
55
|
+
calls.push("press");
|
|
56
|
+
return { handled: true };
|
|
57
|
+
},
|
|
58
|
+
onClick: () => {
|
|
59
|
+
calls.push("click");
|
|
60
|
+
return { handled: true };
|
|
61
|
+
},
|
|
62
|
+
onWheel: () => {
|
|
63
|
+
calls.push("wheel");
|
|
64
|
+
return { handled: true };
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const root = new Container();
|
|
68
|
+
root.addChild(region);
|
|
69
|
+
const height = root.render(40).length;
|
|
70
|
+
const observer = scope.createMouseObserver();
|
|
71
|
+
|
|
72
|
+
assert.equal(dispatch(root, observer, mouse("move", "none", 0, height))?.render, true);
|
|
73
|
+
const press = dispatch(root, observer, mouse("press", "left", 0, height));
|
|
74
|
+
assert.equal(press?.handled, true);
|
|
75
|
+
assert.equal(press?.focus, undefined, "a Text region never claims keyboard focus by itself");
|
|
76
|
+
assert.equal(dispatch(root, observer, mouse("click", "left", 0, height))?.handled, true);
|
|
77
|
+
assert.equal(dispatch(root, observer, mouse("wheel", "none", 0, height))?.handled, true);
|
|
78
|
+
assert.deepEqual(calls, ["hover", "press", "click", "wheel"]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("pointer scopes clear hover only through observed lifecycle boundaries", () => {
|
|
82
|
+
const scope = createNativePointerScope();
|
|
83
|
+
const states: string[] = [];
|
|
84
|
+
const region = scope.wrap(new Text("Hover me", 0, 0), {
|
|
85
|
+
onHover: () => {
|
|
86
|
+
states.push("hover");
|
|
87
|
+
return { handled: true, render: true };
|
|
88
|
+
},
|
|
89
|
+
onLeave: () => states.push("leave"),
|
|
90
|
+
});
|
|
91
|
+
const root = new Container();
|
|
92
|
+
root.addChild(region);
|
|
93
|
+
const observer = scope.createMouseObserver();
|
|
94
|
+
const height = root.render(40).length;
|
|
95
|
+
|
|
96
|
+
dispatch(root, observer, mouse("move", "none", 0, height));
|
|
97
|
+
dispatch(root, observer, mouse("move", "none", height, height + 1));
|
|
98
|
+
assert.deepEqual(states, ["hover", "leave"], "an in-root empty move clears hover");
|
|
99
|
+
|
|
100
|
+
dispatch(root, observer, mouse("move", "none", 0, height));
|
|
101
|
+
region.render(30);
|
|
102
|
+
assert.deepEqual(states, ["hover", "leave", "hover", "leave"], "resize clears hover");
|
|
103
|
+
|
|
104
|
+
dispatch(root, observer, mouse("move", "none", 0, height));
|
|
105
|
+
region.invalidate();
|
|
106
|
+
region.setDisabled(true);
|
|
107
|
+
region.dispose();
|
|
108
|
+
scope.dispose();
|
|
109
|
+
dispatch(root, observer, mouse("move", "none", 0, height));
|
|
110
|
+
assert.deepEqual(
|
|
111
|
+
states,
|
|
112
|
+
["hover", "leave", "hover", "leave", "hover", "leave"],
|
|
113
|
+
"invalidate, disable, disposal, and late events cannot retain or restore hover",
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("a consuming public Box child keeps its exact native target and focus without outer callbacks", () => {
|
|
118
|
+
const scope = createNativePointerScope();
|
|
119
|
+
let outerClicks = 0;
|
|
120
|
+
const child: Component = {
|
|
121
|
+
render: () => ["native child"],
|
|
122
|
+
invalidate() {},
|
|
123
|
+
handleMouse: () => ({ handled: true, focus: true, render: true }),
|
|
124
|
+
};
|
|
125
|
+
const box = new Box(0, 0);
|
|
126
|
+
box.addChild(child);
|
|
127
|
+
const region = scope.wrap(box, {
|
|
128
|
+
onClick: () => {
|
|
129
|
+
outerClicks++;
|
|
130
|
+
return { handled: true };
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const root = new Container();
|
|
134
|
+
root.addChild(region);
|
|
135
|
+
const height = root.render(40).length;
|
|
136
|
+
const result = dispatch(root, scope.createMouseObserver(), mouse("click", "left", 0, height));
|
|
137
|
+
|
|
138
|
+
assert.equal(result?.target.component, child);
|
|
139
|
+
assert.equal(result?.focusTarget, child);
|
|
140
|
+
assert.equal(outerClicks, 0, "a consumed child prevents outer pointer callbacks");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("the same composed region can open an input entry point without answering on press", () => {
|
|
144
|
+
const scope = createNativePointerScope();
|
|
145
|
+
let opened = 0;
|
|
146
|
+
const panel = new Container();
|
|
147
|
+
panel.addChild(scope.wrap(new Text("Open input", 0, 0), {
|
|
148
|
+
onPress: () => ({ handled: true }),
|
|
149
|
+
onClick: () => {
|
|
150
|
+
opened++;
|
|
151
|
+
return { handled: true };
|
|
152
|
+
},
|
|
153
|
+
}));
|
|
154
|
+
const root = new Container();
|
|
155
|
+
root.addChild(panel);
|
|
156
|
+
const height = root.render(40).length;
|
|
157
|
+
const observer = scope.createMouseObserver();
|
|
158
|
+
|
|
159
|
+
assert.equal(dispatch(root, observer, mouse("press", "left", 0, height))?.handled, true);
|
|
160
|
+
assert.equal(opened, 0, "press can prepare a control but cannot answer it");
|
|
161
|
+
assert.equal(dispatch(root, observer, mouse("click", "left", 0, height))?.handled, true);
|
|
162
|
+
assert.equal(opened, 1, "a caller-owned callback opens the input entry point");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("a typed public custom overlay factory composes pointer content and disposes late events", async () => {
|
|
166
|
+
let opened = 0;
|
|
167
|
+
let leaves = 0;
|
|
168
|
+
const component = await mountOverlay(() => {
|
|
169
|
+
const scope = createNativePointerScope();
|
|
170
|
+
const panel = new Container();
|
|
171
|
+
panel.addChild(scope.wrap(new Text("Open overlay input", 0, 0), {
|
|
172
|
+
onHover: () => ({ handled: true, render: true }),
|
|
173
|
+
onLeave: () => { leaves++; },
|
|
174
|
+
onClick: () => {
|
|
175
|
+
opened++;
|
|
176
|
+
return { handled: true };
|
|
177
|
+
},
|
|
178
|
+
}));
|
|
179
|
+
const observer = scope.createMouseObserver();
|
|
180
|
+
return {
|
|
181
|
+
render: (width) => panel.render(width),
|
|
182
|
+
handleMouse: (event) => {
|
|
183
|
+
observer.beforeMouse(event);
|
|
184
|
+
try {
|
|
185
|
+
return panel.handleMouse(event);
|
|
186
|
+
} finally {
|
|
187
|
+
observer.afterMouse(event);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
invalidate: () => panel.invalidate(),
|
|
191
|
+
dispose: () => scope.dispose(),
|
|
192
|
+
};
|
|
193
|
+
}, { overlay: true });
|
|
194
|
+
const height = component.render(40).length;
|
|
195
|
+
const hover = component.handleMouse?.(mouse("move", "none", 0, height));
|
|
196
|
+
const click = component.handleMouse?.(mouse("click", "left", 0, height));
|
|
197
|
+
|
|
198
|
+
assert.equal(hover?.render, true);
|
|
199
|
+
assert.equal(click?.focus, undefined, "overlay content does not claim focus without a caller result");
|
|
200
|
+
assert.equal(opened, 1, "the factory-returned component routes a native click to its callback");
|
|
201
|
+
component.dispose?.();
|
|
202
|
+
assert.equal(component.handleMouse?.(mouse("click", "left", 0, height)), undefined);
|
|
203
|
+
assert.deepEqual([opened, leaves], [1, 1], "disposal clears hover and rejects late overlay events");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("scopes apply disabled and disposed state to later Text regions", () => {
|
|
207
|
+
const calls: string[] = [];
|
|
208
|
+
const disabled = createNativePointerScope();
|
|
209
|
+
disabled.setDisabled(true);
|
|
210
|
+
const disabledRoot = new Container();
|
|
211
|
+
disabledRoot.addChild(disabled.wrap(new Text("Disabled", 0, 0), {
|
|
212
|
+
onClick: () => {
|
|
213
|
+
calls.push("disabled");
|
|
214
|
+
return { handled: true };
|
|
215
|
+
},
|
|
216
|
+
}));
|
|
217
|
+
const disabledHeight = disabledRoot.render(40).length;
|
|
218
|
+
const disabledClick = dispatch(
|
|
219
|
+
disabledRoot, disabled.createMouseObserver(), mouse("click", "left", 0, disabledHeight),
|
|
220
|
+
);
|
|
221
|
+
disabled.setDisabled(false);
|
|
222
|
+
const reenabledClick = dispatch(
|
|
223
|
+
disabledRoot, disabled.createMouseObserver(), mouse("click", "left", 0, disabledHeight),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const disposed = createNativePointerScope();
|
|
227
|
+
disposed.dispose();
|
|
228
|
+
const disposedRoot = new Container();
|
|
229
|
+
disposedRoot.addChild(disposed.wrap(new Text("Disposed", 0, 0), {
|
|
230
|
+
onClick: () => {
|
|
231
|
+
calls.push("disposed");
|
|
232
|
+
return { handled: true };
|
|
233
|
+
},
|
|
234
|
+
}));
|
|
235
|
+
const disposedHeight = disposedRoot.render(40).length;
|
|
236
|
+
const disposedClick = dispatch(
|
|
237
|
+
disposedRoot, disposed.createMouseObserver(), mouse("click", "left", 0, disposedHeight),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
assert.deepEqual(
|
|
241
|
+
[disabledClick?.handled, reenabledClick?.handled, disposedClick?.handled, calls],
|
|
242
|
+
[undefined, true, undefined, ["disabled"]],
|
|
243
|
+
"later regions inherit scope disable/disposal while a live scope can be re-enabled",
|
|
244
|
+
);
|
|
245
|
+
});
|
|
@@ -167,11 +167,37 @@ test("2.6.0 repeats 2.5.0 because the negotiated lane Pi consumes is unchanged",
|
|
|
167
167
|
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.5.0"] as Record<string, boolean>);
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
+
test("2.7.0 repeats 2.6.0 because the negotiated lane Pi consumes is unchanged", () => {
|
|
171
|
+
// Ground-truthed against the published v2.7.0 binary installed from its
|
|
172
|
+
// signed archive: the v2 lane still advertises capabilities/v2.5 and
|
|
173
|
+
// answers status/v7, consent/v3, and the `start/v4` continuation, all
|
|
174
|
+
// already decoded. `review mode status --json` and `review validate --gate
|
|
175
|
+
// pre-commit` were re-run against the binary and still answer
|
|
176
|
+
// `gentle-ai.rdd-mode-status/v1` and `gentle-ai.review-gate-result/v1`
|
|
177
|
+
// respectively. riskEvidence and hint stay dark: still not proven to reach
|
|
178
|
+
// the negotiated START path Pi consumes.
|
|
179
|
+
const contract = NATIVE_CLI_CONTRACTS["2.7.0"] as Record<string, boolean>;
|
|
180
|
+
assert.equal(contract.riskEvidence, false);
|
|
181
|
+
assert.equal(contract.hint, false);
|
|
182
|
+
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.6.0"] as Record<string, boolean>);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("2.8.0 repeats 2.7.0 because the negotiated lane Pi consumes is unchanged", () => {
|
|
186
|
+
// Ground-truthed against the published v2.8.0 linux/amd64 binary from its
|
|
187
|
+
// signed release archive: capabilities/v2.5 still advertises protocol minor
|
|
188
|
+
// 5 with status/v7, consent/v3, and start/v4 schemas. riskEvidence and hint
|
|
189
|
+
// remain dark because neither is proven to reach Pi's negotiated START path.
|
|
190
|
+
const contract = NATIVE_CLI_CONTRACTS["2.8.0"] as Record<string, boolean>;
|
|
191
|
+
assert.equal(contract.riskEvidence, false);
|
|
192
|
+
assert.equal(contract.hint, false);
|
|
193
|
+
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.7.0"] as Record<string, boolean>);
|
|
194
|
+
});
|
|
195
|
+
|
|
170
196
|
test("no shipped version key was added beyond the pin bump", () => {
|
|
171
197
|
// Rows are promises to consumers, so a new key only ever appears in a
|
|
172
198
|
// dedicated commit alongside a pin bump, never as a side effect. v2.2.4 and
|
|
173
199
|
// v2.3.0 shipped upstream while Pi stayed on 2.2.3 and were never pinned,
|
|
174
200
|
// so they get no row: a row asserts ground truth measured against a binary
|
|
175
201
|
// Pi actually ran, and the table only has to be ascending, not gapless.
|
|
176
|
-
assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0"]);
|
|
202
|
+
assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0", "2.7.0", "2.8.0"]);
|
|
177
203
|
});
|