gsd-pi 2.28.0-dev.e19bf89 → 2.29.0-dev.23d50d0
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 +24 -17
- package/dist/cli.js +15 -9
- package/dist/headless.js +4 -0
- package/dist/resource-loader.js +80 -8
- package/dist/resources/extensions/bg-shell/process-manager.ts +13 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +217 -65
- package/dist/resources/extensions/gsd/auto-dispatch.ts +2 -2
- package/dist/resources/extensions/gsd/auto-post-unit.ts +53 -6
- package/dist/resources/extensions/gsd/auto-prompts.ts +27 -14
- package/dist/resources/extensions/gsd/auto-recovery.ts +33 -23
- package/dist/resources/extensions/gsd/auto-start.ts +25 -10
- package/dist/resources/extensions/gsd/auto-verification.ts +41 -7
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +21 -6
- package/dist/resources/extensions/gsd/auto-worktree.ts +9 -0
- package/dist/resources/extensions/gsd/auto.ts +67 -22
- package/dist/resources/extensions/gsd/commands-handlers.ts +3 -11
- package/dist/resources/extensions/gsd/commands-logs.ts +536 -0
- package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +90 -47
- package/dist/resources/extensions/gsd/commands-workflow-templates.ts +544 -0
- package/dist/resources/extensions/gsd/commands.ts +75 -29
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +2 -1
- package/dist/resources/extensions/gsd/doctor-types.ts +13 -0
- package/dist/resources/extensions/gsd/doctor.ts +2 -6
- package/dist/resources/extensions/gsd/export.ts +28 -2
- package/dist/resources/extensions/gsd/gsd-db.ts +19 -0
- package/dist/resources/extensions/gsd/index.ts +2 -1
- package/dist/resources/extensions/gsd/json-persistence.ts +67 -0
- package/dist/resources/extensions/gsd/mechanical-completion.ts +430 -0
- package/dist/resources/extensions/gsd/metrics.ts +17 -31
- package/dist/resources/extensions/gsd/paths.ts +17 -8
- package/dist/resources/extensions/gsd/preferences-models.ts +7 -1
- package/dist/resources/extensions/gsd/preferences-validation.ts +2 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +4 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +26 -2
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +15 -1
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +28 -0
- package/dist/resources/extensions/gsd/queue-order.ts +10 -11
- package/dist/resources/extensions/gsd/routing-history.ts +13 -17
- package/dist/resources/extensions/gsd/session-lock.ts +284 -0
- package/dist/resources/extensions/gsd/session-status-io.ts +23 -41
- package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
- package/dist/resources/extensions/gsd/tests/extension-selector-separator.test.ts +60 -38
- package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/mechanical-completion.test.ts +356 -0
- package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
- package/dist/resources/extensions/gsd/tests/token-profile.test.ts +14 -16
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
- package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
- package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
- package/dist/resources/extensions/gsd/tests/workflow-templates.test.ts +173 -0
- package/dist/resources/extensions/gsd/types.ts +3 -0
- package/dist/resources/extensions/gsd/unit-runtime.ts +16 -13
- package/dist/resources/extensions/gsd/verification-evidence.ts +2 -0
- package/dist/resources/extensions/gsd/verification-gate.ts +13 -2
- package/dist/resources/extensions/gsd/workflow-templates/bugfix.md +87 -0
- package/dist/resources/extensions/gsd/workflow-templates/dep-upgrade.md +74 -0
- package/dist/resources/extensions/gsd/workflow-templates/full-project.md +41 -0
- package/dist/resources/extensions/gsd/workflow-templates/hotfix.md +45 -0
- package/dist/resources/extensions/gsd/workflow-templates/refactor.md +83 -0
- package/dist/resources/extensions/gsd/workflow-templates/registry.json +85 -0
- package/dist/resources/extensions/gsd/workflow-templates/security-audit.md +73 -0
- package/dist/resources/extensions/gsd/workflow-templates/small-feature.md +81 -0
- package/dist/resources/extensions/gsd/workflow-templates/spike.md +69 -0
- package/dist/resources/extensions/gsd/workflow-templates.ts +241 -0
- package/dist/resources/extensions/mcp-client/index.ts +459 -0
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +9 -20
- package/dist/resources/extensions/remote-questions/http-client.ts +76 -0
- package/dist/resources/extensions/remote-questions/notify.ts +1 -2
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +11 -18
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +8 -20
- package/dist/resources/extensions/remote-questions/types.ts +3 -0
- package/dist/resources/extensions/shared/mod.ts +3 -0
- package/dist/resources/skills/create-gsd-extension/SKILL.md +87 -0
- package/dist/resources/skills/create-gsd-extension/references/compaction-session-control.md +77 -0
- package/dist/resources/skills/create-gsd-extension/references/custom-commands.md +139 -0
- package/dist/resources/skills/create-gsd-extension/references/custom-rendering.md +108 -0
- package/dist/resources/skills/create-gsd-extension/references/custom-tools.md +183 -0
- package/dist/resources/skills/create-gsd-extension/references/custom-ui.md +490 -0
- package/dist/resources/skills/create-gsd-extension/references/events-reference.md +126 -0
- package/dist/resources/skills/create-gsd-extension/references/extension-lifecycle.md +64 -0
- package/dist/resources/skills/create-gsd-extension/references/extensionapi-reference.md +75 -0
- package/dist/resources/skills/create-gsd-extension/references/extensioncontext-reference.md +53 -0
- package/dist/resources/skills/create-gsd-extension/references/key-rules-gotchas.md +36 -0
- package/dist/resources/skills/create-gsd-extension/references/mode-behavior.md +32 -0
- package/dist/resources/skills/create-gsd-extension/references/model-provider-management.md +89 -0
- package/dist/resources/skills/create-gsd-extension/references/packaging-distribution.md +55 -0
- package/dist/resources/skills/create-gsd-extension/references/remote-execution-overrides.md +90 -0
- package/dist/resources/skills/create-gsd-extension/references/state-management.md +70 -0
- package/dist/resources/skills/create-gsd-extension/references/system-prompt-modification.md +52 -0
- package/dist/resources/skills/create-gsd-extension/templates/extension-skeleton.ts +51 -0
- package/dist/resources/skills/create-gsd-extension/templates/stateful-tool-skeleton.ts +143 -0
- package/dist/resources/skills/create-gsd-extension/workflows/add-capability.md +57 -0
- package/dist/resources/skills/create-gsd-extension/workflows/create-extension.md +156 -0
- package/dist/resources/skills/create-gsd-extension/workflows/debug-extension.md +74 -0
- package/dist/resources/skills/create-skill/SKILL.md +184 -0
- package/dist/resources/skills/create-skill/references/api-security.md +226 -0
- package/dist/resources/skills/create-skill/references/be-clear-and-direct.md +531 -0
- package/dist/resources/skills/create-skill/references/common-patterns.md +595 -0
- package/dist/resources/skills/create-skill/references/core-principles.md +437 -0
- package/dist/resources/skills/create-skill/references/executable-code.md +175 -0
- package/dist/resources/skills/create-skill/references/gsd-skill-ecosystem.md +68 -0
- package/dist/resources/skills/create-skill/references/iteration-and-testing.md +474 -0
- package/dist/resources/skills/create-skill/references/recommended-structure.md +168 -0
- package/dist/resources/skills/create-skill/references/skill-structure.md +372 -0
- package/dist/resources/skills/create-skill/references/use-xml-tags.md +466 -0
- package/dist/resources/skills/create-skill/references/using-scripts.md +113 -0
- package/dist/resources/skills/create-skill/references/using-templates.md +112 -0
- package/dist/resources/skills/create-skill/references/workflows-and-validation.md +510 -0
- package/dist/resources/skills/create-skill/templates/router-skill.md +73 -0
- package/dist/resources/skills/create-skill/templates/simple-skill.md +33 -0
- package/dist/resources/skills/create-skill/workflows/add-reference.md +96 -0
- package/dist/resources/skills/create-skill/workflows/add-script.md +93 -0
- package/dist/resources/skills/create-skill/workflows/add-template.md +74 -0
- package/dist/resources/skills/create-skill/workflows/add-workflow.md +120 -0
- package/dist/resources/skills/create-skill/workflows/audit-skill.md +148 -0
- package/dist/resources/skills/create-skill/workflows/create-new-skill.md +196 -0
- package/dist/resources/skills/create-skill/workflows/get-guidance.md +121 -0
- package/dist/resources/skills/create-skill/workflows/upgrade-to-router.md +161 -0
- package/dist/resources/skills/create-skill/workflows/verify-skill.md +204 -0
- package/package.json +6 -3
- package/packages/native/dist/native.d.ts +2 -0
- package/packages/native/dist/native.js +19 -5
- package/packages/native/src/native.ts +23 -9
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +13 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +3 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +10 -0
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/scripts/copy-assets.cjs +39 -8
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +13 -0
- package/packages/pi-coding-agent/src/core/lsp/client.ts +3 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
- package/packages/pi-coding-agent/src/core/system-prompt.ts +11 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -1
- package/packages/pi-tui/dist/autocomplete.d.ts +3 -0
- package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/packages/pi-tui/dist/autocomplete.js +14 -0
- package/packages/pi-tui/dist/autocomplete.js.map +1 -1
- package/packages/pi-tui/src/autocomplete.ts +19 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/bg-shell/process-manager.ts +13 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +217 -65
- package/src/resources/extensions/gsd/auto-dispatch.ts +2 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +53 -6
- package/src/resources/extensions/gsd/auto-prompts.ts +27 -14
- package/src/resources/extensions/gsd/auto-recovery.ts +33 -23
- package/src/resources/extensions/gsd/auto-start.ts +25 -10
- package/src/resources/extensions/gsd/auto-verification.ts +41 -7
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +21 -6
- package/src/resources/extensions/gsd/auto-worktree.ts +9 -0
- package/src/resources/extensions/gsd/auto.ts +67 -22
- package/src/resources/extensions/gsd/commands-handlers.ts +3 -11
- package/src/resources/extensions/gsd/commands-logs.ts +536 -0
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +90 -47
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +544 -0
- package/src/resources/extensions/gsd/commands.ts +75 -29
- package/src/resources/extensions/gsd/dashboard-overlay.ts +2 -1
- package/src/resources/extensions/gsd/doctor-types.ts +13 -0
- package/src/resources/extensions/gsd/doctor.ts +2 -6
- package/src/resources/extensions/gsd/export.ts +28 -2
- package/src/resources/extensions/gsd/gsd-db.ts +19 -0
- package/src/resources/extensions/gsd/index.ts +2 -1
- package/src/resources/extensions/gsd/json-persistence.ts +67 -0
- package/src/resources/extensions/gsd/mechanical-completion.ts +430 -0
- package/src/resources/extensions/gsd/metrics.ts +17 -31
- package/src/resources/extensions/gsd/paths.ts +17 -8
- package/src/resources/extensions/gsd/preferences-models.ts +7 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +2 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +4 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +26 -2
- package/src/resources/extensions/gsd/prompts/plan-slice.md +15 -1
- package/src/resources/extensions/gsd/prompts/workflow-start.md +28 -0
- package/src/resources/extensions/gsd/queue-order.ts +10 -11
- package/src/resources/extensions/gsd/routing-history.ts +13 -17
- package/src/resources/extensions/gsd/session-lock.ts +284 -0
- package/src/resources/extensions/gsd/session-status-io.ts +23 -41
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
- package/src/resources/extensions/gsd/tests/extension-selector-separator.test.ts +60 -38
- package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +356 -0
- package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +14 -16
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
- package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +173 -0
- package/src/resources/extensions/gsd/types.ts +3 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +16 -13
- package/src/resources/extensions/gsd/verification-evidence.ts +2 -0
- package/src/resources/extensions/gsd/verification-gate.ts +13 -2
- package/src/resources/extensions/gsd/workflow-templates/bugfix.md +87 -0
- package/src/resources/extensions/gsd/workflow-templates/dep-upgrade.md +74 -0
- package/src/resources/extensions/gsd/workflow-templates/full-project.md +41 -0
- package/src/resources/extensions/gsd/workflow-templates/hotfix.md +45 -0
- package/src/resources/extensions/gsd/workflow-templates/refactor.md +83 -0
- package/src/resources/extensions/gsd/workflow-templates/registry.json +85 -0
- package/src/resources/extensions/gsd/workflow-templates/security-audit.md +73 -0
- package/src/resources/extensions/gsd/workflow-templates/small-feature.md +81 -0
- package/src/resources/extensions/gsd/workflow-templates/spike.md +69 -0
- package/src/resources/extensions/gsd/workflow-templates.ts +241 -0
- package/src/resources/extensions/mcp-client/index.ts +459 -0
- package/src/resources/extensions/remote-questions/discord-adapter.ts +9 -20
- package/src/resources/extensions/remote-questions/http-client.ts +76 -0
- package/src/resources/extensions/remote-questions/notify.ts +1 -2
- package/src/resources/extensions/remote-questions/slack-adapter.ts +11 -18
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +8 -20
- package/src/resources/extensions/remote-questions/types.ts +3 -0
- package/src/resources/extensions/shared/mod.ts +3 -0
- package/src/resources/skills/create-gsd-extension/SKILL.md +87 -0
- package/src/resources/skills/create-gsd-extension/references/compaction-session-control.md +77 -0
- package/src/resources/skills/create-gsd-extension/references/custom-commands.md +139 -0
- package/src/resources/skills/create-gsd-extension/references/custom-rendering.md +108 -0
- package/src/resources/skills/create-gsd-extension/references/custom-tools.md +183 -0
- package/src/resources/skills/create-gsd-extension/references/custom-ui.md +490 -0
- package/src/resources/skills/create-gsd-extension/references/events-reference.md +126 -0
- package/src/resources/skills/create-gsd-extension/references/extension-lifecycle.md +64 -0
- package/src/resources/skills/create-gsd-extension/references/extensionapi-reference.md +75 -0
- package/src/resources/skills/create-gsd-extension/references/extensioncontext-reference.md +53 -0
- package/src/resources/skills/create-gsd-extension/references/key-rules-gotchas.md +36 -0
- package/src/resources/skills/create-gsd-extension/references/mode-behavior.md +32 -0
- package/src/resources/skills/create-gsd-extension/references/model-provider-management.md +89 -0
- package/src/resources/skills/create-gsd-extension/references/packaging-distribution.md +55 -0
- package/src/resources/skills/create-gsd-extension/references/remote-execution-overrides.md +90 -0
- package/src/resources/skills/create-gsd-extension/references/state-management.md +70 -0
- package/src/resources/skills/create-gsd-extension/references/system-prompt-modification.md +52 -0
- package/src/resources/skills/create-gsd-extension/templates/extension-skeleton.ts +51 -0
- package/src/resources/skills/create-gsd-extension/templates/stateful-tool-skeleton.ts +143 -0
- package/src/resources/skills/create-gsd-extension/workflows/add-capability.md +57 -0
- package/src/resources/skills/create-gsd-extension/workflows/create-extension.md +156 -0
- package/src/resources/skills/create-gsd-extension/workflows/debug-extension.md +74 -0
- package/src/resources/skills/create-skill/SKILL.md +184 -0
- package/src/resources/skills/create-skill/references/api-security.md +226 -0
- package/src/resources/skills/create-skill/references/be-clear-and-direct.md +531 -0
- package/src/resources/skills/create-skill/references/common-patterns.md +595 -0
- package/src/resources/skills/create-skill/references/core-principles.md +437 -0
- package/src/resources/skills/create-skill/references/executable-code.md +175 -0
- package/src/resources/skills/create-skill/references/gsd-skill-ecosystem.md +68 -0
- package/src/resources/skills/create-skill/references/iteration-and-testing.md +474 -0
- package/src/resources/skills/create-skill/references/recommended-structure.md +168 -0
- package/src/resources/skills/create-skill/references/skill-structure.md +372 -0
- package/src/resources/skills/create-skill/references/use-xml-tags.md +466 -0
- package/src/resources/skills/create-skill/references/using-scripts.md +113 -0
- package/src/resources/skills/create-skill/references/using-templates.md +112 -0
- package/src/resources/skills/create-skill/references/workflows-and-validation.md +510 -0
- package/src/resources/skills/create-skill/templates/router-skill.md +73 -0
- package/src/resources/skills/create-skill/templates/simple-skill.md +33 -0
- package/src/resources/skills/create-skill/workflows/add-reference.md +96 -0
- package/src/resources/skills/create-skill/workflows/add-script.md +93 -0
- package/src/resources/skills/create-skill/workflows/add-template.md +74 -0
- package/src/resources/skills/create-skill/workflows/add-workflow.md +120 -0
- package/src/resources/skills/create-skill/workflows/audit-skill.md +148 -0
- package/src/resources/skills/create-skill/workflows/create-new-skill.md +196 -0
- package/src/resources/skills/create-skill/workflows/get-guidance.md +121 -0
- package/src/resources/skills/create-skill/workflows/upgrade-to-router.md +161 -0
- package/src/resources/skills/create-skill/workflows/verify-skill.md +204 -0
- package/dist/resources/extensions/gsd/preferences-hooks.ts +0 -10
- package/dist/resources/extensions/mcporter/index.ts +0 -525
- package/dist/resources/extensions/shared/progress-widget.ts +0 -282
- package/dist/resources/extensions/shared/thinking-widget.ts +0 -107
- package/src/resources/extensions/gsd/preferences-hooks.ts +0 -10
- package/src/resources/extensions/mcporter/index.ts +0 -525
- package/src/resources/extensions/shared/progress-widget.ts +0 -282
- package/src/resources/extensions/shared/thinking-widget.ts +0 -107
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mechanical Completion — unit tests (ADR-003).
|
|
3
|
+
*
|
|
4
|
+
* Tests deterministic slice/milestone completion using fixture data.
|
|
5
|
+
* Uses node:test + node:assert for consistency with token-profile.test.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
14
|
+
|
|
15
|
+
// ─── Fixture Helpers ──────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function createTmpBase(): string {
|
|
18
|
+
const base = join(tmpdir(), `gsd-mech-test-${randomBytes(4).toString("hex")}`);
|
|
19
|
+
mkdirSync(base, { recursive: true });
|
|
20
|
+
return base;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function scaffold(base: string, mid: string, sid: string, taskSummaries: Array<{ tid: string; content: string }>) {
|
|
24
|
+
const gsdRoot = join(base, ".gsd");
|
|
25
|
+
const mDir = join(gsdRoot, "milestones", mid);
|
|
26
|
+
const sDir = join(mDir, "slices", sid);
|
|
27
|
+
const tDir = join(sDir, "tasks");
|
|
28
|
+
mkdirSync(tDir, { recursive: true });
|
|
29
|
+
|
|
30
|
+
for (const { tid, content } of taskSummaries) {
|
|
31
|
+
writeFileSync(join(tDir, `${tid}-SUMMARY.md`), content, "utf-8");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { gsdRoot, mDir, sDir, tDir };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeTaskSummary(tid: string, opts: {
|
|
38
|
+
oneLiner?: string;
|
|
39
|
+
provides?: string[];
|
|
40
|
+
key_files?: string[];
|
|
41
|
+
key_decisions?: string[];
|
|
42
|
+
verification_result?: string;
|
|
43
|
+
}): string {
|
|
44
|
+
const lines: string[] = [
|
|
45
|
+
"---",
|
|
46
|
+
`id: ${tid}`,
|
|
47
|
+
`parent: S01`,
|
|
48
|
+
`milestone: M001`,
|
|
49
|
+
];
|
|
50
|
+
if (opts.provides?.length) lines.push(`provides:\n${opts.provides.map(p => ` - ${p}`).join("\n")}`);
|
|
51
|
+
if (opts.key_files?.length) lines.push(`key_files:\n${opts.key_files.map(f => ` - ${f}`).join("\n")}`);
|
|
52
|
+
if (opts.key_decisions?.length) lines.push(`key_decisions:\n${opts.key_decisions.map(d => ` - ${d}`).join("\n")}`);
|
|
53
|
+
lines.push(`verification_result: ${opts.verification_result ?? "passed"}`);
|
|
54
|
+
lines.push("---");
|
|
55
|
+
lines.push("");
|
|
56
|
+
lines.push(`# ${tid}: Test Task`);
|
|
57
|
+
lines.push("");
|
|
58
|
+
if (opts.oneLiner) lines.push(`**${opts.oneLiner}**`);
|
|
59
|
+
lines.push("");
|
|
60
|
+
lines.push("## What Happened");
|
|
61
|
+
lines.push("");
|
|
62
|
+
lines.push(`Implemented the feature described in ${tid}. This was a significant change that modified multiple files across the codebase to support the new functionality.`);
|
|
63
|
+
lines.push("");
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Source-level structural tests ────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
const mechanicalSrc = readFileSync(
|
|
70
|
+
join(import.meta.dirname!, "..", "mechanical-completion.ts"),
|
|
71
|
+
"utf-8",
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
test("mechanical-completion: exports mechanicalSliceCompletion", () => {
|
|
75
|
+
assert.ok(
|
|
76
|
+
mechanicalSrc.includes("export async function mechanicalSliceCompletion"),
|
|
77
|
+
"should export mechanicalSliceCompletion",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("mechanical-completion: exports aggregateMilestoneVerification", () => {
|
|
82
|
+
assert.ok(
|
|
83
|
+
mechanicalSrc.includes("export async function aggregateMilestoneVerification"),
|
|
84
|
+
"should export aggregateMilestoneVerification",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("mechanical-completion: exports generateMilestoneSummary", () => {
|
|
89
|
+
assert.ok(
|
|
90
|
+
mechanicalSrc.includes("export async function generateMilestoneSummary"),
|
|
91
|
+
"should export generateMilestoneSummary",
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("mechanical-completion: exports appendNewDecisions", () => {
|
|
96
|
+
assert.ok(
|
|
97
|
+
mechanicalSrc.includes("export async function appendNewDecisions"),
|
|
98
|
+
"should export appendNewDecisions",
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("mechanical-completion: uses atomicWriteSync for file writes", () => {
|
|
103
|
+
assert.ok(
|
|
104
|
+
mechanicalSrc.includes("atomicWriteSync"),
|
|
105
|
+
"should use atomicWriteSync for safe file writes",
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("mechanical-completion: quality gate checks summary length for multi-task slices", () => {
|
|
110
|
+
assert.ok(
|
|
111
|
+
mechanicalSrc.includes("totalContent.length < 200"),
|
|
112
|
+
"should have quality gate for summary content length",
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("mechanical-completion: marks slice [x] in roadmap", () => {
|
|
117
|
+
assert.ok(
|
|
118
|
+
mechanicalSrc.includes("markSliceInRoadmap"),
|
|
119
|
+
"should mark slice done in roadmap",
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("mechanical-completion: aggregates VERIFY.json files for milestone validation", () => {
|
|
124
|
+
assert.ok(
|
|
125
|
+
mechanicalSrc.includes("resolveTaskJsonFiles") && mechanicalSrc.includes("VERIFY"),
|
|
126
|
+
"should read VERIFY.json files for milestone validation",
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("mechanical-completion: deduplicates decisions against existing DECISIONS.md", () => {
|
|
131
|
+
assert.ok(
|
|
132
|
+
mechanicalSrc.includes("existing.includes(d.trim())"),
|
|
133
|
+
"should deduplicate decisions against existing content",
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("mechanical-completion: produces VALIDATION.md with verdict frontmatter", () => {
|
|
138
|
+
assert.ok(
|
|
139
|
+
mechanicalSrc.includes("verdict:") && mechanicalSrc.includes("remediation_round: 0"),
|
|
140
|
+
"VALIDATION.md should have verdict and remediation_round frontmatter",
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ─── Integration tests with fixture data ──────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
test("mechanical: slice completion with 2 task summaries produces SUMMARY.md", async () => {
|
|
147
|
+
const base = createTmpBase();
|
|
148
|
+
try {
|
|
149
|
+
const mid = "M001";
|
|
150
|
+
const sid = "S01";
|
|
151
|
+
|
|
152
|
+
// Scaffold task summaries
|
|
153
|
+
scaffold(base, mid, sid, [
|
|
154
|
+
{
|
|
155
|
+
tid: "T01",
|
|
156
|
+
content: makeTaskSummary("T01", {
|
|
157
|
+
oneLiner: "Set up project structure",
|
|
158
|
+
provides: ["project-scaffold"],
|
|
159
|
+
key_files: ["src/index.ts", "package.json"],
|
|
160
|
+
verification_result: "passed",
|
|
161
|
+
}),
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
tid: "T02",
|
|
165
|
+
content: makeTaskSummary("T02", {
|
|
166
|
+
oneLiner: "Add core API endpoints",
|
|
167
|
+
provides: ["api-endpoints"],
|
|
168
|
+
key_files: ["src/api.ts"],
|
|
169
|
+
key_decisions: ["Used Express over Fastify"],
|
|
170
|
+
verification_result: "passed",
|
|
171
|
+
}),
|
|
172
|
+
},
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
// Write a roadmap with the slice unchecked
|
|
176
|
+
const roadmapPath = join(base, ".gsd", "milestones", mid, `${mid}-ROADMAP.md`);
|
|
177
|
+
writeFileSync(roadmapPath, `# Roadmap\n\n- [ ] **${sid}: First Slice**\n`, "utf-8");
|
|
178
|
+
|
|
179
|
+
// Write a slice plan with Verification section
|
|
180
|
+
const planPath = join(base, ".gsd", "milestones", mid, "slices", sid, `${sid}-PLAN.md`);
|
|
181
|
+
writeFileSync(planPath, `# Plan\n\n## Verification\n\n- Run \`npm test\`\n- Check output\n`, "utf-8");
|
|
182
|
+
|
|
183
|
+
// Dynamic import to get the actual module
|
|
184
|
+
const { mechanicalSliceCompletion } = await import("../mechanical-completion.js");
|
|
185
|
+
const ok = await mechanicalSliceCompletion(base, mid, sid);
|
|
186
|
+
|
|
187
|
+
assert.ok(ok, "should return true for valid slice completion");
|
|
188
|
+
|
|
189
|
+
// Check SUMMARY.md was written
|
|
190
|
+
const summaryPath = join(base, ".gsd", "milestones", mid, "slices", sid, `${sid}-SUMMARY.md`);
|
|
191
|
+
assert.ok(existsSync(summaryPath), "SUMMARY.md should exist");
|
|
192
|
+
|
|
193
|
+
const summaryContent = readFileSync(summaryPath, "utf-8");
|
|
194
|
+
assert.ok(summaryContent.includes("T01"), "summary should reference T01");
|
|
195
|
+
assert.ok(summaryContent.includes("T02"), "summary should reference T02");
|
|
196
|
+
assert.ok(summaryContent.includes("verification_result: passed"), "should have passed verification");
|
|
197
|
+
|
|
198
|
+
// Check roadmap was updated
|
|
199
|
+
const updatedRoadmap = readFileSync(roadmapPath, "utf-8");
|
|
200
|
+
assert.ok(updatedRoadmap.includes("[x]"), "roadmap should have [x] checkbox");
|
|
201
|
+
|
|
202
|
+
// Check UAT was written
|
|
203
|
+
const uatPath = join(base, ".gsd", "milestones", mid, "slices", sid, `${sid}-UAT.md`);
|
|
204
|
+
assert.ok(existsSync(uatPath), "UAT.md should exist");
|
|
205
|
+
const uatContent = readFileSync(uatPath, "utf-8");
|
|
206
|
+
assert.ok(uatContent.includes("npm test"), "UAT should contain verification content");
|
|
207
|
+
} finally {
|
|
208
|
+
rmSync(base, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("mechanical: returns false for empty task summaries", async () => {
|
|
213
|
+
const base = createTmpBase();
|
|
214
|
+
try {
|
|
215
|
+
const mid = "M001";
|
|
216
|
+
const sid = "S01";
|
|
217
|
+
scaffold(base, mid, sid, []);
|
|
218
|
+
|
|
219
|
+
const { mechanicalSliceCompletion } = await import("../mechanical-completion.js");
|
|
220
|
+
const ok = await mechanicalSliceCompletion(base, mid, sid);
|
|
221
|
+
assert.ok(!ok, "should return false when no summaries exist");
|
|
222
|
+
} finally {
|
|
223
|
+
rmSync(base, { recursive: true, force: true });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("mechanical: returns false for insufficient summary content in multi-task slice", async () => {
|
|
228
|
+
const base = createTmpBase();
|
|
229
|
+
try {
|
|
230
|
+
const mid = "M001";
|
|
231
|
+
const sid = "S01";
|
|
232
|
+
|
|
233
|
+
// Two tasks but with very short content (under 200 chars)
|
|
234
|
+
scaffold(base, mid, sid, [
|
|
235
|
+
{ tid: "T01", content: "---\nid: T01\nparent: S01\nmilestone: M001\n---\n\n# T01: A\n\n**Short**\n" },
|
|
236
|
+
{ tid: "T02", content: "---\nid: T02\nparent: S01\nmilestone: M001\n---\n\n# T02: B\n\n**Brief**\n" },
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
const { mechanicalSliceCompletion } = await import("../mechanical-completion.js");
|
|
240
|
+
const ok = await mechanicalSliceCompletion(base, mid, sid);
|
|
241
|
+
assert.ok(!ok, "should return false when summaries are too short");
|
|
242
|
+
} finally {
|
|
243
|
+
rmSync(base, { recursive: true, force: true });
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("mechanical: milestone verification aggregates VERIFY.json files", async () => {
|
|
248
|
+
const base = createTmpBase();
|
|
249
|
+
try {
|
|
250
|
+
const mid = "M001";
|
|
251
|
+
const sid = "S01";
|
|
252
|
+
const { tDir } = scaffold(base, mid, sid, []);
|
|
253
|
+
|
|
254
|
+
// Write VERIFY.json files
|
|
255
|
+
const evidence = {
|
|
256
|
+
schemaVersion: 1,
|
|
257
|
+
taskId: "T01",
|
|
258
|
+
unitId: "M001/S01/T01",
|
|
259
|
+
timestamp: Date.now(),
|
|
260
|
+
passed: true,
|
|
261
|
+
discoverySource: "plan",
|
|
262
|
+
checks: [
|
|
263
|
+
{ command: "npm test", exitCode: 0, durationMs: 1500, verdict: "pass", blocking: true },
|
|
264
|
+
],
|
|
265
|
+
};
|
|
266
|
+
writeFileSync(join(tDir, "T01-VERIFY.json"), JSON.stringify(evidence), "utf-8");
|
|
267
|
+
|
|
268
|
+
const evidence2 = { ...evidence, taskId: "T02", passed: false, checks: [
|
|
269
|
+
{ command: "npm test", exitCode: 1, durationMs: 500, verdict: "fail", blocking: true },
|
|
270
|
+
]};
|
|
271
|
+
writeFileSync(join(tDir, "T02-VERIFY.json"), JSON.stringify(evidence2), "utf-8");
|
|
272
|
+
|
|
273
|
+
const { aggregateMilestoneVerification } = await import("../mechanical-completion.js");
|
|
274
|
+
const result = await aggregateMilestoneVerification(base, mid);
|
|
275
|
+
|
|
276
|
+
assert.equal(result.verdict, "mixed", "should be mixed when some pass and some fail");
|
|
277
|
+
assert.equal(result.checks.length, 2, "should have 2 checks");
|
|
278
|
+
|
|
279
|
+
// Check VALIDATION.md was written
|
|
280
|
+
const validationPath = join(base, ".gsd", "milestones", mid, `${mid}-VALIDATION.md`);
|
|
281
|
+
assert.ok(existsSync(validationPath), "VALIDATION.md should exist");
|
|
282
|
+
const validationContent = readFileSync(validationPath, "utf-8");
|
|
283
|
+
assert.ok(validationContent.includes("verdict: mixed"), "should have mixed verdict in frontmatter");
|
|
284
|
+
} finally {
|
|
285
|
+
rmSync(base, { recursive: true, force: true });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("mechanical: milestone summary aggregates slice summaries", async () => {
|
|
290
|
+
const base = createTmpBase();
|
|
291
|
+
try {
|
|
292
|
+
const mid = "M001";
|
|
293
|
+
|
|
294
|
+
// Create two slices with summaries
|
|
295
|
+
for (const sid of ["S01", "S02"]) {
|
|
296
|
+
const sDir = join(base, ".gsd", "milestones", mid, "slices", sid);
|
|
297
|
+
mkdirSync(sDir, { recursive: true });
|
|
298
|
+
writeFileSync(
|
|
299
|
+
join(sDir, `${sid}-SUMMARY.md`),
|
|
300
|
+
`---\nid: ${sid}\nprovides:\n - feature-${sid.toLowerCase()}\nkey_files:\n - src/${sid.toLowerCase()}.ts\n---\n\n# ${sid}: Slice\n\n**${sid} implemented**\n`,
|
|
301
|
+
"utf-8",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const { generateMilestoneSummary } = await import("../mechanical-completion.js");
|
|
306
|
+
const content = await generateMilestoneSummary(base, mid);
|
|
307
|
+
|
|
308
|
+
assert.ok(content.includes("S01"), "should reference S01");
|
|
309
|
+
assert.ok(content.includes("S02"), "should reference S02");
|
|
310
|
+
assert.ok(content.includes("feature-s01"), "should aggregate provides");
|
|
311
|
+
assert.ok(content.includes("feature-s02"), "should aggregate provides");
|
|
312
|
+
|
|
313
|
+
const summaryPath = join(base, ".gsd", "milestones", mid, `${mid}-SUMMARY.md`);
|
|
314
|
+
assert.ok(existsSync(summaryPath), "M##-SUMMARY.md should exist");
|
|
315
|
+
} finally {
|
|
316
|
+
rmSync(base, { recursive: true, force: true });
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("mechanical: decision deduplication skips existing decisions", async () => {
|
|
321
|
+
const base = createTmpBase();
|
|
322
|
+
try {
|
|
323
|
+
const gsdRoot = join(base, ".gsd");
|
|
324
|
+
mkdirSync(gsdRoot, { recursive: true });
|
|
325
|
+
|
|
326
|
+
// Write existing decisions
|
|
327
|
+
const decisionsPath = join(gsdRoot, "DECISIONS.md");
|
|
328
|
+
writeFileSync(decisionsPath, "# Decisions\n\n- Used TypeScript for type safety\n", "utf-8");
|
|
329
|
+
|
|
330
|
+
const { appendNewDecisions } = await import("../mechanical-completion.js");
|
|
331
|
+
|
|
332
|
+
// Call with one existing and one new decision
|
|
333
|
+
const mockSummaries = [
|
|
334
|
+
{
|
|
335
|
+
frontmatter: {
|
|
336
|
+
id: "T01", parent: "S01", milestone: "M001",
|
|
337
|
+
provides: [], requires: [], affects: [],
|
|
338
|
+
key_files: [], key_decisions: ["Used TypeScript for type safety", "Chose Express over Koa"],
|
|
339
|
+
patterns_established: [], drill_down_paths: [], observability_surfaces: [],
|
|
340
|
+
duration: "", verification_result: "passed", completed_at: "", blocker_discovered: false,
|
|
341
|
+
},
|
|
342
|
+
title: "T01", oneLiner: "", whatHappened: "", deviations: "", filesModified: [],
|
|
343
|
+
},
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
await appendNewDecisions(base, mockSummaries as any);
|
|
347
|
+
|
|
348
|
+
const updated = readFileSync(decisionsPath, "utf-8");
|
|
349
|
+
assert.ok(updated.includes("Chose Express over Koa"), "should append new decision");
|
|
350
|
+
// The existing decision should not be duplicated
|
|
351
|
+
const matches = updated.match(/Used TypeScript for type safety/g);
|
|
352
|
+
assert.equal(matches?.length, 1, "should not duplicate existing decision");
|
|
353
|
+
} finally {
|
|
354
|
+
rmSync(base, { recursive: true, force: true });
|
|
355
|
+
}
|
|
356
|
+
});
|
|
@@ -25,6 +25,7 @@ const BASE_VARS = {
|
|
|
25
25
|
outputPath: "/tmp/test-project/.gsd/milestones/M001/slices/S01/S01-PLAN.md",
|
|
26
26
|
inlinedContext: "--- test inlined context ---",
|
|
27
27
|
dependencySummaries: "", executorContextConstraints: "",
|
|
28
|
+
sourceFilePaths: "- **Requirements**: `.gsd/REQUIREMENTS.md`",
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
test("plan-slice prompt: commit step present when commit_docs=true", () => {
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdirSync, mkdtempSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
acquireSessionLock,
|
|
9
|
+
releaseSessionLock,
|
|
10
|
+
updateSessionLock,
|
|
11
|
+
validateSessionLock,
|
|
12
|
+
readSessionLockData,
|
|
13
|
+
isSessionLockHeld,
|
|
14
|
+
isSessionLockProcessAlive,
|
|
15
|
+
} from "../session-lock.ts";
|
|
16
|
+
|
|
17
|
+
// ─── acquireSessionLock ──────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
test("acquireSessionLock succeeds on empty directory", () => {
|
|
20
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
21
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
22
|
+
|
|
23
|
+
const result = acquireSessionLock(dir);
|
|
24
|
+
assert.equal(result.acquired, true, "should acquire lock on empty dir");
|
|
25
|
+
|
|
26
|
+
// Verify lock file was created with correct data
|
|
27
|
+
const lockPath = join(dir, ".gsd", "auto.lock");
|
|
28
|
+
assert.ok(existsSync(lockPath), "auto.lock should exist after acquire");
|
|
29
|
+
|
|
30
|
+
const data = JSON.parse(readFileSync(lockPath, "utf-8"));
|
|
31
|
+
assert.equal(data.pid, process.pid, "lock should contain current PID");
|
|
32
|
+
assert.equal(data.unitType, "starting", "initial unit type should be 'starting'");
|
|
33
|
+
|
|
34
|
+
releaseSessionLock(dir);
|
|
35
|
+
rmSync(dir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("acquireSessionLock rejects when another live process holds lock", () => {
|
|
39
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
40
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
41
|
+
|
|
42
|
+
// Simulate another process holding the lock by writing a lock with parent PID
|
|
43
|
+
const fakeLockData = {
|
|
44
|
+
pid: process.ppid,
|
|
45
|
+
startedAt: new Date().toISOString(),
|
|
46
|
+
unitType: "execute-task",
|
|
47
|
+
unitId: "M001/S01/T01",
|
|
48
|
+
unitStartedAt: new Date().toISOString(),
|
|
49
|
+
completedUnits: 2,
|
|
50
|
+
};
|
|
51
|
+
writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(fakeLockData, null, 2));
|
|
52
|
+
|
|
53
|
+
// First acquire to set up proper-lockfile state
|
|
54
|
+
const result1 = acquireSessionLock(dir);
|
|
55
|
+
|
|
56
|
+
// If proper-lockfile is available, it should manage the OS lock.
|
|
57
|
+
// If not (fallback mode), the PID check should detect the live process.
|
|
58
|
+
// Either way, we can't fully simulate another process holding an OS lock
|
|
59
|
+
// from within the same process, so we test the fallback path.
|
|
60
|
+
if (result1.acquired) {
|
|
61
|
+
// We got the lock (proper-lockfile saw no OS lock from another process)
|
|
62
|
+
// This is expected since we're in the same process
|
|
63
|
+
releaseSessionLock(dir);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
rmSync(dir, { recursive: true, force: true });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("acquireSessionLock takes over stale lock from dead process", () => {
|
|
70
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
71
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
72
|
+
|
|
73
|
+
// Write a lock from a dead process
|
|
74
|
+
const staleLockData = {
|
|
75
|
+
pid: 9999999,
|
|
76
|
+
startedAt: "2026-03-01T00:00:00Z",
|
|
77
|
+
unitType: "execute-task",
|
|
78
|
+
unitId: "M001/S01/T01",
|
|
79
|
+
unitStartedAt: "2026-03-01T00:00:00Z",
|
|
80
|
+
completedUnits: 0,
|
|
81
|
+
};
|
|
82
|
+
writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(staleLockData, null, 2));
|
|
83
|
+
|
|
84
|
+
const result = acquireSessionLock(dir);
|
|
85
|
+
assert.equal(result.acquired, true, "should take over lock from dead process");
|
|
86
|
+
|
|
87
|
+
// Verify our PID is now in the lock
|
|
88
|
+
const data = readSessionLockData(dir);
|
|
89
|
+
assert.ok(data, "lock data should exist after acquire");
|
|
90
|
+
assert.equal(data!.pid, process.pid, "lock should contain our PID now");
|
|
91
|
+
|
|
92
|
+
releaseSessionLock(dir);
|
|
93
|
+
rmSync(dir, { recursive: true, force: true });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ─── releaseSessionLock ─────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
test("releaseSessionLock removes the lock file", () => {
|
|
99
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
100
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
101
|
+
|
|
102
|
+
const result = acquireSessionLock(dir);
|
|
103
|
+
assert.equal(result.acquired, true);
|
|
104
|
+
|
|
105
|
+
releaseSessionLock(dir);
|
|
106
|
+
|
|
107
|
+
const lockPath = join(dir, ".gsd", "auto.lock");
|
|
108
|
+
assert.ok(!existsSync(lockPath), "auto.lock should be removed after release");
|
|
109
|
+
|
|
110
|
+
rmSync(dir, { recursive: true, force: true });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("releaseSessionLock is safe when no lock exists", () => {
|
|
114
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
115
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
116
|
+
|
|
117
|
+
// Should not throw
|
|
118
|
+
releaseSessionLock(dir);
|
|
119
|
+
|
|
120
|
+
rmSync(dir, { recursive: true, force: true });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ─── updateSessionLock ──────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
test("updateSessionLock updates the lock data without re-acquiring", () => {
|
|
126
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
127
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
128
|
+
|
|
129
|
+
const result = acquireSessionLock(dir);
|
|
130
|
+
assert.equal(result.acquired, true);
|
|
131
|
+
|
|
132
|
+
updateSessionLock(dir, "execute-task", "M001/S01/T02", 3, "/tmp/session.jsonl");
|
|
133
|
+
|
|
134
|
+
const data = readSessionLockData(dir);
|
|
135
|
+
assert.ok(data, "lock data should exist after update");
|
|
136
|
+
assert.equal(data!.pid, process.pid, "PID should still be ours");
|
|
137
|
+
assert.equal(data!.unitType, "execute-task", "unit type should be updated");
|
|
138
|
+
assert.equal(data!.unitId, "M001/S01/T02", "unit ID should be updated");
|
|
139
|
+
assert.equal(data!.completedUnits, 3, "completed count should be updated");
|
|
140
|
+
assert.equal(data!.sessionFile, "/tmp/session.jsonl", "session file should be recorded");
|
|
141
|
+
|
|
142
|
+
releaseSessionLock(dir);
|
|
143
|
+
rmSync(dir, { recursive: true, force: true });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ─── validateSessionLock ────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
test("validateSessionLock returns true when we hold the lock", () => {
|
|
149
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
150
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
151
|
+
|
|
152
|
+
const result = acquireSessionLock(dir);
|
|
153
|
+
assert.equal(result.acquired, true);
|
|
154
|
+
|
|
155
|
+
assert.equal(validateSessionLock(dir), true, "should validate when we hold the lock");
|
|
156
|
+
|
|
157
|
+
releaseSessionLock(dir);
|
|
158
|
+
rmSync(dir, { recursive: true, force: true });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("validateSessionLock returns false after release", () => {
|
|
162
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
163
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
164
|
+
|
|
165
|
+
const result = acquireSessionLock(dir);
|
|
166
|
+
assert.equal(result.acquired, true);
|
|
167
|
+
assert.equal(validateSessionLock(dir), true, "should be valid while held");
|
|
168
|
+
|
|
169
|
+
// Release the lock — both OS lock and lock file are removed
|
|
170
|
+
releaseSessionLock(dir);
|
|
171
|
+
|
|
172
|
+
// After release, _lockedPath is cleared and lock file is gone
|
|
173
|
+
assert.equal(isSessionLockHeld(dir), false, "should not be held after release");
|
|
174
|
+
|
|
175
|
+
rmSync(dir, { recursive: true, force: true });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("validateSessionLock returns false when another PID owns the lock", () => {
|
|
179
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
180
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
181
|
+
|
|
182
|
+
// Write lock data with a different PID (parent process)
|
|
183
|
+
const foreignLockData = {
|
|
184
|
+
pid: process.ppid,
|
|
185
|
+
startedAt: new Date().toISOString(),
|
|
186
|
+
unitType: "execute-task",
|
|
187
|
+
unitId: "M001/S01/T01",
|
|
188
|
+
unitStartedAt: new Date().toISOString(),
|
|
189
|
+
completedUnits: 0,
|
|
190
|
+
};
|
|
191
|
+
writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(foreignLockData, null, 2));
|
|
192
|
+
|
|
193
|
+
// Without holding the OS lock, validate should check PID
|
|
194
|
+
assert.equal(validateSessionLock(dir), false, "should fail when another PID owns lock");
|
|
195
|
+
|
|
196
|
+
rmSync(dir, { recursive: true, force: true });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ─── isSessionLockHeld ──────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
test("isSessionLockHeld returns true after acquire", () => {
|
|
202
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
203
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
204
|
+
|
|
205
|
+
acquireSessionLock(dir);
|
|
206
|
+
assert.equal(isSessionLockHeld(dir), true);
|
|
207
|
+
|
|
208
|
+
releaseSessionLock(dir);
|
|
209
|
+
assert.equal(isSessionLockHeld(dir), false, "should return false after release");
|
|
210
|
+
|
|
211
|
+
rmSync(dir, { recursive: true, force: true });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ─── isSessionLockProcessAlive ──────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
test("isSessionLockProcessAlive returns false for dead PID", () => {
|
|
217
|
+
const data = {
|
|
218
|
+
pid: 9999999,
|
|
219
|
+
startedAt: new Date().toISOString(),
|
|
220
|
+
unitType: "starting",
|
|
221
|
+
unitId: "bootstrap",
|
|
222
|
+
unitStartedAt: new Date().toISOString(),
|
|
223
|
+
completedUnits: 0,
|
|
224
|
+
};
|
|
225
|
+
assert.equal(isSessionLockProcessAlive(data), false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("isSessionLockProcessAlive returns false for own PID (recycled)", () => {
|
|
229
|
+
const data = {
|
|
230
|
+
pid: process.pid,
|
|
231
|
+
startedAt: new Date().toISOString(),
|
|
232
|
+
unitType: "starting",
|
|
233
|
+
unitId: "bootstrap",
|
|
234
|
+
unitStartedAt: new Date().toISOString(),
|
|
235
|
+
completedUnits: 0,
|
|
236
|
+
};
|
|
237
|
+
// Own PID returns false because it means the lock is from a recycled PID
|
|
238
|
+
assert.equal(isSessionLockProcessAlive(data), false);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ─── readSessionLockData ────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
test("readSessionLockData returns null when no lock exists", () => {
|
|
244
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
245
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
246
|
+
|
|
247
|
+
const data = readSessionLockData(dir);
|
|
248
|
+
assert.equal(data, null);
|
|
249
|
+
|
|
250
|
+
rmSync(dir, { recursive: true, force: true });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("readSessionLockData reads existing lock data", () => {
|
|
254
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
255
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
256
|
+
|
|
257
|
+
const lockData = {
|
|
258
|
+
pid: 12345,
|
|
259
|
+
startedAt: "2026-03-18T00:00:00Z",
|
|
260
|
+
unitType: "execute-task",
|
|
261
|
+
unitId: "M001/S01/T01",
|
|
262
|
+
unitStartedAt: "2026-03-18T00:01:00Z",
|
|
263
|
+
completedUnits: 2,
|
|
264
|
+
sessionFile: "/tmp/session.jsonl",
|
|
265
|
+
};
|
|
266
|
+
writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2));
|
|
267
|
+
|
|
268
|
+
const data = readSessionLockData(dir);
|
|
269
|
+
assert.ok(data, "should read lock data");
|
|
270
|
+
assert.equal(data!.pid, 12345);
|
|
271
|
+
assert.equal(data!.unitType, "execute-task");
|
|
272
|
+
assert.equal(data!.unitId, "M001/S01/T01");
|
|
273
|
+
assert.equal(data!.completedUnits, 2);
|
|
274
|
+
assert.equal(data!.sessionFile, "/tmp/session.jsonl");
|
|
275
|
+
|
|
276
|
+
rmSync(dir, { recursive: true, force: true });
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ─── Acquire → Release → Re-Acquire lifecycle ──────────────────────────
|
|
280
|
+
|
|
281
|
+
test("session lock supports acquire → release → re-acquire cycle", () => {
|
|
282
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
283
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
284
|
+
|
|
285
|
+
// First acquire
|
|
286
|
+
const r1 = acquireSessionLock(dir);
|
|
287
|
+
assert.equal(r1.acquired, true, "first acquire should succeed");
|
|
288
|
+
assert.equal(isSessionLockHeld(dir), true);
|
|
289
|
+
|
|
290
|
+
// Release
|
|
291
|
+
releaseSessionLock(dir);
|
|
292
|
+
assert.equal(isSessionLockHeld(dir), false);
|
|
293
|
+
|
|
294
|
+
// Re-acquire
|
|
295
|
+
const r2 = acquireSessionLock(dir);
|
|
296
|
+
assert.equal(r2.acquired, true, "re-acquire after release should succeed");
|
|
297
|
+
assert.equal(isSessionLockHeld(dir), true);
|
|
298
|
+
|
|
299
|
+
releaseSessionLock(dir);
|
|
300
|
+
rmSync(dir, { recursive: true, force: true });
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ─── Lock creates .gsd/ directory if needed ─────────────────────────────
|
|
304
|
+
|
|
305
|
+
test("acquireSessionLock creates .gsd/ if it does not exist", () => {
|
|
306
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
|
|
307
|
+
// Do NOT create .gsd/ — let the lock function do it
|
|
308
|
+
|
|
309
|
+
const result = acquireSessionLock(dir);
|
|
310
|
+
assert.equal(result.acquired, true, "should succeed even without .gsd/");
|
|
311
|
+
assert.ok(existsSync(join(dir, ".gsd")), ".gsd/ should be created");
|
|
312
|
+
|
|
313
|
+
releaseSessionLock(dir);
|
|
314
|
+
rmSync(dir, { recursive: true, force: true });
|
|
315
|
+
});
|