gentle-pi 2.3.0 → 2.5.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 +195 -11
- package/assets/agents/gentle-ai-worker.md +9 -0
- package/assets/agents/sdd-explore.md +1 -0
- package/assets/orchestrator-delegation.md +21 -10
- package/assets/orchestrator.md +8 -12
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
- package/docs/delegated-verification.md +25 -0
- package/docs/review-integration.md +1 -1
- package/docs/telemetry.md +38 -0
- package/extensions/ask-user-choice.ts +26 -20
- package/extensions/codegraph-tools.ts +94 -5
- package/extensions/gentle-agents.ts +588 -0
- package/extensions/gentle-ai.ts +1421 -143
- package/extensions/gentle-shell.ts +547 -0
- package/extensions/gentle-todo.ts +199 -0
- package/extensions/quiet-tools.ts +1 -1
- package/lib/agent-home.ts +8 -0
- package/lib/agents-config.ts +318 -0
- package/lib/agents-history.ts +80 -0
- package/lib/agents-protocol.ts +429 -0
- package/lib/agents-runner.ts +490 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view.ts +557 -0
- package/lib/agents-widget.ts +222 -0
- package/lib/gentle-ai-renderer.ts +142 -26
- 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 +103 -12
- package/lib/provider-contract-bundle.ts +88 -6
- package/lib/review-candidate-view-owner.ts +177 -0
- package/lib/review-candidate-view.ts +127 -35
- package/lib/review-consent-ui.ts +65 -0
- package/lib/review-host-relay.ts +146 -60
- package/lib/review-integration-v2.ts +92 -13
- package/lib/review-last-event-controller.ts +1 -0
- package/lib/review-relay-contract.ts +11 -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 +219 -0
- package/lib/sdd-preflight.ts +2 -2
- package/lib/shell-bar.ts +138 -0
- package/lib/shell-card.ts +136 -0
- package/lib/shell-changes-view.ts +205 -0
- package/lib/shell-changes.ts +210 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +119 -0
- package/lib/shell-todo.ts +280 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +151 -0
- package/package.json +4 -4
- package/runtime/native-review-cli.mjs +102 -11
- package/runtime/review-integration-v2.mjs +92 -13
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +152 -0
- package/scripts/build-runtime-modules.mjs +2 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/test-packed-runner.mjs +22 -0
- package/scripts/verify-package-files.mjs +18 -13
- package/skills/_shared/review-ledger-contract.md +9 -1
- package/skills/issue-creation/SKILL.md +53 -93
- package/tests/agents-config.test.ts +143 -0
- package/tests/agents-fake-child.ts +52 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-protocol.test.ts +153 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +402 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +274 -0
- package/tests/agents-widget.test.ts +111 -0
- package/tests/ask-user-choice.test.ts +157 -3
- package/tests/codegraph-tools.test.ts +110 -1
- package/tests/devbinary/native-review-parity.devtest.ts +108 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
- package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
- package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-agents.test.ts +741 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +65 -0
- package/tests/gentle-ai.test.ts +31 -14
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +527 -0
- package/tests/gentle-todo.test.ts +182 -0
- package/tests/issue-creation-skill.test.ts +103 -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 +33 -1
- package/tests/native-review-cli.test.ts +40 -0
- 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 +29 -22
- package/tests/orchestrator-budget.test.ts +71 -2
- package/tests/orchestrator-rdd-ownership.test.ts +10 -1
- package/tests/package-manifest.test.ts +134 -9
- package/tests/provider-contract-bundle.test.ts +76 -0
- package/tests/provider-contract-mirror.test.ts +19 -0
- package/tests/quiet-tool-rendering.test.ts +96 -37
- package/tests/rdd-aware-verification-contract.test.ts +216 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +408 -0
- package/tests/review-candidate-view.test.ts +452 -6
- package/tests/review-contract-prompt.test.ts +142 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +321 -4
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-routing.test.ts +229 -11
- package/tests/review-host-relay.test.ts +195 -7
- package/tests/review-integration-v2-forward.test.ts +47 -0
- package/tests/review-integration-v2.test.ts +112 -0
- package/tests/review-last-event-closure.test.ts +7 -2
- package/tests/review-ledger-contract.test.ts +1 -1
- package/tests/review-relay-contract.test.ts +26 -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 +608 -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 +126 -0
- package/tests/runtime-harness.mjs +1 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +118 -0
- package/tests/shell-changes-view.test.ts +146 -0
- package/tests/shell-changes.test.ts +182 -0
- package/tests/shell-prompt.test.ts +118 -0
- package/tests/shell-todo.test.ts +170 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/telemetry-trigger.test.ts +349 -0
- package/tests/writer-edit-surface-scope.test.ts +153 -17
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
- /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
|
@@ -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
|
+
});
|
|
@@ -150,11 +150,43 @@ test("2.5.0 repeats 2.5.0-rc.3 because the stable lane Pi consumes is unchanged"
|
|
|
150
150
|
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.5.0-rc.3"] as Record<string, boolean>);
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
+
test("2.6.0 repeats 2.5.0 because the negotiated lane Pi consumes is unchanged", () => {
|
|
154
|
+
// Ground-truthed against the published v2.6.0 binary installed from its
|
|
155
|
+
// signed archive: the v2 lane advertises capabilities/v2.5 and answers
|
|
156
|
+
// status/v7, consent/v3, and the `start/v4` continuation, all already
|
|
157
|
+
// decoded. `review mode status --json` and `review validate --gate
|
|
158
|
+
// pre-commit` were re-run against the binary and still answer
|
|
159
|
+
// `gentle-ai.rdd-mode-status/v1` and `gentle-ai.review-gate-result/v1`
|
|
160
|
+
// respectively. Declining the consent/v3 prompt during `review start`
|
|
161
|
+
// still surfaces `risk_evidence` only on that blocking envelope, not on
|
|
162
|
+
// the negotiated `start/v4` continuation, so riskEvidence and hint stay
|
|
163
|
+
// dark: still not proven to reach the negotiated START path Pi consumes.
|
|
164
|
+
const contract = NATIVE_CLI_CONTRACTS["2.6.0"] as Record<string, boolean>;
|
|
165
|
+
assert.equal(contract.riskEvidence, false);
|
|
166
|
+
assert.equal(contract.hint, false);
|
|
167
|
+
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.5.0"] as Record<string, boolean>);
|
|
168
|
+
});
|
|
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
|
+
|
|
153
185
|
test("no shipped version key was added beyond the pin bump", () => {
|
|
154
186
|
// Rows are promises to consumers, so a new key only ever appears in a
|
|
155
187
|
// dedicated commit alongside a pin bump, never as a side effect. v2.2.4 and
|
|
156
188
|
// v2.3.0 shipped upstream while Pi stayed on 2.2.3 and were never pinned,
|
|
157
189
|
// so they get no row: a row asserts ground truth measured against a binary
|
|
158
190
|
// Pi actually ran, and the table only has to be ascending, not gapless.
|
|
159
|
-
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"]);
|
|
191
|
+
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"]);
|
|
160
192
|
});
|
|
@@ -609,6 +609,46 @@ test("current native process boundary preserves caps, timeout classes, and sanit
|
|
|
609
609
|
);
|
|
610
610
|
});
|
|
611
611
|
|
|
612
|
+
test("authority inventory accepts compact discarded work and historical entry statuses", async () => {
|
|
613
|
+
const repository = process.cwd();
|
|
614
|
+
const discardedWork = { captured_lens_results: [], findings_present: false };
|
|
615
|
+
const statuses = ["incomplete-store-entry", "historical-pre-receipt", "invalidated"] as const;
|
|
616
|
+
const body = {
|
|
617
|
+
schema: "gentle-ai.review-authority-status/v1",
|
|
618
|
+
operation: "review/status",
|
|
619
|
+
repository,
|
|
620
|
+
complete: true,
|
|
621
|
+
authoritative: true,
|
|
622
|
+
status: "active",
|
|
623
|
+
entries: statuses.map((status) => ({
|
|
624
|
+
version: "compact-v2",
|
|
625
|
+
lineage_id: `review-${status}`,
|
|
626
|
+
path: `${repository}/.git/gentle-ai/${status}`,
|
|
627
|
+
status,
|
|
628
|
+
discarded_work: discardedWork,
|
|
629
|
+
problems: [],
|
|
630
|
+
})),
|
|
631
|
+
locks: [],
|
|
632
|
+
diagnostics: [],
|
|
633
|
+
};
|
|
634
|
+
const decoded = await client(queuedAdapter([{ stdout: JSON.stringify(body) }]).adapter).reviewStatus({ cwd: repository });
|
|
635
|
+
assert.deepEqual(decoded.entries.map((entry) => ({ status: entry.status, discardedWork: entry.discardedWork })), statuses.map((status) => ({
|
|
636
|
+
status,
|
|
637
|
+
discardedWork: { capturedLensResults: [], findingsPresent: false },
|
|
638
|
+
})));
|
|
639
|
+
|
|
640
|
+
for (const entry of [
|
|
641
|
+
{ ...body.entries[0], discarded_work: { captured_lens_results: "not-an-array", findings_present: false } },
|
|
642
|
+
{ ...body.entries[0], discarded_work: { ...discardedWork, unexpected: true } },
|
|
643
|
+
{ ...body.entries[0], status: "future-status" },
|
|
644
|
+
]) {
|
|
645
|
+
await assert.rejects(
|
|
646
|
+
() => client(queuedAdapter([{ stdout: JSON.stringify({ ...body, entries: [entry] }) }]).adapter).reviewStatus({ cwd: repository }),
|
|
647
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE,
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
612
652
|
test("current review STATUS retains compact snapshot and released-lock wire fields", async (t) => {
|
|
613
653
|
const repository = process.cwd();
|
|
614
654
|
const snapshotIdentity = `sha256:${"d".repeat(64)}`;
|
|
@@ -176,6 +176,97 @@ test("consent follow-up executes the provider-named invocation exactly once and
|
|
|
176
176
|
|
|
177
177
|
// A binding mismatch is decided entirely inside Pi, before the provider is
|
|
178
178
|
// launched, so it must not be reported as a provider failure (issue #247).
|
|
179
|
+
test("consent answer preserves quoted Windows provider cwd tokens and launches the provider exactly once", async () => {
|
|
180
|
+
const windowsCwd = "C:\\Users\\x\\repo with spaces";
|
|
181
|
+
const gitBashCwd = "/c/Users/x/repo with spaces";
|
|
182
|
+
const cwdPairs = process.platform === "win32"
|
|
183
|
+
? [{ providerCwd: windowsCwd, requestedCwd: gitBashCwd }, { providerCwd: gitBashCwd, requestedCwd: windowsCwd }, { providerCwd: gitBashCwd, requestedCwd: gitBashCwd }, { providerCwd: windowsCwd, requestedCwd: windowsCwd }]
|
|
184
|
+
: [{ providerCwd: windowsCwd, requestedCwd: windowsCwd }];
|
|
185
|
+
for (const { providerCwd, requestedCwd } of cwdPairs) {
|
|
186
|
+
const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV2(fixture<Record<string, unknown>>("consent.fixture.json"));
|
|
187
|
+
const lineage = "windows-cwd-lineage";
|
|
188
|
+
for (const choice of decoded.choices) {
|
|
189
|
+
choice.invocation = choice.invocation
|
|
190
|
+
.replace("--cwd /repo", `--cwd "${providerCwd}"`)
|
|
191
|
+
.replace("review-consent-fixture", lineage);
|
|
192
|
+
}
|
|
193
|
+
const started = JSON.parse(JSON.stringify(fixture<Record<string, unknown>>("start.fixture.json")).replaceAll("review-start-fixture", lineage)) as Record<string, unknown>;
|
|
194
|
+
const expected = [
|
|
195
|
+
"review", "start", "--contract", "gentle-ai.review-integration/v2", "--cwd", providerCwd,
|
|
196
|
+
"--target", decoded.targetIdentity, "--projection", "workspace", "--lineage", lineage, "--consent", "granted",
|
|
197
|
+
];
|
|
198
|
+
for (const createClient of [client, runtimeClient]) {
|
|
199
|
+
const queue = queuedAdapter([capabilities(), structuredClone(started)]);
|
|
200
|
+
const result = await createClient(queue.adapter).answerConsent({ cwd: requestedCwd, consent: decoded, answer: "granted" });
|
|
201
|
+
assert.equal(result.kind, "started");
|
|
202
|
+
assert.deepEqual(queue.calls, [expected]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("consent answer preserves quoted UNC and drive-root cwd tokens", async () => {
|
|
208
|
+
for (const providerCwd of ["\\\\server\\share", "C:\\"]) {
|
|
209
|
+
const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV2(fixture<Record<string, unknown>>("consent.fixture.json"));
|
|
210
|
+
const lineage = "windows-root-lineage";
|
|
211
|
+
for (const choice of decoded.choices) {
|
|
212
|
+
choice.invocation = choice.invocation
|
|
213
|
+
.replace("--cwd /repo", `--cwd "${providerCwd}"`)
|
|
214
|
+
.replace("review-consent-fixture", lineage);
|
|
215
|
+
}
|
|
216
|
+
const started = JSON.parse(JSON.stringify(fixture<Record<string, unknown>>("start.fixture.json")).replaceAll("review-start-fixture", lineage)) as Record<string, unknown>;
|
|
217
|
+
const expected = [
|
|
218
|
+
"review", "start", "--contract", "gentle-ai.review-integration/v2", "--cwd", providerCwd,
|
|
219
|
+
"--target", decoded.targetIdentity, "--projection", "workspace", "--lineage", lineage, "--consent", "granted",
|
|
220
|
+
];
|
|
221
|
+
for (const createClient of [client, runtimeClient]) {
|
|
222
|
+
const queue = queuedAdapter([capabilities(), structuredClone(started)]);
|
|
223
|
+
const result = await createClient(queue.adapter).answerConsent({ cwd: providerCwd, consent: decoded, answer: "granted" });
|
|
224
|
+
assert.equal(result.kind, "started");
|
|
225
|
+
assert.deepEqual(queue.calls, [expected]);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("unmatched consent invocation quotes reject before provider launch", async () => {
|
|
231
|
+
for (const quote of ["'", '"']) {
|
|
232
|
+
const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV2(fixture<Record<string, unknown>>("consent.fixture.json"));
|
|
233
|
+
const choice = decoded.choices.find((candidate) => candidate.answer === "granted") as { invocation: string };
|
|
234
|
+
choice.invocation = `${choice.invocation} ${quote}`;
|
|
235
|
+
for (const createClient of [client, runtimeClient]) {
|
|
236
|
+
const queue = queuedAdapter([]);
|
|
237
|
+
await assert.rejects(
|
|
238
|
+
() => createClient(queue.adapter).answerConsent({ cwd: "/repo", consent: decoded, answer: "granted" }),
|
|
239
|
+
(error: unknown) => error instanceof TypeError && error.message === "Native consent invocation has invalid quoting",
|
|
240
|
+
);
|
|
241
|
+
assert.deepEqual(queue.calls, []);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("different, duplicate, missing, and malformed consent cwd options reject before provider launch", async () => {
|
|
247
|
+
const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV2(fixture<Record<string, unknown>>("consent.fixture.json"));
|
|
248
|
+
const drifted = (replace: (invocation: string) => string): ReviewConsentV2 => {
|
|
249
|
+
const consent = structuredClone(decoded);
|
|
250
|
+
const choice = consent.choices.find((candidate) => candidate.answer === "granted") as { invocation: string };
|
|
251
|
+
choice.invocation = replace(choice.invocation);
|
|
252
|
+
return consent;
|
|
253
|
+
};
|
|
254
|
+
const cases = [
|
|
255
|
+
{ label: "different", reason: "consent-invocation-cwd-changed", consent: drifted((value) => value.replace("--cwd /repo", "--cwd /another-repo")) },
|
|
256
|
+
{ label: "duplicate", reason: "consent-invocation-option-invalid", consent: drifted((value) => `${value} --cwd /repo`) },
|
|
257
|
+
{ label: "missing", reason: "consent-invocation-option-invalid", consent: drifted((value) => value.replace("--cwd /repo ", "")) },
|
|
258
|
+
{ label: "malformed", reason: "consent-invocation-cwd-changed", consent: drifted((value) => value.replace("--cwd /repo", '--cwd ""')) },
|
|
259
|
+
] as const;
|
|
260
|
+
for (const scenario of cases) {
|
|
261
|
+
const queue = queuedAdapter([]);
|
|
262
|
+
await assert.rejects(
|
|
263
|
+
() => client(queue.adapter).answerConsent!({ cwd: "/repo", consent: scenario.consent, answer: "granted" }),
|
|
264
|
+
(error: unknown) => error instanceof NativeReviewConsentBindingError && error.reason === scenario.reason,
|
|
265
|
+
);
|
|
266
|
+
assert.deepEqual(queue.calls, [], `${scenario.label} cwd must not launch the provider`);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
179
270
|
test("a consent invocation binding mismatch is a typed pre-native error that never launches the provider", async () => {
|
|
180
271
|
const consent = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV2(fixture<Record<string, unknown>>("consent.fixture.json"));
|
|
181
272
|
const queue = queuedAdapter([]);
|
|
@@ -140,6 +140,8 @@ test("generated runtime decoder consumes the captured terminal closure directly"
|
|
|
140
140
|
|
|
141
141
|
test("registered gentle_review surfaces the package-pinned Pi transport refusal before native START for a safe internal symlink candidate", async (t) => {
|
|
142
142
|
await reviewEnabledHome(t);
|
|
143
|
+
const handshakeLessEnvironment = { ...process.env };
|
|
144
|
+
delete handshakeLessEnvironment.GENTLE_PI_REVIEW_RELAY_CONTRACT;
|
|
143
145
|
const workspace = await realpath(await mkdtemp(join(tmpdir(), "gentle-pi-v215-symlink-candidate-")));
|
|
144
146
|
const repository = join(workspace, "repository");
|
|
145
147
|
t.after(async () => {
|
|
@@ -174,11 +176,15 @@ test("registered gentle_review surfaces the package-pinned Pi transport refusal
|
|
|
174
176
|
// refusal happens before native START, regardless of candidate materialization.
|
|
175
177
|
const native = new NativeReviewCliV216(async (request) => {
|
|
176
178
|
if (request.arguments[0] === "review" && request.arguments[1] === "start") nativeStartReached = true;
|
|
177
|
-
const command = await run(binary, request.arguments, request.cwd, true);
|
|
179
|
+
const command = await run(binary, request.arguments, request.cwd, true, handshakeLessEnvironment);
|
|
178
180
|
return { ...command, signal: null, timedOut: false, outputLimitExceeded: false };
|
|
179
181
|
});
|
|
180
182
|
const tools = new Map<string, RegisteredController>();
|
|
181
|
-
|
|
183
|
+
// The extension declares the relay handshake in the session environment on
|
|
184
|
+
// load (gentle-pi#550). Keep registration isolated in a throwaway environment
|
|
185
|
+
// and spawn the pinned binary with an explicit handshake-less environment so
|
|
186
|
+
// ambient valid relay configuration cannot change this fixture's behavior.
|
|
187
|
+
createGentleAiExtension({ nativeReviewCli: native, candidateViews, processEnv: {} } as Parameters<typeof createGentleAiExtension>[0])({
|
|
182
188
|
on() {},
|
|
183
189
|
registerTool(definition: RegisteredController & { name: string }) { tools.set(definition.name, definition); },
|
|
184
190
|
registerCommand() {},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
@@ -183,7 +183,6 @@ interface ParityRuntimeOptions {
|
|
|
183
183
|
pendingReviewConsentRegistry?: PendingReviewConsentRegistry;
|
|
184
184
|
now?: () => number;
|
|
185
185
|
scheduleTimer?: (callback: () => void, delayMs: number) => { unref: () => void };
|
|
186
|
-
writeConsentLatch?: (cwd: string) => void;
|
|
187
186
|
}
|
|
188
187
|
|
|
189
188
|
function parityRuntime(nativeReviewCli: NativeReviewCli | null, options: ParityRuntimeOptions = {}): ParityRuntime {
|
|
@@ -197,7 +196,7 @@ function parityRuntime(nativeReviewCli: NativeReviewCli | null, options: ParityR
|
|
|
197
196
|
now: options.now,
|
|
198
197
|
scheduleTimer: options.scheduleTimer,
|
|
199
198
|
} as unknown as Parameters<typeof createGentleAiExtension>[0];
|
|
200
|
-
__testing.createGentleAiExtension(dependencies
|
|
199
|
+
__testing.createGentleAiExtension(dependencies)({
|
|
201
200
|
on(name: string, handler: RegisteredEvent) { events.set(name, handler); },
|
|
202
201
|
registerTool(definition: RegisteredControllerTool & { name: string }) { tools.set(definition.name, definition); },
|
|
203
202
|
registerCommand(name: string, definition: RegisteredCommand) { commands.set(name, definition); },
|
|
@@ -401,7 +400,7 @@ test("public gentle:review-mode handler reports current operations, global-off w
|
|
|
401
400
|
assert.deepEqual(unavailableNotices, [{ message: "Gentle AI review mode is not available with the currently negotiated native version.", type: "info" }]);
|
|
402
401
|
});
|
|
403
402
|
|
|
404
|
-
test("public consent relay
|
|
403
|
+
test("public consent relay allows any active session to resolve a live binding, is one-shot, and is candidate-scoped", async (t) => {
|
|
405
404
|
const cwd = repository(t);
|
|
406
405
|
const sharedRegistry = new PendingReviewConsentRegistry();
|
|
407
406
|
const fixture = consentNative(cwd);
|
|
@@ -411,14 +410,22 @@ test("public consent relay is session-bound, one-shot, and candidate-scoped", as
|
|
|
411
410
|
const sameSessionResult = await answerConsent(second, cwd, sameSession.consent_binding, "declined", "same-session");
|
|
412
411
|
assert.equal(sameSessionResult.outcome, "consent-declined-this-candidate");
|
|
413
412
|
|
|
413
|
+
// gentle-pi#455: a binding is answerable by whichever active Pi session
|
|
414
|
+
// presents it, not only the session whose START created it.
|
|
414
415
|
const blockedA = await beginConsent(first, cwd, "session-a");
|
|
415
416
|
assert.deepEqual(blockedA.consent, decodeReviewConsentV3(captured("consent-v3.captured.json")).raw);
|
|
416
|
-
const
|
|
417
|
-
assert.equal(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
417
|
+
const declinedFromOtherSession = await answerConsent(second, cwd, blockedA.consent_binding, "declined", "session-b");
|
|
418
|
+
assert.equal(declinedFromOtherSession.operation, "answer-consent");
|
|
419
|
+
assert.equal(declinedFromOtherSession.outcome, "consent-declined-this-candidate");
|
|
420
|
+
assert.deepEqual(fixture.answers, ["declined", "declined"]);
|
|
421
|
+
|
|
422
|
+
// Cross-session resolution still enforces single use: once answered, no
|
|
423
|
+
// session -- including the one whose START created it -- may answer it again.
|
|
424
|
+
const staleAfterCrossSessionAnswer = await answerConsent(first, cwd, blockedA.consent_binding, "declined", "session-a");
|
|
425
|
+
assert.equal(staleAfterCrossSessionAnswer.operation, "answer-consent");
|
|
426
|
+
assertStaleConsentBinding(staleAfterCrossSessionAnswer, blockedA.consent_binding, "consent-binding-already-consumed");
|
|
427
|
+
assert.deepEqual(fixture.answers, ["declined", "declined"]);
|
|
428
|
+
|
|
422
429
|
const blockedB = await beginConsent(second, cwd, "session-b");
|
|
423
430
|
const declined = await answerConsent(second, cwd, blockedB.consent_binding, "declined", "session-b");
|
|
424
431
|
assert.equal(declined.outcome, "consent-declined-this-candidate");
|
|
@@ -426,15 +433,19 @@ test("public consent relay is session-bound, one-shot, and candidate-scoped", as
|
|
|
426
433
|
const staleConsumed = await answerConsent(second, cwd, blockedB.consent_binding, "declined", "session-b");
|
|
427
434
|
assert.equal(staleConsumed.operation, "answer-consent");
|
|
428
435
|
assertStaleConsentBinding(staleConsumed, blockedB.consent_binding, "consent-binding-already-consumed");
|
|
429
|
-
assert.deepEqual(fixture.answers, ["declined", "declined"]);
|
|
436
|
+
assert.deepEqual(fixture.answers, ["declined", "declined", "declined"]);
|
|
430
437
|
|
|
438
|
+
// A binding still pending when its owning session shuts down is discarded
|
|
439
|
+
// and becomes unreachable to every session -- cross-session resolution
|
|
440
|
+
// included.
|
|
441
|
+
const blockedShutdown = await beginConsent(first, cwd, "session-a");
|
|
431
442
|
const shutdown = first.events.get("session_shutdown");
|
|
432
443
|
assert.ok(shutdown);
|
|
433
444
|
await shutdown!({}, context(cwd, "session-a"));
|
|
434
445
|
await shutdown!({}, context(cwd, "session-a"));
|
|
435
|
-
const staleShutdown = await answerConsent(
|
|
446
|
+
const staleShutdown = await answerConsent(second, cwd, blockedShutdown.consent_binding, "declined", "session-b");
|
|
436
447
|
assert.equal(staleShutdown.operation, "answer-consent");
|
|
437
|
-
assertStaleConsentBinding(staleShutdown,
|
|
448
|
+
assertStaleConsentBinding(staleShutdown, blockedShutdown.consent_binding, "consent-binding-unknown");
|
|
438
449
|
|
|
439
450
|
writeFileSync(join(cwd, "app.ts"), "export const value = 3;\n");
|
|
440
451
|
const nextCandidate = await beginConsent(second, cwd, "session-b");
|
|
@@ -450,7 +461,7 @@ test("public consent relay is session-bound, one-shot, and candidate-scoped", as
|
|
|
450
461
|
const staleModeCleared = await answerConsent(second, cwd, nextCandidate.consent_binding, "declined", "session-b");
|
|
451
462
|
assert.equal(staleModeCleared.operation, "answer-consent");
|
|
452
463
|
assertStaleConsentBinding(staleModeCleared, nextCandidate.consent_binding, "consent-binding-unknown");
|
|
453
|
-
assert.equal(fixture.starts.count,
|
|
464
|
+
assert.equal(fixture.starts.count, 5);
|
|
454
465
|
});
|
|
455
466
|
|
|
456
467
|
test("already-consumed local consent bindings reconcile exactly once with the native status transition", async (t) => {
|
|
@@ -783,18 +794,14 @@ test("native START maps only provider facts and omits absent evidence", async (t
|
|
|
783
794
|
assert.equal(rendered.next_transition, nextTransition);
|
|
784
795
|
});
|
|
785
796
|
|
|
786
|
-
test("consent completion
|
|
797
|
+
test("candidate consent completion writes no persistent asked latch", async (t) => {
|
|
787
798
|
const cwd = repository(t);
|
|
788
799
|
const fixture = consentNative(cwd);
|
|
789
|
-
const
|
|
790
|
-
const runtime = parityRuntime(fixture.native, {
|
|
791
|
-
writeConsentLatch: () => { throw new Error("injected latch failure"); },
|
|
792
|
-
});
|
|
800
|
+
const runtime = parityRuntime(fixture.native);
|
|
793
801
|
const blocked = await beginConsent(runtime, cwd);
|
|
794
802
|
const completed = await answerConsent(runtime, cwd, blocked.consent_binding, "granted");
|
|
795
803
|
assert.equal((completed.result as { lineage_id?: string }).lineage_id, "consent-lineage");
|
|
796
804
|
assert.deepEqual(fixture.answers, ["granted"]);
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
// result remains authoritative even when the local best-effort latch fails.
|
|
805
|
+
const commonDir = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd, encoding: "utf8" }).trim();
|
|
806
|
+
assert.equal(existsSync(join(commonDir, "gentle-pi", "review-consent", "asked.json")), false);
|
|
800
807
|
});
|