gentle-pi 2.2.0 → 2.3.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 +51 -80
- 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-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 +54 -248
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +22 -38
- 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/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 +1778 -3673
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +372 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +210 -68
- package/lib/review-integration-v2.ts +839 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +177 -66
- 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 +322 -890
- package/runtime/review-integration-v2.mjs +789 -257
- 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 +12 -13
- package/skills/_shared/review-ledger-contract.md +8 -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/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/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 +820 -131
- package/tests/gentle-theme.test.ts +133 -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 +28 -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 +37 -35
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- 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-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 +117 -82
- package/tests/review-host-relay.test.ts +242 -8
- package/tests/review-integration-v2-forward.test.ts +576 -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 +361 -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 +230 -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
|
@@ -17,11 +17,10 @@ test("recovery guidance documents the narrow published native maintenance contra
|
|
|
17
17
|
assert.match(README, /review dispose-result.*unsupported.*pending.*design/i);
|
|
18
18
|
assert.match(README, /RESET.*RECOVER.*destructive/i);
|
|
19
19
|
assert.match(README, /typed envelopes/i);
|
|
20
|
-
assert.match(README, /pre-commit.*pre-push.*pre-PR.*release/is);
|
|
21
20
|
});
|
|
22
21
|
|
|
23
22
|
test("controller help keeps authorization, blocked outcomes, and recovery boundaries explicit", () => {
|
|
24
|
-
assert.match(CONTROLLER, /
|
|
23
|
+
assert.match(CONTROLLER, /REPAIR_LEGACY_ALIAS.*freshly reads native inventory.*interactive approval/is);
|
|
25
24
|
assert.match(CONTROLLER, /unchanged_target,malformed_recovery_authorization/);
|
|
26
25
|
assert.match(CONTROLLER, /provider-selected recovery disposition/);
|
|
27
26
|
assert.match(CONTROLLER, /headlessly|headless/i);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
4
6
|
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
7
|
import { tmpdir } from "node:os";
|
|
6
8
|
import { join } from "node:path";
|
|
@@ -351,6 +353,66 @@ test("candidate view materializes exact tracked and initially-untracked content
|
|
|
351
353
|
view.cleanup();
|
|
352
354
|
});
|
|
353
355
|
|
|
356
|
+
test("candidate view preserves staged additions with explicit intended-untracked selection in its private index", (t) => {
|
|
357
|
+
const contributorRoot = repository(t);
|
|
358
|
+
writeFileSync(join(contributorRoot, "tracked.txt"), "tracked selection\n");
|
|
359
|
+
writeFileSync(join(contributorRoot, "staged-addition.txt"), "staged addition\n");
|
|
360
|
+
git(contributorRoot, "add", "staged-addition.txt");
|
|
361
|
+
git(contributorRoot, "update-index", "--split-index");
|
|
362
|
+
writeFileSync(join(contributorRoot, "selected.txt"), "selected\n");
|
|
363
|
+
writeFileSync(join(contributorRoot, "excluded.txt"), "excluded\n");
|
|
364
|
+
const indexBefore = readFileSync(join(contributorRoot, ".git", "index"));
|
|
365
|
+
const all = createCandidateView({ contributorRoot });
|
|
366
|
+
const excluded = createCandidateView({ contributorRoot, intendedUntracked: [] });
|
|
367
|
+
const selected = createCandidateView({ contributorRoot, intendedUntracked: ["selected.txt"] });
|
|
368
|
+
try {
|
|
369
|
+
assert.deepEqual(all.paths, ["excluded.txt", "selected.txt", "staged-addition.txt", "tracked.txt"]);
|
|
370
|
+
assert.deepEqual(excluded.paths, ["staged-addition.txt", "tracked.txt"]);
|
|
371
|
+
assert.deepEqual(selected.paths, ["selected.txt", "staged-addition.txt", "tracked.txt"]);
|
|
372
|
+
assert.equal(readFileSync(join(selected.root, "staged-addition.txt"), "utf8"), "staged addition\n");
|
|
373
|
+
assert.equal(lstatSync(join(selected.root, "excluded.txt"), { throwIfNoEntry: false }), undefined);
|
|
374
|
+
assert.deepEqual(readFileSync(join(contributorRoot, ".git", "index")), indexBefore);
|
|
375
|
+
} finally {
|
|
376
|
+
all.cleanup();
|
|
377
|
+
excluded.cleanup();
|
|
378
|
+
selected.cleanup();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("candidate view skips a shared index that disappears during stat or copy", (t) => {
|
|
383
|
+
const contributorRoot = repository(t);
|
|
384
|
+
writeFileSync(join(contributorRoot, "tracked.txt"), "tracked selection\n");
|
|
385
|
+
writeFileSync(join(contributorRoot, "staged-addition.txt"), "staged addition\n");
|
|
386
|
+
git(contributorRoot, "add", "staged-addition.txt");
|
|
387
|
+
git(contributorRoot, "update-index", "--split-index");
|
|
388
|
+
const sharedIndexName = readdirSync(join(contributorRoot, ".git")).find((name) => /^sharedindex\.[0-9a-f]+$/.test(name));
|
|
389
|
+
assert.ok(sharedIndexName, "split index must create a shared index fixture");
|
|
390
|
+
const sharedIndexPath = join(contributorRoot, ".git", sharedIndexName);
|
|
391
|
+
for (const phase of ["stat", "copy"] as const) {
|
|
392
|
+
const originalLstatSync = fs.lstatSync;
|
|
393
|
+
const originalCopyFileSync = fs.copyFileSync;
|
|
394
|
+
fs.lstatSync = ((path: string | Buffer, options?: Parameters<typeof lstatSync>[1]) => {
|
|
395
|
+
if (phase === "stat" && path === sharedIndexPath) throw Object.assign(new Error("shared index disappeared"), { code: "ENOENT" });
|
|
396
|
+
return originalLstatSync(path, options);
|
|
397
|
+
}) as typeof fs.lstatSync;
|
|
398
|
+
fs.copyFileSync = ((source: string | Buffer, destination: string | Buffer) => {
|
|
399
|
+
if (phase === "copy" && source === sharedIndexPath) throw Object.assign(new Error("shared index disappeared"), { code: "ENOENT" });
|
|
400
|
+
return originalCopyFileSync(source, destination);
|
|
401
|
+
}) as typeof fs.copyFileSync;
|
|
402
|
+
syncBuiltinESMExports();
|
|
403
|
+
let view: ReturnType<typeof createCandidateView> | undefined;
|
|
404
|
+
try {
|
|
405
|
+
view = createCandidateView({ contributorRoot, intendedUntracked: [] });
|
|
406
|
+
assert.equal(view.paths.includes("staged-addition.txt"), true, phase);
|
|
407
|
+
} finally {
|
|
408
|
+
fs.lstatSync = originalLstatSync;
|
|
409
|
+
fs.copyFileSync = originalCopyFileSync;
|
|
410
|
+
syncBuiltinESMExports();
|
|
411
|
+
view?.cleanup();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
354
416
|
test("candidate view recursively protects nested content and worktree metadata, and rejects injected untracked entries", (t) => {
|
|
355
417
|
const contributorRoot = repository(t);
|
|
356
418
|
mkdirSync(join(contributorRoot, "nested", "deeper"), { recursive: true });
|
|
@@ -395,6 +457,56 @@ test("candidate view registry rejects unsafe, moved, writable, stale, and unsele
|
|
|
395
457
|
registry.cleanup(view.token);
|
|
396
458
|
});
|
|
397
459
|
|
|
460
|
+
test("candidate registry isolates replay, projection, current, and cleanup state by target root plus lineage", (t) => {
|
|
461
|
+
const rootA = repository(t);
|
|
462
|
+
const rootB = repository(t);
|
|
463
|
+
writeFileSync(join(rootA, "tracked.txt"), "candidate A\n");
|
|
464
|
+
writeFileSync(join(rootB, "tracked.txt"), "candidate B\n");
|
|
465
|
+
const registry = new CandidateViewRegistry();
|
|
466
|
+
t.after(() => registry.cleanupAll());
|
|
467
|
+
const viewA = registry.createOrReuse({ contributorRoot: rootA, replayKey: "same-replay" });
|
|
468
|
+
const viewB = registry.createOrReuse({ contributorRoot: rootB, replayKey: "same-replay" });
|
|
469
|
+
assert.notEqual(viewA.token, viewB.token);
|
|
470
|
+
registry.bindCurrent({ token: viewA.token, lineageId: "same-lineage", selectedLenses: ["review-reliability"] });
|
|
471
|
+
registry.bindCurrent({ token: viewB.token, lineageId: "same-lineage", selectedLenses: ["review-reliability"] });
|
|
472
|
+
assert.equal(registry.resolveForLens("same-lineage", "review-reliability", rootA).root, viewA.root);
|
|
473
|
+
assert.equal(registry.resolveForLens("same-lineage", "review-reliability", rootB).root, viewB.root);
|
|
474
|
+
assert.equal(registry.resolveForFinalize("same-lineage", rootA).root, viewA.root);
|
|
475
|
+
assert.equal(registry.resolveForFinalize("same-lineage", rootB).root, viewB.root);
|
|
476
|
+
writeFileSync(join(rootA, "tracked.txt"), "corrected A\n");
|
|
477
|
+
writeFileSync(join(rootB, "tracked.txt"), "corrected B\n");
|
|
478
|
+
const correctedA = registry.createCorrected("same-lineage", rootA, "same-correction-replay");
|
|
479
|
+
const correctedB = registry.createCorrected("same-lineage", rootB, "same-correction-replay");
|
|
480
|
+
assert.notEqual(correctedA.token, correctedB.token);
|
|
481
|
+
registry.promoteCorrected("same-lineage", correctedA.token, rootA);
|
|
482
|
+
registry.promoteCorrected("same-lineage", correctedB.token, rootB);
|
|
483
|
+
assert.equal(registry.resolveForFinalize("same-lineage", rootA).root, correctedA.root);
|
|
484
|
+
assert.equal(registry.resolveForFinalize("same-lineage", rootB).root, correctedB.root);
|
|
485
|
+
assert.throws(() => registry.resolveForLens("same-lineage", "review-reliability"), /workspaceRoot/);
|
|
486
|
+
assert.throws(() => registry.resolveWorkspaceRoot("same-lineage"), /workspaceRoot/);
|
|
487
|
+
registry.cleanupTerminal("same-lineage", "approved", rootB);
|
|
488
|
+
assert.equal(registry.resolveWorkspaceRoot("same-lineage"), realpathSync(rootA));
|
|
489
|
+
assert.equal(registry.resolveForFinalize("same-lineage", rootA).root, correctedA.root);
|
|
490
|
+
registry.cleanupTerminal("same-lineage", "approved", rootA);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("candidate registry rejects a lineage target whose authorized symlink was replaced", (t) => {
|
|
494
|
+
const root = repository(t);
|
|
495
|
+
const replacement = repository(t);
|
|
496
|
+
const alias = join(tmpdir(), `gentle-pi-candidate-alias-${process.pid}-${Date.now()}`);
|
|
497
|
+
t.after(() => rmSync(alias, { recursive: true, force: true }));
|
|
498
|
+
symlinkSync(root, alias, "dir");
|
|
499
|
+
const registry = new CandidateViewRegistry();
|
|
500
|
+
t.after(() => registry.cleanupAll());
|
|
501
|
+
const view = registry.create({ contributorRoot: root });
|
|
502
|
+
registry.bindCurrent({ token: view.token, lineageId: "symlink-lineage", selectedLenses: ["review-reliability"] });
|
|
503
|
+
assert.doesNotThrow(() => registry.assertWorkspaceRoot("symlink-lineage", alias));
|
|
504
|
+
rmSync(alias, { recursive: true, force: true });
|
|
505
|
+
symlinkSync(replacement, alias, "dir");
|
|
506
|
+
assert.throws(() => registry.assertWorkspaceRoot("symlink-lineage", alias), /workspaceRoot|bound to/);
|
|
507
|
+
registry.cleanupTerminal("symlink-lineage", "approved", root);
|
|
508
|
+
});
|
|
509
|
+
|
|
398
510
|
test("review subagent dispatch rejects missing candidate views and uses the explicitly current overlapping lens", (t) => {
|
|
399
511
|
const missing = new CandidateViewRegistry();
|
|
400
512
|
assert.throws(
|
|
@@ -434,6 +546,16 @@ test("candidate view cleanup is confined and idempotent", (t) => {
|
|
|
434
546
|
assert.equal(lstatSync(view.root, { throwIfNoEntry: false }), undefined);
|
|
435
547
|
});
|
|
436
548
|
|
|
549
|
+
test("candidate cleanup removes a readonly root when Git reports success without deleting it", (t) => {
|
|
550
|
+
const registry = new CandidateViewRegistry((file, arguments_, options) =>
|
|
551
|
+
arguments_[0] === "worktree" && arguments_[1] === "remove" ? "" : execFileSync(file, arguments_, options));
|
|
552
|
+
const view = registry.create({ contributorRoot: repository(t) });
|
|
553
|
+
assert.equal(lstatSync(view.root).mode & 0o222, 0);
|
|
554
|
+
view.cleanup();
|
|
555
|
+
view.cleanup();
|
|
556
|
+
assert.equal(lstatSync(view.root, { throwIfNoEntry: false }), undefined);
|
|
557
|
+
});
|
|
558
|
+
|
|
437
559
|
test("corrected views stay within frozen scope and replace projections only when promoted", (t) => {
|
|
438
560
|
const contributorRoot = repository(t);
|
|
439
561
|
writeFileSync(join(contributorRoot, "tracked.txt"), "reviewed\n");
|
|
@@ -518,13 +640,16 @@ test("candidate view derives deletion, rename, executable, and symlink scope fro
|
|
|
518
640
|
const registry = new CandidateViewRegistry();
|
|
519
641
|
const view = registry.create({ contributorRoot });
|
|
520
642
|
try {
|
|
521
|
-
|
|
522
|
-
|
|
643
|
+
// The rename source (tracked.txt) is frozen as a deletion next to its
|
|
644
|
+
// destination: native STATUS projects the rename as both paths
|
|
645
|
+
// (gentle-pi#518), and the reviewer scope carries the same identity.
|
|
646
|
+
assert.deepEqual(view.paths, ["deleted.txt", "linked.sh", "renamed.txt", "script.sh", "tracked.txt"]);
|
|
647
|
+
assert.deepEqual(view.deletedPaths, ["deleted.txt", "tracked.txt"]);
|
|
523
648
|
assert.deepEqual(view.modes, { "linked.sh": "120000", "renamed.txt": "100644", "script.sh": "100755" });
|
|
524
649
|
registry.bind({ token: view.token, lineageId: "scope-kinds", selectedLenses: ["review-risk"] });
|
|
525
650
|
const dispatch = { agent: "review-risk", task: "review", mode: "task" };
|
|
526
651
|
injectReviewCandidateView(dispatch, registry);
|
|
527
|
-
assert.match(dispatch.task, /Frozen changed scope by mode: .*"deleted":\["deleted\.txt"\]/);
|
|
652
|
+
assert.match(dispatch.task, /Frozen changed scope by mode: .*"deleted":\["deleted\.txt","tracked\.txt"\]/);
|
|
528
653
|
assert.doesNotMatch(dispatch.task, /Frozen paths:|Frozen modes:/);
|
|
529
654
|
view.verify();
|
|
530
655
|
} finally {
|
|
@@ -532,6 +657,40 @@ test("candidate view derives deletion, rename, executable, and symlink scope fro
|
|
|
532
657
|
}
|
|
533
658
|
});
|
|
534
659
|
|
|
660
|
+
// gentle-pi#518: native STATUS projects every path whose state changed between
|
|
661
|
+
// the frozen trees (Go diffs with --no-renames), so a staged exact rename is
|
|
662
|
+
// its source deletion plus its destination addition. The candidate view must
|
|
663
|
+
// project the same identity or ordinary START is rejected before native
|
|
664
|
+
// admission with candidate-target-projection-drift.
|
|
665
|
+
test("candidate view projects a staged exact rename as its source and destination, matching the native no-renames projection", (t) => {
|
|
666
|
+
const contributorRoot = repository(t);
|
|
667
|
+
mkdirSync(join(contributorRoot, "active"));
|
|
668
|
+
writeFileSync(join(contributorRoot, "active", "document.md"), "line one\nline two\n");
|
|
669
|
+
git(contributorRoot, "add", "active/document.md");
|
|
670
|
+
git(contributorRoot, "-c", "user.name=Candidate Test", "-c", "user.email=candidate@example.invalid", "commit", "-m", "rename base");
|
|
671
|
+
mkdirSync(join(contributorRoot, "archive"));
|
|
672
|
+
git(contributorRoot, "mv", "active/document.md", "archive/document.md");
|
|
673
|
+
writeFileSync(join(contributorRoot, "report.md"), "report\n");
|
|
674
|
+
git(contributorRoot, "add", "-A");
|
|
675
|
+
const view = createCandidateView({ contributorRoot });
|
|
676
|
+
try {
|
|
677
|
+
const nativeProjection = git(contributorRoot, "diff-tree", "--no-commit-id", "--name-only", "-r", "-z", "--no-renames", view.baseTree, view.candidateTree)
|
|
678
|
+
.split("\0").filter((path) => path.length > 0).sort();
|
|
679
|
+
assert.deepEqual(nativeProjection, ["active/document.md", "archive/document.md", "report.md"]);
|
|
680
|
+
assert.deepEqual(view.paths, nativeProjection);
|
|
681
|
+
assert.deepEqual(view.deletedPaths, ["active/document.md"]);
|
|
682
|
+
assert.deepEqual(view.modes, { "archive/document.md": "100644", "report.md": "100644" });
|
|
683
|
+
const manifest = deriveChangedPathManifest(contributorRoot, view.baseTree, view.candidateTree);
|
|
684
|
+
assert.deepEqual(manifest.map((entry) => [entry.path, entry.status, entry.deleted]), [
|
|
685
|
+
["active/document.md", "D", true],
|
|
686
|
+
["archive/document.md", "A", false],
|
|
687
|
+
["report.md", "A", false],
|
|
688
|
+
]);
|
|
689
|
+
} finally {
|
|
690
|
+
view.cleanup();
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
|
|
535
694
|
test("candidate view verifies unchanged tree entries even when they are absent from changed scope", (t) => {
|
|
536
695
|
const contributorRoot = repository(t);
|
|
537
696
|
writeFileSync(join(contributorRoot, "unchanged.txt"), "base\n");
|
|
@@ -832,6 +991,30 @@ test("candidate views freeze gitlinks as immutable metadata without materializin
|
|
|
832
991
|
}
|
|
833
992
|
});
|
|
834
993
|
|
|
994
|
+
test("native projection reconstruction retains its selected untracked subset", (t) => {
|
|
995
|
+
const contributorRoot = repository(t);
|
|
996
|
+
writeFileSync(join(contributorRoot, "tracked.txt"), "tracked selection\n");
|
|
997
|
+
writeFileSync(join(contributorRoot, "selected.txt"), "selected\n");
|
|
998
|
+
writeFileSync(join(contributorRoot, "excluded.txt"), "excluded\n");
|
|
999
|
+
const source = new CandidateViewRegistry();
|
|
1000
|
+
const selected = source.create({ contributorRoot, intendedUntracked: ["selected.txt"] });
|
|
1001
|
+
const restored = new CandidateViewRegistry();
|
|
1002
|
+
try {
|
|
1003
|
+
const view = restored.restoreForFinalizeFromNative("selected-native", contributorRoot, {
|
|
1004
|
+
baseTree: selected.baseTree,
|
|
1005
|
+
currentCandidateTree: selected.candidateTree,
|
|
1006
|
+
paths: selected.paths,
|
|
1007
|
+
intendedUntracked: ["selected.txt"],
|
|
1008
|
+
projection: "workspace",
|
|
1009
|
+
});
|
|
1010
|
+
assert.deepEqual(view.paths, ["selected.txt", "tracked.txt"]);
|
|
1011
|
+
assert.equal(lstatSync(join(view.root, "excluded.txt"), { throwIfNoEntry: false }), undefined);
|
|
1012
|
+
view.cleanup();
|
|
1013
|
+
} finally {
|
|
1014
|
+
selected.cleanup();
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
|
|
835
1018
|
test("native projections reconstruct symlink, intended-untracked, and gitlink identity from Git objects", (t) => {
|
|
836
1019
|
const contributorRoot = repository(t);
|
|
837
1020
|
const baseTree = git(contributorRoot, "rev-parse", "HEAD^{tree}");
|
|
@@ -960,6 +1143,229 @@ test("fresh registries restore only one exact authoritative reviewing candidate
|
|
|
960
1143
|
}
|
|
961
1144
|
});
|
|
962
1145
|
|
|
1146
|
+
interface CandidateViewRegistryInternals {
|
|
1147
|
+
records: Map<string, unknown>;
|
|
1148
|
+
bindRecord: (token: string, lineageId: string, selectedLenses: readonly unknown[]) => unknown;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function bindingFailureRegistry(t: test.TestContext): { registry: CandidateViewRegistry; materializedRoot: () => string | undefined; internals: CandidateViewRegistryInternals } {
|
|
1152
|
+
let root: string | undefined;
|
|
1153
|
+
const registry = new CandidateViewRegistry((file, arguments_, options) => {
|
|
1154
|
+
if (arguments_[0] === "worktree" && arguments_[1] === "add") root = arguments_[arguments_.indexOf("--no-checkout") + 1];
|
|
1155
|
+
return execFileSync(file, arguments_, options);
|
|
1156
|
+
});
|
|
1157
|
+
t.after(() => registry.cleanupAll());
|
|
1158
|
+
return { registry, materializedRoot: () => root, internals: registry as unknown as CandidateViewRegistryInternals };
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
test("failed authoritative restores remove the inserted registry record and readonly worktree", (t) => {
|
|
1162
|
+
const contributorRoot = repository(t);
|
|
1163
|
+
writeFileSync(join(contributorRoot, "tracked.txt"), "reviewing\n");
|
|
1164
|
+
const source = new CandidateViewRegistry();
|
|
1165
|
+
const frozen = source.create({ contributorRoot });
|
|
1166
|
+
const state = {
|
|
1167
|
+
lineageId: "restore-bind-failure",
|
|
1168
|
+
contributorRoot,
|
|
1169
|
+
baseCommit: frozen.baseCommit,
|
|
1170
|
+
baseTree: frozen.baseTree,
|
|
1171
|
+
candidateTree: frozen.candidateTree,
|
|
1172
|
+
paths: frozen.paths,
|
|
1173
|
+
modes: frozen.modes,
|
|
1174
|
+
deletedPaths: frozen.deletedPaths,
|
|
1175
|
+
selectedLenses: ["review-reliability"],
|
|
1176
|
+
};
|
|
1177
|
+
const { registry, materializedRoot, internals } = bindingFailureRegistry(t);
|
|
1178
|
+
const originalBindRecord = internals.bindRecord;
|
|
1179
|
+
internals.bindRecord = (token) => {
|
|
1180
|
+
assert.equal(internals.records.has(token), true, "the failure must occur after insertion");
|
|
1181
|
+
throw new CandidateViewError("test-only bind failure");
|
|
1182
|
+
};
|
|
1183
|
+
try {
|
|
1184
|
+
assert.throws(
|
|
1185
|
+
() => registry.restoreCurrentFromAuthoritativeReviewingStates(contributorRoot, [state]),
|
|
1186
|
+
CandidateViewError,
|
|
1187
|
+
);
|
|
1188
|
+
} finally {
|
|
1189
|
+
internals.bindRecord = originalBindRecord;
|
|
1190
|
+
source.cleanup(frozen.token);
|
|
1191
|
+
}
|
|
1192
|
+
assert.equal(internals.records.size, 0);
|
|
1193
|
+
assert.ok(materializedRoot(), "the restore must have created a candidate worktree");
|
|
1194
|
+
assert.equal(existsSync(materializedRoot()!), false);
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
test("failed finalize restores remove the inserted registry record and readonly worktree", (t) => {
|
|
1198
|
+
const contributorRoot = repository(t);
|
|
1199
|
+
writeFileSync(join(contributorRoot, "tracked.txt"), "candidate\n");
|
|
1200
|
+
const source = new CandidateViewRegistry();
|
|
1201
|
+
const frozen = source.create({ contributorRoot });
|
|
1202
|
+
const { registry, materializedRoot, internals } = bindingFailureRegistry(t);
|
|
1203
|
+
const originalBindRecord = internals.bindRecord;
|
|
1204
|
+
internals.bindRecord = (token) => {
|
|
1205
|
+
assert.equal(internals.records.has(token), true, "the failure must occur after insertion");
|
|
1206
|
+
throw new CandidateViewError("test-only bind failure");
|
|
1207
|
+
};
|
|
1208
|
+
try {
|
|
1209
|
+
assert.throws(
|
|
1210
|
+
() => registry.restoreForFinalizeFromNative("finalize-bind-failure", contributorRoot, {
|
|
1211
|
+
baseTree: frozen.baseTree,
|
|
1212
|
+
currentCandidateTree: frozen.candidateTree,
|
|
1213
|
+
paths: frozen.paths,
|
|
1214
|
+
intendedUntracked: [],
|
|
1215
|
+
projection: "workspace",
|
|
1216
|
+
}),
|
|
1217
|
+
CandidateViewError,
|
|
1218
|
+
);
|
|
1219
|
+
} finally {
|
|
1220
|
+
internals.bindRecord = originalBindRecord;
|
|
1221
|
+
source.cleanup(frozen.token);
|
|
1222
|
+
}
|
|
1223
|
+
assert.equal(internals.records.size, 0);
|
|
1224
|
+
assert.ok(materializedRoot(), "the restore must have created a candidate worktree");
|
|
1225
|
+
assert.equal(existsSync(materializedRoot()!), false);
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
function materializationFailureRegistry(t: test.TestContext, failureAt: number): { registry: CandidateViewRegistry; roots: () => readonly string[]; removalAttempts: (root: string) => number; internals: CandidateViewRegistryInternals } {
|
|
1229
|
+
const roots: string[] = [];
|
|
1230
|
+
const removalAttempts = new Map<string, number>();
|
|
1231
|
+
let materializations = 0;
|
|
1232
|
+
let failPending = true;
|
|
1233
|
+
const registry = new CandidateViewRegistry((file, arguments_, options) => {
|
|
1234
|
+
if (arguments_[0] === "worktree" && arguments_[1] === "add") {
|
|
1235
|
+
const root = arguments_[arguments_.indexOf("--no-checkout") + 1];
|
|
1236
|
+
assert.equal(typeof root, "string", "worktree add must name its candidate root");
|
|
1237
|
+
roots.push(root);
|
|
1238
|
+
materializations += 1;
|
|
1239
|
+
}
|
|
1240
|
+
if (arguments_[0] === "worktree" && arguments_[1] === "remove") {
|
|
1241
|
+
const root = arguments_[arguments_.indexOf("--force") + 1];
|
|
1242
|
+
assert.equal(typeof root, "string", "worktree remove must name its candidate root");
|
|
1243
|
+
removalAttempts.set(root, (removalAttempts.get(root) ?? 0) + 1);
|
|
1244
|
+
}
|
|
1245
|
+
if (failPending && materializations === failureAt && options.cwd === roots.at(-1) && arguments_[0] === "read-tree") {
|
|
1246
|
+
failPending = false;
|
|
1247
|
+
throw Object.assign(new Error("test-only materialization failure"), { status: 1 });
|
|
1248
|
+
}
|
|
1249
|
+
return execFileSync(file, arguments_, options);
|
|
1250
|
+
});
|
|
1251
|
+
t.after(() => registry.cleanupAll());
|
|
1252
|
+
return { registry, roots: () => roots, removalAttempts: (root) => removalAttempts.get(root) ?? 0, internals: registry as unknown as CandidateViewRegistryInternals };
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
test("finalize restore removes a registered projection when its first materialization fails and retries cleanly", (t) => {
|
|
1256
|
+
const contributorRoot = repository(t);
|
|
1257
|
+
writeFileSync(join(contributorRoot, "tracked.txt"), "candidate\n");
|
|
1258
|
+
const source = new CandidateViewRegistry();
|
|
1259
|
+
const frozen = source.create({ contributorRoot });
|
|
1260
|
+
const lineageId = "finalize-materialization-failure";
|
|
1261
|
+
const descriptor = {
|
|
1262
|
+
baseTree: frozen.baseTree,
|
|
1263
|
+
currentCandidateTree: frozen.candidateTree,
|
|
1264
|
+
paths: frozen.paths,
|
|
1265
|
+
intendedUntracked: [],
|
|
1266
|
+
projection: "workspace" as const,
|
|
1267
|
+
};
|
|
1268
|
+
const baselineWorktrees = git(contributorRoot, "worktree", "list", "--porcelain");
|
|
1269
|
+
const { registry, roots, internals } = materializationFailureRegistry(t, 1);
|
|
1270
|
+
try {
|
|
1271
|
+
assert.throws(() => registry.restoreForFinalizeFromNative(lineageId, contributorRoot, descriptor), CandidateViewError);
|
|
1272
|
+
assert.equal(registry.hasProjection(lineageId, contributorRoot), false);
|
|
1273
|
+
assert.equal(internals.records.size, 0);
|
|
1274
|
+
assert.equal(git(contributorRoot, "worktree", "list", "--porcelain"), baselineWorktrees);
|
|
1275
|
+
assert.equal(existsSync(roots()[0]!), false);
|
|
1276
|
+
|
|
1277
|
+
const restored = registry.restoreForFinalizeFromNative(lineageId, contributorRoot, descriptor);
|
|
1278
|
+
assert.equal(registry.resolveForFinalize(lineageId, contributorRoot).token, restored.token);
|
|
1279
|
+
registry.cleanup(restored.token);
|
|
1280
|
+
} finally {
|
|
1281
|
+
source.cleanup(frozen.token);
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
test("finalize restore does not re-remove its first fallback record when the empty-untracked retry materialization fails", (t) => {
|
|
1286
|
+
const contributorRoot = repository(t);
|
|
1287
|
+
writeFileSync(join(contributorRoot, "tracked.txt"), "candidate\n");
|
|
1288
|
+
writeFileSync(join(contributorRoot, "excluded.txt"), "excluded\n");
|
|
1289
|
+
const source = new CandidateViewRegistry();
|
|
1290
|
+
const frozen = source.create({ contributorRoot, intendedUntracked: [] });
|
|
1291
|
+
const lineageId = "finalize-fallback-materialization-failure";
|
|
1292
|
+
const descriptor = {
|
|
1293
|
+
baseTree: frozen.baseTree,
|
|
1294
|
+
currentCandidateTree: frozen.candidateTree,
|
|
1295
|
+
paths: frozen.paths,
|
|
1296
|
+
intendedUntracked: [],
|
|
1297
|
+
projection: "workspace" as const,
|
|
1298
|
+
};
|
|
1299
|
+
const baselineWorktrees = git(contributorRoot, "worktree", "list", "--porcelain");
|
|
1300
|
+
const { registry, roots, removalAttempts, internals } = materializationFailureRegistry(t, 2);
|
|
1301
|
+
try {
|
|
1302
|
+
assert.throws(() => registry.restoreForFinalizeFromNative(lineageId, contributorRoot, descriptor), CandidateViewError);
|
|
1303
|
+
assert.equal(removalAttempts(roots()[0]!), 1, "the first mismatched record must receive exactly one physical worktree removal");
|
|
1304
|
+
assert.equal(registry.hasProjection(lineageId, contributorRoot), false);
|
|
1305
|
+
assert.equal(internals.records.size, 0);
|
|
1306
|
+
assert.equal(git(contributorRoot, "worktree", "list", "--porcelain"), baselineWorktrees);
|
|
1307
|
+
assert.equal(existsSync(roots()[0]!), false);
|
|
1308
|
+
assert.equal(existsSync(roots()[1]!), false);
|
|
1309
|
+
|
|
1310
|
+
const restored = registry.restoreForFinalizeFromNative(lineageId, contributorRoot, descriptor);
|
|
1311
|
+
assert.equal(registry.resolveForFinalize(lineageId, contributorRoot).token, restored.token);
|
|
1312
|
+
registry.cleanup(restored.token);
|
|
1313
|
+
} finally {
|
|
1314
|
+
source.cleanup(frozen.token);
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
test("dispatch restore removes a registered projection when materialization fails and records the hydration failure before retrying", (t) => {
|
|
1319
|
+
const contributorRoot = repository(t);
|
|
1320
|
+
writeFileSync(join(contributorRoot, "tracked.txt"), "candidate\n");
|
|
1321
|
+
const source = new CandidateViewRegistry();
|
|
1322
|
+
const frozen = source.create({ contributorRoot });
|
|
1323
|
+
const lineageId = "dispatch-materialization-failure";
|
|
1324
|
+
const descriptor = {
|
|
1325
|
+
baseTree: frozen.baseTree,
|
|
1326
|
+
currentCandidateTree: frozen.candidateTree,
|
|
1327
|
+
paths: frozen.paths,
|
|
1328
|
+
intendedUntracked: [],
|
|
1329
|
+
projection: "workspace" as const,
|
|
1330
|
+
};
|
|
1331
|
+
const baselineWorktrees = git(contributorRoot, "worktree", "list", "--porcelain");
|
|
1332
|
+
const { registry, roots, internals } = materializationFailureRegistry(t, 1);
|
|
1333
|
+
try {
|
|
1334
|
+
let failure: unknown;
|
|
1335
|
+
try {
|
|
1336
|
+
registry.restoreCurrentForDispatchFromNative(lineageId, contributorRoot, descriptor, ["review-reliability"]);
|
|
1337
|
+
} catch (error) {
|
|
1338
|
+
failure = error;
|
|
1339
|
+
}
|
|
1340
|
+
assert.ok(failure instanceof CandidateViewError);
|
|
1341
|
+
assert.equal(registry.hasProjection(lineageId, contributorRoot), false);
|
|
1342
|
+
assert.equal(registry.hasCurrentBinding(contributorRoot), false);
|
|
1343
|
+
assert.equal(internals.records.size, 0);
|
|
1344
|
+
assert.equal(git(contributorRoot, "worktree", "list", "--porcelain"), baselineWorktrees);
|
|
1345
|
+
assert.equal(existsSync(roots()[0]!), false);
|
|
1346
|
+
assert.deepEqual(registry.lastDispatchHydrationFailure(contributorRoot), {
|
|
1347
|
+
lineageId,
|
|
1348
|
+
reason: failure.reason,
|
|
1349
|
+
message: failure.message,
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
registry.restoreCurrentForDispatchFromNative(lineageId, contributorRoot, descriptor, ["review-reliability"]);
|
|
1353
|
+
const restored = registry.resolveCurrentForLens("review-reliability", contributorRoot);
|
|
1354
|
+
assert.equal(registry.lastDispatchHydrationFailure(contributorRoot), undefined);
|
|
1355
|
+
registry.cleanup(restored.token);
|
|
1356
|
+
} finally {
|
|
1357
|
+
source.cleanup(frozen.token);
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
test("candidate root resolution drift is exposed as a typed candidate-view error", () => {
|
|
1362
|
+
const missingRoot = join(tmpdir(), `gentle-pi-missing-root-${process.pid}-${Date.now()}`);
|
|
1363
|
+
assert.throws(
|
|
1364
|
+
() => new CandidateViewRegistry().hasCurrentBinding(missingRoot),
|
|
1365
|
+
(error: unknown) => error instanceof CandidateViewError && error.reason === "contributor-root-unresolvable",
|
|
1366
|
+
);
|
|
1367
|
+
});
|
|
1368
|
+
|
|
963
1369
|
test("live candidate drift blocks dispatch before candidate text can be injected", (t) => {
|
|
964
1370
|
const contributorRoot = repository(t);
|
|
965
1371
|
writeFileSync(join(contributorRoot, "tracked.txt"), "reviewed\n");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import * as nativeReviewCli from "../lib/native-review-cli.ts";
|
|
3
4
|
import {
|
|
4
5
|
CompactReviewContractError,
|
|
5
6
|
parseNativeCompactFinalizeInput,
|
|
@@ -8,22 +9,8 @@ import {
|
|
|
8
9
|
|
|
9
10
|
const REQUEST_HASH = "a".repeat(64);
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// payloads are retired — lens results are admitted natively, and the
|
|
14
|
-
// adversarial roles execute through Go-owned pi processes via
|
|
15
|
-
// provider-rendered self-contained vectors.
|
|
16
|
-
test("compact finalize parser rejects the retired Pi-authored payload fields", () => {
|
|
17
|
-
for (const retired of [
|
|
18
|
-
{ review_result: { lens_results: [] } },
|
|
19
|
-
{ refuter_batch: { schema: "gentle-ai.refuter-result-batch/v1", request_hash: REQUEST_HASH, results: [] } },
|
|
20
|
-
{ validation_proof: { original_criteria: { passed: true, evidence: ["ok"] }, correction_regression: { passed: true, evidence: ["ok"] } } },
|
|
21
|
-
]) {
|
|
22
|
-
assert.throws(
|
|
23
|
-
() => parseNativeCompactFinalizeInput({ cwd: "/repo", ...retired }),
|
|
24
|
-
(error: unknown) => error instanceof CompactReviewContractError && error.code === "unknown-key",
|
|
25
|
-
);
|
|
26
|
-
}
|
|
12
|
+
test("the native client exposes no legacy compact input parser", () => {
|
|
13
|
+
assert.equal("parseNativeCompactFinalizeInput" in nativeReviewCli, false);
|
|
27
14
|
});
|
|
28
15
|
|
|
29
16
|
test("compact finalize parser enforces the final evidence pairing and forecast range", () => {
|
|
@@ -46,8 +33,6 @@ test("native finalize preserves arbitrary non-empty evidence text byte-for-byte"
|
|
|
46
33
|
for (const outcome of ["passed", "verification_failed", "procedural_tooling_failed"] as const) {
|
|
47
34
|
assert.equal(parseNativeCompactFinalizeInput({ cwd: "/repo", final_evidence: evidence, final_verification_outcome: outcome }).final_verification_outcome, outcome);
|
|
48
35
|
}
|
|
49
|
-
assert.throws(() => parseNativeCompactFinalizeInput({ cwd: "/repo", final_evidence: evidence, final_verification_outcome: "failed" }), CompactReviewContractError);
|
|
50
|
-
assert.throws(() => parseNativeCompactFinalizeInput({ cwd: "/repo", final_evidence: evidence, final_verification_passed: true, final_verification_outcome: "passed" }), CompactReviewContractError);
|
|
51
36
|
assert.throws(() => parseNativeCompactFinalizeInput({ cwd: "/repo", final_evidence: "", final_verification_passed: true }), CompactReviewContractError);
|
|
52
37
|
});
|
|
53
38
|
|
|
@@ -61,21 +46,10 @@ test("targeted validation document keeps its strict provider-bound shape", () =>
|
|
|
61
46
|
follow_ups: [{ finding_id: "RISK-001", location: "lib/a.ts:1", summary: "Track the remaining cleanup", proof_refs: ["differential-test:covered"] }],
|
|
62
47
|
};
|
|
63
48
|
const parsed = parseNativeCompactFinalizeInput({ cwd: "/repo", validation, final_evidence: "full suite passed", final_verification_passed: true });
|
|
64
|
-
assert.deepEqual(parsed.validation, validation);
|
|
65
49
|
assert.deepEqual(toNativeValidatorDocument(parsed.validation!), {
|
|
66
50
|
original_criteria: validation.original_criteria,
|
|
67
51
|
correction_regression: validation.correction_regression,
|
|
68
52
|
follow_ups: [{ observation: "Track the remaining cleanup", proof_refs: ["differential-test:covered"] }],
|
|
69
53
|
});
|
|
70
|
-
|
|
71
|
-
for (const invalid of [
|
|
72
|
-
{ ...validation, request_hash: "not-a-digest" },
|
|
73
|
-
{ ...validation, fix_caused_findings: [{ id: "FIX-001" }] },
|
|
74
|
-
{ ...validation, original_criteria: { passed: true, evidence: [] } },
|
|
75
|
-
{ ...validation, correction_regression: { passed: "yes", evidence: ["ok"] } },
|
|
76
|
-
{ ...validation, follow_ups: [{ ...validation.follow_ups[0], proof_refs: [] }] },
|
|
77
|
-
{ ...validation, extra_field: true },
|
|
78
|
-
]) {
|
|
79
|
-
assert.throws(() => parseNativeCompactFinalizeInput({ cwd: "/repo", validation: invalid, final_evidence: "evidence", final_verification_passed: true }), CompactReviewContractError);
|
|
80
|
-
}
|
|
54
|
+
assert.throws(() => parseNativeCompactFinalizeInput({ cwd: "/repo", validation: { ...validation, request_hash: "not-a-digest" }, final_evidence: "evidence", final_verification_passed: true }), CompactReviewContractError);
|
|
81
55
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
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, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
@@ -47,7 +47,7 @@ function context(cwd: string): ExtensionContext {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function repository(t: test.TestContext): string {
|
|
50
|
-
const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-lock-status-"));
|
|
50
|
+
const cwd = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-lock-status-")));
|
|
51
51
|
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
52
52
|
execFileSync("git", ["init", "-b", "main"], { cwd });
|
|
53
53
|
writeFileSync(join(cwd, "app.ts"), "export const value = 1;\n");
|