gentle-pi 2.2.0 → 2.4.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 +76 -79
- package/assets/agents/review-readability.md +0 -2
- package/assets/agents/review-reliability.md +0 -2
- package/assets/agents/review-resilience.md +0 -2
- package/assets/agents/review-risk.md +0 -2
- package/assets/agents/sdd-apply.md +5 -3
- package/assets/agents/sdd-explore.md +1 -0
- package/assets/agents/sdd-proposal.md +2 -0
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-status.md +4 -4
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +21 -1
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +56 -249
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +20 -40
- package/assets/sdd-orchestrator-workflow.md +30 -37
- package/assets/support/sdd-status-contract.md +7 -7
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- 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/native-authority-architecture.md +9 -11
- package/docs/review-integration.md +27 -373
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +2349 -3785
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/agent-home.ts +8 -0
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +387 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +88 -6
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +324 -96
- package/lib/review-integration-v2.ts +868 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +179 -68
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -82
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +337 -890
- package/runtime/review-integration-v2.mjs +816 -255
- package/runtime/review-relay-contract.mjs +1 -1
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
- package/scripts/gentle-ai-installer.mjs +75 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
- package/scripts/test-packed-runner.mjs +4 -7
- package/scripts/verify-package-files.mjs +23 -23
- package/skills/_shared/review-ledger-contract.md +14 -14
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/issue-creation/SKILL.md +53 -93
- package/skills/judgment-day/SKILL.md +5 -7
- package/skills/rdd-defect-workflow/SKILL.md +3 -3
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +15 -1168
- package/tests/delegated-key-learnings-contract.test.ts +8 -6
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -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-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary.test.ts +51 -228
- package/tests/gentle-ai-installer.test.ts +137 -47
- package/tests/gentle-ai.test.ts +821 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/issue-creation-skill.test.ts +103 -0
- package/tests/maintainer/provider-relay.maintest.ts +340 -4
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +45 -1
- package/tests/native-review-cli.test.ts +586 -962
- package/tests/native-review-consent.test.ts +150 -66
- package/tests/native-review-parity-runtime.test.ts +80 -359
- package/tests/native-review-parity.test.ts +674 -925
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +132 -88
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +152 -36
- package/tests/provider-contract-bundle.test.ts +76 -0
- package/tests/provider-contract-mirror.test.ts +19 -0
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-agent-end-preflight.test.ts +408 -0
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +409 -3
- package/tests/review-compact-contract.test.ts +4 -30
- package/tests/review-contract-prompt.test.ts +139 -0
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +363 -1450
- package/tests/review-controller-native-routing.test.ts +1251 -5642
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +222 -71
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +112 -153
- package/tests/review-dispatch-hydration-gap.test.ts +1 -53
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +328 -91
- package/tests/review-host-relay.test.ts +436 -14
- package/tests/review-integration-v2-forward.test.ts +623 -553
- package/tests/review-integration-v2.test.ts +239 -165
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +97 -35
- package/tests/review-recovered-lineage-routing.test.ts +0 -47
- package/tests/review-relay-transport-agent.test.ts +119 -46
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +362 -196
- package/tests/sdd-agent-tools.test.ts +36 -0
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +5 -8
- package/tests/writer-edit-surface-scope.test.ts +366 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/lib/git-commit-transaction.ts +0 -861
- package/runtime/git-commit-transaction.mjs +0 -862
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -530
- /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
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdtempSync, readFileSync, 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";
|
|
7
7
|
import { __testing } from "../extensions/gentle-ai.ts";
|
|
8
8
|
import type { NativeReviewCli } from "../lib/native-review-cli.ts";
|
|
9
|
-
import { REVIEW_HOST_RELAY_FAILURE, REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE, REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE, ReviewHostRelayError, type ReviewHostRelayRequest } from "../lib/review-host-relay.ts";
|
|
10
|
-
import type
|
|
9
|
+
import { REVIEW_HOST_RELAY_FAILURE, REVIEW_HOST_RELAY_PI_TIMEOUT_ENV, REVIEW_HOST_RELAY_PI_TIMEOUT_MAX_MS, REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE, REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE, ReviewHostRelayError, type ReviewHostRelayRequest } from "../lib/review-host-relay.ts";
|
|
10
|
+
import { decodeReviewStatusV3, type ReviewArtifactSubjectV2, type ReviewCaptureSubmissionV1, type ReviewCollectInputV3, type ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
// routes pi-slot capture inputs through the host relay ONLY when the
|
|
12
|
+
// One-slot capture routing: the host relay runs only when the selected
|
|
14
13
|
// provider-returned collect input carries the --materialize token. Every
|
|
15
|
-
// other
|
|
14
|
+
// other capture form stays untouched.
|
|
16
15
|
|
|
17
16
|
const SHA = `sha256:${"1".repeat(64)}`;
|
|
17
|
+
const PHASE_REVISION = `sha256:${"3".repeat(64)}`;
|
|
18
18
|
const TREE = "2".repeat(40);
|
|
19
19
|
|
|
20
20
|
function repository(t: test.TestContext): string {
|
|
@@ -27,10 +27,10 @@ function repository(t: test.TestContext): string {
|
|
|
27
27
|
return cwd;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function bindingArguments(lineageId: string, lens:
|
|
30
|
+
function bindingArguments(lineageId: string, lens: ReviewArtifactSubjectV2["lens"], order: number, revision = SHA): ReviewCollectInputV3["arguments"] {
|
|
31
31
|
return [
|
|
32
32
|
{ name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
|
|
33
|
-
{ name: "expected-revision", value:
|
|
33
|
+
{ name: "expected-revision", value: revision, token: `--expected-revision=${revision}` },
|
|
34
34
|
{ name: "target", value: SHA, token: `--target=${SHA}` },
|
|
35
35
|
{ name: "repository-context", value: `rctx1_${"e".repeat(64)}`, token: `--repository-context=rctx1_${"e".repeat(64)}` },
|
|
36
36
|
{ name: "lens", value: lens, token: `--lens=${lens}` },
|
|
@@ -39,8 +39,8 @@ function bindingArguments(lineageId: string, lens: string, order: number): Revie
|
|
|
39
39
|
];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function providerSubmission(lineageId: string, lens:
|
|
43
|
-
const bindingTokens = bindingArguments(lineageId, lens, order).map((argument) => argument.token!);
|
|
42
|
+
function providerSubmission(lineageId: string, lens: ReviewArtifactSubjectV2["lens"], order: number, revision = SHA): ReviewCaptureSubmissionV1 {
|
|
43
|
+
const bindingTokens = bindingArguments(lineageId, lens, order, revision).map((argument) => argument.token!);
|
|
44
44
|
return {
|
|
45
45
|
operationToken: "capture-result",
|
|
46
46
|
argumentTokens: [...bindingTokens, "--input={{value}}"],
|
|
@@ -48,29 +48,77 @@ function providerSubmission(lineageId: string, lens: string, order: number): Rev
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function relayCollectInput(lineageId: string, lens:
|
|
51
|
+
function relayCollectInput(lineageId: string, lens: ReviewArtifactSubjectV2["lens"], order: number, materialize = true, submission: ReviewCaptureSubmissionV1 | "provider" | "absent" = "provider", revision = SHA): ReviewCollectInputV3 {
|
|
52
52
|
return {
|
|
53
53
|
name: "reviewer_result",
|
|
54
54
|
schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
|
|
55
55
|
captureOperation: "review.capture-result",
|
|
56
56
|
arguments: [
|
|
57
|
-
...bindingArguments(lineageId, lens, order),
|
|
57
|
+
...bindingArguments(lineageId, lens, order, revision),
|
|
58
58
|
...(materialize ? [
|
|
59
59
|
{ name: "agent", value: "pi", token: "--agent=pi" },
|
|
60
60
|
{ name: "materialize", value: "true", token: "--materialize=true" },
|
|
61
61
|
] : []),
|
|
62
62
|
],
|
|
63
|
-
|
|
63
|
+
artifactSubject: {
|
|
64
|
+
schema: "gentle-ai.review-artifact-subject/v2", subjectHash: `sha256:${String(order).repeat(64)}`,
|
|
65
|
+
lineageId, authorityRevision: revision, targetIdentity: SHA, baseTree: TREE, candidateTree: TREE,
|
|
66
|
+
changedPathManifestSha256: SHA, lens, selectedOrder: order,
|
|
67
|
+
},
|
|
68
|
+
baseTree: TREE, candidateTree: TREE, changedPathManifest: [],
|
|
69
|
+
...(materialize && submission !== "absent" ? { submission: submission === "provider" ? providerSubmission(lineageId, lens, order, revision) : submission } : {}),
|
|
64
70
|
};
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
function capturedGroupedStatus(lineageId: string, authorityRevision = SHA): ReviewStatusV3 {
|
|
74
|
+
const raw = JSON.parse(readFileSync(new URL("./fixtures/devbinary/status-v5-capture-result-submission.captured.json", import.meta.url), "utf8")) as Record<string, unknown>;
|
|
75
|
+
// The captured binary's forward-only `finalize` action is not accepted by
|
|
76
|
+
// this checked-out decoder; retain the v5 capture and normalize only that
|
|
77
|
+
// unrelated routing action before decoding its provider-owned group.
|
|
78
|
+
raw.action = "stop";
|
|
79
|
+
const authority = raw.authority as Record<string, unknown>, repositoryContext = raw.repository_context as Record<string, unknown>;
|
|
80
|
+
authority.lineage_id = lineageId;
|
|
81
|
+
authority.revision = authorityRevision;
|
|
82
|
+
const phaseRevision = String(repositoryContext.revision), targetIdentity = String(raw.target_identity);
|
|
83
|
+
const source = ((((raw.next_transition as Record<string, unknown>).collect as Record<string, unknown>).inputs as Array<Record<string, unknown>>)[0]!);
|
|
84
|
+
const lenses = ["review-risk", "review-resilience", "review-readability", "review-reliability"];
|
|
85
|
+
((raw.next_transition as Record<string, unknown>).collect as Record<string, unknown>).inputs = lenses.map((lens, order) => {
|
|
86
|
+
const input = JSON.parse(JSON.stringify(source)) as Record<string, unknown>;
|
|
87
|
+
const subjectHash = `sha256:${String(order + 1).repeat(64)}`;
|
|
88
|
+
const arguments_ = input.arguments as Array<Record<string, unknown>>;
|
|
89
|
+
for (const argument of arguments_) {
|
|
90
|
+
const value = argument.name === "lineage" ? lineageId
|
|
91
|
+
: argument.name === "lens" ? lens
|
|
92
|
+
: argument.name === "order" ? String(order)
|
|
93
|
+
: argument.name === "subject-hash" ? subjectHash
|
|
94
|
+
: argument.value;
|
|
95
|
+
argument.value = value;
|
|
96
|
+
argument.token = `--${String(argument.name)}=${String(value)}`;
|
|
97
|
+
}
|
|
98
|
+
const submission = input.submission as Record<string, unknown>;
|
|
99
|
+
submission.argument_tokens = [...arguments_.filter((argument) => argument.name !== "agent" && argument.name !== "materialize").map((argument) => argument.token), "--input={{value}}"];
|
|
100
|
+
const artifactSubject = input.artifact_subject as Record<string, unknown>;
|
|
101
|
+
artifactSubject.subject_hash = subjectHash;
|
|
102
|
+
artifactSubject.lineage_id = lineageId;
|
|
103
|
+
artifactSubject.authority_revision = phaseRevision;
|
|
104
|
+
artifactSubject.target_identity = targetIdentity;
|
|
105
|
+
artifactSubject.lens = lens;
|
|
106
|
+
artifactSubject.selected_order = order;
|
|
107
|
+
return input;
|
|
108
|
+
});
|
|
109
|
+
return decodeReviewStatusV3(raw);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function replaceArgument(input: ReviewCollectInputV3, name: string, value: string): void {
|
|
113
|
+
input.arguments = input.arguments.map((argument) => argument.name === name ? { ...argument, value, token: `--${name}=${value}` } : argument);
|
|
114
|
+
}
|
|
115
|
+
|
|
67
116
|
function finalizeStatus(lineageId: string, inputs?: readonly ReviewCollectInputV3[]): ReviewStatusV3 {
|
|
68
117
|
return {
|
|
69
118
|
contract: "gentle-ai.review-integration/v2",
|
|
70
119
|
applicability: "current_target",
|
|
71
120
|
authority: { version: "compact-v2", lineageId, state: "reviewing", generation: 1, revision: SHA },
|
|
72
|
-
|
|
73
|
-
action: "finalize",
|
|
121
|
+
action: "stop",
|
|
74
122
|
replayability: "unknown",
|
|
75
123
|
targetIdentity: SHA,
|
|
76
124
|
projection: {
|
|
@@ -88,15 +136,15 @@ function finalizeStatus(lineageId: string, inputs?: readonly ReviewCollectInputV
|
|
|
88
136
|
currentSnapshotIdentity: SHA,
|
|
89
137
|
},
|
|
90
138
|
candidates: [],
|
|
139
|
+
repositoryContext: { capability: "review.opaque_repository_context", handle: `rctx1_${"e".repeat(64)}`, revision: SHA, targetIdentity: SHA },
|
|
91
140
|
...(inputs === undefined ? {} : { nextTransition: { kind: "collect", reasonCode: "reviewer_results_required", collect: { inputs: [...inputs] } } }),
|
|
92
|
-
raw: { schema: "gentle-ai.review-integration.status/v3", action: "
|
|
141
|
+
raw: { schema: "gentle-ai.review-integration.status/v3", action: "stop", lineage_id: lineageId },
|
|
93
142
|
} as unknown as ReviewStatusV3;
|
|
94
143
|
}
|
|
95
144
|
|
|
96
145
|
interface RoutingHarness {
|
|
97
146
|
statusQueue: ReviewStatusV3[];
|
|
98
147
|
statusCalls: Array<{ cwd: string; lineageId?: string }>;
|
|
99
|
-
finalizeCalls: number;
|
|
100
148
|
native: NativeReviewCli;
|
|
101
149
|
}
|
|
102
150
|
|
|
@@ -104,19 +152,9 @@ function nativeHarness(statuses: readonly ReviewStatusV3[]): RoutingHarness {
|
|
|
104
152
|
const harness: RoutingHarness = {
|
|
105
153
|
statusQueue: [...statuses],
|
|
106
154
|
statusCalls: [],
|
|
107
|
-
finalizeCalls: 0,
|
|
108
155
|
native: undefined as unknown as NativeReviewCli,
|
|
109
156
|
};
|
|
110
157
|
harness.native = {
|
|
111
|
-
start: async () => { throw new Error("unexpected start"); },
|
|
112
|
-
finalize: async () => {
|
|
113
|
-
harness.finalizeCalls += 1;
|
|
114
|
-
return { lineageId: "relay-lineage", state: "approved", action: "approved", storeRevision: "r1" };
|
|
115
|
-
},
|
|
116
|
-
validate: async () => { throw new Error("unexpected validate"); },
|
|
117
|
-
bindSdd: async () => { throw new Error("unexpected bindSdd"); },
|
|
118
|
-
sddStatus: async () => ({ ready: false, artifactStore: "none", artifacts: {} as never, nextRecommended: "" }),
|
|
119
|
-
reviewStatus: async () => { throw new Error("unexpected reviewStatus"); },
|
|
120
158
|
targetStatus: async (request) => {
|
|
121
159
|
harness.statusCalls.push({ cwd: request.cwd, ...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }) });
|
|
122
160
|
const next = harness.statusQueue.shift();
|
|
@@ -127,63 +165,41 @@ function nativeHarness(statuses: readonly ReviewStatusV3[]): RoutingHarness {
|
|
|
127
165
|
return harness;
|
|
128
166
|
}
|
|
129
167
|
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// finalize now forecasts its reviewer model runs and spends nothing until the
|
|
136
|
-
// caller acknowledges them (see review-relay-transport-agent.test.ts), so these
|
|
137
|
-
// routing cases authorize the run up front and keep asserting where the slots
|
|
138
|
-
// go. A case that must observe the forecast passes the acknowledgement off.
|
|
139
|
-
async function runFinalize(cwd: string, harness: RoutingHarness, lineageId: string, input: Record<string, unknown> = { reviewer_run_acknowledged: true }): Promise<Record<string, unknown>> {
|
|
140
|
-
return await __testing.executeReviewControllerOperation(
|
|
141
|
-
finalizeParameters(lineageId, input),
|
|
168
|
+
async function runCapture(cwd: string, harness: RoutingHarness, lineageId: string, input: Record<string, unknown> = { reviewerRunAcknowledged: true }): Promise<Record<string, unknown>> {
|
|
169
|
+
const selected = harness.statusQueue[0]?.nextTransition?.collect?.inputs[0];
|
|
170
|
+
if (selected === undefined) throw new Error("capture test requires one current collect input");
|
|
171
|
+
return await __testing.executeReviewCaptureOperation(
|
|
172
|
+
{ lineageId, collectBinding: JSON.stringify(selected), ...input },
|
|
142
173
|
cwd,
|
|
143
|
-
new Map(),
|
|
144
174
|
harness.native,
|
|
145
175
|
) as Record<string, unknown>;
|
|
146
176
|
}
|
|
147
177
|
|
|
148
|
-
test("materialize
|
|
178
|
+
test("one materialize binding routes exactly one provider slot through the host relay", async (t) => {
|
|
149
179
|
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
150
180
|
const cwd = repository(t);
|
|
151
181
|
const lineageId = "relay-lineage";
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
relayCollectInput(lineageId, "review-reliability", 1),
|
|
155
|
-
];
|
|
156
|
-
const harness = nativeHarness([finalizeStatus(lineageId, inputs), finalizeStatus(lineageId)]);
|
|
182
|
+
const input = relayCollectInput(lineageId, "review-risk", 0);
|
|
183
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [input])]);
|
|
157
184
|
const relayed: ReviewHostRelayRequest[] = [];
|
|
158
185
|
__testing.setReviewHostRelayRunnerForTesting(async (request: ReviewHostRelayRequest) => {
|
|
159
186
|
relayed.push(request);
|
|
160
187
|
return { promptByteLength: 64, resultByteLength: 32, submission: '{"admission_decision":"completed"}' };
|
|
161
188
|
});
|
|
162
189
|
|
|
163
|
-
const result = await
|
|
190
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
164
191
|
|
|
165
|
-
assert.equal(relayed.length,
|
|
166
|
-
assert.deepEqual(relayed[0]!.captureArgumentTokens,
|
|
167
|
-
// The provider-owned completing form reaches the relay verbatim.
|
|
192
|
+
assert.equal(relayed.length, 1);
|
|
193
|
+
assert.deepEqual(relayed[0]!.captureArgumentTokens, input.arguments.map((argument) => argument.token));
|
|
168
194
|
assert.deepEqual(relayed[0]!.submission, providerSubmission(lineageId, "review-risk", 0));
|
|
169
|
-
assert.
|
|
170
|
-
assert.
|
|
171
|
-
|
|
172
|
-
assert.equal(harness.finalizeCalls, 0, "the relay never invokes native finalize itself");
|
|
173
|
-
assert.equal(harness.statusCalls.length, 2, "STATUS is re-queried after all slots are captured");
|
|
174
|
-
assert.deepEqual(harness.statusCalls[1], { cwd, lineageId });
|
|
175
|
-
|
|
176
|
-
const hostRelay = result.host_relay as { transport: string; captured_slots: Array<Record<string, unknown>> };
|
|
177
|
-
assert.equal(hostRelay.transport, "pi_host_relay");
|
|
178
|
-
assert.equal(hostRelay.captured_slots.length, 2);
|
|
179
|
-
assert.deepEqual(hostRelay.captured_slots.map((slot) => slot.lens), ["review-risk", "review-reliability"]);
|
|
180
|
-
assert.equal(result.status, "in-progress");
|
|
195
|
+
assert.equal(harness.statusCalls.length, 1, "a nonterminal capture does not auto-follow STATUS");
|
|
196
|
+
assert.equal(result.status, "captured");
|
|
197
|
+
assert.equal((result.host_relay as { transport: string }).transport, "pi_host_relay");
|
|
181
198
|
});
|
|
182
199
|
|
|
183
|
-
test("Pi-authored review documents are rejected at the
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// a Pi-authored reviewer document can never race a host-mediated slot.
|
|
200
|
+
test("Pi-authored review documents are rejected at the capture input boundary", async (t) => {
|
|
201
|
+
// The narrow capture schema rejects a caller-authored reviewer document
|
|
202
|
+
// before any relay launch or native call.
|
|
187
203
|
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
188
204
|
const cwd = repository(t);
|
|
189
205
|
const lineageId = "relay-lineage";
|
|
@@ -195,11 +211,161 @@ test("Pi-authored review documents are rejected at the FINALIZE input boundary",
|
|
|
195
211
|
});
|
|
196
212
|
|
|
197
213
|
await assert.rejects(
|
|
198
|
-
() =>
|
|
199
|
-
/
|
|
214
|
+
() => runCapture(cwd, harness, lineageId, { review_result: { lens_results: [{ findings: [], evidence: ["reviewed"] }] } }),
|
|
215
|
+
/does not accept review_result/,
|
|
200
216
|
);
|
|
201
217
|
assert.equal(relayCalls, 0);
|
|
202
|
-
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
function groupInputs(lineageId: string, revision = SHA): ReviewCollectInputV3[] { return ["review-risk", "review-resilience", "review-readability", "review-reliability"].map((lens, order) => relayCollectInput(lineageId, lens, order, true, "provider", revision)); }
|
|
221
|
+
|
|
222
|
+
async function runCaptureGroup(cwd: string, harness: RoutingHarness, lineageId: string, inputs: readonly ReviewCollectInputV3[], reviewerRunAcknowledged = true): Promise<Record<string, unknown>> { return await __testing.executeReviewCaptureGroupOperation({ lineageId, collectBindings: inputs.map((input) => JSON.stringify(input)), reviewerRunAcknowledged }, cwd, harness.native) as Record<string, unknown>; }
|
|
223
|
+
|
|
224
|
+
function prepared(request: ReviewHostRelayRequest) { return { request, promptByteLength: 64, resultByteLength: 32 }; }
|
|
225
|
+
|
|
226
|
+
test("grouped capture forecasts once, reaches a four-reviewer barrier, and reconciles unclosed submissions", async (t) => {
|
|
227
|
+
t.after(() => __testing.setReviewHostRelayGroupRunnersForTesting());
|
|
228
|
+
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))), finalizeStatus(lineageId)]);
|
|
230
|
+
let ready!: () => void;
|
|
231
|
+
const allStarted = new Promise<void>((resolve) => { ready = resolve; });
|
|
232
|
+
const releases = new Map<string, () => void>(), completed: string[] = [], submitted: string[] = [];
|
|
233
|
+
const reviewerGroup = async (requests: readonly ReviewHostRelayRequest[]) => await Promise.all(requests.map(async (request) => {
|
|
234
|
+
const lens = request.captureArgumentTokens.find((token) => token.startsWith("--lens="))!.slice(7);
|
|
235
|
+
await new Promise<void>((resolve) => { releases.set(lens, resolve); if (releases.size === requests.length) ready(); });
|
|
236
|
+
completed.push(lens); return prepared(request);
|
|
237
|
+
}));
|
|
238
|
+
__testing.setReviewHostRelayGroupRunnersForTesting(reviewerGroup, async (result) => {
|
|
239
|
+
submitted.push(result.request.captureArgumentTokens.find((token) => token.startsWith("--lens="))!.slice(7));
|
|
240
|
+
return { promptByteLength: result.promptByteLength, resultByteLength: result.resultByteLength, submission: "{}" };
|
|
241
|
+
});
|
|
242
|
+
const forecast = await runCaptureGroup(cwd, harness, lineageId, inputs, false);
|
|
243
|
+
assert.deepEqual(forecast.cost_forecast, { transport: "pi_host_relay", model_runs: 4, lenses });
|
|
244
|
+
const run = runCaptureGroup(cwd, harness, lineageId, inputs);
|
|
245
|
+
await allStarted;
|
|
246
|
+
assert.deepEqual([...releases.keys()], lenses, "all reviewers start before the group waits");
|
|
247
|
+
for (const lens of [...lenses].reverse()) releases.get(lens!)!();
|
|
248
|
+
const result = await run;
|
|
249
|
+
assert.deepEqual(completed, [...lenses].reverse(), "reviewer completion order is not submission order");
|
|
250
|
+
assert.deepEqual(submitted, lenses);
|
|
251
|
+
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(result.next_transition, undefined);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("captured v5 STATUS accepts capture-phase Pn when authority has advanced to Rn at the forecast boundary", async (t) => {
|
|
256
|
+
t.after(() => __testing.setReviewHostRelayGroupRunnersForTesting());
|
|
257
|
+
const cwd = repository(t), lineageId = "relay-group-phase-revision", status = capturedGroupedStatus(lineageId, SHA), inputs = status.nextTransition!.collect!.inputs!;
|
|
258
|
+
let launches = 0;
|
|
259
|
+
__testing.setReviewHostRelayGroupRunnersForTesting(async (requests) => { launches += requests.length; return requests.map(prepared); });
|
|
260
|
+
|
|
261
|
+
assert.notEqual(status.authority!.revision, status.repositoryContext!.revision, "captured v5 group keeps capture-phase Pn distinct from authority Rn");
|
|
262
|
+
const forecast = await runCaptureGroup(cwd, nativeHarness([status]), lineageId, inputs, false);
|
|
263
|
+
|
|
264
|
+
assert.equal(forecast.outcome, "reviewer-model-run-forecast");
|
|
265
|
+
assert.deepEqual(forecast.cost_forecast, { transport: "pi_host_relay", model_runs: 4, lenses: ["review-risk", "review-resilience", "review-readability", "review-reliability"] });
|
|
266
|
+
assert.equal(launches, 0, "decoder-valid capture-phase Pn must pass admission before any reviewer model launch");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("group rejects STATUS repository target mismatch and per-slot revision, context, or target drift before launch", async (t) => {
|
|
270
|
+
t.after(() => __testing.setReviewHostRelayGroupRunnersForTesting());
|
|
271
|
+
const cwd = repository(t), lineageId = "relay-group-slot-consistency";
|
|
272
|
+
const repositoryTargetMismatch = capturedGroupedStatus(lineageId, SHA);
|
|
273
|
+
repositoryTargetMismatch.repositoryContext!.targetIdentity = SHA;
|
|
274
|
+
const revisionMismatch = capturedGroupedStatus(lineageId, SHA);
|
|
275
|
+
replaceArgument(revisionMismatch.nextTransition!.collect!.inputs![1]!, "expected-revision", SHA);
|
|
276
|
+
const contextMismatch = capturedGroupedStatus(lineageId, SHA);
|
|
277
|
+
replaceArgument(contextMismatch.nextTransition!.collect!.inputs![1]!, "repository-context", `rctx1_${"f".repeat(64)}`);
|
|
278
|
+
const targetMismatch = capturedGroupedStatus(lineageId, SHA);
|
|
279
|
+
replaceArgument(targetMismatch.nextTransition!.collect!.inputs![1]!, "target", SHA);
|
|
280
|
+
let launches = 0;
|
|
281
|
+
__testing.setReviewHostRelayGroupRunnersForTesting(async (requests) => { launches += requests.length; return requests.map(prepared); });
|
|
282
|
+
|
|
283
|
+
// Each call forwards this STATUS's own current set, so rejection proves an
|
|
284
|
+
// internal STATUS guard rather than stale or caller/canonical mismatch.
|
|
285
|
+
const repositoryTargetResult = await runCaptureGroup(cwd, nativeHarness([repositoryTargetMismatch]), lineageId, repositoryTargetMismatch.nextTransition!.collect!.inputs!);
|
|
286
|
+
assert.deepEqual({ outcome: repositoryTargetResult.outcome, reason: repositoryTargetResult.reason }, {
|
|
287
|
+
outcome: "capture-group-rejected",
|
|
288
|
+
reason: "current STATUS repository context does not match the reviewer group binding",
|
|
289
|
+
});
|
|
290
|
+
const revisionResult = await runCaptureGroup(cwd, nativeHarness([revisionMismatch]), lineageId, revisionMismatch.nextTransition!.collect!.inputs!);
|
|
291
|
+
assert.deepEqual({ outcome: revisionResult.outcome, reason: revisionResult.reason }, {
|
|
292
|
+
outcome: "capture-group-rejected",
|
|
293
|
+
reason: "current STATUS carries an incomplete or mismatched materialize reviewer binding",
|
|
294
|
+
});
|
|
295
|
+
const contextResult = await runCaptureGroup(cwd, nativeHarness([contextMismatch]), lineageId, contextMismatch.nextTransition!.collect!.inputs!);
|
|
296
|
+
assert.deepEqual({ outcome: contextResult.outcome, reason: contextResult.reason }, {
|
|
297
|
+
outcome: "capture-group-rejected",
|
|
298
|
+
reason: "current STATUS carries an incomplete or mismatched materialize reviewer binding",
|
|
299
|
+
});
|
|
300
|
+
const targetResult = await runCaptureGroup(cwd, nativeHarness([targetMismatch]), lineageId, targetMismatch.nextTransition!.collect!.inputs!);
|
|
301
|
+
assert.deepEqual({ outcome: targetResult.outcome, reason: targetResult.reason }, {
|
|
302
|
+
outcome: "capture-group-rejected",
|
|
303
|
+
reason: "current STATUS carries an incomplete or mismatched materialize reviewer binding",
|
|
304
|
+
});
|
|
305
|
+
assert.equal(launches, 0, "current STATUS consistency checks reject before reviewer model launch");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("group rejects absent or mismatched current repository context before launch", async (t) => {
|
|
309
|
+
t.after(() => __testing.setReviewHostRelayGroupRunnersForTesting());
|
|
310
|
+
const cwd = repository(t), lineageId = "relay-group-context-reject", inputs = groupInputs(lineageId, PHASE_REVISION);
|
|
311
|
+
const revisionMismatch = finalizeStatus(lineageId, inputs);
|
|
312
|
+
const handleMismatch = finalizeStatus(lineageId, inputs);
|
|
313
|
+
const absent = finalizeStatus(lineageId, inputs);
|
|
314
|
+
handleMismatch.repositoryContext = { capability: "review.opaque_repository_context", handle: `rctx1_${"f".repeat(64)}`, revision: PHASE_REVISION, targetIdentity: SHA };
|
|
315
|
+
delete absent.repositoryContext;
|
|
316
|
+
let launches = 0;
|
|
317
|
+
__testing.setReviewHostRelayGroupRunnersForTesting(async (requests) => { launches += requests.length; return requests.map(prepared); });
|
|
318
|
+
|
|
319
|
+
for (const status of [revisionMismatch, handleMismatch, absent]) {
|
|
320
|
+
const result = await runCaptureGroup(cwd, nativeHarness([status]), lineageId, inputs);
|
|
321
|
+
assert.equal(result.outcome, "capture-group-rejected");
|
|
322
|
+
}
|
|
323
|
+
assert.equal(launches, 0, "repository-context validation must reject before reviewer model launch");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("group rejects partial, duplicate, reordered, stale, mixed, and late-invalid bindings before launch", async (t) => {
|
|
327
|
+
t.after(() => __testing.setReviewHostRelayGroupRunnersForTesting());
|
|
328
|
+
const cwd = repository(t), lineageId = "relay-group-reject", inputs = groupInputs(lineageId);
|
|
329
|
+
const stale = JSON.parse(JSON.stringify(inputs[3]!)) as ReviewCollectInputV3;
|
|
330
|
+
stale.arguments = [...stale.arguments, { name: "replacement", value: "stale", token: "--replacement=stale" }];
|
|
331
|
+
const mixed = [...inputs.slice(0, 3), relayCollectInput(lineageId, "review-reliability", 3, false)];
|
|
332
|
+
const malformed = JSON.parse(JSON.stringify(inputs)) as ReviewCollectInputV3[];
|
|
333
|
+
malformed[3]!.submission!.argumentTokens = ["--input={{value}}"];
|
|
334
|
+
const cases = [
|
|
335
|
+
{ bindings: inputs.slice(0, 3), current: inputs }, { bindings: [...inputs].reverse(), current: inputs },
|
|
336
|
+
{ bindings: [...inputs.slice(0, 3), inputs[2]!], current: inputs }, { bindings: [...inputs.slice(0, 3), stale], current: inputs },
|
|
337
|
+
{ bindings: mixed, current: mixed }, { bindings: malformed, current: malformed },
|
|
338
|
+
];
|
|
339
|
+
let launches = 0;
|
|
340
|
+
__testing.setReviewHostRelayGroupRunnersForTesting(async (requests) => { launches += requests.length; return requests.map(prepared); });
|
|
341
|
+
for (const entry of cases) {
|
|
342
|
+
const result = await runCaptureGroup(cwd, nativeHarness([finalizeStatus(lineageId, entry.current)]), lineageId, entry.bindings);
|
|
343
|
+
assert.equal(result.outcome, "capture-group-rejected");
|
|
344
|
+
}
|
|
345
|
+
assert.equal(launches, 0);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("group preserves earlier admission when a later STATUS drifts, and stops on closure or unknown outcome", async (t) => {
|
|
349
|
+
t.after(() => __testing.setReviewHostRelayGroupRunnersForTesting());
|
|
350
|
+
const cwd = repository(t), lineageId = "relay-group-stop", inputs = groupInputs(lineageId);
|
|
351
|
+
const reviewerGroup = async (requests: readonly ReviewHostRelayRequest[]) => requests.map(prepared);
|
|
352
|
+
let submissions = 0;
|
|
353
|
+
__testing.setReviewHostRelayGroupRunnersForTesting(reviewerGroup, async (result) => { submissions += 1; return { promptByteLength: result.promptByteLength, resultByteLength: result.resultByteLength, submission: "{}" }; });
|
|
354
|
+
const staleStatus = finalizeStatus(lineageId, inputs), stale = await runCaptureGroup(cwd, nativeHarness([finalizeStatus(lineageId, inputs), finalizeStatus(lineageId, inputs), staleStatus]), lineageId, inputs);
|
|
355
|
+
assert.deepEqual({ outcome: stale.outcome, submissions, mutation: stale.mutation_performed, mutationOutcome: stale.mutation_outcome }, { outcome: "capture-group-authority-drift", submissions: 1, mutation: true, mutationOutcome: "partial" });
|
|
356
|
+
assert.equal(stale.reconciliation, staleStatus.raw);
|
|
357
|
+
submissions = 0;
|
|
358
|
+
const drift = await runCaptureGroup(cwd, nativeHarness([finalizeStatus(lineageId, inputs), finalizeStatus(lineageId, inputs), finalizeStatus(lineageId, inputs.slice(2))]), lineageId, inputs);
|
|
359
|
+
assert.deepEqual({ outcome: drift.outcome, submissions, mutation: drift.mutation_performed, mutationOutcome: drift.mutation_outcome }, { outcome: "capture-group-authority-drift", submissions: 1, mutation: true, mutationOutcome: "partial" });
|
|
360
|
+
assert.deepEqual(((drift.host_relay as { reviewers: Array<{ lens: string }> }).reviewers).map((reviewer) => reviewer.lens), ["review-risk"]);
|
|
361
|
+
submissions = 0;
|
|
362
|
+
__testing.setReviewHostRelayGroupRunnersForTesting(reviewerGroup, async (result) => { submissions += 1; return { promptByteLength: result.promptByteLength, resultByteLength: result.resultByteLength, submission: JSON.stringify({ schema: "gentle-ai.review-last-event-closure/v1", operation: "review/capture-result", lineage_id: lineageId, state: "approved", store_revision: SHA, action: "native last event closed the review" }) }; });
|
|
363
|
+
const terminal = await runCaptureGroup(cwd, nativeHarness([finalizeStatus(lineageId, inputs), finalizeStatus(lineageId, inputs)]), lineageId, inputs);
|
|
364
|
+
assert.deepEqual({ status: terminal.status, submissions }, { status: "closed", submissions: 1 });
|
|
365
|
+
submissions = 0;
|
|
366
|
+
__testing.setReviewHostRelayGroupRunnersForTesting(reviewerGroup, async () => { submissions += 1; throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", "unknown", { timedOut: true }); });
|
|
367
|
+
const unknown = await runCaptureGroup(cwd, nativeHarness([finalizeStatus(lineageId, inputs), finalizeStatus(lineageId, inputs), finalizeStatus(lineageId, inputs)]), lineageId, inputs);
|
|
368
|
+
assert.deepEqual({ status: unknown.status, submissions }, { status: "reconciled", submissions: 1 });
|
|
203
369
|
});
|
|
204
370
|
|
|
205
371
|
test("an old binary reports the relay as unavailable without touching existing behavior", async (t) => {
|
|
@@ -211,14 +377,13 @@ test("an old binary reports the relay as unavailable without touching existing b
|
|
|
211
377
|
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.RELAY_UNAVAILABLE, "materialize", REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE, { exitCode: 2, stderr: "flag provided but not defined: -materialize" });
|
|
212
378
|
});
|
|
213
379
|
|
|
214
|
-
const result = await
|
|
380
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
215
381
|
|
|
216
382
|
assert.equal(result.status, "blocked");
|
|
217
383
|
assert.equal(result.outcome, "pi-host-relay-unavailable");
|
|
218
384
|
assert.equal(result.reason, REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE);
|
|
219
385
|
assert.equal(result.mutation_performed, false);
|
|
220
386
|
assert.equal(result.mutation_outcome, "none");
|
|
221
|
-
assert.equal(harness.finalizeCalls, 0);
|
|
222
387
|
});
|
|
223
388
|
|
|
224
389
|
test("a handshake refusal surfaces the provider refusal verbatim through the controller envelope", async (t) => {
|
|
@@ -231,43 +396,116 @@ test("a handshake refusal surfaces the provider refusal verbatim through the con
|
|
|
231
396
|
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.HANDSHAKE_REFUSED, "materialize", refusal, { exitCode: 1, stderr: refusal });
|
|
232
397
|
});
|
|
233
398
|
|
|
234
|
-
const result = await
|
|
399
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
235
400
|
|
|
236
401
|
assert.equal(result.outcome, "pi-host-relay-handshake-refused");
|
|
237
402
|
assert.equal(result.reason, refusal);
|
|
238
403
|
assert.equal(result.refusal, refusal);
|
|
239
404
|
});
|
|
240
405
|
|
|
241
|
-
test("a transport failure
|
|
406
|
+
test("a transport failure stops the selected capture without auto-follow", async (t) => {
|
|
242
407
|
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
243
408
|
const cwd = repository(t);
|
|
244
409
|
const lineageId = "relay-lineage";
|
|
245
|
-
const
|
|
246
|
-
relayCollectInput(lineageId, "review-risk", 0),
|
|
247
|
-
relayCollectInput(lineageId, "review-reliability", 1),
|
|
248
|
-
];
|
|
249
|
-
const harness = nativeHarness([finalizeStatus(lineageId, inputs)]);
|
|
250
|
-
let calls = 0;
|
|
410
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)])]);
|
|
251
411
|
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
252
|
-
calls += 1;
|
|
253
|
-
if (calls === 1) return { promptByteLength: 8, resultByteLength: 8, submission: '{"admission_decision":"completed"}' };
|
|
254
412
|
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", "pi subprocess failed", { exitCode: 4 });
|
|
255
413
|
});
|
|
256
414
|
|
|
257
|
-
const result = await
|
|
415
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
258
416
|
|
|
259
|
-
assert.equal(calls, 2);
|
|
260
417
|
assert.equal(result.status, "blocked");
|
|
261
418
|
assert.equal(result.outcome, "pi-host-relay-transport-failure");
|
|
262
419
|
assert.deepEqual(result.failure, { kind: "pi-failed", stage: "pi", exit_code: 4, timed_out: false });
|
|
263
|
-
assert.
|
|
264
|
-
assert.equal(result.mutation_performed, true);
|
|
265
|
-
assert.match(String(result.next_action), /Re-query negotiated STATUS/);
|
|
266
|
-
assert.match(String(result.next_action), /exact same bound slot/);
|
|
267
|
-
assert.equal(harness.finalizeCalls, 0);
|
|
420
|
+
assert.match(String(result.next_action), /fresh STATUS/);
|
|
268
421
|
assert.equal(harness.statusCalls.length, 1, "no automatic relaunch after transport failure");
|
|
269
422
|
});
|
|
270
423
|
|
|
424
|
+
// gentle-pi#522 / #524: a submission Go refused at admission is a proven
|
|
425
|
+
// non-mutation. The model must see the refusal text, mutation_outcome none,
|
|
426
|
+
// and a continuation for the reoffered slot, never an unknown outcome that the
|
|
427
|
+
// contract forbids replaying.
|
|
428
|
+
test("an admission refusal reaches the model as a proven non-mutation carrying the refusal and a continuation", async (t) => {
|
|
429
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
430
|
+
const cwd = repository(t);
|
|
431
|
+
const lineageId = "relay-lineage";
|
|
432
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)])]);
|
|
433
|
+
const refusal = "Error: reviewer artifact admission binding_mismatch: reviewer result echoed a different artifact subject: the rejected admission did not consume the lens slot, so re-run the lens and invoke gentle-ai review capture-result again on the same lineage with a result that echoes the binding's top-level subject_hash [invalid_request]\n";
|
|
434
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
435
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", refusal.trim(), { exitCode: 1, stderr: refusal, elapsedMs: 40, timeoutMs: 120_000, mutationOutcome: "none" });
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
439
|
+
|
|
440
|
+
assert.equal(result.status, "blocked");
|
|
441
|
+
assert.equal(result.outcome, "pi-host-relay-transport-failure");
|
|
442
|
+
assert.deepEqual(result.failure, { kind: "submission-refused", stage: "submit", exit_code: 1, timed_out: false, elapsed_ms: 40, timeout_ms: 120_000, stderr: refusal });
|
|
443
|
+
assert.equal(result.reason, refusal.trim());
|
|
444
|
+
assert.equal(result.mutation_performed, false);
|
|
445
|
+
assert.equal(result.mutation_outcome, "none");
|
|
446
|
+
assert.match(String(result.next_action), /did not consume the lens slot/);
|
|
447
|
+
assert.match(String(result.next_action), /fresh STATUS/);
|
|
448
|
+
assert.equal(harness.statusCalls.length, 1, "a proven non-mutation needs no STATUS reconciliation and no relaunch");
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("a submission whose outcome is genuinely indeterminate still reconciles through STATUS and carries its evidence", async (t) => {
|
|
452
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
453
|
+
const cwd = repository(t);
|
|
454
|
+
const lineageId = "relay-lineage";
|
|
455
|
+
const pending = finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)]);
|
|
456
|
+
const harness = nativeHarness([pending, pending]);
|
|
457
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
458
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", "gentle-ai capture submission exceeded its 120000ms bound after 120004ms", { exitCode: null, stderr: "", timedOut: true, elapsedMs: 120_004, timeoutMs: 120_000 });
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
462
|
+
|
|
463
|
+
assert.equal(result.status, "reconciled");
|
|
464
|
+
assert.equal(result.outcome, "native-capture-outcome-unknown");
|
|
465
|
+
assert.deepEqual(result.failure, { kind: "submission-refused", stage: "submit", exit_code: null, timed_out: true, elapsed_ms: 120_004, timeout_ms: 120_000 });
|
|
466
|
+
assert.match(String(result.reason), /exceeded its 120000ms bound/);
|
|
467
|
+
assert.equal(harness.statusCalls.length, 2, "an indeterminate submission reconciles exactly once through STATUS");
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test("a relay timeout reports its one-slot measurements and no auto-follow", async (t) => {
|
|
471
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
472
|
+
const cwd = repository(t);
|
|
473
|
+
const lineageId = "relay-lineage";
|
|
474
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)])]);
|
|
475
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
476
|
+
throw new ReviewHostRelayError(
|
|
477
|
+
REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT,
|
|
478
|
+
"pi",
|
|
479
|
+
"pi reviewer subprocess exceeded the relay bound",
|
|
480
|
+
{ exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 },
|
|
481
|
+
);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
485
|
+
assert.equal(result.status, "blocked");
|
|
486
|
+
assert.equal(result.outcome, "pi-host-relay-timeout");
|
|
487
|
+
assert.deepEqual(result.failure, { kind: "pi-timed-out", stage: "pi", exit_code: null, timed_out: true, elapsed_ms: 2_256_004, timeout_ms: 2_256_000 });
|
|
488
|
+
assert.match(String(result.next_action), new RegExp(`${REVIEW_HOST_RELAY_PI_TIMEOUT_ENV}=<milliseconds>`));
|
|
489
|
+
assert.match(String(result.next_action), new RegExp(String(REVIEW_HOST_RELAY_PI_TIMEOUT_MAX_MS)));
|
|
490
|
+
assert.equal(harness.statusCalls.length, 1);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("a non-timeout transport failure keeps its generic continuation and now carries its measurements", async (t) => {
|
|
494
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
495
|
+
const cwd = repository(t);
|
|
496
|
+
const lineageId = "relay-lineage";
|
|
497
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-reliability", 0)])]);
|
|
498
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
499
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", "pi subprocess failed", { exitCode: 4, elapsedMs: 1_200, timeoutMs: 900_000 });
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
503
|
+
|
|
504
|
+
assert.equal(result.outcome, "pi-host-relay-transport-failure");
|
|
505
|
+
assert.deepEqual(result.failure, { kind: "pi-failed", stage: "pi", exit_code: 4, timed_out: false, elapsed_ms: 1_200, timeout_ms: 900_000 });
|
|
506
|
+
assert.match(String(result.next_action), /fresh STATUS/);
|
|
507
|
+
});
|
|
508
|
+
|
|
271
509
|
test("materialize tokens without a provider submission fail closed as a typed contract mismatch", async (t) => {
|
|
272
510
|
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
273
511
|
const cwd = repository(t);
|
|
@@ -279,7 +517,7 @@ test("materialize tokens without a provider submission fail closed as a typed co
|
|
|
279
517
|
return { promptByteLength: 1, resultByteLength: 1, submission: "{}" };
|
|
280
518
|
});
|
|
281
519
|
|
|
282
|
-
const result = await
|
|
520
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
283
521
|
|
|
284
522
|
assert.equal(relayCalls, 0, "the completing form is never synthesized, so nothing launches");
|
|
285
523
|
assert.equal(result.status, "blocked");
|
|
@@ -288,7 +526,6 @@ test("materialize tokens without a provider submission fail closed as a typed co
|
|
|
288
526
|
assert.equal(result.reason, REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE);
|
|
289
527
|
assert.equal(result.mutation_performed, false);
|
|
290
528
|
assert.equal(result.mutation_outcome, "none");
|
|
291
|
-
assert.equal(harness.finalizeCalls, 0);
|
|
292
529
|
assert.equal(harness.statusCalls.length, 1, "no relaunch and no post-capture STATUS after the contract mismatch");
|
|
293
530
|
});
|
|
294
531
|
|
|
@@ -309,7 +546,7 @@ test("collect inputs without the provider-issued materialize token never reach t
|
|
|
309
546
|
// The existing lane may fail on the synthetic projection; only the relay
|
|
310
547
|
// boundary is under test here: it must never be consulted.
|
|
311
548
|
try {
|
|
312
|
-
await
|
|
549
|
+
await runCapture(cwd, harness, lineageId);
|
|
313
550
|
} catch {
|
|
314
551
|
// Existing-lane behavior for this synthetic fixture is out of scope.
|
|
315
552
|
}
|