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
|
@@ -95,9 +95,9 @@ async function writeWindowsSourceBinary(packageRoot: string): Promise<{ binaryPa
|
|
|
95
95
|
await writeFile(manifestPath, `${JSON.stringify({
|
|
96
96
|
version: GENTLE_AI_VERSION,
|
|
97
97
|
method: "go-sumdb-source-build",
|
|
98
|
-
package: "github.com/gentleman-programming/gentle-ai/
|
|
99
|
-
module: "github.com/gentleman-programming/gentle-ai/
|
|
100
|
-
tag: "
|
|
98
|
+
package: "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
|
|
99
|
+
module: "github.com/gentleman-programming/gentle-ai/v4",
|
|
100
|
+
tag: "v4.0.0",
|
|
101
101
|
architecture: process.arch === "x64" ? "x64" : "arm64",
|
|
102
102
|
binarySha256: createHash("sha256").update(binary).digest("hex"),
|
|
103
103
|
moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
GENTLE_AI_TIMING_ENTRY,
|
|
5
|
+
GentleAiElapsedTimingLedger,
|
|
6
|
+
parseGentleAiTimingData,
|
|
7
|
+
readGentleAiTimings,
|
|
8
|
+
type GentleAiTimingEntry,
|
|
9
|
+
} from "../lib/gentle-ai-elapsed-store.ts";
|
|
10
|
+
|
|
11
|
+
function entry(data: unknown): GentleAiTimingEntry {
|
|
12
|
+
return { type: "custom", customType: GENTLE_AI_TIMING_ENTRY, data };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test("timing entry parsing accepts well-formed records and rejects the rest", () => {
|
|
16
|
+
assert.deepEqual(parseGentleAiTimingData({ toolCallId: "c1", startedAt: 1 }), { toolCallId: "c1", startedAt: 1 });
|
|
17
|
+
assert.deepEqual(parseGentleAiTimingData({ toolCallId: "c1", startedAt: 1, endedAt: 2 }), { toolCallId: "c1", startedAt: 1, endedAt: 2 });
|
|
18
|
+
for (const bad of [undefined, null, "x", 5, [], {}, { startedAt: 1 }, { toolCallId: "" }, { toolCallId: "c1" }, { toolCallId: "c1", startedAt: "x" }, { toolCallId: "c1", startedAt: Number.NaN }, { toolCallId: "c1", startedAt: 1, endedAt: "x" }]) {
|
|
19
|
+
assert.equal(parseGentleAiTimingData(bad), undefined, JSON.stringify(bad) ?? String(bad));
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("replay keeps the last timing entry per tool call and ignores foreign entries", () => {
|
|
24
|
+
const entries: GentleAiTimingEntry[] = [
|
|
25
|
+
{ type: "message", customType: undefined, data: null },
|
|
26
|
+
{ type: "custom", customType: "gentle-pi.session-worktree/v1", data: { root: "/x" } },
|
|
27
|
+
entry({ toolCallId: "a", startedAt: 1 }),
|
|
28
|
+
entry({ toolCallId: "a", startedAt: 1, endedAt: 9 }),
|
|
29
|
+
entry({ toolCallId: "b", startedAt: 2 }),
|
|
30
|
+
entry({ toolCallId: "a", startedAt: "corrupt" }),
|
|
31
|
+
];
|
|
32
|
+
const timings = readGentleAiTimings(entries);
|
|
33
|
+
assert.deepEqual(timings.get("a"), { toolCallId: "a", startedAt: 1, endedAt: 9 }, "the corrupt last entry is ignored, not merged");
|
|
34
|
+
assert.deepEqual(timings.get("b"), { toolCallId: "b", startedAt: 2 });
|
|
35
|
+
assert.equal(timings.size, 2);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("the ledger persists start and end once each and skips ends without a durable start", () => {
|
|
39
|
+
const appended: Array<{ type: string; data?: unknown }> = [];
|
|
40
|
+
const host = { appendEntry: (type: string, data?: unknown) => appended.push({ type, data }) };
|
|
41
|
+
let stored: GentleAiTimingEntry[] = [];
|
|
42
|
+
const session = { getEntries: () => stored };
|
|
43
|
+
const ledger = new GentleAiElapsedTimingLedger(session, host);
|
|
44
|
+
ledger.recordEnd("call-1", 500);
|
|
45
|
+
assert.equal(appended.length, 0, "a terminal without a durable start has nothing honest to freeze");
|
|
46
|
+
ledger.recordStart("call-1", 1_000);
|
|
47
|
+
ledger.recordStart("call-1", 2_000);
|
|
48
|
+
assert.equal(appended.length, 1, "a repeated start stage does not append again");
|
|
49
|
+
stored = appended.map((append) => ({ type: "custom", customType: append.type, data: append.data }));
|
|
50
|
+
ledger.recordEnd("call-1", 31_000);
|
|
51
|
+
ledger.recordEnd("call-1", 32_000);
|
|
52
|
+
stored = appended.map((append) => ({ type: "custom", customType: append.type, data: append.data }));
|
|
53
|
+
assert.equal(appended.length, 2, "a repeated end stage does not append again");
|
|
54
|
+
assert.deepEqual(appended.map((append) => append.data), [
|
|
55
|
+
{ toolCallId: "call-1", startedAt: 1_000 },
|
|
56
|
+
{ toolCallId: "call-1", startedAt: 1_000, endedAt: 31_000 },
|
|
57
|
+
]);
|
|
58
|
+
assert.deepEqual(ledger.lookup("call-1"), { toolCallId: "call-1", startedAt: 1_000, endedAt: 31_000 });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("a fresh ledger restores timings recorded by a previous one", () => {
|
|
62
|
+
const appended: Array<{ type: string; data?: unknown }> = [];
|
|
63
|
+
const first = new GentleAiElapsedTimingLedger({ getEntries: () => [] }, { appendEntry: (type, data) => appended.push({ type, data }) });
|
|
64
|
+
first.recordStart("call-1", 1_000);
|
|
65
|
+
const stored: GentleAiTimingEntry[] = appended.map((append) => ({ type: "custom", customType: append.type, data: append.data }));
|
|
66
|
+
const second = new GentleAiElapsedTimingLedger({ getEntries: () => stored });
|
|
67
|
+
assert.deepEqual(second.lookup("call-1"), { toolCallId: "call-1", startedAt: 1_000 });
|
|
68
|
+
});
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
GENTLE_AI_PENDING_DIGEST,
|
|
13
13
|
INSTALLER_VERSION,
|
|
14
14
|
GENTLE_AI_RELEASE_ASSETS,
|
|
15
|
+
GENTLE_AI_WINDOWS_SOURCE_MODULE,
|
|
15
16
|
GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|
|
16
17
|
downloadGentleAiAsset,
|
|
17
18
|
gentleAiAssetForm,
|
|
@@ -21,18 +22,20 @@ import {
|
|
|
21
22
|
trustedSystemExtractor,
|
|
22
23
|
} from "../scripts/gentle-ai-installer.mjs";
|
|
23
24
|
|
|
24
|
-
//
|
|
25
|
+
// v4.0.0 archive digests independently match the minisign-signed release
|
|
25
26
|
// checksums; binary digests were computed from the extracted executables.
|
|
26
27
|
const EXPECTED_ASSETS = {
|
|
27
|
-
"darwin/amd64": { name: "gentle-
|
|
28
|
-
"darwin/arm64": { name: "gentle-
|
|
29
|
-
"linux/amd64": { name: "gentle-
|
|
30
|
-
"linux/arm64": { name: "gentle-
|
|
28
|
+
"darwin/amd64": { name: "gentle-ai_4.0.0_darwin_amd64.tar.gz", sha256: "b5b74f22b38ec3339b38e8c68f797dc76ff12ed6580826a6e425d5c718da80c1", binarySha256: "d4a5b16ff70e65331e17a62356941bb0c75ecb9dbe3a0d98a6b54cfbd76cd6b0" },
|
|
29
|
+
"darwin/arm64": { name: "gentle-ai_4.0.0_darwin_arm64.tar.gz", sha256: "d2159caf6d68f367b18830ece6af71ef26963d5f5320d7df6a794773f45cc7e9", binarySha256: "18a9f7fae55d85c95684b6d512a4a148d0cb24a856325f72573c34caf65159eb" },
|
|
30
|
+
"linux/amd64": { name: "gentle-ai_4.0.0_linux_amd64.tar.gz", sha256: "5f4417cf29c969c86da4799942fd673368840901be1bb09c779a12d7ed6096ea", binarySha256: "50ba217b5138c1a9c7d5bf2f79931b1bb89b89c4cf650dcd7ee037657c88158d" },
|
|
31
|
+
"linux/arm64": { name: "gentle-ai_4.0.0_linux_arm64.tar.gz", sha256: "1383b040c95cfc69206660d73c21907b14ad70ab913f410917c54f43d3147e57", binarySha256: "6703704f0c4a5b70c36fbdc44db641e810871cab16bc28040d06aa1d10704ad3" },
|
|
31
32
|
} as const;
|
|
32
33
|
|
|
33
|
-
test("published Gentle AI
|
|
34
|
-
assert.equal(INSTALLER_VERSION, "
|
|
35
|
-
|
|
34
|
+
test("published Gentle AI v4.0.0 is the installer pin", () => {
|
|
35
|
+
assert.equal(INSTALLER_VERSION, "4.0.0");
|
|
36
|
+
// v4.0.0 moved the Go module path to the /v4 major-version suffix.
|
|
37
|
+
assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE, "github.com/gentleman-programming/gentle-ai/v4");
|
|
38
|
+
assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM, "h1:pZ/XZ2Pk3U9lgXigOTY62zlxxFOHnc9CjQhLgaV/Hfc=");
|
|
36
39
|
});
|
|
37
40
|
|
|
38
41
|
test("default installer package root is the package containing scripts, not its parent", () => {
|
|
@@ -43,15 +46,15 @@ test("default installer package root is the package containing scripts, not its
|
|
|
43
46
|
assert.notEqual(resolveGentleAiInstallerPackageRoot(), dirname(expectedPackageRoot));
|
|
44
47
|
});
|
|
45
48
|
|
|
46
|
-
test("release mapping selects only the supported official
|
|
49
|
+
test("release mapping selects only the supported official v4.0.0 assets and pinned digests", () => {
|
|
47
50
|
assert.deepEqual(
|
|
48
51
|
Object.fromEntries(Object.entries(GENTLE_AI_RELEASE_ASSETS).map(([key, asset]) => [key, { name: asset.name, sha256: asset.sha256, binarySha256: asset.binarySha256 }])),
|
|
49
52
|
EXPECTED_ASSETS,
|
|
50
53
|
);
|
|
51
|
-
assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-
|
|
52
|
-
assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-
|
|
54
|
+
assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_4.0.0_linux_amd64.tar.gz");
|
|
55
|
+
assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_4.0.0_darwin_arm64.tar.gz");
|
|
53
56
|
for (const asset of Object.values(GENTLE_AI_RELEASE_ASSETS)) {
|
|
54
|
-
assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/
|
|
57
|
+
assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v4\.0\.0\//);
|
|
55
58
|
}
|
|
56
59
|
});
|
|
57
60
|
|
|
@@ -61,7 +64,7 @@ test("raw release assets are admitted only under a prerelease pin", () => {
|
|
|
61
64
|
assert.equal(gentleAiAssetForm("gentle-ai_2.5.0-rc.3_windows_amd64.exe", "2.5.0-rc.3"), "raw-binary");
|
|
62
65
|
// A raw binary under a stable pin means the pin itself is wrong: stable
|
|
63
66
|
// releases publish signed archives only, so this fails closed pre-download.
|
|
64
|
-
assert.throws(() => gentleAiAssetForm("gentle-
|
|
67
|
+
assert.throws(() => gentleAiAssetForm("gentle-ai_4.0.0_linux_amd64", "4.0.0"), /only admitted for a prerelease pin/);
|
|
65
68
|
assert.throws(() => gentleAiAssetForm("gentle-ai.dmg", "2.5.0-rc.3"), /unsupported Gentle AI release asset form/);
|
|
66
69
|
// The current stable pin admits every pinned asset row through the same
|
|
67
70
|
// gate the installer uses at download time (default installerVersion
|
|
@@ -88,7 +91,7 @@ test("release digests are all-or-none and install fails closed while any digest
|
|
|
88
91
|
}),
|
|
89
92
|
/checksum mismatch/,
|
|
90
93
|
);
|
|
91
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "
|
|
94
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai")), false);
|
|
92
95
|
}
|
|
93
96
|
});
|
|
94
97
|
|
|
@@ -116,8 +119,8 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
|
|
|
116
119
|
let goExecutable = "go";
|
|
117
120
|
const metadata = [
|
|
118
121
|
"gentle-ai.exe: go1.25.10",
|
|
119
|
-
"\tpath\tgithub.com/gentleman-programming/gentle-ai/
|
|
120
|
-
`\tmod\tgithub.com/gentleman-programming/gentle-ai/
|
|
122
|
+
"\tpath\tgithub.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
|
|
123
|
+
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.0.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
|
|
121
124
|
"\tbuild\t-buildmode=exe", "\tbuild\t-compiler=gc", "\tbuild\tCGO_ENABLED=0", `\tbuild\tGOARCH=${fixtureOptions.goArchitecture ?? "amd64"}`, "\tbuild\tGOOS=windows",
|
|
122
125
|
].join("\n");
|
|
123
126
|
const run = async (file: string, arguments_: string[], options: WindowsGoCall["options"]) => {
|
|
@@ -135,7 +138,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
|
|
|
135
138
|
return { stdout: "", stderr: "" };
|
|
136
139
|
}
|
|
137
140
|
if (file === goExecutable && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
|
|
138
|
-
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai
|
|
141
|
+
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 4.0.0\n", stderr: "" };
|
|
139
142
|
throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
|
|
140
143
|
};
|
|
141
144
|
return { calls, run, setGoExecutable: (path: string) => { goExecutable = path; } };
|
|
@@ -151,7 +154,7 @@ test("win32 x64 and arm64 install the exact Go SumDB source tag without archive
|
|
|
151
154
|
const result = await installGentleAi({ packageRoot, platform: "win32", arch, execFile: fixture.run, resolveGoExecutable: async () => goPath });
|
|
152
155
|
assert.equal(result.installed, true);
|
|
153
156
|
assert.deepEqual(fixture.calls.filter((call) => call.file === goPath).map((call) => call.arguments_.slice(0, 2)), [
|
|
154
|
-
["version"], ["install", "github.com/gentleman-programming/gentle-ai/
|
|
157
|
+
["version"], ["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.0.0"], ["version", "-m"],
|
|
155
158
|
]);
|
|
156
159
|
}
|
|
157
160
|
});
|
|
@@ -170,7 +173,7 @@ test("Windows source install reports missing or too-old Go without publishing a
|
|
|
170
173
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
|
|
171
174
|
(error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
|
|
172
175
|
);
|
|
173
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "
|
|
176
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai.exe")), false);
|
|
174
177
|
assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.includes("install-")), []);
|
|
175
178
|
}
|
|
176
179
|
});
|
|
@@ -189,7 +192,7 @@ test("Windows source install cleans staging after Go failure or wrong built vers
|
|
|
189
192
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
|
|
190
193
|
(error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
|
|
191
194
|
);
|
|
192
|
-
const runtimeDirectory = join(packageRoot, ".gentle-ai", "
|
|
195
|
+
const runtimeDirectory = join(packageRoot, ".gentle-ai", "v4.0.0");
|
|
193
196
|
assert.ok(fixture.calls.some((call) => call.arguments_[0] === "install"));
|
|
194
197
|
assert.equal(existsSync(join(runtimeDirectory, "gentle-ai.exe")), false);
|
|
195
198
|
assert.equal(existsSync(runtimeDirectory) && (await readdir(runtimeDirectory)).some((entry) => entry.startsWith(".go-install-") || entry.endsWith(".tmp")), false);
|
|
@@ -210,7 +213,7 @@ test("Windows source installs reuse only a fully verified package-local binary",
|
|
|
210
213
|
assert.equal(reused.installed, false);
|
|
211
214
|
assert.ok(reuse.calls.every((call) => call.file !== "gentle-ai"), "the installer must never fall back to ambient gentle-ai on PATH");
|
|
212
215
|
|
|
213
|
-
await writeFile(join(packageRoot, ".gentle-ai", "
|
|
216
|
+
await writeFile(join(packageRoot, ".gentle-ai", "v4.0.0", "integrity.json"), "{}\n");
|
|
214
217
|
const repaired = windowsGoFixture();
|
|
215
218
|
repaired.setGoExecutable(goPath);
|
|
216
219
|
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: repaired.run, resolveGoExecutable: async () => goPath })).installed, true);
|
|
@@ -251,8 +254,8 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
|
|
|
251
254
|
: undefined;
|
|
252
255
|
const metadata = fixtureOptions.metadataOverride ?? [
|
|
253
256
|
"gentle-ai.exe: go1.25.10",
|
|
254
|
-
"\tpath\tgithub.com/gentleman-programming/gentle-ai/
|
|
255
|
-
`\tmod\tgithub.com/gentleman-programming/gentle-ai/
|
|
257
|
+
"\tpath\tgithub.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
|
|
258
|
+
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.0.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
|
|
256
259
|
"\tbuild\t-buildmode=exe",
|
|
257
260
|
"\tbuild\t-compiler=gc",
|
|
258
261
|
"\tbuild\tCGO_ENABLED=0",
|
|
@@ -272,7 +275,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
|
|
|
272
275
|
return { stdout: "", stderr: "" };
|
|
273
276
|
}
|
|
274
277
|
if (file === goPath && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
|
|
275
|
-
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai
|
|
278
|
+
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 4.0.0\n", stderr: "" };
|
|
276
279
|
throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
|
|
277
280
|
};
|
|
278
281
|
return {
|
|
@@ -314,7 +317,7 @@ test("Windows source installation resolves one validated Go executable and seals
|
|
|
314
317
|
const goCalls = fixture.calls.filter((call) => call.file === fixture.goPath);
|
|
315
318
|
assert.deepEqual(goCalls.map((call) => call.arguments_.slice(0, 2)), [
|
|
316
319
|
["version"],
|
|
317
|
-
["install", "github.com/gentleman-programming/gentle-ai/
|
|
320
|
+
["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.0.0"],
|
|
318
321
|
["version", "-m"],
|
|
319
322
|
]);
|
|
320
323
|
for (const call of goCalls) {
|
|
@@ -340,14 +343,14 @@ test("Windows source manifest binds verified Go metadata and architecture", asyn
|
|
|
340
343
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-provenance-"));
|
|
341
344
|
const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: "arm64" });
|
|
342
345
|
const result = await installGentleAi({ packageRoot, platform: "win32", arch: "arm64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
343
|
-
const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "
|
|
346
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v4.0.0", "integrity.json"), "utf8")) as Record<string, string>;
|
|
344
347
|
assert.equal(result.installed, true);
|
|
345
348
|
assert.deepEqual(manifest, {
|
|
346
|
-
version: "
|
|
349
|
+
version: "4.0.0",
|
|
347
350
|
method: "go-sumdb-source-build",
|
|
348
|
-
package: "github.com/gentleman-programming/gentle-ai/
|
|
349
|
-
module: "github.com/gentleman-programming/gentle-ai/
|
|
350
|
-
tag: "
|
|
351
|
+
package: "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
|
|
352
|
+
module: "github.com/gentleman-programming/gentle-ai/v4",
|
|
353
|
+
tag: "v4.0.0",
|
|
351
354
|
architecture: "arm64",
|
|
352
355
|
binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"),
|
|
353
356
|
moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|
|
@@ -367,12 +370,12 @@ test("Windows source installation rejects Go metadata for a different architectu
|
|
|
367
370
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
|
|
368
371
|
(error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED",
|
|
369
372
|
);
|
|
370
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "
|
|
373
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai.exe")), false);
|
|
371
374
|
});
|
|
372
375
|
|
|
373
376
|
test("Windows source installation treats a fresh ownerless lock as active", async () => {
|
|
374
377
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-lock-"));
|
|
375
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".
|
|
378
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock");
|
|
376
379
|
await mkdir(lockPath, { recursive: true });
|
|
377
380
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
378
381
|
await assertManualLockRecoveryRequired(packageRoot, fixture);
|
|
@@ -380,7 +383,7 @@ test("Windows source installation treats a fresh ownerless lock as active", asyn
|
|
|
380
383
|
|
|
381
384
|
test("Windows source installation preserves a lock whose owner nonce changed before release", async () => {
|
|
382
385
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owner-lock-"));
|
|
383
|
-
const lockOwnerPath = join(packageRoot, ".gentle-ai", ".
|
|
386
|
+
const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock", "owner.json");
|
|
384
387
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
385
388
|
let replacedOwner = false;
|
|
386
389
|
const run = async (...arguments_: Parameters<typeof fixture.run>) => {
|
|
@@ -397,7 +400,7 @@ test("Windows source installation preserves a lock whose owner nonce changed bef
|
|
|
397
400
|
});
|
|
398
401
|
|
|
399
402
|
async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Awaited<ReturnType<typeof hardenedWindowsGoFixture>>, options: Record<string, unknown> = {}) {
|
|
400
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".
|
|
403
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock");
|
|
401
404
|
await assert.rejects(
|
|
402
405
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, ...options }),
|
|
403
406
|
(error: unknown) => error instanceof Error && error.message.includes(lockPath) && /confirm no installer is active.*remove.*manually/i.test(error.message),
|
|
@@ -407,18 +410,18 @@ async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Aw
|
|
|
407
410
|
}
|
|
408
411
|
|
|
409
412
|
function tombstonePath(packageRoot: string, nonce: string): string {
|
|
410
|
-
return join(packageRoot, ".gentle-ai", `.
|
|
413
|
+
return join(packageRoot, ".gentle-ai", `.v4.0.0.install.tombstone-${nonce}`);
|
|
411
414
|
}
|
|
412
415
|
|
|
413
416
|
async function tombstones(packageRoot: string): Promise<string[]> {
|
|
414
417
|
const runtimeRoot = join(packageRoot, ".gentle-ai");
|
|
415
418
|
return existsSync(runtimeRoot)
|
|
416
|
-
? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".
|
|
419
|
+
? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v4.0.0.install.tombstone-"))
|
|
417
420
|
: [];
|
|
418
421
|
}
|
|
419
422
|
|
|
420
423
|
function backupBundlePath(packageRoot: string, nonce: string): string {
|
|
421
|
-
return join(packageRoot, ".gentle-ai", `.
|
|
424
|
+
return join(packageRoot, ".gentle-ai", `.v4.0.0.backup-${nonce}`);
|
|
422
425
|
}
|
|
423
426
|
|
|
424
427
|
async function copyWindowsBundle(source: string, destination: string): Promise<void> {
|
|
@@ -429,7 +432,7 @@ async function copyWindowsBundle(source: string, destination: string): Promise<v
|
|
|
429
432
|
|
|
430
433
|
test("Windows source installation fails closed for an ownerless lock even after the stale threshold", async () => {
|
|
431
434
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-stale-lock-"));
|
|
432
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".
|
|
435
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock");
|
|
433
436
|
await mkdir(lockPath, { recursive: true });
|
|
434
437
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
435
438
|
await assertManualLockRecoveryRequired(packageRoot, fixture, { now: () => Date.now() + 10 * 60_000 });
|
|
@@ -437,7 +440,7 @@ test("Windows source installation fails closed for an ownerless lock even after
|
|
|
437
440
|
|
|
438
441
|
test("Windows source installation fails closed for a stale owner lock", async () => {
|
|
439
442
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-stale-lock-"));
|
|
440
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".
|
|
443
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock");
|
|
441
444
|
await mkdir(lockPath, { recursive: true });
|
|
442
445
|
await writeFile(join(lockPath, "owner.json"), `${JSON.stringify({ createdAt: 0, nonce: "0".repeat(64) })}\n`);
|
|
443
446
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -489,7 +492,7 @@ test("Windows source acquisition fails closed when a tombstone appears after its
|
|
|
489
492
|
(error: unknown) => error instanceof Error && error.message.includes(foreignTombstone),
|
|
490
493
|
);
|
|
491
494
|
assert.equal(existsSync(foreignTombstone), true);
|
|
492
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".
|
|
495
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock")), false);
|
|
493
496
|
assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false);
|
|
494
497
|
});
|
|
495
498
|
|
|
@@ -497,7 +500,7 @@ test("Windows source release deletes its matching tombstone and allows reuse", a
|
|
|
497
500
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owned-lock-release-"));
|
|
498
501
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
499
502
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
500
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".
|
|
503
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock")), false);
|
|
501
504
|
assert.deepEqual(await tombstones(packageRoot), []);
|
|
502
505
|
const reuse = await hardenedWindowsGoFixture(packageRoot);
|
|
503
506
|
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false);
|
|
@@ -507,7 +510,7 @@ test("Windows source recovers a valid backup after a crash between publication r
|
|
|
507
510
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-crash-"));
|
|
508
511
|
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
509
512
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
510
|
-
const live = join(packageRoot, ".gentle-ai", "
|
|
513
|
+
const live = join(packageRoot, ".gentle-ai", "v4.0.0");
|
|
511
514
|
const backup = backupBundlePath(packageRoot, "crash");
|
|
512
515
|
await rename(live, backup);
|
|
513
516
|
const recovery = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -543,7 +546,7 @@ test("Windows source cleans one validated backup only when the live bundle is va
|
|
|
543
546
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-valid-live-"));
|
|
544
547
|
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
545
548
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
546
|
-
const live = join(packageRoot, ".gentle-ai", "
|
|
549
|
+
const live = join(packageRoot, ".gentle-ai", "v4.0.0");
|
|
547
550
|
const backup = backupBundlePath(packageRoot, "valid");
|
|
548
551
|
await copyWindowsBundle(live, backup);
|
|
549
552
|
const reuse = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -556,7 +559,7 @@ test("Windows source preserves a valid backup when the live bundle is invalid",
|
|
|
556
559
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-invalid-live-"));
|
|
557
560
|
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
558
561
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
559
|
-
const live = join(packageRoot, ".gentle-ai", "
|
|
562
|
+
const live = join(packageRoot, ".gentle-ai", "v4.0.0");
|
|
560
563
|
const backup = backupBundlePath(packageRoot, "valid");
|
|
561
564
|
await copyWindowsBundle(live, backup);
|
|
562
565
|
await writeFile(join(live, "integrity.json"), "{}\n");
|
|
@@ -574,7 +577,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
|
|
|
574
577
|
const fixture = await hardenedWindowsGoFixture(packageRoot, { blockInstall: true });
|
|
575
578
|
const first = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
576
579
|
await fixture.waitForInstall();
|
|
577
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".
|
|
580
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock");
|
|
578
581
|
await assert.rejects(
|
|
579
582
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
|
|
580
583
|
(error: unknown) => error instanceof Error && error.message.includes(lockPath),
|
|
@@ -588,7 +591,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
|
|
|
588
591
|
|
|
589
592
|
test("Windows source publication rolls back a prior bundle when final directory swap fails", async () => {
|
|
590
593
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-rollback-"));
|
|
591
|
-
const versionDirectory = join(packageRoot, ".gentle-ai", "
|
|
594
|
+
const versionDirectory = join(packageRoot, ".gentle-ai", "v4.0.0");
|
|
592
595
|
await mkdir(versionDirectory, { recursive: true });
|
|
593
596
|
await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
|
|
594
597
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -625,7 +628,7 @@ test("Darwin/Linux signed bundles retain their four-field manifest and reusable
|
|
|
625
628
|
},
|
|
626
629
|
};
|
|
627
630
|
await installGentleAi(options);
|
|
628
|
-
const manifestPath = join(packageRoot, ".gentle-ai", "
|
|
631
|
+
const manifestPath = join(packageRoot, ".gentle-ai", "v4.0.0", "integrity.json");
|
|
629
632
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, string>;
|
|
630
633
|
assert.deepEqual(Object.keys(manifest), ["version", "asset", "assetSha256", "binarySha256"]);
|
|
631
634
|
assert.equal((await installGentleAi({ ...options, download: async () => { throw new Error("signed bundle must be reused"); } })).installed, false);
|
|
@@ -638,7 +641,7 @@ test("a pinned asset falls back to its gentle-pi mirror only on download failure
|
|
|
638
641
|
// under the current stable pin, whose form gate refuses raw binaries.
|
|
639
642
|
const payload = Buffer.from("signed archive fixture");
|
|
640
643
|
const binary = "mirrored binary";
|
|
641
|
-
const baseAsset = { name: "gentle-
|
|
644
|
+
const baseAsset = { name: "gentle-ai_4.0.0_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), binarySha256: createHash("sha256").update(binary).digest("hex"), url: "https://example.invalid/upstream", mirrorUrl: "https://example.invalid/mirror", executable: "gentle-ai" };
|
|
642
645
|
const extractArchive = async (_archive: string, destination: string) => {
|
|
643
646
|
await mkdir(destination, { recursive: true });
|
|
644
647
|
await writeFile(join(destination, "gentle-ai"), binary);
|
|
@@ -756,7 +759,7 @@ test("checksum mismatch cleans temporary state without promoting a binary", asyn
|
|
|
756
759
|
}),
|
|
757
760
|
/checksum mismatch/,
|
|
758
761
|
);
|
|
759
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "
|
|
762
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai")), false);
|
|
760
763
|
assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.startsWith(".gentle-ai-install-")), []);
|
|
761
764
|
});
|
|
762
765
|
|
|
@@ -778,7 +781,7 @@ test("installer promotes only the expected regular executable with executable PO
|
|
|
778
781
|
await chmod(extracted, 0o700);
|
|
779
782
|
},
|
|
780
783
|
});
|
|
781
|
-
const binary = join(packageRoot, ".gentle-ai", "
|
|
784
|
+
const binary = join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai");
|
|
782
785
|
assert.equal(existsSync(binary), true);
|
|
783
786
|
assert.equal(await readFile(binary, "utf8"), "native executable");
|
|
784
787
|
assert.ok(((await stat(binary)).mode & 0o111) !== 0);
|
|
@@ -809,7 +812,7 @@ test("installer rejects an extracted binary that differs from its pinned digest"
|
|
|
809
812
|
}),
|
|
810
813
|
/binary checksum mismatch/,
|
|
811
814
|
);
|
|
812
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "
|
|
815
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai")), false);
|
|
813
816
|
});
|
|
814
817
|
|
|
815
818
|
test("installer repairs a valid non-executable POSIX binary instead of reusing it", async (t) => {
|
|
@@ -832,7 +835,7 @@ test("installer repairs a valid non-executable POSIX binary instead of reusing i
|
|
|
832
835
|
},
|
|
833
836
|
};
|
|
834
837
|
await installGentleAi(options);
|
|
835
|
-
const binary = join(packageRoot, ".gentle-ai", "
|
|
838
|
+
const binary = join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai");
|
|
836
839
|
await chmod(binary, 0o600);
|
|
837
840
|
const repaired = await installGentleAi(options);
|
|
838
841
|
assert.equal(repaired.installed, true);
|
|
@@ -869,7 +872,7 @@ test("installer rejects archives with multiple expected executable entries", asy
|
|
|
869
872
|
}),
|
|
870
873
|
/exactly one regular gentle-ai/,
|
|
871
874
|
);
|
|
872
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "
|
|
875
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai")), false);
|
|
873
876
|
});
|
|
874
877
|
|
|
875
878
|
test("installer rejects an archive without the expected regular executable", async () => {
|
|
@@ -887,7 +890,18 @@ test("installer rejects an archive without the expected regular executable", asy
|
|
|
887
890
|
}),
|
|
888
891
|
/non-regular gentle-ai/,
|
|
889
892
|
);
|
|
890
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "
|
|
893
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai")), false);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
test("safeRemoveDirectory passes Windows-safe retry options to recursive rm cleanup", async () => {
|
|
897
|
+
const installerSource = await readFile(new URL("../scripts/gentle-ai-installer.mjs", import.meta.url), "utf8");
|
|
898
|
+
const match = /async function safeRemoveDirectory\(path\) \{(?<body>[\s\S]*?)\n\}/.exec(installerSource);
|
|
899
|
+
assert.ok(match?.groups?.body, "safeRemoveDirectory must remain available for scoped cleanup verification");
|
|
900
|
+
assert.match(
|
|
901
|
+
match.groups.body,
|
|
902
|
+
/await rm\(path, \{ recursive: true, force: true, maxRetries: 3, retryDelay: 100 \}\);/,
|
|
903
|
+
"safeRemoveDirectory cleanup must retry Windows EPERM failures from read-only Go module cache files",
|
|
904
|
+
);
|
|
891
905
|
});
|
|
892
906
|
|
|
893
907
|
// #400: a clean Windows source build measured 232 s on a cold module cache,
|