gentle-pi 3.7.0 → 4.0.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 +37 -6
- package/assets/agents/gentle-ai-explore.md +4 -4
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -9
- package/assets/orchestrator-delegation.md +40 -41
- package/assets/orchestrator-memory.md +1 -22
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator.md +12 -24
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/gentle-shell.mjs +254 -29
- package/docs/delegated-verification.md +26 -1
- package/docs/gentle-agents-activity.md +24 -0
- package/docs/gentle-shell.md +92 -28
- package/docs/native-authority-architecture.md +2 -2
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +112 -206
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +26 -0
- package/extensions/child-safety.ts +23 -0
- package/extensions/gentle-agents.ts +429 -262
- package/extensions/gentle-ai.ts +781 -683
- package/extensions/gentle-shell.ts +1375 -88
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +18 -7
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1419 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/quiet-tools.ts +179 -87
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +16 -12
- package/extensions/startup-banner.ts +60 -31
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-protocol.ts +60 -0
- package/lib/agents-runner.ts +96 -98
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/bounded-writer-admission.ts +147 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/codemode-renderer.ts +185 -0
- package/lib/command-palette-catalog.ts +3 -9
- package/lib/command-palette.ts +25 -14
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-launcher.ts +24 -13
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +227 -0
- package/lib/native-review-cli.ts +49 -108
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +67 -17
- package/lib/review-candidate-view.ts +112 -25
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +156 -11
- package/lib/review-sidebar-state.ts +223 -0
- package/lib/selection-engine.ts +515 -0
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-worktree-registry.ts +14 -2
- package/lib/shell-bar.ts +179 -24
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +2 -1
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +78 -14
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +23 -13
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +66 -10
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +379 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +108 -0
- package/lib/visual-customize-view.ts +330 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/yolo-session-policy.ts +240 -0
- package/package.json +20 -8
- package/runtime/gentle-shell-launcher.mjs +23 -12
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/native-review-cli.mjs +49 -108
- package/runtime/review-risk-assessment.mjs +154 -9
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/gentle-ai-installer.mjs +14 -13
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/run-test-suite.mjs +76 -0
- package/scripts/test-packed-runner.mjs +31 -14
- package/scripts/verify-package-files.mjs +11 -22
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +9 -15
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-profiles.test.ts +18 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +40 -0
- package/tests/agents-runner.test.ts +378 -89
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/bounded-writer-admission.test.ts +95 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/card-style-policy.test.ts +55 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-safety.test.ts +82 -0
- package/tests/codemode-rendering.test.ts +491 -0
- package/tests/command-palette.test.ts +39 -3
- package/tests/delegated-key-learnings-contract.test.ts +0 -76
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +170 -2
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +54 -0
- package/tests/gentle-agents.test.ts +1047 -252
- package/tests/gentle-ai-binary.test.ts +3 -3
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +68 -54
- package/tests/gentle-ai-renderer.test.ts +487 -8
- package/tests/gentle-ai.test.ts +288 -65
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-shell-bin.test.ts +651 -114
- package/tests/gentle-shell-launcher.test.ts +99 -52
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell.test.ts +3839 -187
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +80 -8
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +29 -19
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/nan-provider.test.ts +471 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +6 -120
- package/tests/native-review-parity-runtime.test.ts +100 -3
- package/tests/odd-integration.test.ts +67 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
- package/tests/odd-routing-contract.test.ts +86 -35
- package/tests/orchestrator-budget.test.ts +14 -39
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +252 -115
- package/tests/packed-runner-owned-path.test.ts +46 -0
- package/tests/persona-single-channel.test.ts +6 -6
- package/tests/provider-defect-handoff.test.ts +3 -11
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +409 -184
- package/tests/rdd-aware-verification-contract.test.ts +76 -1
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +176 -12
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +298 -0
- package/tests/review-contract-prompt.test.ts +108 -43
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-routing.test.ts +611 -5
- package/tests/review-controller-workspace-root.test.ts +163 -4
- package/tests/review-host-relay-routing.test.ts +338 -2
- package/tests/review-integration-v2-forward.test.ts +200 -0
- package/tests/review-ledger-contract.test.ts +10 -34
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +498 -6
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/run-test-suite.test.ts +124 -0
- package/tests/runtime-harness.mjs +145 -786
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/selection-engine.test.ts +421 -0
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-worktree-registry.test.ts +77 -0
- package/tests/shell-bar.test.ts +382 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +52 -0
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-sidebar-layout.test.ts +325 -21
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +87 -1
- package/tests/shell-usage-view.test.ts +27 -0
- package/tests/shell-usage.test.ts +73 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/startup-banner.test.ts +130 -2
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/telemetry-trigger.test.ts +81 -20
- package/tests/theme-customization.test.ts +72 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +110 -0
- package/tests/visual-customize-view.test.ts +418 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/yolo-customize.test.ts +256 -0
- package/tests/yolo-mode-runtime.test.ts +161 -0
- package/tests/yolo-mode.test.ts +261 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
+
import { renderShellSidebarBar, type ShellBarModel, type ShellBarTheme } from "../lib/shell-bar.ts";
|
|
6
|
+
import { createReviewSidebarPublisher, REVIEW_SCOPE_UNAVAILABLE, REVIEW_SIDEBAR_LABELS, reviewSidebarSnapshot } from "../lib/review-sidebar-state.ts";
|
|
7
|
+
import { __testing } from "../extensions/gentle-ai.ts";
|
|
8
|
+
import { NATIVE_REVIEW_ERROR_CODE, NATIVE_REVIEW_OPERATION, NativeReviewCliError, type NativeReviewCli } from "../lib/native-review-cli.ts";
|
|
9
|
+
import { decodeReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
10
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
|
|
12
|
+
function toolContext(context: ExtensionContext): ExtensionToolContext {
|
|
13
|
+
return {
|
|
14
|
+
...context,
|
|
15
|
+
tools: [],
|
|
16
|
+
async executeTool() {
|
|
17
|
+
assert.fail("Sidebar publication must not execute nested tools");
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Display contract only: lifecycle evidence must be normalized by the producer,
|
|
23
|
+
// never inferred from a successful tool execution by the renderer.
|
|
24
|
+
const theme: ShellBarTheme = { fg: (_color, text) => text, bold: (text) => text };
|
|
25
|
+
type ReviewDisplay = { state: "reviewing" | "in_review" | "approved" | "closed" | "unavailable"; scope: string };
|
|
26
|
+
function model(review?: ReviewDisplay): ShellBarModel & { review?: ReviewDisplay } {
|
|
27
|
+
return {
|
|
28
|
+
cwd: "/repo",
|
|
29
|
+
branch: "main",
|
|
30
|
+
dirty: undefined,
|
|
31
|
+
sessionName: undefined,
|
|
32
|
+
modelId: "test-model",
|
|
33
|
+
effort: undefined,
|
|
34
|
+
contextPercent: null,
|
|
35
|
+
contextWindow: 0,
|
|
36
|
+
costTotal: 0,
|
|
37
|
+
subscription: false,
|
|
38
|
+
usage: undefined,
|
|
39
|
+
statuses: [],
|
|
40
|
+
review,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("RDD maps explicit native evidence conservatively without parsing opaque bindings", () => {
|
|
45
|
+
const nativeStatus = (state: string, paths: unknown = ["src/app.ts", "test/app.test.ts", "README.md"]) => ({
|
|
46
|
+
result: {
|
|
47
|
+
schema: "gentle-ai.review-integration.status/v9",
|
|
48
|
+
authority: { state }, projection: { paths }, next_transition: { kind: "collect" },
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
assert.deepEqual(reviewSidebarSnapshot("status", nativeStatus("reviewing")), { state: "in_review", scope: "app.ts +2 files" });
|
|
52
|
+
assert.equal(reviewSidebarSnapshot("status", nativeStatus("reviewing", ["src/app.ts", "test/app.test.ts"])).scope, "app.ts +1 file");
|
|
53
|
+
for (const [state, expected] of [["approved", "approved"], ["correction_required", "correction"], ["invalidated", "invalidated"], ["validating", "in_review"]] as const) {
|
|
54
|
+
assert.equal(reviewSidebarSnapshot("status", nativeStatus(state)).state, expected);
|
|
55
|
+
}
|
|
56
|
+
const closure = { status: "closed", outcome: "native-last-event-closure", closure: { schema: "gentle-ai.review-last-event-closure/v1", state: "approved" } };
|
|
57
|
+
assert.equal(reviewSidebarSnapshot("gentle_review_capture", closure).state, "approved");
|
|
58
|
+
assert.equal(reviewSidebarSnapshot("gentle_review_capture_group", closure).state, "approved");
|
|
59
|
+
assert.equal(reviewSidebarSnapshot("gentle_review_capture_group", { status: "blocked", outcome: "reviewer-model-run-forecast" }).state, "forecast");
|
|
60
|
+
const burned = { operation: "acknowledge-approved", status: "closed", outcome: "native-approved-acknowledgement-completed", authority: "burned" };
|
|
61
|
+
assert.equal(reviewSidebarSnapshot("acknowledge-approved", burned).state, "closed");
|
|
62
|
+
for (const missing of ["operation", "status", "outcome", "authority"]) {
|
|
63
|
+
const incomplete: Record<string, unknown> = { ...burned };
|
|
64
|
+
delete incomplete[missing];
|
|
65
|
+
assert.notEqual(reviewSidebarSnapshot("acknowledge-approved", incomplete).state, "closed");
|
|
66
|
+
}
|
|
67
|
+
assert.notEqual(reviewSidebarSnapshot("gentle_review_capture", burned).state, "closed");
|
|
68
|
+
assert.equal(reviewSidebarSnapshot("answer-consent", { outcome: "consent-declined-this-candidate" }).state, "declined");
|
|
69
|
+
assert.equal(reviewSidebarSnapshot("start", { outcome: "native-review-consent-required" }).state, "consent");
|
|
70
|
+
assert.deepEqual(reviewSidebarSnapshot("inspect", {
|
|
71
|
+
operation: "answer-consent", result: { action: "created", state: "reviewing" },
|
|
72
|
+
actor_binding: { candidate_paths: ["lib/app.ts"] },
|
|
73
|
+
}), { state: "in_review", scope: "app.ts" });
|
|
74
|
+
assert.equal(reviewSidebarSnapshot("status", nativeStatus("reviewing", ["src\\app.ts"])).scope, "app.ts");
|
|
75
|
+
assert.equal(reviewSidebarSnapshot("status", { ...nativeStatus("approved"), native_failure: {} }).state, "unavailable");
|
|
76
|
+
assert.equal(reviewSidebarSnapshot("status", { result: { ...nativeStatus("approved").result, next_transition: { kind: "stop" } } }).state, "unavailable");
|
|
77
|
+
for (const paths of [null, [], [null], ["/"]]) {
|
|
78
|
+
assert.equal(reviewSidebarSnapshot("status", nativeStatus("reviewing", paths)).scope, "Candidate scope unavailable");
|
|
79
|
+
}
|
|
80
|
+
assert.deepEqual(reviewSidebarSnapshot("status", { status: "closed", collectBinding: "opaque", title: "Invented title" }), { state: "unknown", scope: "Candidate scope unavailable" });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
for (const action of ["created", "resumed", "replayed"]) {
|
|
85
|
+
test(`RDD treats ${action} START results as pending review, not active execution`, () => {
|
|
86
|
+
for (const operation of ["start", "answer-consent", "select-intended-untracked"]) {
|
|
87
|
+
for (const state of ["reviewing", "validating"]) {
|
|
88
|
+
assert.deepEqual(reviewSidebarSnapshot(operation, {
|
|
89
|
+
operation, result: { action, state },
|
|
90
|
+
actor_binding: { candidate_paths: ["lib/app.ts"] },
|
|
91
|
+
}), { state: "in_review", scope: "app.ts" });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test("RDD completed STATUS evidence does not imply capture execution", () => {
|
|
98
|
+
for (const state of ["reviewing", "validating"]) {
|
|
99
|
+
for (const kind of ["collect", "execute"]) {
|
|
100
|
+
const snapshot = reviewSidebarSnapshot("status", {
|
|
101
|
+
result: {
|
|
102
|
+
schema: "gentle-ai.review-integration.status/v9",
|
|
103
|
+
authority: { state }, projection: { paths: ["lib/app.ts"] },
|
|
104
|
+
next_transition: { kind },
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
assert.deepEqual(snapshot, { state: "in_review", scope: "app.ts" });
|
|
108
|
+
const pending = renderShellSidebarBar({ ...model(), review: snapshot }, theme, 46).join("\n");
|
|
109
|
+
assert.match(pending, /Review in progress/);
|
|
110
|
+
assert.doesNotMatch(pending, /Reviewers running/);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// The future publisher owns execution evidence; this slice only renders its
|
|
114
|
+
// explicit display state, never inferring it from a completed native result.
|
|
115
|
+
const active = renderShellSidebarBar(model({ state: "reviewing", scope: "app.ts" }), theme, 46).join("\n");
|
|
116
|
+
assert.match(active, /Reviewers running…/);
|
|
117
|
+
assert.doesNotMatch(active, /Review in progress/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("publisher retains scope only for issued capture bindings and matching closure", async () => {
|
|
121
|
+
const events: Array<{ snapshot: { state: string; scope: string } }> = [];
|
|
122
|
+
const publisher = createReviewSidebarPublisher({ events: { emit: (_name: string, event: typeof events[number]) => events.push(event) } } as unknown as ExtensionAPI);
|
|
123
|
+
const ctx = { cwd: "/repo", sessionManager: { getSessionId: () => "session" } } as unknown as ExtensionContext;
|
|
124
|
+
publisher.reset(ctx);
|
|
125
|
+
const run = (name: string, params: Record<string, unknown>, details: unknown) => publisher.tool({
|
|
126
|
+
name, label: "Test", description: "Test", parameters: { type: "object" } as never,
|
|
127
|
+
async execute() { return { content: [], details }; },
|
|
128
|
+
}).execute("call", params as never, undefined, undefined, toolContext(ctx));
|
|
129
|
+
const status = { result: { schema: "gentle-ai.review-integration.status/v9", authority: { state: "reviewing", lineage_id: "lineage" }, target_identity: "target", applicability: "current_target", projection: { paths: ["src/app.ts"] }, next_transition: { kind: "collect" } }, collectBindings: [{ collectBinding: "issued" }] };
|
|
130
|
+
await run("gentle_review", { operation: "status", lineageId: "lineage" }, status);
|
|
131
|
+
assert.deepEqual(events.at(-1)?.snapshot, { state: "in_review", scope: "app.ts" });
|
|
132
|
+
await run("gentle_review_capture", { lineageId: "lineage", collectBinding: "issued" }, { outcome: "reviewer-model-run-forecast" });
|
|
133
|
+
assert.deepEqual(events.at(-1)?.snapshot, { state: "forecast", scope: "app.ts" });
|
|
134
|
+
await run("gentle_review_capture", { lineageId: "lineage", collectBinding: "wrong" }, { outcome: "reviewer-model-run-forecast" });
|
|
135
|
+
assert.equal(events.at(-1)?.snapshot.scope, "Candidate scope unavailable");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("nested raw workspaceRoot loses only display correlation against canonical cwd", async () => {
|
|
139
|
+
const h = publisherFixture();
|
|
140
|
+
await h.seed();
|
|
141
|
+
await h.run("gentle_review_capture", { lineageId: "lineage", collectBinding: "first", workspaceRoot: "/repo/nested" }, { outcome: "reviewer-model-run-forecast" });
|
|
142
|
+
assert.deepEqual(h.snapshot(), { state: "forecast", scope: REVIEW_SCOPE_UNAVAILABLE });
|
|
143
|
+
await h.seed();
|
|
144
|
+
await h.run("gentle_review_capture", { lineageId: "lineage", collectBinding: "first", workspaceRoot: "/repo" }, { outcome: "reviewer-model-run-forecast" });
|
|
145
|
+
assert.deepEqual(h.snapshot(), { state: "forecast", scope: "app.ts" });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("real facade STATUS binding retains sidebar scope through capture forecast", async () => {
|
|
149
|
+
const raw = JSON.parse(readFileSync(new URL("./fixtures/devbinary/status-v5-capture-result-submission.captured.json", import.meta.url), "utf8"));
|
|
150
|
+
raw.action = "stop";
|
|
151
|
+
const status = decodeReviewStatusV3(raw);
|
|
152
|
+
const lineageId = status.authority!.lineageId;
|
|
153
|
+
const native = { targetStatus: async () => status } as unknown as NativeReviewCli;
|
|
154
|
+
const listed = await __testing.executeReviewControllerOperation({ operation: "status", lineageId }, process.cwd(), native, undefined, null);
|
|
155
|
+
const binding = (listed.collectBindings as Array<{ collectBinding: string }>)[0].collectBinding;
|
|
156
|
+
const h = publisherFixture();
|
|
157
|
+
await h.run("gentle_review", { operation: "status", lineageId }, listed);
|
|
158
|
+
assert.deepEqual(h.snapshot(), { state: "in_review", scope: "add.js" });
|
|
159
|
+
const params = { lineageId, collectBinding: binding };
|
|
160
|
+
const forecast = await __testing.executeReviewCaptureOperation(params, process.cwd(), native, undefined, null);
|
|
161
|
+
assert.equal(forecast.outcome, "reviewer-model-run-forecast");
|
|
162
|
+
await h.run("gentle_review_capture", params, forecast);
|
|
163
|
+
assert.deepEqual(h.snapshot(), { state: "forecast", scope: "add.js" });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("publisher ignores late completions after reset without changing results", async () => {
|
|
167
|
+
const events: unknown[] = [];
|
|
168
|
+
const publisher = createReviewSidebarPublisher({ events: { emit: (_name: string, event: unknown) => events.push(event) } } as unknown as ExtensionAPI);
|
|
169
|
+
const ctx = { cwd: "/repo", sessionManager: { getSessionId: () => "session" } } as unknown as ExtensionContext;
|
|
170
|
+
let finish!: () => void;
|
|
171
|
+
const pending = new Promise<void>((resolve) => { finish = resolve; });
|
|
172
|
+
const result = { content: [], details: { status: "blocked" } };
|
|
173
|
+
const tool = publisher.tool({ name: "gentle_review", label: "Test", description: "Test", parameters: { type: "object" } as never, async execute() { await pending; return result; } });
|
|
174
|
+
publisher.reset(ctx);
|
|
175
|
+
const call = tool.execute("call", { operation: "status" } as never, undefined, undefined, toolContext(ctx));
|
|
176
|
+
publisher.reset(ctx);
|
|
177
|
+
finish();
|
|
178
|
+
assert.equal(await call, result);
|
|
179
|
+
assert.equal(events.length, 1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
function publisherFixture() {
|
|
183
|
+
const events: Array<{ sessionId: string; snapshot: { state: string; scope: string } }> = [];
|
|
184
|
+
const publisher = createReviewSidebarPublisher({ events: { emit: (_name: string, event: typeof events[number]) => events.push(event) } } as unknown as ExtensionAPI);
|
|
185
|
+
const ctx = { cwd: "/repo", sessionManager: { getSessionId: () => "own" } } as unknown as ExtensionContext;
|
|
186
|
+
publisher.reset(ctx);
|
|
187
|
+
const run = (name: string, params: Record<string, unknown>, details: unknown | Promise<unknown>, context = ctx) => publisher.tool({
|
|
188
|
+
name, label: "Test", description: "Test", parameters: { type: "object" } as never,
|
|
189
|
+
async execute() { return { content: [], details: await details }; },
|
|
190
|
+
}).execute("call", params as never, undefined, undefined, toolContext(context));
|
|
191
|
+
const status = { result: { schema: "gentle-ai.review-integration.status/v9", authority: { state: "reviewing", lineage_id: "lineage" }, target_identity: "target", applicability: "current_target", projection: { paths: ["src/app.ts"] }, next_transition: { kind: "collect" } }, collectBindings: [{ collectBinding: "first" }, { collectBinding: "second" }] };
|
|
192
|
+
const seed = () => run("gentle_review", { operation: "status", lineageId: "lineage" }, status);
|
|
193
|
+
const snapshot = () => events.at(-1)!.snapshot;
|
|
194
|
+
const closure = { tool: "gentle_review_capture", status: "closed", outcome: "native-last-event-closure", lineage_id: "lineage", closure: { schema: "gentle-ai.review-last-event-closure/v1", lineage_id: "lineage", target_identity: "target", state: "approved" } };
|
|
195
|
+
const acknowledged = { operation: "acknowledge-approved", status: "closed", outcome: "native-approved-acknowledgement-completed", lineage_id: "lineage", target_identity: "target", authority: "burned" };
|
|
196
|
+
const captured = { tool: "gentle_review_capture", status: "captured", outcome: "native-reviewer-result-captured", lineage_id: "lineage", host_relay: { transport: "pi_host_relay", lens: "review-risk", order: "0", subject_hash: "sha256:fixture", prompt_bytes: 10, result_bytes: 20, submission: '{"state":"approved","authority":"burned"}' } };
|
|
197
|
+
return { run, seed, snapshot, publisher, ctx, events, closure, acknowledged, captured };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const name of ["gentle_review_capture", "gentle_review_capture_group"]) {
|
|
201
|
+
test(`${name} retains issued scope through forecast, active capture, closure and matching acknowledgement`, async () => {
|
|
202
|
+
const h = publisherFixture();
|
|
203
|
+
await h.seed();
|
|
204
|
+
assert.deepEqual(h.snapshot(), { state: "in_review", scope: "app.ts" });
|
|
205
|
+
const params = { lineageId: "lineage", ...(name.endsWith("group") ? { collectBindings: ["first", "second"] } : { collectBinding: "first" }) };
|
|
206
|
+
await h.run(name, params, { outcome: "reviewer-model-run-forecast" });
|
|
207
|
+
assert.deepEqual(h.snapshot(), { state: "forecast", scope: "app.ts" });
|
|
208
|
+
let finish!: (value: unknown) => void;
|
|
209
|
+
const pending = h.run(name, { ...params, reviewerRunAcknowledged: true }, new Promise((resolve) => { finish = resolve; }));
|
|
210
|
+
try {
|
|
211
|
+
assert.deepEqual(h.snapshot(), { state: "reviewing", scope: "app.ts" });
|
|
212
|
+
} finally {
|
|
213
|
+
finish({ ...h.closure, tool: name });
|
|
214
|
+
await pending;
|
|
215
|
+
}
|
|
216
|
+
assert.deepEqual(h.snapshot(), { state: "approved", scope: "app.ts" });
|
|
217
|
+
await h.run("gentle_review", { operation: "acknowledge-approved", lineageId: "lineage" }, h.acknowledged);
|
|
218
|
+
assert.deepEqual(h.snapshot(), { state: "closed", scope: "app.ts" });
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
test("separate issued captures retain only remaining binding until terminal closure", async () => {
|
|
223
|
+
const h = publisherFixture();
|
|
224
|
+
await h.seed();
|
|
225
|
+
for (const binding of ["first", "second"]) {
|
|
226
|
+
await h.run("gentle_review_capture", { lineageId: "lineage", collectBinding: binding }, h.captured);
|
|
227
|
+
assert.deepEqual(h.snapshot(), { state: "in_review", scope: "app.ts" });
|
|
228
|
+
}
|
|
229
|
+
await h.run("gentle_review_capture", { lineageId: "lineage", collectBinding: "first" }, h.captured);
|
|
230
|
+
assert.deepEqual(h.snapshot(), { state: "unknown", scope: "Candidate scope unavailable" });
|
|
231
|
+
await h.seed();
|
|
232
|
+
await h.run("gentle_review_capture_group", { lineageId: "lineage", collectBindings: ["first", "second"] }, { ...h.closure, tool: "gentle_review_capture_group" });
|
|
233
|
+
await h.run("gentle_review_capture", { lineageId: "lineage", collectBinding: "second" }, h.captured);
|
|
234
|
+
assert.deepEqual(h.snapshot(), { state: "unknown", scope: "Candidate scope unavailable" });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("terminal closure requires its own matching identity despite matching wrapper", async () => {
|
|
238
|
+
const h = publisherFixture();
|
|
239
|
+
const params = { lineageId: "lineage", collectBinding: "first" };
|
|
240
|
+
for (const closure of [
|
|
241
|
+
{ ...h.closure.closure, lineage_id: "other" },
|
|
242
|
+
{ ...h.closure.closure, target_identity: "other" },
|
|
243
|
+
{ ...h.closure.closure, lineage_id: undefined },
|
|
244
|
+
{ ...h.closure.closure, target_identity: undefined },
|
|
245
|
+
]) {
|
|
246
|
+
await h.seed();
|
|
247
|
+
await h.run("gentle_review_capture", params, { ...h.closure, closure });
|
|
248
|
+
assert.deepEqual(h.snapshot(), { state: "approved", scope: "Candidate scope unavailable" });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("nonterminal capture requires exact issued input and well-formed matching output", async () => {
|
|
253
|
+
const h = publisherFixture();
|
|
254
|
+
const bound = { lineageId: "lineage", collectBinding: "first", reviewerRunAcknowledged: true };
|
|
255
|
+
for (const params of [
|
|
256
|
+
{ lineageId: "lineage" }, { ...bound, collectBinding: "unissued" },
|
|
257
|
+
{ ...bound, lineageId: "other" }, { ...bound, workspaceRoot: "/other" },
|
|
258
|
+
]) {
|
|
259
|
+
await h.seed();
|
|
260
|
+
await h.run("gentle_review_capture", params, h.captured);
|
|
261
|
+
assert.deepEqual(h.snapshot(), { state: "unknown", scope: "Candidate scope unavailable" });
|
|
262
|
+
}
|
|
263
|
+
for (const details of [
|
|
264
|
+
{ ...h.captured, lineage_id: "other" }, { ...h.captured, target_identity: "other" },
|
|
265
|
+
{ ...h.captured, target_identity: null }, { ...h.captured, lineage_id: undefined },
|
|
266
|
+
{ ...h.captured, status: "closed" }, { ...h.captured, outcome: "unspecified" },
|
|
267
|
+
{ ...h.captured, tool: "unrelated" }, { ...h.captured, host_relay: null },
|
|
268
|
+
{ ...h.captured, host_relay: { ...h.captured.host_relay, transport: "other" } },
|
|
269
|
+
{ ...h.captured, host_relay: { ...h.captured.host_relay, prompt_bytes: "10" } },
|
|
270
|
+
{ ...h.captured, host_relay: { ...h.captured.host_relay, result_bytes: -1 } },
|
|
271
|
+
{ ...h.captured, host_relay: { ...h.captured.host_relay, submission: {} } },
|
|
272
|
+
{ ...h.captured, failure: { reason: "failed" } }, { status: "captured" },
|
|
273
|
+
]) {
|
|
274
|
+
await h.seed();
|
|
275
|
+
await h.run("gentle_review_capture", bound, details);
|
|
276
|
+
assert.equal(h.snapshot().scope, "Candidate scope unavailable");
|
|
277
|
+
assert.ok(["unknown", "unavailable"].includes(h.snapshot().state));
|
|
278
|
+
}
|
|
279
|
+
await h.seed();
|
|
280
|
+
await h.run("gentle_review_capture", bound, h.captured);
|
|
281
|
+
assert.deepEqual(h.snapshot(), { state: "in_review", scope: "app.ts" }, "opaque submission must not imply closure");
|
|
282
|
+
await h.run("gentle_review_capture", bound, h.captured);
|
|
283
|
+
assert.deepEqual(h.snapshot(), { state: "unknown", scope: "Candidate scope unavailable" }, "completed capture consumes display binding");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("unbound operations, mismatched acknowledgements, failures and resets discard stale identity", async () => {
|
|
287
|
+
const h = publisherFixture();
|
|
288
|
+
for (const params of [
|
|
289
|
+
{ lineageId: "lineage" }, { lineageId: "lineage", collectBinding: "{}" },
|
|
290
|
+
{ lineageId: "other", collectBinding: "first" },
|
|
291
|
+
{ lineageId: "lineage", collectBinding: "first", workspaceRoot: "/other" },
|
|
292
|
+
{ lineageId: "lineage", collectBindings: ["second", "first"] },
|
|
293
|
+
]) {
|
|
294
|
+
await h.seed();
|
|
295
|
+
const name = "collectBindings" in params ? "gentle_review_capture_group" : "gentle_review_capture";
|
|
296
|
+
const pending = h.run(name, params, { status: "blocked", outcome: "capture-binding-rejected" });
|
|
297
|
+
assert.equal(h.snapshot().scope, "Candidate scope unavailable");
|
|
298
|
+
await pending;
|
|
299
|
+
assert.equal(h.snapshot().scope, "Candidate scope unavailable");
|
|
300
|
+
}
|
|
301
|
+
for (const operation of ["inspect", "start", "status"]) {
|
|
302
|
+
await h.seed();
|
|
303
|
+
const pending = h.run("gentle_review", { operation, lineageId: "lineage" }, { status: "blocked" });
|
|
304
|
+
assert.deepEqual(h.snapshot(), { state: "checking", scope: "Candidate scope unavailable" });
|
|
305
|
+
await pending;
|
|
306
|
+
}
|
|
307
|
+
for (const mismatch of [{ ...h.acknowledged, lineage_id: "other" }, { ...h.acknowledged, target_identity: "other" }]) {
|
|
308
|
+
await h.seed();
|
|
309
|
+
await h.run("gentle_review", { operation: "acknowledge-approved", lineageId: "lineage" }, mismatch);
|
|
310
|
+
assert.deepEqual(h.snapshot(), { state: "closed", scope: "Candidate scope unavailable" });
|
|
311
|
+
}
|
|
312
|
+
await h.seed();
|
|
313
|
+
await assert.rejects(h.run("gentle_review_capture", { lineageId: "lineage", collectBinding: "first" }, Promise.reject(new Error("capture failed"))), /capture failed/);
|
|
314
|
+
assert.deepEqual(h.snapshot(), { state: "unavailable", scope: "Candidate scope unavailable" });
|
|
315
|
+
await h.run("gentle_review_capture", { lineageId: "lineage", collectBinding: "first" }, h.closure);
|
|
316
|
+
assert.equal(h.snapshot().scope, "Candidate scope unavailable");
|
|
317
|
+
await h.seed();
|
|
318
|
+
h.publisher.reset(h.ctx);
|
|
319
|
+
await h.run("gentle_review_capture", { lineageId: "lineage", collectBinding: "first" }, h.closure);
|
|
320
|
+
assert.equal(h.snapshot().scope, "Candidate scope unavailable");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("cancellation errors remain unknown while other failures are unavailable, regardless of the signal", async () => {
|
|
324
|
+
const h = publisherFixture();
|
|
325
|
+
const abortError = () => Object.assign(new Error("Review controller operation was cancelled"), { name: "AbortError" });
|
|
326
|
+
const cases: Array<{ error: Error; aborted: boolean; state: string }> = [
|
|
327
|
+
{ error: abortError(), aborted: true, state: "unknown" },
|
|
328
|
+
{ error: new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, NATIVE_REVIEW_OPERATION.STATUS, true, false, "native process was cancelled"), aborted: true, state: "unknown" },
|
|
329
|
+
// An abort that races an ordinary failure (for example a later authorization denial) is not a cancellation.
|
|
330
|
+
{ error: new Error("destructive review operation was not authorized"), aborted: true, state: "unavailable" },
|
|
331
|
+
{ error: new Error("execution stopped"), aborted: false, state: "unavailable" },
|
|
332
|
+
];
|
|
333
|
+
for (const { error, aborted, state } of cases) {
|
|
334
|
+
await h.seed();
|
|
335
|
+
const controller = new AbortController();
|
|
336
|
+
if (aborted) controller.abort();
|
|
337
|
+
const tool = h.publisher.tool({ name: "gentle_review", label: "Test", description: "Test", parameters: { type: "object" } as never,
|
|
338
|
+
async execute() { throw error; },
|
|
339
|
+
});
|
|
340
|
+
await assert.rejects(tool.execute("call", { operation: "status" } as never, controller.signal, undefined, toolContext(h.ctx)), (caught) => caught === error);
|
|
341
|
+
assert.deepEqual(h.snapshot(), { state, scope: REVIEW_SCOPE_UNAVAILABLE });
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("foreign sessions, disabled publisher and event failures never change tool outcomes", async () => {
|
|
346
|
+
const h = publisherFixture();
|
|
347
|
+
await h.seed();
|
|
348
|
+
const before = h.events.length;
|
|
349
|
+
const foreign = { cwd: "/repo", sessionManager: { getSessionId: () => "foreign" } } as unknown as ExtensionContext;
|
|
350
|
+
const result = await h.run("gentle_review", { operation: "status" }, { status: "blocked" }, foreign);
|
|
351
|
+
assert.deepEqual(result.details, { status: "blocked" });
|
|
352
|
+
assert.equal(h.events.length, before);
|
|
353
|
+
h.publisher.reset();
|
|
354
|
+
await h.run("gentle_review", { operation: "status" }, { status: "blocked" });
|
|
355
|
+
assert.equal(h.events.length, before);
|
|
356
|
+
const throwing = createReviewSidebarPublisher({ events: { emit: () => { throw Error("display failed"); } } } as unknown as ExtensionAPI);
|
|
357
|
+
throwing.reset(h.ctx);
|
|
358
|
+
const tool = throwing.tool({ name: "gentle_review", label: "Test", description: "Test", parameters: { type: "object" } as never, async execute() { return result; } });
|
|
359
|
+
assert.equal(await tool.execute("call", { operation: "status" } as never, undefined, undefined, toolContext(h.ctx)), result);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("RDD labels say what is happening and who acts", () => {
|
|
363
|
+
assert.deepEqual(REVIEW_SIDEBAR_LABELS, {
|
|
364
|
+
checking: "Updating…",
|
|
365
|
+
reviewing: "Reviewers running…",
|
|
366
|
+
in_review: "Review in progress",
|
|
367
|
+
forecast: "Preparing reviewers…",
|
|
368
|
+
ready: "Not reviewed yet",
|
|
369
|
+
consent: "Needs your consent",
|
|
370
|
+
correction: "Fixing findings…",
|
|
371
|
+
approved: "Approved · finalizing…",
|
|
372
|
+
closed: "✓ Approved",
|
|
373
|
+
declined: "Skipped for this change",
|
|
374
|
+
invalidated: "Outdated · code changed",
|
|
375
|
+
unavailable: "Review unavailable",
|
|
376
|
+
unknown: "Status unknown",
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("RDD stays hidden without current-session evidence and occupies one group between Changes and Integrations", () => {
|
|
381
|
+
assert.doesNotMatch(renderShellSidebarBar(model(), theme, 46).join("\n"), /RDD/);
|
|
382
|
+
const text = renderShellSidebarBar(model({ state: "reviewing", scope: "app.ts +2 files" }), theme, 46).join("\n");
|
|
383
|
+
assert.equal((text.match(/RDD/g) ?? []).length, 1);
|
|
384
|
+
assert.ok(text.indexOf("Changes") < text.indexOf("RDD"));
|
|
385
|
+
assert.ok(text.indexOf("RDD") < text.indexOf("Integrations"));
|
|
386
|
+
assert.match(text, /Reviewers running…/);
|
|
387
|
+
assert.match(text, /app\.ts \+2 files/);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("RDD distinguishes approval still finalizing from confirmed closure and wraps narrow scopes", () => {
|
|
391
|
+
const approved = renderShellSidebarBar(model({ state: "approved", scope: REVIEW_SCOPE_UNAVAILABLE }), theme, 60).join("\n");
|
|
392
|
+
assert.match(approved, /Approved · finalizing…/);
|
|
393
|
+
assert.doesNotMatch(approved, /✓ Approved/);
|
|
394
|
+
assert.doesNotMatch(approved, /Candidate scope unavailable/);
|
|
395
|
+
const closed = renderShellSidebarBar(model({ state: "closed", scope: "app.ts" }), theme, 46).join("\n");
|
|
396
|
+
assert.match(closed, /✓ Approved/);
|
|
397
|
+
assert.doesNotMatch(closed, /finalizing/);
|
|
398
|
+
for (const width of [20, 32, 46]) {
|
|
399
|
+
const lines = renderShellSidebarBar(model({ state: "reviewing", scope: "長い候補ファイル-name-with-many-characters.ts +2 files" }), theme, width);
|
|
400
|
+
assert.ok(lines.every((line) => visibleWidth(line) <= width), `RDD must fit ${width} columns`);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import { DEFAULT_STAGES, runTestSuite } from "../scripts/run-test-suite.mjs";
|
|
9
|
+
|
|
10
|
+
// `pnpm test` chains its three stages; #1285 requires that a stage-1 failure
|
|
11
|
+
// never suppresses the later stages. These tests cover the runner's
|
|
12
|
+
// orchestration contract with fake stage implementations, so no real test
|
|
13
|
+
// process is spawned.
|
|
14
|
+
|
|
15
|
+
function fakeRunStage(codes) {
|
|
16
|
+
let index = 0;
|
|
17
|
+
return async (stage) => {
|
|
18
|
+
const code = codes[index++] ?? 0;
|
|
19
|
+
return { name: stage.name, code };
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("default stages cover unit tests, provider contract, and runtime harness", () => {
|
|
24
|
+
assert.deepEqual(
|
|
25
|
+
DEFAULT_STAGES.map((stage) => stage.name),
|
|
26
|
+
["unit-tests", "provider-contract", "runtime-harness"],
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("every stage runs even when an earlier stage fails", async () => {
|
|
31
|
+
const run = fakeRunStage([1, 0, 1]);
|
|
32
|
+
const results = await runTestSuite(DEFAULT_STAGES, { runStageImpl: run, write: () => {} });
|
|
33
|
+
assert.deepEqual(
|
|
34
|
+
results,
|
|
35
|
+
[
|
|
36
|
+
{ name: "unit-tests", code: 1 },
|
|
37
|
+
{ name: "provider-contract", code: 0 },
|
|
38
|
+
{ name: "runtime-harness", code: 1 },
|
|
39
|
+
],
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("summary reports each stage outcome and names the failures", async () => {
|
|
44
|
+
const lines: string[] = [];
|
|
45
|
+
await runTestSuite(DEFAULT_STAGES, {
|
|
46
|
+
runStageImpl: fakeRunStage([1, 0, 0]),
|
|
47
|
+
write: (line: string) => lines.push(line),
|
|
48
|
+
});
|
|
49
|
+
const summary = lines.slice(lines.findIndex((line) => line.includes("=== test suite summary ===")));
|
|
50
|
+
assert.ok(summary.includes("FAIL unit-tests"));
|
|
51
|
+
assert.ok(summary.includes("PASS provider-contract"));
|
|
52
|
+
assert.ok(summary.includes("PASS runtime-harness"));
|
|
53
|
+
assert.ok(summary.includes("1 stage(s) failed: unit-tests"));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("all-pass summary reports success", async () => {
|
|
57
|
+
const lines: string[] = [];
|
|
58
|
+
await runTestSuite(DEFAULT_STAGES, {
|
|
59
|
+
runStageImpl: fakeRunStage([0, 0, 0]),
|
|
60
|
+
write: (line: string) => lines.push(line),
|
|
61
|
+
});
|
|
62
|
+
assert.ok(lines.includes("all stages passed"));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// The CLI branch is what `pnpm test` actually executes, so its exit contract
|
|
66
|
+
// is covered with a real child process and a temporary stages JSON file.
|
|
67
|
+
|
|
68
|
+
const runnerPath = join(dirname(fileURLToPath(import.meta.url)), "..", "scripts", "run-test-suite.mjs");
|
|
69
|
+
|
|
70
|
+
function writeStagesFile(t: test.TestContext, stages: Array<{ name: string; command: string }>): string {
|
|
71
|
+
const dir = mkdtempSync(join(tmpdir(), "gentle-pi-run-test-suite-"));
|
|
72
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
73
|
+
const stagesPath = join(dir, "stages.json");
|
|
74
|
+
writeFileSync(stagesPath, JSON.stringify(stages));
|
|
75
|
+
return stagesPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test("direct invocation exits 1 and names the failed stage when a stage fails", (t) => {
|
|
79
|
+
const stagesPath = writeStagesFile(t, [
|
|
80
|
+
{ name: "failing-stage", command: "node -e \"process.exit(1)\"" },
|
|
81
|
+
{ name: "ok-stage", command: "node -e \"process.exit(0)\"" },
|
|
82
|
+
]);
|
|
83
|
+
const result = spawnSync(process.execPath, [runnerPath, stagesPath], { encoding: "utf8" });
|
|
84
|
+
assert.equal(result.status, 1);
|
|
85
|
+
assert.match(result.stdout ?? "", /FAIL failing-stage/);
|
|
86
|
+
assert.match(result.stdout ?? "", /1 stage\(s\) failed: failing-stage/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("direct invocation exits 0 when every stage passes", (t) => {
|
|
90
|
+
const stagesPath = writeStagesFile(t, [
|
|
91
|
+
{ name: "first", command: "node -e \"process.exit(0)\"" },
|
|
92
|
+
{ name: "second", command: "node -e \"process.exit(0)\"" },
|
|
93
|
+
]);
|
|
94
|
+
const result = spawnSync(process.execPath, [runnerPath, stagesPath], { encoding: "utf8" });
|
|
95
|
+
assert.equal(result.status, 0);
|
|
96
|
+
assert.match(result.stdout ?? "", /all stages passed/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("direct invocation still runs stages when reached through a file symlink", (t) => {
|
|
100
|
+
const dir = mkdtempSync(join(tmpdir(), "gentle-pi-run-test-suite-"));
|
|
101
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
102
|
+
const linkPath = join(dir, "runner-link.mjs");
|
|
103
|
+
try {
|
|
104
|
+
symlinkSync(runnerPath, linkPath);
|
|
105
|
+
} catch {
|
|
106
|
+
t.skip("symlinks unavailable on this filesystem");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const stagesPath = writeStagesFile(t, [{ name: "only", command: "node -e \"process.exit(0)\"" }]);
|
|
110
|
+
const result = spawnSync(process.execPath, [linkPath, stagesPath], { encoding: "utf8" });
|
|
111
|
+
assert.equal(result.status, 0);
|
|
112
|
+
assert.match(result.stdout ?? "", /PASS only/);
|
|
113
|
+
assert.match(result.stdout ?? "", /all stages passed/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("direct invocation rejects a malformed stages file with a non-zero exit", (t) => {
|
|
117
|
+
const dir = mkdtempSync(join(tmpdir(), "gentle-pi-run-test-suite-"));
|
|
118
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
119
|
+
const stagesPath = join(dir, "stages.json");
|
|
120
|
+
writeFileSync(stagesPath, JSON.stringify([{ name: "missing-command" }]));
|
|
121
|
+
const result = spawnSync(process.execPath, [runnerPath, stagesPath], { encoding: "utf8" });
|
|
122
|
+
assert.equal(result.status, 1);
|
|
123
|
+
assert.match(result.stderr ?? "", /stages file must be an array/);
|
|
124
|
+
});
|