gentle-pi 2.4.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 +170 -12
- package/assets/agents/gentle-ai-worker.md +9 -0
- package/assets/orchestrator-delegation.md +19 -9
- package/assets/orchestrator.md +5 -5
- package/docs/delegated-verification.md +25 -0
- 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 +898 -79
- package/extensions/gentle-shell.ts +547 -0
- package/extensions/gentle-todo.ts +199 -0
- package/extensions/quiet-tools.ts +1 -1
- 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 +88 -12
- 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-integration-v2.ts +58 -8
- 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/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 +87 -11
- package/runtime/review-integration-v2.mjs +58 -8
- 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 +6 -2
- package/skills/_shared/review-ledger-contract.md +3 -1
- 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/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 +28 -12
- 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/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 +16 -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 +69 -0
- package/tests/orchestrator-rdd-ownership.test.ts +9 -0
- package/tests/package-manifest.test.ts +17 -6
- 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-candidate-view.test.ts +452 -6
- package/tests/review-contract-prompt.test.ts +3 -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 +25 -0
- package/tests/review-host-relay-routing.test.ts +20 -4
- package/tests/review-integration-v2.test.ts +112 -0
- package/tests/review-last-event-closure.test.ts +7 -2
- 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/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
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import fsp from "node:fs/promises";
|
|
6
|
+
import os, { tmpdir } from "node:os";
|
|
7
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
8
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
6
9
|
import test from "node:test";
|
|
7
10
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
11
|
import { __testing, createGentleAiExtension, PendingReviewConsentRegistry } from "../extensions/gentle-ai.ts";
|
|
@@ -95,6 +98,37 @@ function burnedAcknowledgementStatus(lineageId: string): ReviewStatusV3 {
|
|
|
95
98
|
return burned;
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
// gentle-pi#627: gentle-ai reports a stale managed-asset set as a typed stop
|
|
102
|
+
// carrying the exact `gentle-ai sync` invocation that resolves it.
|
|
103
|
+
function managedAssetsOutdatedStatus(lineageId: string): ReviewStatusV3 {
|
|
104
|
+
const stopped = status(lineageId, [], "approved");
|
|
105
|
+
stopped.nextTransition = { kind: "stop", reasonCode: "managed_assets_outdated", continuation: { operation: "sync", command: "gentle-ai sync --agent claude-code", agent: "claude-code", staleAssets: ["orchestration/claude-code.md"] } };
|
|
106
|
+
return stopped;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
test("STATUS renders the managed_assets_outdated continuation command as the actionable next step", async () => {
|
|
110
|
+
const lineageId = "managed-assets-outdated";
|
|
111
|
+
const native = { targetStatus: async () => managedAssetsOutdatedStatus(lineageId) } as unknown as NativeReviewCli;
|
|
112
|
+
const blocked = await __testing.executeReviewControllerOperation({ operation: "status", lineageId }, process.cwd(), native);
|
|
113
|
+
assert.equal(blocked.status, "blocked");
|
|
114
|
+
assert.equal(blocked.hint, "run gentle-ai sync --agent claude-code");
|
|
115
|
+
|
|
116
|
+
// an unknown stop reason code keeps rendering as a plain blocked result
|
|
117
|
+
const otherStopNative = { targetStatus: async () => burnedAcknowledgementStatus(lineageId) } as unknown as NativeReviewCli;
|
|
118
|
+
const plainBlocked = await __testing.executeReviewControllerOperation({ operation: "status", lineageId }, process.cwd(), otherStopNative);
|
|
119
|
+
assert.equal(plainBlocked.status, "blocked");
|
|
120
|
+
assert.equal(plainBlocked.hint, undefined);
|
|
121
|
+
|
|
122
|
+
// an older gentle-ai may report this same stop with no continuation; the
|
|
123
|
+
// renderer degrades to a plain blocked result instead of losing the status
|
|
124
|
+
const noContinuation = status(lineageId, [], "approved");
|
|
125
|
+
noContinuation.nextTransition = { kind: "stop", reasonCode: "managed_assets_outdated" };
|
|
126
|
+
const noContinuationNative = { targetStatus: async () => noContinuation } as unknown as NativeReviewCli;
|
|
127
|
+
const degraded = await __testing.executeReviewControllerOperation({ operation: "status", lineageId }, process.cwd(), noContinuationNative);
|
|
128
|
+
assert.equal(degraded.status, "blocked");
|
|
129
|
+
assert.equal(degraded.hint, undefined);
|
|
130
|
+
});
|
|
131
|
+
|
|
98
132
|
test("public acknowledgement relays one current provider vector and never replays after authority burn", async () => {
|
|
99
133
|
const lineageId = "acknowledge-approved";
|
|
100
134
|
const requests: Array<Record<string, unknown>> = [];
|
|
@@ -423,6 +457,72 @@ test("REPAIR retains frozen committed collect selectors and leaves workspace rou
|
|
|
423
457
|
assert.deepEqual(workspaceRequests, [{ cwd, lineageId: workspaceLineage }]);
|
|
424
458
|
});
|
|
425
459
|
|
|
460
|
+
test("selectorless STATUS resumes a retained committed correction lineage", async (t) => {
|
|
461
|
+
const candidateViews = new CandidateViewRegistry();
|
|
462
|
+
t.after(() => candidateViews.cleanupAll());
|
|
463
|
+
const cwd = repository(t);
|
|
464
|
+
const lineageId = "retained-committed-correction";
|
|
465
|
+
const view = candidateViews.create({
|
|
466
|
+
contributorRoot: cwd,
|
|
467
|
+
baseRef: execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim(),
|
|
468
|
+
committedOnly: true,
|
|
469
|
+
});
|
|
470
|
+
candidateViews.retain(view.token, lineageId);
|
|
471
|
+
const frozenTarget = candidateViews.resolveProjection(lineageId, cwd);
|
|
472
|
+
const requests: Array<Record<string, unknown>> = [];
|
|
473
|
+
const selections = new Map();
|
|
474
|
+
let captures = 0;
|
|
475
|
+
const native = {
|
|
476
|
+
targetStatus: async (request: Record<string, unknown>) => {
|
|
477
|
+
requests.push(request);
|
|
478
|
+
return status(lineageId, [correctionPlanInput(lineageId)], "correction_required");
|
|
479
|
+
},
|
|
480
|
+
captureCorrectionPlan: async () => {
|
|
481
|
+
captures += 1;
|
|
482
|
+
return { schema: "gentle-ai.review-last-event-closure/v1", operation: "review.capture-correction-plan", lineageId, state: "correction_required", storeRevision: SHA };
|
|
483
|
+
},
|
|
484
|
+
} as unknown as NativeReviewCli;
|
|
485
|
+
|
|
486
|
+
const listed = await __testing.executeReviewControllerOperation({ operation: "status", lineageId }, cwd, native, undefined, candidateViews, undefined, selections);
|
|
487
|
+
const captured = await __testing.executeReviewCaptureOperation({ lineageId, collectBinding: bindingOf(listed), correctionLines: 1 }, cwd, native, undefined, candidateViews, selections, true);
|
|
488
|
+
|
|
489
|
+
assert.equal(captured.outcome, "native-last-event-closure");
|
|
490
|
+
assert.equal(captures, 1);
|
|
491
|
+
assert.deepEqual(requests, Array.from({ length: 2 }, () => ({ cwd, lineageId, agent: "pi", baseRef: frozenTarget.baseCommit, committedOnly: true })));
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("selectorless STATUS uses a retained committed selector only at its retained workspace", async (t) => {
|
|
495
|
+
const candidateViews = new CandidateViewRegistry();
|
|
496
|
+
t.after(() => candidateViews.cleanupAll());
|
|
497
|
+
const retainedRoot = repository(t);
|
|
498
|
+
const otherRoot = repository(t);
|
|
499
|
+
const lineageId = "retained-selector-boundary";
|
|
500
|
+
const view = candidateViews.create({
|
|
501
|
+
contributorRoot: retainedRoot,
|
|
502
|
+
baseRef: execFileSync("git", ["rev-parse", "HEAD"], { cwd: retainedRoot, encoding: "utf8" }).trim(),
|
|
503
|
+
committedOnly: true,
|
|
504
|
+
});
|
|
505
|
+
candidateViews.retain(view.token, lineageId);
|
|
506
|
+
const frozenTarget = candidateViews.resolveProjection(lineageId, retainedRoot);
|
|
507
|
+
const requests: Array<Record<string, unknown>> = [];
|
|
508
|
+
const native = {
|
|
509
|
+
targetStatus: async (request: Record<string, unknown>) => {
|
|
510
|
+
requests.push(request);
|
|
511
|
+
return status(String(request.lineageId ?? "unretained"));
|
|
512
|
+
},
|
|
513
|
+
} as unknown as NativeReviewCli;
|
|
514
|
+
|
|
515
|
+
await __testing.executeReviewControllerOperation({ operation: "status", lineageId, input: JSON.stringify({ baseRef: "explicit-base", committedOnly: true }) }, retainedRoot, native, undefined, candidateViews);
|
|
516
|
+
await __testing.executeReviewControllerOperation({ operation: "status", lineageId: "unretained" }, retainedRoot, native, undefined, candidateViews);
|
|
517
|
+
await __testing.executeReviewControllerOperation({ operation: "status", lineageId }, otherRoot, native, undefined, candidateViews);
|
|
518
|
+
|
|
519
|
+
assert.deepEqual(requests, [
|
|
520
|
+
{ cwd: retainedRoot, lineageId, agent: "pi", baseRef: "explicit-base", committedOnly: true },
|
|
521
|
+
{ cwd: retainedRoot, lineageId: "unretained", agent: "pi" },
|
|
522
|
+
{ cwd: retainedRoot, lineageId, agent: "pi", baseRef: frozenTarget.baseCommit, committedOnly: true },
|
|
523
|
+
]);
|
|
524
|
+
});
|
|
525
|
+
|
|
426
526
|
test("STATUS preserves retained intended-untracked selection through selectorless same-lineage replacement collection", async () => {
|
|
427
527
|
const cwd = process.cwd();
|
|
428
528
|
const lineageId = "selectorless-replacement";
|
|
@@ -638,6 +738,94 @@ function repository(t: test.TestContext): string {
|
|
|
638
738
|
return cwd;
|
|
639
739
|
}
|
|
640
740
|
|
|
741
|
+
test("candidate lifecycle sweeps startup and cleans every shutdown including reload", async (t) => {
|
|
742
|
+
const cwd = repository(t);
|
|
743
|
+
for (const key of ["HOME", "GENTLE_PI_AGENT_HOME", "PI_CODING_AGENT_DIR", "GENTLE_PI_CONFIG_HOME", "GENTLE_PI_GENTLE_AI_DEV_BINARY"]) {
|
|
744
|
+
const previous = process.env[key];
|
|
745
|
+
process.env[key] = key === "GENTLE_PI_GENTLE_AI_DEV_BINARY" ? "" : join(cwd, key);
|
|
746
|
+
if (process.env[key]) mkdirSync(process.env[key]!, { recursive: true });
|
|
747
|
+
t.after(() => { if (previous === undefined) delete process.env[key]; else process.env[key] = previous; });
|
|
748
|
+
}
|
|
749
|
+
t.mock.method(os, "homedir", () => join(cwd, "HOME"));
|
|
750
|
+
const homeAgent = join(cwd, "HOME", ".agents", "isolation-probe.md");
|
|
751
|
+
mkdirSync(dirname(homeAgent), { recursive: true });
|
|
752
|
+
writeFileSync(homeAgent, "---\nname: isolation-probe\ndescription: Fixture\n---\n");
|
|
753
|
+
writeFileSync(join(cwd, "GENTLE_PI_CONFIG_HOME", "models.json"), JSON.stringify({ "isolation-probe": { model: "fixture/model" } }));
|
|
754
|
+
t.diagnostic(`startup routing: cwd=${cwd}; HOME, GENTLE_PI_AGENT_HOME, PI_CODING_AGENT_DIR, GENTLE_PI_CONFIG_HOME are same-named children; dev-binary override disabled`);
|
|
755
|
+
const calls: string[] = [];
|
|
756
|
+
const destinations: string[] = [];
|
|
757
|
+
const violations: string[] = [];
|
|
758
|
+
const discovery: string[] = [];
|
|
759
|
+
const assets = resolve("assets");
|
|
760
|
+
const inside = (root: string, path: string) => path === root || path.startsWith(`${root}${sep}`);
|
|
761
|
+
const originalExists = fs.existsSync;
|
|
762
|
+
const originalRealpath = fs.realpathSync;
|
|
763
|
+
// Install guards BEFORE startup, including RED: a swallowed startup error
|
|
764
|
+
// must never hide an attempted external write or read real-home contents.
|
|
765
|
+
for (const [module, names] of [
|
|
766
|
+
[fs, ["existsSync", "lstatSync", "readdirSync", "readFileSync", "mkdirSync", "writeFileSync", "rmSync"]],
|
|
767
|
+
[fsp, ["access", "readdir", "readFile", "mkdir", "writeFile"]],
|
|
768
|
+
] as const) {
|
|
769
|
+
for (const name of names) {
|
|
770
|
+
const original = (module as any)[name];
|
|
771
|
+
t.mock.method(module as any, name, (value: string, ...args: unknown[]) => {
|
|
772
|
+
const path = resolve(value);
|
|
773
|
+
const writes = /^(mkdir|writeFile|rm)/.test(name);
|
|
774
|
+
const allowed = inside(cwd, path) || (!writes && inside(assets, path));
|
|
775
|
+
if (!allowed && name === "existsSync") return false;
|
|
776
|
+
if (!allowed && ["access", "readdir"].includes(name)) return Promise.reject(Object.assign(new Error("fixture discovery boundary"), { code: "ENOENT" }));
|
|
777
|
+
if (!allowed) { violations.push(`${name}:${path}`); throw new Error("fixture filesystem boundary"); }
|
|
778
|
+
if (writes) {
|
|
779
|
+
let ancestor = path;
|
|
780
|
+
while (!originalExists(ancestor)) ancestor = dirname(ancestor);
|
|
781
|
+
assert.ok(inside(cwd, originalRealpath(ancestor)), "write ancestor escaped fixture");
|
|
782
|
+
destinations.push(path);
|
|
783
|
+
}
|
|
784
|
+
if (name.startsWith("readdir")) discovery.push(path);
|
|
785
|
+
return original(value, ...args);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
syncBuiltinESMExports();
|
|
790
|
+
// Prove the guard refuses an escape without performing the attempted write.
|
|
791
|
+
const escape = join(cwd, "..", "startup-isolation-escape");
|
|
792
|
+
assert.throws(() => fs.writeFileSync(escape, "blocked"), /fixture filesystem boundary/);
|
|
793
|
+
assert.deepEqual(violations, [`writeFileSync:${escape}`]);
|
|
794
|
+
violations.length = 0;
|
|
795
|
+
const hooks = new Map<string, Array<(event: unknown, ctx: ExtensionContext) => unknown>>();
|
|
796
|
+
const registry = new CandidateViewRegistry();
|
|
797
|
+
t.mock.method(registry, "sweepOrphans", (root: string) => { assert.equal(root, cwd); calls.push("sweep"); });
|
|
798
|
+
t.mock.method(registry, "cleanupAll", () => { calls.push("cleanup"); });
|
|
799
|
+
createGentleAiExtension({ nativeReviewCli: null, candidateViews: registry, processEnv: {} })({
|
|
800
|
+
on(name: string, handler: (event: unknown, ctx: ExtensionContext) => unknown) {
|
|
801
|
+
hooks.set(name, [...(hooks.get(name) ?? []), handler]);
|
|
802
|
+
},
|
|
803
|
+
registerTool() {}, registerCommand() {},
|
|
804
|
+
} as unknown as ExtensionAPI);
|
|
805
|
+
try {
|
|
806
|
+
assert.equal(hooks.get("session_start")?.length, 1);
|
|
807
|
+
await hooks.get("session_start")![0]!({ reason: "startup" }, reviewContext(cwd));
|
|
808
|
+
assert.deepEqual(violations, []);
|
|
809
|
+
assert.ok(destinations.length > 0, "startup must actually write fixture assets");
|
|
810
|
+
assert.ok(discovery.includes(join(cwd, "HOME", ".agents")), "home agent discovery must use the fixture");
|
|
811
|
+
assert.equal(existsSync(join(cwd, "GENTLE_PI_AGENT_HOME", "gentle-ai", "managed-assets.json")), true);
|
|
812
|
+
assert.match(readFileSync(homeAgent, "utf8"), /model: fixture\/model/);
|
|
813
|
+
assert.ok(destinations.includes(homeAgent), "the discovered fixture agent must actually receive routing");
|
|
814
|
+
assert.ok(destinations.includes(join(cwd, "GENTLE_PI_AGENT_HOME", "subagents.json")));
|
|
815
|
+
t.diagnostic(`confined startup: ${destinations.length} filesystem mutations; fixture home-agent routing and managed-assets manifest verified; external violations=0`);
|
|
816
|
+
assert.deepEqual(calls, ["sweep"]);
|
|
817
|
+
} finally {
|
|
818
|
+
// Restore only mocks before fixture teardown; never clean external paths.
|
|
819
|
+
t.mock.restoreAll();
|
|
820
|
+
syncBuiltinESMExports();
|
|
821
|
+
}
|
|
822
|
+
t.mock.method(registry, "cleanupAll", () => { calls.push("cleanup"); });
|
|
823
|
+
for (const reason of ["quit", "reload", "new", "resume", "fork"]) {
|
|
824
|
+
for (const hook of hooks.get("session_shutdown")!) await hook({ reason }, reviewContext(cwd));
|
|
825
|
+
}
|
|
826
|
+
assert.deepEqual(calls, ["sweep", ...Array(5).fill("cleanup")]);
|
|
827
|
+
});
|
|
828
|
+
|
|
641
829
|
interface RegisteredControllerTool {
|
|
642
830
|
execute: (toolCallId: string, params: unknown, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ details?: unknown }>;
|
|
643
831
|
}
|
|
@@ -722,7 +910,7 @@ test("ordinary START binds the native workspace candidate and returns the native
|
|
|
722
910
|
assert.equal(startCalls, 1);
|
|
723
911
|
});
|
|
724
912
|
|
|
725
|
-
test(
|
|
913
|
+
for (const statusSchema of ["gentle-ai.review-integration.status/v6", "gentle-ai.review-integration.status/v7"]) test(`pre-lineage intended-untracked selection revalidates and starts at an explicit workspace root (${statusSchema})`, async (t) => {
|
|
726
914
|
const cwd = realpathSync(repository(t)), sessionCwd = repository(t), eligible = "selected.md";
|
|
727
915
|
writeFileSync(join(cwd, eligible), "selected\n");
|
|
728
916
|
const initialTarget = startStatus(cwd), target = startStatus(cwd, undefined, [eligible]);
|
|
@@ -735,7 +923,7 @@ test("pre-lineage intended-untracked selection revalidates and starts at an expl
|
|
|
735
923
|
],
|
|
736
924
|
submission: { operationToken: "status", argumentTokens: ["--contract=gentle-ai.review-integration/v2", "--next-transition=true", "--agent=pi", "--projection=workspace", "--intended-untracked-selection={{value}}"], values: [{ slot: "intended_untracked_selection", domain: "schema_bound_json", schema: "gentle-ai.review-intended-untracked-selection/v1", substitutionLocation: 4 }] },
|
|
737
925
|
};
|
|
738
|
-
const initial = { ...initialTarget, nextTransition: { kind: "collect", reasonCode: "intended_untracked_selection_required", collect: { inputs: [selection] } }, raw: { schema:
|
|
926
|
+
const initial = { ...initialTarget, nextTransition: { kind: "collect", reasonCode: "intended_untracked_selection_required", collect: { inputs: [selection] } }, raw: { schema: statusSchema } } as ReviewStatusV3;
|
|
739
927
|
const requests: Array<Record<string, unknown>> = [], starts: Array<Record<string, unknown>> = [], retained = new Map();
|
|
740
928
|
const native = {
|
|
741
929
|
reviewMode: async () => ({ operation: "status", scope: "clone", status: { global: "on", cloneLocal: "on", effective: "on", source: "clone_local" } }),
|
|
@@ -778,6 +966,22 @@ test("ordinary START relays native consent without authoring or advancing it", a
|
|
|
778
966
|
assert.equal(result.mutation_outcome, "none");
|
|
779
967
|
});
|
|
780
968
|
|
|
969
|
+
test("shutdown preserves failed consent candidates without interrupting session teardown", async (t) => {
|
|
970
|
+
const cwd = repository(t);
|
|
971
|
+
const consent = decodeReviewConsentV3(JSON.parse(readFileSync(join(process.cwd(), "tests", "fixtures", "devbinary", "consent-v3.captured.json"), "utf8")));
|
|
972
|
+
const native = {
|
|
973
|
+
targetStatus: async () => startStatus(cwd),
|
|
974
|
+
start: async () => { throw new NativeReviewConsentRequiredError(consent); },
|
|
975
|
+
} as unknown as NativeReviewCli;
|
|
976
|
+
const registry = new CandidateViewRegistry();
|
|
977
|
+
const runtime = reviewRuntime(native, registry);
|
|
978
|
+
const ctx = reviewContext(cwd);
|
|
979
|
+
const result = await runtime.controller.execute("pending", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, ctx);
|
|
980
|
+
assert.equal((result.details as { outcome: string }).outcome, "native-review-consent-required");
|
|
981
|
+
t.mock.method(registry, "cleanup", () => { throw new Error("fixture unsafe ownership"); });
|
|
982
|
+
assert.doesNotThrow(() => runtime.sessionShutdown({ reason: "reload" }, ctx));
|
|
983
|
+
});
|
|
984
|
+
|
|
781
985
|
test("ordinary START obeys the review-mode kill switch before target STATUS", async () => {
|
|
782
986
|
let targetCalls = 0;
|
|
783
987
|
const native = {
|
|
@@ -1220,6 +1424,119 @@ test("ordinary START refuses a target projection that no longer matches the froz
|
|
|
1220
1424
|
assert.equal(startCalls, 0);
|
|
1221
1425
|
});
|
|
1222
1426
|
|
|
1427
|
+
// gentle-pi#455: a consent binding one active Pi session's START created must
|
|
1428
|
+
// be answerable from another active session presenting the same opaque
|
|
1429
|
+
// binding id -- consent bindings are not partitioned by which session's
|
|
1430
|
+
// START created them, only by the repository and candidate they are scoped to.
|
|
1431
|
+
test("answer-consent resolves a valid binding presented by a different active Pi session", async (t) => {
|
|
1432
|
+
const cwd = repository(t);
|
|
1433
|
+
const consent = decodeReviewConsentV3(JSON.parse(readFileSync(join(process.cwd(), "tests", "fixtures", "devbinary", "consent-v3.captured.json"), "utf8")));
|
|
1434
|
+
const registry = new PendingReviewConsentRegistry();
|
|
1435
|
+
const sessionA = Symbol("session-a"), sessionB = Symbol("session-b");
|
|
1436
|
+
let answerCalls = 0;
|
|
1437
|
+
const native = {
|
|
1438
|
+
targetStatus: async () => startStatus(cwd),
|
|
1439
|
+
start: async () => { throw new NativeReviewConsentRequiredError(consent); },
|
|
1440
|
+
answerConsent: async () => {
|
|
1441
|
+
answerCalls += 1;
|
|
1442
|
+
return { kind: "granted", start: { lineageId: "cross-session", state: "reviewing", riskLevel: "low", selectedLenses: [], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false, riskReasons: [] } };
|
|
1443
|
+
},
|
|
1444
|
+
} as unknown as NativeReviewCli;
|
|
1445
|
+
const started = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwd, native, undefined, undefined, undefined, undefined, registry, sessionA);
|
|
1446
|
+
assert.equal(typeof started.consent_binding, "string", JSON.stringify(started));
|
|
1447
|
+
const answered = await __testing.executeReviewControllerOperation({ operation: "answer-consent", input: JSON.stringify({ consentBinding: started.consent_binding, answer: "granted" }) }, cwd, native, undefined, undefined, undefined, undefined, registry, sessionB);
|
|
1448
|
+
assert.equal(answerCalls, 1, JSON.stringify(answered));
|
|
1449
|
+
assert.equal((answered.result as { lineage_id?: string } | undefined)?.lineage_id, "cross-session", JSON.stringify(answered));
|
|
1450
|
+
assert.notEqual(answered.status, "blocked", JSON.stringify(answered));
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
// gentle-pi#455: cross-session resolution must still enforce single use --
|
|
1454
|
+
// once a binding is answered by any session, no session (including the one
|
|
1455
|
+
// whose START created it) may answer it again.
|
|
1456
|
+
test("a consumed consent binding cannot be answered a second time from any session", async (t) => {
|
|
1457
|
+
const cwd = repository(t);
|
|
1458
|
+
const consent = decodeReviewConsentV3(JSON.parse(readFileSync(join(process.cwd(), "tests", "fixtures", "devbinary", "consent-v3.captured.json"), "utf8")));
|
|
1459
|
+
const registry = new PendingReviewConsentRegistry();
|
|
1460
|
+
const sessionA = Symbol("session-a"), sessionB = Symbol("session-b");
|
|
1461
|
+
let answerCalls = 0;
|
|
1462
|
+
const native = {
|
|
1463
|
+
targetStatus: async () => startStatus(cwd),
|
|
1464
|
+
start: async () => { throw new NativeReviewConsentRequiredError(consent); },
|
|
1465
|
+
answerConsent: async () => {
|
|
1466
|
+
answerCalls += 1;
|
|
1467
|
+
return { kind: "granted", start: { lineageId: "single-use", state: "reviewing", riskLevel: "low", selectedLenses: [], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false, riskReasons: [] } };
|
|
1468
|
+
},
|
|
1469
|
+
} as unknown as NativeReviewCli;
|
|
1470
|
+
const started = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwd, native, undefined, undefined, undefined, undefined, registry, sessionA);
|
|
1471
|
+
const binding = started.consent_binding as string;
|
|
1472
|
+
const first = await __testing.executeReviewControllerOperation({ operation: "answer-consent", input: JSON.stringify({ consentBinding: binding, answer: "granted" }) }, cwd, native, undefined, undefined, undefined, undefined, registry, sessionB);
|
|
1473
|
+
assert.equal(answerCalls, 1, JSON.stringify(first));
|
|
1474
|
+
const replay = await __testing.executeReviewControllerOperation({ operation: "answer-consent", input: JSON.stringify({ consentBinding: binding, answer: "granted" }) }, cwd, native, undefined, undefined, undefined, undefined, registry, sessionA);
|
|
1475
|
+
assert.equal(answerCalls, 1, JSON.stringify(replay));
|
|
1476
|
+
assert.equal(replay.status, "blocked", JSON.stringify(replay));
|
|
1477
|
+
assert.equal(replay.outcome, "consent-binding-stale", JSON.stringify(replay));
|
|
1478
|
+
assert.equal((replay.diagnostics as { code?: string } | undefined)?.code, "consent-binding-already-consumed", JSON.stringify(replay));
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
// gentle-pi#455 correction: cross-session resolution looks a binding up by
|
|
1482
|
+
// its opaque id alone, so it must independently refuse an answer presented
|
|
1483
|
+
// from a different repository than the one its owning START minted it for,
|
|
1484
|
+
// without ever running the mode gate or native answerConsent, and without
|
|
1485
|
+
// consuming the binding -- leaving it answerable from the right repository.
|
|
1486
|
+
test("answer-consent refuses a binding presented from a different repository than the one its START minted, and leaves it answerable from the right one", async (t) => {
|
|
1487
|
+
const cwdA = repository(t), cwdB = repository(t);
|
|
1488
|
+
const consent = decodeReviewConsentV3(JSON.parse(readFileSync(join(process.cwd(), "tests", "fixtures", "devbinary", "consent-v3.captured.json"), "utf8")));
|
|
1489
|
+
const registry = new PendingReviewConsentRegistry();
|
|
1490
|
+
const sessionA = Symbol("session-a"), sessionB = Symbol("session-b");
|
|
1491
|
+
let answerCalls = 0;
|
|
1492
|
+
const native = {
|
|
1493
|
+
targetStatus: async () => startStatus(cwdA),
|
|
1494
|
+
start: async () => { throw new NativeReviewConsentRequiredError(consent); },
|
|
1495
|
+
answerConsent: async () => {
|
|
1496
|
+
answerCalls += 1;
|
|
1497
|
+
return { kind: "granted", start: { lineageId: "right-repo", state: "reviewing", riskLevel: "low", selectedLenses: [], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false, riskReasons: [] } };
|
|
1498
|
+
},
|
|
1499
|
+
} as unknown as NativeReviewCli;
|
|
1500
|
+
const started = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwdA, native, undefined, undefined, undefined, undefined, registry, sessionA);
|
|
1501
|
+
const binding = started.consent_binding as string;
|
|
1502
|
+
const wrongRepo = await __testing.executeReviewControllerOperation({ operation: "answer-consent", input: JSON.stringify({ consentBinding: binding, answer: "granted" }) }, cwdB, native, undefined, undefined, undefined, undefined, registry, sessionB);
|
|
1503
|
+
assert.equal(answerCalls, 0, JSON.stringify(wrongRepo));
|
|
1504
|
+
assert.equal(wrongRepo.status, "blocked", JSON.stringify(wrongRepo));
|
|
1505
|
+
assert.equal(wrongRepo.outcome, "consent-binding-repository-mismatch", JSON.stringify(wrongRepo));
|
|
1506
|
+
assert.equal((wrongRepo.diagnostics as { code?: string } | undefined)?.code, "consent-binding-repository-mismatch", JSON.stringify(wrongRepo));
|
|
1507
|
+
assert.equal(wrongRepo.mutation_performed, false, JSON.stringify(wrongRepo));
|
|
1508
|
+
const rightRepo = await __testing.executeReviewControllerOperation({ operation: "answer-consent", input: JSON.stringify({ consentBinding: binding, answer: "granted" }) }, cwdA, native, undefined, undefined, undefined, undefined, registry, sessionA);
|
|
1509
|
+
assert.equal(answerCalls, 1, JSON.stringify(rightRepo));
|
|
1510
|
+
assert.equal((rightRepo.result as { lineage_id?: string } | undefined)?.lineage_id, "right-repo", JSON.stringify(rightRepo));
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
// gentle-pi#323: within the live consent TTL window, a content-independent
|
|
1514
|
+
// replay key let a second ordinary START reuse the first START's still-live
|
|
1515
|
+
// (never lineage-bound) frozen candidate view even though the live candidate
|
|
1516
|
+
// content changed underneath it, and then dead-ended at
|
|
1517
|
+
// candidate-target-projection-drift instead of minting a fresh view and a
|
|
1518
|
+
// fresh consent envelope. An intended-untracked selection is retained across
|
|
1519
|
+
// both calls so the pre-existing empty-untracked drift-recovery branch does
|
|
1520
|
+
// not mask the general content-change gap this covers.
|
|
1521
|
+
test("ordinary START mints a fresh candidate view when candidate content changes within the live consent TTL window", async (t) => {
|
|
1522
|
+
const candidateViews = new CandidateViewRegistry();
|
|
1523
|
+
t.after(() => candidateViews.cleanupAll());
|
|
1524
|
+
const cwd = repository(t);
|
|
1525
|
+
writeFileSync(join(cwd, "extra.md"), "kept\n");
|
|
1526
|
+
const consent = decodeReviewConsentV3(JSON.parse(readFileSync(join(process.cwd(), "tests", "fixtures", "devbinary", "consent-v3.captured.json"), "utf8")));
|
|
1527
|
+
const native = {
|
|
1528
|
+
targetStatus: async () => startStatus(cwd, undefined, ["extra.md"]),
|
|
1529
|
+
start: async () => { throw new NativeReviewConsentRequiredError(consent); },
|
|
1530
|
+
} as unknown as NativeReviewCli;
|
|
1531
|
+
const first = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwd, native, undefined, candidateViews);
|
|
1532
|
+
assert.equal(first.outcome, "native-review-consent-required", JSON.stringify(first));
|
|
1533
|
+
writeFileSync(join(cwd, "tracked.txt"), "candidate two\n");
|
|
1534
|
+
const second = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwd, native, undefined, candidateViews);
|
|
1535
|
+
assert.notEqual(second.outcome, "native-operation-failed", JSON.stringify(second));
|
|
1536
|
+
assert.equal(second.outcome, "native-review-consent-required", JSON.stringify(second));
|
|
1537
|
+
assert.notEqual(second.consent_binding, first.consent_binding, JSON.stringify(second));
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1223
1540
|
test("controller-owned dispatch confines single and parallel graph actors to the current candidate view", async (t) => {
|
|
1224
1541
|
const cwd = repository(t);
|
|
1225
1542
|
const candidateViews = new CandidateViewRegistry();
|
|
@@ -6,9 +6,9 @@ import { join } from "node:path";
|
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
9
|
-
import type
|
|
9
|
+
import { NativeReviewIntegrationError, type NativeReviewCli } from "../lib/native-review-cli.ts";
|
|
10
10
|
import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
|
|
11
|
-
import type
|
|
11
|
+
import { decodeReviewFailureV2, type AuthorityRepairAssessmentV1, type ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
12
12
|
|
|
13
13
|
interface RegisteredTool {
|
|
14
14
|
execute: (
|
|
@@ -240,6 +240,49 @@ test("INSPECT and STATUS operate on the explicit workspace root while the sessio
|
|
|
240
240
|
assert.deepEqual(observedCwds, [realpathSync(worktree), realpathSync(worktree), sessionCwd]);
|
|
241
241
|
});
|
|
242
242
|
|
|
243
|
+
// gentle-pi#599: inspect without an explicit workspaceRoot against a parent
|
|
244
|
+
// repository that wraps an untracked nested Git repository fails at the
|
|
245
|
+
// native preflight with a decoded, actionable `invalid_request` envelope. The
|
|
246
|
+
// facade used to discard that envelope's cause, code, retry_safe, and
|
|
247
|
+
// next_action in favor of a generic `native-status-unavailable` outcome
|
|
248
|
+
// indistinguishable from authority corruption. It must preserve the native
|
|
249
|
+
// envelope instead.
|
|
250
|
+
test("inspect without workspaceRoot preserves the native invalid_request cause for a nested foreign Git repository", async (t) => {
|
|
251
|
+
const sessionCwd = repository(t);
|
|
252
|
+
const { controller } = runtime(fakeNative({
|
|
253
|
+
targetStatus: async () => {
|
|
254
|
+
throw new NativeReviewIntegrationError(decodeReviewFailureV2({
|
|
255
|
+
schema: "gentle-ai.review-integration.failure/v2",
|
|
256
|
+
contract: "gentle-ai.review-integration/v2",
|
|
257
|
+
operation: "review.status",
|
|
258
|
+
phase: "preflight",
|
|
259
|
+
code: "invalid_request",
|
|
260
|
+
message: "The negotiated review request is invalid.",
|
|
261
|
+
mutation_outcome: "not_started",
|
|
262
|
+
authority_applicability: "not_evaluated",
|
|
263
|
+
retry_safe: true,
|
|
264
|
+
replayability: "not_replayable",
|
|
265
|
+
required_inputs: [],
|
|
266
|
+
next_action: "correct_request",
|
|
267
|
+
cause: "untracked directory \"engram\" holds another Git repository that is not a linked worktree of this one, so it cannot enter the review candidate: add it to .gitignore, move it outside this repository, or register it as a linked worktree",
|
|
268
|
+
}));
|
|
269
|
+
},
|
|
270
|
+
}));
|
|
271
|
+
const result = await controller.execute("inspect-nested-foreign-repo", { operation: "inspect" }, undefined, undefined, context(sessionCwd));
|
|
272
|
+
const details = result.details as {
|
|
273
|
+
status?: string;
|
|
274
|
+
outcome?: string;
|
|
275
|
+
next_action?: string;
|
|
276
|
+
native_failure?: { code?: string; cause?: string; retry_safe?: boolean };
|
|
277
|
+
};
|
|
278
|
+
assert.equal(details.status, "blocked");
|
|
279
|
+
assert.equal(details.outcome, "native-status-unavailable");
|
|
280
|
+
assert.equal(details.native_failure?.code, "invalid_request");
|
|
281
|
+
assert.equal(details.native_failure?.retry_safe, true);
|
|
282
|
+
assert.match(details.native_failure?.cause ?? "", /engram/);
|
|
283
|
+
assert.equal(details.next_action, "correct_request");
|
|
284
|
+
});
|
|
285
|
+
|
|
243
286
|
test("START freezes the candidate from the explicit workspace root and returns the actor binding envelope", async (t) => {
|
|
244
287
|
const sessionCwd = repository(t);
|
|
245
288
|
writeFileSync(join(sessionCwd, "unrelated.ts"), "export const unrelated = true;\n");
|
|
@@ -363,6 +363,7 @@ test("general STATUS returns the typed native-status-unsupported boundary withou
|
|
|
363
363
|
mutation_performed: false,
|
|
364
364
|
inventory_complete: false,
|
|
365
365
|
next_action: "require-upstream-read-only-native-status-inventory",
|
|
366
|
+
remediation_command: "gentle-ai review status --cwd <repo> --contract gentle-ai.review-integration/v2 --agent pi --next-transition",
|
|
366
367
|
evidence: {
|
|
367
368
|
native_contract: "gentle-ai/2.1.4",
|
|
368
369
|
general_status: "unsupported",
|
|
@@ -371,6 +372,30 @@ test("general STATUS returns the typed native-status-unsupported boundary withou
|
|
|
371
372
|
});
|
|
372
373
|
});
|
|
373
374
|
|
|
375
|
+
test("gentle-pi#185: general STATUS on a non-negotiated native CLI names the exact status command to run", async (t) => {
|
|
376
|
+
// The legacy `correctionForecast` restoration guard this issue originally
|
|
377
|
+
// reported (extensions/gentle-ai.ts, then around line 5329) was scoped to
|
|
378
|
+
// `targetStatus !== undefined` and skipped restoring a candidate view when
|
|
379
|
+
// the native CLI lacked negotiated STATUS support, reproducing the #176
|
|
380
|
+
// empty-registry failure. That entire manual FINALIZE lifecycle (and the
|
|
381
|
+
// `correctionForecast` guard with it) has since been replaced by the
|
|
382
|
+
// STATUS/capture-driven flow: every operation now checks negotiated STATUS
|
|
383
|
+
// support up front, so no candidate-view restoration is ever attempted on
|
|
384
|
+
// a non-negotiated CLI. This test proves the current equivalent boundary
|
|
385
|
+
// (`nativeStatusUnsupported`) fails closed with an actionable envelope
|
|
386
|
+
// instead of a bare machine token, so a non-negotiated CLI can never
|
|
387
|
+
// surface as a silent empty-registry failure.
|
|
388
|
+
const fixture = createRepository(t);
|
|
389
|
+
const { controller } = registerRuntime();
|
|
390
|
+
const result = await controllerCall(controller, extensionContext(fixture.repository), { operation: "status" });
|
|
391
|
+
assert.equal(result.status, "blocked");
|
|
392
|
+
assert.equal(result.outcome, "native-status-unsupported");
|
|
393
|
+
assert.equal(
|
|
394
|
+
result.remediation_command,
|
|
395
|
+
"gentle-ai review status --cwd <repo> --contract gentle-ai.review-integration/v2 --agent pi --next-transition",
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
374
399
|
test("failed START gives exact mode and serialization guidance and creates no lineage", async (t) => {
|
|
375
400
|
const fixture = createRepository(t);
|
|
376
401
|
const { controller } = registerRuntime();
|
|
@@ -142,9 +142,22 @@ function finalizeStatus(lineageId: string, inputs?: readonly ReviewCollectInputV
|
|
|
142
142
|
} as unknown as ReviewStatusV3;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
function providerRefuterRequiredStatus(lineageId: string): ReviewStatusV3 {
|
|
146
|
+
const refuter: ReviewCollectInputV3 = {
|
|
147
|
+
name: "provider_refuter",
|
|
148
|
+
schema: "https://gentle-ai.dev/schema/review/refuter/v1",
|
|
149
|
+
captureOperation: "review.capture-refuter",
|
|
150
|
+
arguments: bindingArguments(lineageId, "review-risk", 0),
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
...finalizeStatus(lineageId),
|
|
154
|
+
nextTransition: { kind: "collect", reasonCode: "provider_refuter_required", collect: { inputs: [refuter] } },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
145
158
|
interface RoutingHarness {
|
|
146
159
|
statusQueue: ReviewStatusV3[];
|
|
147
|
-
statusCalls: Array<{ cwd: string; lineageId?: string }>;
|
|
160
|
+
statusCalls: Array<{ cwd: string; lineageId?: string; agent?: "pi" }>;
|
|
148
161
|
native: NativeReviewCli;
|
|
149
162
|
}
|
|
150
163
|
|
|
@@ -156,7 +169,7 @@ function nativeHarness(statuses: readonly ReviewStatusV3[]): RoutingHarness {
|
|
|
156
169
|
};
|
|
157
170
|
harness.native = {
|
|
158
171
|
targetStatus: async (request) => {
|
|
159
|
-
harness.statusCalls.push({ cwd: request.cwd, ...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }) });
|
|
172
|
+
harness.statusCalls.push({ cwd: request.cwd, ...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }), ...(request.agent === undefined ? {} : { agent: request.agent }) });
|
|
160
173
|
const next = harness.statusQueue.shift();
|
|
161
174
|
if (next === undefined) throw new Error("status queue exhausted");
|
|
162
175
|
return next;
|
|
@@ -226,7 +239,7 @@ function prepared(request: ReviewHostRelayRequest) { return { request, promptByt
|
|
|
226
239
|
test("grouped capture forecasts once, reaches a four-reviewer barrier, and reconciles unclosed submissions", async (t) => {
|
|
227
240
|
t.after(() => __testing.setReviewHostRelayGroupRunnersForTesting());
|
|
228
241
|
const cwd = repository(t), lineageId = "relay-group", inputs = groupInputs(lineageId), lenses = inputs.map((input) => input.arguments.find((argument) => argument.name === "lens")!.value);
|
|
229
|
-
const harness = nativeHarness([finalizeStatus(lineageId, inputs), finalizeStatus(lineageId, inputs), ...inputs.map((_input, index) => finalizeStatus(lineageId, index === 1 ? [...inputs.slice(index), { name: "unrelated", schema: "example", captureOperation: "external.example", arguments: [] }] : inputs.slice(index))),
|
|
242
|
+
const harness = nativeHarness([finalizeStatus(lineageId, inputs), finalizeStatus(lineageId, inputs), ...inputs.map((_input, index) => finalizeStatus(lineageId, index === 1 ? [...inputs.slice(index), { name: "unrelated", schema: "example", captureOperation: "external.example", arguments: [] }] : inputs.slice(index))), providerRefuterRequiredStatus(lineageId)]);
|
|
230
243
|
let ready!: () => void;
|
|
231
244
|
const allStarted = new Promise<void>((resolve) => { ready = resolve; });
|
|
232
245
|
const releases = new Map<string, () => void>(), completed: string[] = [], submitted: string[] = [];
|
|
@@ -249,7 +262,9 @@ test("grouped capture forecasts once, reaches a four-reviewer barrier, and recon
|
|
|
249
262
|
assert.deepEqual(completed, [...lenses].reverse(), "reviewer completion order is not submission order");
|
|
250
263
|
assert.deepEqual(submitted, lenses);
|
|
251
264
|
assert.deepEqual({ outcome: result.outcome, statusCalls: harness.statusCalls.length, providerAction: result.provider_action }, { outcome: "native-reviewer-group-status-reconciled", statusCalls: 7, providerAction: "stop" });
|
|
252
|
-
assert.equal(
|
|
265
|
+
assert.equal(harness.statusCalls.at(-1)?.agent, "pi", "post-last-capture reconciliation preserves the Pi host runtime");
|
|
266
|
+
assert.equal((result.next_transition as { kind?: string; reasonCode?: string } | undefined)?.kind, "collect");
|
|
267
|
+
assert.equal((result.next_transition as { kind?: string; reasonCode?: string } | undefined)?.reasonCode, "provider_refuter_required");
|
|
253
268
|
});
|
|
254
269
|
|
|
255
270
|
test("captured v5 STATUS accepts capture-phase Pn when authority has advanced to Rn at the forecast boundary", async (t) => {
|
|
@@ -465,6 +480,7 @@ test("a submission whose outcome is genuinely indeterminate still reconciles thr
|
|
|
465
480
|
assert.deepEqual(result.failure, { kind: "submission-refused", stage: "submit", exit_code: null, timed_out: true, elapsed_ms: 120_004, timeout_ms: 120_000 });
|
|
466
481
|
assert.match(String(result.reason), /exceeded its 120000ms bound/);
|
|
467
482
|
assert.equal(harness.statusCalls.length, 2, "an indeterminate submission reconciles exactly once through STATUS");
|
|
483
|
+
assert.equal(harness.statusCalls.at(-1)?.agent, "pi", "ordinary host-relay reconciliation preserves the Pi host runtime");
|
|
468
484
|
});
|
|
469
485
|
|
|
470
486
|
test("a relay timeout reports its one-slot measurements and no auto-follow", async (t) => {
|