gentle-pi 3.7.0 → 4.1.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 +155 -10
- package/assets/agents/gentle-ai-explore.md +13 -6
- package/assets/agents/gentle-ai-verify.md +27 -6
- package/assets/agents/gentle-ai-worker.md +13 -15
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +75 -169
- package/assets/orchestrator-memory.md +12 -26
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +30 -38
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +956 -50
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/bundled-install.md +397 -0
- package/docs/delegated-verification.md +27 -4
- package/docs/gentle-agents-activity.md +537 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +165 -51
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +3 -3
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +364 -231
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +35 -0
- package/extensions/child-safety.ts +25 -0
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +1093 -326
- package/extensions/gentle-ai.ts +1636 -890
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +1619 -136
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +126 -20
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1424 -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/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +254 -92
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +191 -58
- package/extensions/startup-banner.ts +71 -33
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +99 -0
- package/lib/agents-runner.ts +180 -107
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +220 -0
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +195 -0
- package/lib/command-palette-catalog.ts +5 -10
- package/lib/command-palette.ts +25 -14
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +89 -27
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +249 -0
- package/lib/native-review-cli.ts +242 -180
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +73 -21
- package/lib/review-candidate-view.ts +115 -27
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +227 -56
- package/lib/review-sidebar-state.ts +224 -0
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +527 -0
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +87 -9
- package/lib/shell-bar.ts +215 -30
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +42 -14
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +83 -15
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +84 -31
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +70 -13
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +383 -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 +119 -0
- package/lib/visual-customize-view.ts +481 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +317 -0
- package/package.json +20 -8
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +87 -25
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +239 -177
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +225 -54
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +8 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +83 -35
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +90 -0
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +54 -19
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +91 -38
- 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 +16 -21
- 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-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +30 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +97 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +467 -93
- 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/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +146 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +87 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +94 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +596 -0
- package/tests/command-palette.test.ts +43 -7
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -78
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +171 -3
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -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 +69 -0
- package/tests/gentle-agents.test.ts +2804 -346
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +21 -4
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +566 -62
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +493 -14
- package/tests/gentle-ai.test.ts +1427 -236
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +896 -115
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +333 -65
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +4216 -119
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +333 -10
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-child-guard.test.ts +70 -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 +49 -19
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +481 -0
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/native-review-cli.test.ts +239 -118
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +101 -4
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +76 -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 +60 -14
- package/tests/odd-routing-contract.test.ts +158 -50
- package/tests/orchestrator-budget.test.ts +43 -45
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +8 -7
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +344 -123
- package/tests/packed-runner-owned-path.test.ts +80 -0
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +10 -18
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +7 -14
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +569 -187
- package/tests/rdd-aware-verification-contract.test.ts +103 -40
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +237 -21
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +399 -12
- package/tests/review-contract-prompt.test.ts +138 -44
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +866 -6
- package/tests/review-controller-workspace-root.test.ts +206 -6
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +342 -4
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +221 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +14 -37
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +594 -26
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +202 -0
- package/tests/runtime-harness.mjs +186 -800
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +451 -0
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +134 -2
- package/tests/shell-bar.test.ts +422 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +135 -32
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +351 -22
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +207 -3
- package/tests/shell-usage-view.test.ts +28 -1
- package/tests/shell-usage.test.ts +108 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +357 -2
- package/tests/startup-banner.test.ts +246 -4
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +107 -20
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/theme-customization.test.ts +72 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -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 +139 -0
- package/tests/visual-customize-view.test.ts +696 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +271 -0
- package/tests/yolo-mode-runtime.test.ts +188 -0
- package/tests/yolo-mode.test.ts +289 -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
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { globalSeedPath, migrateLegacyStores } from "../extensions/history/store.ts";
|
|
7
|
+
|
|
8
|
+
function makeDirs(): { root: string; agentDir: string } {
|
|
9
|
+
const base = fs.mkdtempSync(path.join(os.tmpdir(), "pi-history-mig-"));
|
|
10
|
+
const root = path.join(base, "pi-history");
|
|
11
|
+
const agentDir = path.join(base, "agent");
|
|
12
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
13
|
+
return { root, agentDir };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fileTexts(file: string): string[] {
|
|
17
|
+
if (!fs.existsSync(file)) return [];
|
|
18
|
+
return fs
|
|
19
|
+
.readFileSync(file, "utf8")
|
|
20
|
+
.trim()
|
|
21
|
+
.split("\n")
|
|
22
|
+
.filter((l) => l.length > 0)
|
|
23
|
+
.map((l) => (JSON.parse(l) as { text: string }).text);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("no legacy files: migration is a no-op, nothing created", () => {
|
|
27
|
+
const { root, agentDir } = makeDirs();
|
|
28
|
+
const result = migrateLegacyStores(root, agentDir);
|
|
29
|
+
assert.deepEqual(result, { migrated: 0, ran: false });
|
|
30
|
+
assert.equal(fs.existsSync(globalSeedPath(root)), false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("v1 jsonl migrates into the global seed chronologically", () => {
|
|
34
|
+
const { root, agentDir } = makeDirs();
|
|
35
|
+
const v1 = path.join(agentDir, "editor-history.jsonl");
|
|
36
|
+
fs.writeFileSync(
|
|
37
|
+
v1,
|
|
38
|
+
`${[
|
|
39
|
+
JSON.stringify({ v: 1, text: "old" }),
|
|
40
|
+
JSON.stringify({ v: 1, text: "new" }),
|
|
41
|
+
].join("\n")}\n`,
|
|
42
|
+
"utf8",
|
|
43
|
+
);
|
|
44
|
+
const result = migrateLegacyStores(root, agentDir);
|
|
45
|
+
assert.deepEqual(result, { migrated: 2, ran: true });
|
|
46
|
+
assert.deepEqual(fileTexts(globalSeedPath(root)), ["old", "new"]);
|
|
47
|
+
assert.equal(fs.existsSync(v1), true);
|
|
48
|
+
assert.equal(fs.existsSync(`${v1}.imported`), false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("competing migration cannot publish a partial seed over a complete one", () => {
|
|
52
|
+
const { root, agentDir } = makeDirs();
|
|
53
|
+
const array = path.join(agentDir, "editor-history.json");
|
|
54
|
+
const v1 = path.join(agentDir, "editor-history.jsonl");
|
|
55
|
+
fs.writeFileSync(array, JSON.stringify(["array prompt"]));
|
|
56
|
+
fs.writeFileSync(v1, `${JSON.stringify({ text: "v1 prompt" })}\n`);
|
|
57
|
+
const original = fs.readFileSync;
|
|
58
|
+
let competing: ReturnType<typeof migrateLegacyStores> | undefined;
|
|
59
|
+
fs.readFileSync = ((file: fs.PathOrFileDescriptor, ...args: unknown[]) => {
|
|
60
|
+
if (file === v1 && !competing) {
|
|
61
|
+
// The first migration has already read the array; the competing one
|
|
62
|
+
// must not publish a seed while that snapshot is incomplete.
|
|
63
|
+
competing = { migrated: -1, ran: true };
|
|
64
|
+
competing = migrateLegacyStores(root, agentDir);
|
|
65
|
+
}
|
|
66
|
+
return (original as (...args: unknown[]) => unknown)(file, ...args);
|
|
67
|
+
}) as typeof fs.readFileSync;
|
|
68
|
+
try {
|
|
69
|
+
migrateLegacyStores(root, agentDir);
|
|
70
|
+
} finally {
|
|
71
|
+
fs.readFileSync = original;
|
|
72
|
+
}
|
|
73
|
+
assert.deepEqual(competing, { migrated: 0, ran: false });
|
|
74
|
+
assert.deepEqual(fileTexts(globalSeedPath(root)), ["array prompt", "v1 prompt"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("an ownerless crash lock fails closed without changing sources or seed", () => {
|
|
78
|
+
const { root, agentDir } = makeDirs();
|
|
79
|
+
const array = path.join(agentDir, "editor-history.json");
|
|
80
|
+
const v1 = path.join(agentDir, "editor-history.jsonl");
|
|
81
|
+
const seed = globalSeedPath(root);
|
|
82
|
+
const arrayBytes = JSON.stringify(["array prompt"]);
|
|
83
|
+
const v1Bytes = `${JSON.stringify({ text: "after crash" })}\n`;
|
|
84
|
+
const seedBytes = `${JSON.stringify({ text: "already seeded" })}\n`;
|
|
85
|
+
fs.writeFileSync(array, arrayBytes);
|
|
86
|
+
fs.writeFileSync(v1, v1Bytes);
|
|
87
|
+
fs.mkdirSync(root, { recursive: true });
|
|
88
|
+
fs.writeFileSync(seed, seedBytes);
|
|
89
|
+
// A process can die immediately after mkdir, before recording ownership.
|
|
90
|
+
// An ownerless lock needs manual recovery; do not claim automatic import.
|
|
91
|
+
const lock = `${seed}.migration-lock`;
|
|
92
|
+
fs.mkdirSync(lock);
|
|
93
|
+
assert.deepEqual(migrateLegacyStores(root, agentDir), { migrated: 0, ran: false });
|
|
94
|
+
assert.equal(fs.readFileSync(array, "utf8"), arrayBytes);
|
|
95
|
+
assert.equal(fs.readFileSync(v1, "utf8"), v1Bytes);
|
|
96
|
+
assert.equal(fs.existsSync(`${array}.imported`), false);
|
|
97
|
+
assert.equal(fs.existsSync(`${v1}.imported`), false);
|
|
98
|
+
assert.equal(fs.readFileSync(seed, "utf8"), seedBytes);
|
|
99
|
+
assert.equal(fs.existsSync(lock), true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("a live legacy writer remains reachable for prompts appended after migration", () => {
|
|
103
|
+
const { root, agentDir } = makeDirs();
|
|
104
|
+
const v1 = path.join(agentDir, "editor-history.jsonl");
|
|
105
|
+
fs.writeFileSync(v1, `${JSON.stringify({ text: "before" })}\n`);
|
|
106
|
+
const fd = fs.openSync(v1, "a");
|
|
107
|
+
try {
|
|
108
|
+
migrateLegacyStores(root, agentDir);
|
|
109
|
+
fs.writeSync(fd, `${JSON.stringify({ text: "after" })}\n`);
|
|
110
|
+
assert.deepEqual(migrateLegacyStores(root, agentDir), { migrated: 1, ran: true });
|
|
111
|
+
assert.deepEqual(fileTexts(globalSeedPath(root)), ["before", "after"]);
|
|
112
|
+
} finally {
|
|
113
|
+
fs.closeSync(fd);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("prompts appended to an already archived live v1 file are recovered", () => {
|
|
118
|
+
const { root, agentDir } = makeDirs();
|
|
119
|
+
const archived = path.join(agentDir, "editor-history.jsonl.imported");
|
|
120
|
+
fs.writeFileSync(archived, `${JSON.stringify({ text: "archived" })}\n`);
|
|
121
|
+
migrateLegacyStores(root, agentDir);
|
|
122
|
+
fs.appendFileSync(archived, `${JSON.stringify({ text: "late" })}\n`);
|
|
123
|
+
assert.deepEqual(migrateLegacyStores(root, agentDir), { migrated: 1, ran: true });
|
|
124
|
+
assert.deepEqual(fileTexts(globalSeedPath(root)), ["archived", "late"]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("legacy array file also migrates (newest-first reversed)", () => {
|
|
128
|
+
const { root, agentDir } = makeDirs();
|
|
129
|
+
const legacy = path.join(agentDir, "editor-history.json");
|
|
130
|
+
fs.writeFileSync(legacy, JSON.stringify(["newest", "oldest"]), "utf8");
|
|
131
|
+
const result = migrateLegacyStores(root, agentDir);
|
|
132
|
+
assert.deepEqual(result, { migrated: 2, ran: true });
|
|
133
|
+
assert.deepEqual(fileTexts(globalSeedPath(root)), ["oldest", "newest"]);
|
|
134
|
+
assert.equal(fs.existsSync(`${legacy}.imported`), true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("both legacy files: v1 jsonl content appends after array content", () => {
|
|
138
|
+
const { root, agentDir } = makeDirs();
|
|
139
|
+
const legacy = path.join(agentDir, "editor-history.json");
|
|
140
|
+
const v1 = path.join(agentDir, "editor-history.jsonl");
|
|
141
|
+
fs.writeFileSync(legacy, JSON.stringify(["from-array"]), "utf8");
|
|
142
|
+
fs.writeFileSync(
|
|
143
|
+
v1,
|
|
144
|
+
`${JSON.stringify({ v: 1, text: "from-jsonl" })}\n`,
|
|
145
|
+
"utf8",
|
|
146
|
+
);
|
|
147
|
+
migrateLegacyStores(root, agentDir);
|
|
148
|
+
assert.deepEqual(fileTexts(globalSeedPath(root)), [
|
|
149
|
+
"from-array",
|
|
150
|
+
"from-jsonl",
|
|
151
|
+
]);
|
|
152
|
+
assert.equal(fs.existsSync(`${legacy}.imported`), true);
|
|
153
|
+
assert.equal(fs.existsSync(v1), true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("existing global seed is preserved while new v1 prompts are imported", () => {
|
|
157
|
+
const { root, agentDir } = makeDirs();
|
|
158
|
+
fs.mkdirSync(path.dirname(globalSeedPath(root)), { recursive: true });
|
|
159
|
+
fs.writeFileSync(
|
|
160
|
+
globalSeedPath(root),
|
|
161
|
+
`${JSON.stringify({ v: 1, text: "already-here" })}\n`,
|
|
162
|
+
"utf8",
|
|
163
|
+
);
|
|
164
|
+
const v1 = path.join(agentDir, "editor-history.jsonl");
|
|
165
|
+
fs.writeFileSync(
|
|
166
|
+
v1,
|
|
167
|
+
`${JSON.stringify({ v: 1, text: "would-migrate" })}\n`,
|
|
168
|
+
"utf8",
|
|
169
|
+
);
|
|
170
|
+
const result = migrateLegacyStores(root, agentDir);
|
|
171
|
+
assert.deepEqual(result, { migrated: 1, ran: true });
|
|
172
|
+
assert.deepEqual(fileTexts(globalSeedPath(root)), ["already-here", "would-migrate"]);
|
|
173
|
+
assert.deepEqual(migrateLegacyStores(root, agentDir), { migrated: 0, ran: false });
|
|
174
|
+
assert.equal(fs.existsSync(v1), true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("unreadable legacy source leaves both sources for a complete retry", () => {
|
|
178
|
+
const { root, agentDir } = makeDirs();
|
|
179
|
+
const array = path.join(agentDir, "editor-history.json");
|
|
180
|
+
const v1 = path.join(agentDir, "editor-history.jsonl");
|
|
181
|
+
fs.writeFileSync(array, JSON.stringify(["array prompt"]));
|
|
182
|
+
fs.writeFileSync(v1, `${JSON.stringify({ text: "v1 prompt" })}\n`);
|
|
183
|
+
const original = fs.readFileSync;
|
|
184
|
+
fs.readFileSync = ((file: fs.PathOrFileDescriptor, ...args: unknown[]) => {
|
|
185
|
+
if (file === v1) throw new Error("injected read failure");
|
|
186
|
+
return (original as (...args: unknown[]) => unknown)(file, ...args);
|
|
187
|
+
}) as typeof fs.readFileSync;
|
|
188
|
+
try {
|
|
189
|
+
assert.throws(() => migrateLegacyStores(root, agentDir), /injected read failure/);
|
|
190
|
+
assert.equal(fs.existsSync(globalSeedPath(root)), false);
|
|
191
|
+
assert.equal(fs.existsSync(array), true);
|
|
192
|
+
assert.equal(fs.existsSync(v1), true);
|
|
193
|
+
} finally {
|
|
194
|
+
fs.readFileSync = original;
|
|
195
|
+
}
|
|
196
|
+
assert.deepEqual(migrateLegacyStores(root, agentDir), { migrated: 2, ran: true });
|
|
197
|
+
assert.deepEqual(fileTexts(globalSeedPath(root)), ["array prompt", "v1 prompt"]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("malformed legacy array cannot archive a readable v1 source", () => {
|
|
201
|
+
const { root, agentDir } = makeDirs();
|
|
202
|
+
const array = path.join(agentDir, "editor-history.json");
|
|
203
|
+
const v1 = path.join(agentDir, "editor-history.jsonl");
|
|
204
|
+
fs.writeFileSync(array, "{torn");
|
|
205
|
+
fs.writeFileSync(v1, `${JSON.stringify({ text: "survives" })}\n`);
|
|
206
|
+
assert.throws(() => migrateLegacyStores(root, agentDir), SyntaxError);
|
|
207
|
+
assert.equal(fs.existsSync(globalSeedPath(root)), false);
|
|
208
|
+
assert.equal(fs.existsSync(v1), true);
|
|
209
|
+
fs.writeFileSync(array, JSON.stringify(["repaired"]));
|
|
210
|
+
assert.deepEqual(migrateLegacyStores(root, agentDir), { migrated: 2, ran: true });
|
|
211
|
+
assert.deepEqual(fileTexts(globalSeedPath(root)), ["repaired", "survives"]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("malformed v1 jsonl lines are skipped, not fatal", () => {
|
|
215
|
+
const { root, agentDir } = makeDirs();
|
|
216
|
+
const v1 = path.join(agentDir, "editor-history.jsonl");
|
|
217
|
+
fs.writeFileSync(
|
|
218
|
+
v1,
|
|
219
|
+
`${["{torn", JSON.stringify({ v: 1, text: "good" })].join("\n")}\n`,
|
|
220
|
+
"utf8",
|
|
221
|
+
);
|
|
222
|
+
const result = migrateLegacyStores(root, agentDir);
|
|
223
|
+
assert.deepEqual(result, { migrated: 1, ran: true });
|
|
224
|
+
assert.deepEqual(fileTexts(globalSeedPath(root)), ["good"]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// node:test has no test.skipIf (Bun-ism): root skips via the options
|
|
228
|
+
// object — chmod 000 is invisible to the superuser.
|
|
229
|
+
const sealedLegacyTest = (name: string, fn: () => void) =>
|
|
230
|
+
test(
|
|
231
|
+
name,
|
|
232
|
+
{ skip: process.getuid?.() === 0 ? "requires non-root" : false },
|
|
233
|
+
fn,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// chmod-based failure injection is also invisible to the superuser.
|
|
237
|
+
const seedFailureTest = (name: string, fn: () => void) =>
|
|
238
|
+
test(
|
|
239
|
+
name,
|
|
240
|
+
{ skip: process.getuid?.() === 0 ? "requires non-root" : false },
|
|
241
|
+
fn,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
seedFailureTest(
|
|
245
|
+
"a failed seed write leaves legacy sources untouched for retry",
|
|
246
|
+
() => {
|
|
247
|
+
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "migrate-fail-"));
|
|
248
|
+
const v1 = path.join(agentDir, "editor-history.jsonl");
|
|
249
|
+
fs.writeFileSync(
|
|
250
|
+
v1,
|
|
251
|
+
`${JSON.stringify({ v: 1, text: "survives-retry" })}\n`,
|
|
252
|
+
"utf8",
|
|
253
|
+
);
|
|
254
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "migrate-fail-root-"));
|
|
255
|
+
// A read-only store root makes the seed write fail AFTER the sources
|
|
256
|
+
// have been read but BEFORE any rename.
|
|
257
|
+
fs.chmodSync(root, 0o555);
|
|
258
|
+
try {
|
|
259
|
+
assert.throws(() => migrateLegacyStores(root, agentDir));
|
|
260
|
+
// The source was NOT renamed: the retry path is intact.
|
|
261
|
+
assert.equal(fs.existsSync(v1), true);
|
|
262
|
+
assert.equal(fs.existsSync(`${v1}.imported`), false);
|
|
263
|
+
assert.equal(fs.existsSync(globalSeedPath(root)), false);
|
|
264
|
+
} finally {
|
|
265
|
+
fs.chmodSync(root, 0o755);
|
|
266
|
+
}
|
|
267
|
+
// Retry after the failure clears: full migration, then rename.
|
|
268
|
+
const result = migrateLegacyStores(root, agentDir);
|
|
269
|
+
assert.deepEqual(result, { migrated: 1, ran: true });
|
|
270
|
+
assert.equal(fs.existsSync(v1), true);
|
|
271
|
+
assert.deepEqual(fileTexts(globalSeedPath(root)), ["survives-retry"]);
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
sealedLegacyTest(
|
|
276
|
+
"an unreadable legacy file defers migration without archiving either source",
|
|
277
|
+
() => {
|
|
278
|
+
const { root, agentDir } = makeDirs();
|
|
279
|
+
const readable = path.join(agentDir, "editor-history.json");
|
|
280
|
+
fs.writeFileSync(readable, JSON.stringify(["from-array"]), "utf8");
|
|
281
|
+
const sealed = path.join(agentDir, "editor-history.jsonl");
|
|
282
|
+
fs.writeFileSync(
|
|
283
|
+
sealed,
|
|
284
|
+
`${JSON.stringify({ v: 1, text: "sealed-content" })}\n`,
|
|
285
|
+
"utf8",
|
|
286
|
+
);
|
|
287
|
+
fs.chmodSync(sealed, 0o000);
|
|
288
|
+
try {
|
|
289
|
+
assert.throws(() => migrateLegacyStores(root, agentDir));
|
|
290
|
+
assert.equal(fs.existsSync(globalSeedPath(root)), false);
|
|
291
|
+
assert.equal(fs.existsSync(readable), true);
|
|
292
|
+
assert.equal(fs.existsSync(sealed), true);
|
|
293
|
+
} finally {
|
|
294
|
+
fs.chmodSync(sealed, 0o644);
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { loadSharedHistory } from "../extensions/history/load-shared-history.ts";
|
|
7
|
+
|
|
8
|
+
function makeTempFile(content: string): string {
|
|
9
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "history-load-"));
|
|
10
|
+
const file = path.join(dir, "editor-history.json");
|
|
11
|
+
fs.writeFileSync(file, content, "utf8");
|
|
12
|
+
return file;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test("returns empty array when file is missing", () => {
|
|
16
|
+
assert.deepEqual(loadSharedHistory("/definitely/missing.json"), []);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("returns empty array for malformed json", () => {
|
|
20
|
+
const file = makeTempFile("{not-json");
|
|
21
|
+
assert.deepEqual(loadSharedHistory(file), []);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("supports string entries", () => {
|
|
25
|
+
const file = makeTempFile(JSON.stringify(["one", "two"]));
|
|
26
|
+
assert.deepEqual(loadSharedHistory(file), ["one", "two"]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("supports object entries with text field", () => {
|
|
30
|
+
const file = makeTempFile(JSON.stringify([{ text: "one" }, { text: "two" }]));
|
|
31
|
+
assert.deepEqual(loadSharedHistory(file), ["one", "two"]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("ignores malformed object entries", () => {
|
|
35
|
+
const file = makeTempFile(
|
|
36
|
+
JSON.stringify([{ text: "one" }, { text: 2 }, { nope: "three" }]),
|
|
37
|
+
);
|
|
38
|
+
assert.deepEqual(loadSharedHistory(file), ["one"]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("ignores empty string entries", () => {
|
|
42
|
+
const file = makeTempFile(
|
|
43
|
+
JSON.stringify(["", "one", { text: "" }, { text: "two" }]),
|
|
44
|
+
);
|
|
45
|
+
assert.deepEqual(loadSharedHistory(file), ["one", "two"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("keeps only valid strings from mixed arrays", () => {
|
|
49
|
+
const file = makeTempFile(
|
|
50
|
+
JSON.stringify(["one", null, false, 42, { text: "two" }, { text: 1 }]),
|
|
51
|
+
);
|
|
52
|
+
assert.deepEqual(loadSharedHistory(file), ["one", "two"]);
|
|
53
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { filterPrompts } from "../extensions/history/selector-helpers.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* WU5 tests (AC-S5-1, AC-S5-2): the MAX_RESULTS raise 1000 → 10000 is an
|
|
10
|
+
* OUTPUT cap only — filterPrompts caps both of its slice sites; the load
|
|
11
|
+
* path never snapshots. Pure import (no pi-tui graph) plus a source-parse
|
|
12
|
+
* pin on the selector-helpers slice sites.
|
|
13
|
+
*
|
|
14
|
+
* The dev suite's openHistorySelector body pin (no slice() on the load
|
|
15
|
+
* path) covers the slice-3 selector wiring in extensions/history/index.ts
|
|
16
|
+
* and ports with that slice — index.ts stays at its slice-1 surface here.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface CapRecord {
|
|
20
|
+
text: string;
|
|
21
|
+
searchText: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function record(text: string): CapRecord {
|
|
25
|
+
return { text, searchText: text.toLowerCase() };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// T29 — AC-S5-1: the cap value. More than 10,000 records → exactly 10,000 at
|
|
29
|
+
// the empty-query slice AND at a filtered-query slice. NEW pin — no existing
|
|
30
|
+
// test pins the old 1000 literal (design §D9; zero existing-test edits
|
|
31
|
+
// repo-wide, so this file ADDS the pin instead of editing one).
|
|
32
|
+
|
|
33
|
+
test("T29 (AC-S5-1): empty-query slice caps at the raised 10000", () => {
|
|
34
|
+
const records: CapRecord[] = [];
|
|
35
|
+
for (let i = 0; i < 10500; i++) {
|
|
36
|
+
records.push(record(`unique prompt number ${i}`));
|
|
37
|
+
}
|
|
38
|
+
const result = filterPrompts(records, "");
|
|
39
|
+
assert.equal(result.length, 10000);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("T29 (AC-S5-1): filtered-query slice caps at the raised 10000", () => {
|
|
43
|
+
const records: CapRecord[] = [];
|
|
44
|
+
// 10,500 matches for the query token plus non-matching padding rows: the
|
|
45
|
+
// FILTERED set alone is above the cap, so the filtered slice site is the
|
|
46
|
+
// one being exercised here.
|
|
47
|
+
for (let i = 0; i < 10500; i++) {
|
|
48
|
+
records.push(record(`match me ${i}`));
|
|
49
|
+
}
|
|
50
|
+
records.push(record("unrelated row one"));
|
|
51
|
+
records.push(record("unrelated row two"));
|
|
52
|
+
const result = filterPrompts(records, "match");
|
|
53
|
+
assert.ok(result.length > 1000, "the filtered set must exceed the old cap");
|
|
54
|
+
assert.equal(result.length, 10000);
|
|
55
|
+
assert.ok(result.every((r) => r.searchText.includes("match")));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// T30 — AC-S5-2: output-cap-only semantics (source-parse). Selector-helpers
|
|
59
|
+
// reads the constant at exactly the two sanctioned filterPrompts slice
|
|
60
|
+
// sites — no other cap exists in the helper module.
|
|
61
|
+
|
|
62
|
+
const helperSource = fs.readFileSync(
|
|
63
|
+
fileURLToPath(
|
|
64
|
+
new URL("../extensions/history/selector-helpers.ts", import.meta.url),
|
|
65
|
+
),
|
|
66
|
+
"utf8",
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
test("T30 (AC-S5-2): filterPrompts hosts exactly the two sanctioned cap slice sites", () => {
|
|
70
|
+
const sliceSites = helperSource.split("slice(0, MAX_RESULTS)").length - 1;
|
|
71
|
+
assert.equal(
|
|
72
|
+
sliceSites,
|
|
73
|
+
2,
|
|
74
|
+
"filterPrompts hosts exactly the two sanctioned cap slice sites",
|
|
75
|
+
);
|
|
76
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { writeJsonAtomic } from "../extensions/history/atomic-write.ts";
|
|
7
|
+
import {
|
|
8
|
+
appendSessionCapture,
|
|
9
|
+
ensureRegistryEntry,
|
|
10
|
+
openSessionWriter,
|
|
11
|
+
parseStoreLine,
|
|
12
|
+
projectDir,
|
|
13
|
+
projectHash,
|
|
14
|
+
registryPath,
|
|
15
|
+
sessionFilePath,
|
|
16
|
+
} from "../extensions/history/store.ts";
|
|
17
|
+
|
|
18
|
+
// Slice-1 concurrency/recovery coverage. The dev-suite multi-reader drain
|
|
19
|
+
// scenarios are re-expressed against the slice-1 surface (per-instance
|
|
20
|
+
// writers, parseStoreLine, atomic writes): parallel writers on one project
|
|
21
|
+
// dir, torn-line tolerance, and same-target atomic-write collisions. The
|
|
22
|
+
// drain/read ordering scenarios themselves arrive with the slice-2 reader.
|
|
23
|
+
|
|
24
|
+
const PROJECT_A = "/pi-history-test/project-a";
|
|
25
|
+
const PROJECT_B = "/pi-history-test/project-b";
|
|
26
|
+
|
|
27
|
+
function makeRoot(): string {
|
|
28
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "pi-history-multi-reader-"));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function storedTexts(file: string): string[] {
|
|
32
|
+
return fs
|
|
33
|
+
.readFileSync(file, "utf8")
|
|
34
|
+
.split("\n")
|
|
35
|
+
.filter((l) => l.trim().length > 0)
|
|
36
|
+
.map((l) => (JSON.parse(l) as { text: string }).text);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --- parallel writers ---
|
|
40
|
+
|
|
41
|
+
test("growth from a concurrent instance is visible on the next read", () => {
|
|
42
|
+
const root = makeRoot();
|
|
43
|
+
const a = openSessionWriter(root, PROJECT_A, "inst-a");
|
|
44
|
+
appendSessionCapture(a, "first");
|
|
45
|
+
const dirA = projectDir(root, PROJECT_A);
|
|
46
|
+
assert.deepEqual(fs.readdirSync(dirA), ["inst-a.jsonl"]);
|
|
47
|
+
|
|
48
|
+
// A second pi instance grows the SAME project dir through its OWN file;
|
|
49
|
+
// neither writer reads or rewrites the other's bytes.
|
|
50
|
+
const b = openSessionWriter(root, PROJECT_A, "inst-b");
|
|
51
|
+
appendSessionCapture(b, "from-other-instance");
|
|
52
|
+
|
|
53
|
+
assert.deepEqual(fs.readdirSync(dirA).sort(), [
|
|
54
|
+
"inst-a.jsonl",
|
|
55
|
+
"inst-b.jsonl",
|
|
56
|
+
]);
|
|
57
|
+
assert.deepEqual(storedTexts(sessionFilePath(root, PROJECT_A, "inst-a")), [
|
|
58
|
+
"first",
|
|
59
|
+
]);
|
|
60
|
+
assert.deepEqual(storedTexts(sessionFilePath(root, PROJECT_A, "inst-b")), [
|
|
61
|
+
"from-other-instance",
|
|
62
|
+
]);
|
|
63
|
+
assert.equal(a.lineCount, 1);
|
|
64
|
+
assert.equal(b.lineCount, 1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("interleaved captures from multiple writers never clobber each other", () => {
|
|
68
|
+
const root = makeRoot();
|
|
69
|
+
const writers = [
|
|
70
|
+
openSessionWriter(root, PROJECT_A, "w0"),
|
|
71
|
+
openSessionWriter(root, PROJECT_A, "w1"),
|
|
72
|
+
openSessionWriter(root, PROJECT_A, "w2"),
|
|
73
|
+
];
|
|
74
|
+
for (let i = 0; i < 10; i++) {
|
|
75
|
+
for (let w = 0; w < writers.length; w++) {
|
|
76
|
+
appendSessionCapture(writers[w], `w${w}-line-${i}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const dir = projectDir(root, PROJECT_A);
|
|
80
|
+
assert.deepEqual(fs.readdirSync(dir).sort(), [
|
|
81
|
+
"w0.jsonl",
|
|
82
|
+
"w1.jsonl",
|
|
83
|
+
"w2.jsonl",
|
|
84
|
+
]);
|
|
85
|
+
for (let w = 0; w < writers.length; w++) {
|
|
86
|
+
assert.equal(writers[w].lineCount, 10);
|
|
87
|
+
assert.deepEqual(
|
|
88
|
+
storedTexts(writers[w].filePath),
|
|
89
|
+
Array.from({ length: 10 }, (_, i) => `w${w}-line-${i}`),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a high-volume burst on one writer keeps every line, in order", () => {
|
|
95
|
+
const root = makeRoot();
|
|
96
|
+
const writer = openSessionWriter(root, PROJECT_A, "burst");
|
|
97
|
+
const expected: string[] = [];
|
|
98
|
+
for (let i = 0; i < 100; i++) {
|
|
99
|
+
const text = `burst-${i}`;
|
|
100
|
+
expected.push(text);
|
|
101
|
+
appendSessionCapture(writer, text, 1000 + i);
|
|
102
|
+
}
|
|
103
|
+
assert.equal(writer.lineCount, 100);
|
|
104
|
+
const lines = fs.readFileSync(writer.filePath, "utf8").trim().split("\n");
|
|
105
|
+
assert.equal(lines.length, 100);
|
|
106
|
+
const parsed = lines.map((l) => JSON.parse(l) as { text: string; ts: number });
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
parsed.map((e) => e.text),
|
|
109
|
+
expected,
|
|
110
|
+
);
|
|
111
|
+
assert.equal(parsed[42].ts, 1042);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// --- torn-line / crash recovery ---
|
|
115
|
+
|
|
116
|
+
test("torn and malformed lines parse to null (crash garbage never resurfaces)", () => {
|
|
117
|
+
// A torn final line (process died mid-write) is a truncated JSON doc.
|
|
118
|
+
const torn = JSON.stringify({ v: 1, text: "survivor" }).slice(0, 12);
|
|
119
|
+
const malformed: string[] = [
|
|
120
|
+
"",
|
|
121
|
+
"{torn",
|
|
122
|
+
torn,
|
|
123
|
+
"not json at all",
|
|
124
|
+
JSON.stringify([]),
|
|
125
|
+
JSON.stringify("scalar"),
|
|
126
|
+
JSON.stringify(null),
|
|
127
|
+
JSON.stringify({ v: 1 }),
|
|
128
|
+
JSON.stringify({ text: 42 }),
|
|
129
|
+
JSON.stringify({ text: " " }),
|
|
130
|
+
];
|
|
131
|
+
for (const line of malformed) {
|
|
132
|
+
assert.equal(parseStoreLine(line), null, JSON.stringify(line));
|
|
133
|
+
}
|
|
134
|
+
// Valid lines keep parsing: absent v defaults to 1, ts/v flow through.
|
|
135
|
+
assert.deepEqual(parseStoreLine(JSON.stringify({ v: 1, text: "survivor" })), {
|
|
136
|
+
v: 1,
|
|
137
|
+
text: "survivor",
|
|
138
|
+
});
|
|
139
|
+
assert.deepEqual(parseStoreLine(JSON.stringify({ text: "y" })), {
|
|
140
|
+
v: 1,
|
|
141
|
+
text: "y",
|
|
142
|
+
});
|
|
143
|
+
assert.deepEqual(parseStoreLine(JSON.stringify({ v: 2, text: "x", ts: 7 })), {
|
|
144
|
+
v: 2,
|
|
145
|
+
text: "x",
|
|
146
|
+
ts: 7,
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("a torn final line is tolerated: skipped by readers, later appends continue", () => {
|
|
151
|
+
const root = makeRoot();
|
|
152
|
+
const writer = openSessionWriter(root, PROJECT_A, "torn");
|
|
153
|
+
appendSessionCapture(writer, "before-crash");
|
|
154
|
+
// Crash mid-write: a partial line lands WITHOUT its trailing newline.
|
|
155
|
+
fs.appendFileSync(writer.filePath, `{"v":1,"text":"tor`, "utf8");
|
|
156
|
+
// parseStoreLine skips the torn tail instead of throwing...
|
|
157
|
+
assert.equal(parseStoreLine('{"v":1,"text":"tor'), null);
|
|
158
|
+
// ...and the instance keeps capturing. The first append after a
|
|
159
|
+
// newline-less torn tail merges with the fragment (one accepted lost
|
|
160
|
+
// entry — the same crash window the design documents for lost writes);
|
|
161
|
+
// the next full line parses cleanly again.
|
|
162
|
+
appendSessionCapture(writer, "after-crash");
|
|
163
|
+
appendSessionCapture(writer, "after-crash-2");
|
|
164
|
+
assert.equal(writer.lineCount, 3);
|
|
165
|
+
const lines = fs.readFileSync(writer.filePath, "utf8").trim().split("\n");
|
|
166
|
+
assert.equal(lines.length, 3);
|
|
167
|
+
assert.equal((JSON.parse(lines[0]) as { text: string }).text, "before-crash");
|
|
168
|
+
assert.equal(parseStoreLine(lines[1]), null); // torn fragment + merged entry
|
|
169
|
+
assert.equal(
|
|
170
|
+
(JSON.parse(lines[2]) as { text: string }).text,
|
|
171
|
+
"after-crash-2",
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// --- atomic-write collisions ---
|
|
176
|
+
|
|
177
|
+
test("rapid same-target atomic writes leave one valid document and no staging files", () => {
|
|
178
|
+
const root = makeRoot();
|
|
179
|
+
const target = path.join(root, "shared-state.json");
|
|
180
|
+
for (let i = 0; i < 25; i++) {
|
|
181
|
+
assert.equal(writeJsonAtomic(target, { writer: i }), true);
|
|
182
|
+
}
|
|
183
|
+
const final = JSON.parse(fs.readFileSync(target, "utf8")) as {
|
|
184
|
+
writer: number;
|
|
185
|
+
};
|
|
186
|
+
assert.ok(final.writer >= 0 && final.writer <= 24);
|
|
187
|
+
const leftovers = fs.readdirSync(root).filter((f) => f.includes(".tmp-"));
|
|
188
|
+
assert.deepEqual(leftovers, []);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("interleaved registry updates from two instances keep both entries", () => {
|
|
192
|
+
const root = makeRoot();
|
|
193
|
+
for (let i = 0; i < 3; i++) {
|
|
194
|
+
ensureRegistryEntry(root, PROJECT_A);
|
|
195
|
+
ensureRegistryEntry(root, PROJECT_B);
|
|
196
|
+
}
|
|
197
|
+
const raw = JSON.parse(
|
|
198
|
+
fs.readFileSync(registryPath(root), "utf8"),
|
|
199
|
+
) as Record<string, string>;
|
|
200
|
+
assert.equal(Object.keys(raw).length, 2);
|
|
201
|
+
assert.equal(raw[projectHash(PROJECT_A)], PROJECT_A);
|
|
202
|
+
assert.equal(raw[projectHash(PROJECT_B)], PROJECT_B);
|
|
203
|
+
});
|