@opengsd/gsd-pi 1.5.0-dev.4fb2d460 → 1.5.0-dev.c4c8885f
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/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +114 -36
- package/dist/resources/extensions/gsd/activity-log.js +1 -1
- package/dist/resources/extensions/gsd/auto/orchestrator.js +16 -10
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +53 -15
- package/dist/resources/extensions/gsd/auto-model-selection.js +28 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +4 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +29 -117
- package/dist/resources/extensions/gsd/auto-start.js +53 -30
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +12 -2
- package/dist/resources/extensions/gsd/auto-timers.js +9 -2
- package/dist/resources/extensions/gsd/auto.js +97 -31
- package/dist/resources/extensions/gsd/compat/compat-marker.js +44 -0
- package/dist/resources/extensions/gsd/config-overlay.js +7 -2
- package/dist/resources/extensions/gsd/db/engine.js +33 -0
- package/dist/resources/extensions/gsd/db-adapter.js +30 -3
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +3 -3
- package/dist/resources/extensions/gsd/journal.js +1 -1
- package/dist/resources/extensions/gsd/markdown-renderer.js +34 -91
- package/dist/resources/extensions/gsd/metrics.js +2 -2
- package/dist/resources/extensions/gsd/paths.js +70 -49
- package/dist/resources/extensions/gsd/post-unit-hooks.js +27 -0
- package/dist/resources/extensions/gsd/preferences-models.js +66 -15
- package/dist/resources/extensions/gsd/preferences-validation.js +66 -10
- package/dist/resources/extensions/gsd/rule-registry.js +106 -10
- package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +1 -10
- package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +3 -24
- package/dist/resources/extensions/gsd/state-reconciliation/index.js +10 -0
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -5
- package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +16 -10
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +3 -8
- package/dist/resources/extensions/gsd/uat-dispatch.js +94 -0
- package/dist/resources/extensions/gsd/uok/audit-toggle.js +31 -2
- package/dist/resources/extensions/gsd/uok/audit.js +3 -0
- package/dist/resources/extensions/gsd/uok/gate-runner.js +22 -30
- package/dist/resources/extensions/gsd/uok/gitops.js +18 -15
- package/dist/resources/extensions/gsd/uok/kernel.js +120 -46
- package/dist/resources/extensions/gsd/uok/model-policy.js +17 -14
- package/dist/resources/extensions/gsd/workflow-event-ledger.js +1 -1
- package/dist/resources/extensions/gsd/workflow-logger.js +1 -1
- package/dist/resources/extensions/gsd/workflow-projections.js +9 -26
- package/dist/resources/extensions/gsd/workspace.js +5 -14
- package/dist/resources/extensions/shared/gsd-browser-cli.js +29 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/chunks/{1128.js → 1991.js} +1 -1
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/worktree-cli-create.d.ts +10 -0
- package/dist/worktree-cli-create.js +11 -0
- package/dist/worktree-cli-format.d.ts +12 -0
- package/dist/worktree-cli-format.js +38 -0
- package/dist/worktree-cli-plan.d.ts +16 -0
- package/dist/worktree-cli-plan.js +19 -0
- package/dist/worktree-cli-session.d.ts +13 -0
- package/dist/worktree-cli-session.js +18 -0
- package/dist/worktree-cli-status.d.ts +35 -0
- package/dist/worktree-cli-status.js +54 -0
- package/dist/worktree-cli.d.ts +1 -16
- package/dist/worktree-cli.js +28 -122
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
- package/integrations/hermes/pyproject.toml +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/dist/orchestrator-agent.d.ts +21 -0
- package/packages/daemon/dist/orchestrator-agent.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator-agent.js +113 -0
- package/packages/daemon/dist/orchestrator-agent.js.map +1 -0
- package/packages/daemon/dist/orchestrator-tools.d.ts +12 -0
- package/packages/daemon/dist/orchestrator-tools.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator-tools.js +137 -0
- package/packages/daemon/dist/orchestrator-tools.js.map +1 -0
- package/packages/daemon/dist/orchestrator.d.ts +4 -56
- package/packages/daemon/dist/orchestrator.d.ts.map +1 -1
- package/packages/daemon/dist/orchestrator.js +13 -303
- package/packages/daemon/dist/orchestrator.js.map +1 -1
- package/packages/daemon/dist/orchestrator.test.js +11 -1
- package/packages/daemon/dist/orchestrator.test.js.map +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +146 -33
- package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +25 -0
- package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +93 -0
- package/src/resources/extensions/gsd/activity-log.ts +1 -1
- package/src/resources/extensions/gsd/auto/orchestrator.ts +17 -11
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +59 -14
- package/src/resources/extensions/gsd/auto-model-selection.ts +33 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +4 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +53 -113
- package/src/resources/extensions/gsd/auto-start.ts +57 -28
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +12 -2
- package/src/resources/extensions/gsd/auto-timers.ts +9 -2
- package/src/resources/extensions/gsd/auto.ts +103 -30
- package/src/resources/extensions/gsd/compat/compat-marker.ts +47 -0
- package/src/resources/extensions/gsd/config-overlay.ts +7 -1
- package/src/resources/extensions/gsd/db/engine.ts +36 -0
- package/src/resources/extensions/gsd/db-adapter.ts +29 -3
- package/src/resources/extensions/gsd/docs/preferences-reference.md +3 -3
- package/src/resources/extensions/gsd/journal.ts +1 -1
- package/src/resources/extensions/gsd/markdown-renderer.ts +52 -103
- package/src/resources/extensions/gsd/metrics.ts +2 -2
- package/src/resources/extensions/gsd/paths.ts +87 -48
- package/src/resources/extensions/gsd/post-unit-hooks.ts +32 -0
- package/src/resources/extensions/gsd/preferences-models.ts +74 -20
- package/src/resources/extensions/gsd/preferences-types.ts +24 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +77 -9
- package/src/resources/extensions/gsd/rule-registry.ts +111 -9
- package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +1 -9
- package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +2 -22
- package/src/resources/extensions/gsd/state-reconciliation/index.ts +14 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +41 -2
- package/src/resources/extensions/gsd/tests/auto-post-unit-artifact-diagnostic.test.ts +48 -1
- package/src/resources/extensions/gsd/tests/auto-start-project-milestone-reconcile.test.ts +29 -1
- package/src/resources/extensions/gsd/tests/auto-timeout-recovery-supervisor-inflight-guard.test.ts +159 -0
- package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +3 -1
- package/src/resources/extensions/gsd/tests/compat-marker.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/db-adapter.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/db-writable-probe.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/flat-phase-renderer.test.ts +22 -1
- package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +72 -32
- package/src/resources/extensions/gsd/tests/integration/paths.test.ts +88 -1
- package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +269 -0
- package/src/resources/extensions/gsd/tests/post-unit-retry-on-orchestrator-bridge.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/reassess-handler.test.ts +49 -1
- package/src/resources/extensions/gsd/tests/rule-registry.test.ts +95 -1
- package/src/resources/extensions/gsd/tests/sibling-model-fallbacks.test.ts +189 -0
- package/src/resources/extensions/gsd/tests/single-field-model-thinking.test.ts +156 -0
- package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/supervisor-model-swap-inflight-guard.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/supervisor-model-swap-thinking-clamp.test.ts +122 -0
- package/src/resources/extensions/gsd/tests/uat-dispatch.test.ts +211 -0
- package/src/resources/extensions/gsd/tests/uok-gate-runner.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/uok-kernel-path.test.ts +226 -2
- package/src/resources/extensions/gsd/tests/uok-model-policy.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +213 -2
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +9 -5
- package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +19 -8
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +13 -9
- package/src/resources/extensions/gsd/types.ts +23 -4
- package/src/resources/extensions/gsd/uat-dispatch.ts +176 -0
- package/src/resources/extensions/gsd/uok/audit-toggle.ts +32 -2
- package/src/resources/extensions/gsd/uok/audit.ts +2 -0
- package/src/resources/extensions/gsd/uok/gate-runner.ts +26 -36
- package/src/resources/extensions/gsd/uok/gitops.ts +21 -18
- package/src/resources/extensions/gsd/uok/kernel.ts +169 -48
- package/src/resources/extensions/gsd/uok/model-policy.ts +20 -17
- package/src/resources/extensions/gsd/workflow-event-ledger.ts +1 -1
- package/src/resources/extensions/gsd/workflow-logger.ts +1 -1
- package/src/resources/extensions/gsd/workflow-projections.ts +9 -24
- package/src/resources/extensions/gsd/workspace.ts +5 -21
- package/src/resources/extensions/shared/gsd-browser-cli.ts +29 -0
- /package/dist/web/standalone/.next/static/{gXoWec4UNqcR0tkuHwYCr → NPMNi2HV8BuK3baQsEUhk}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{gXoWec4UNqcR0tkuHwYCr → NPMNi2HV8BuK3baQsEUhk}/_ssgManifest.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
maybeWriteParallelResearchCostSpikeBlocker,
|
|
9
9
|
} from "../auto-post-unit.ts";
|
|
10
10
|
import { resolveExpectedArtifactPath } from "../auto-recovery.ts";
|
|
11
|
+
import { _clearGsdRootCache, clearPathCache } from "../paths.ts";
|
|
11
12
|
|
|
12
13
|
test("missing execute-task artifact includes completion contract and completion-tool hint", () => {
|
|
13
14
|
const base = mkdtempSync(join(tmpdir(), "gsd-artifact-diag-"));
|
|
@@ -56,3 +57,49 @@ test("parallel research cost spike writes durable PARALLEL-BLOCKER", () => {
|
|
|
56
57
|
rmSync(base, { recursive: true, force: true });
|
|
57
58
|
}
|
|
58
59
|
});
|
|
60
|
+
|
|
61
|
+
// ── #1238: symlinked project root must not produce a ../ path walk ────────────
|
|
62
|
+
//
|
|
63
|
+
// On WSL (and any layout where the project root is reached through a symlink),
|
|
64
|
+
// artifact resolution canonicalizes through gsdRoot (realpathSync), so the
|
|
65
|
+
// resolved artifact path lives under the REAL root while `basePath` is still the
|
|
66
|
+
// symlink. Before the fix, relative(symlinkBase, realArtifactPath) walked up out
|
|
67
|
+
// of the symlink (../../…/home/…) and the diagnostic reported that broken path as
|
|
68
|
+
// "not found on disk". Normalizing basePath to its real path first keeps the
|
|
69
|
+
// displayed path anchored at the real .gsd root.
|
|
70
|
+
|
|
71
|
+
test("symlinked project root yields a clean .gsd-relative artifact path, not a ../ walk (#1238)", () => {
|
|
72
|
+
const realRoot = realpathSync(mkdtempSync(join(tmpdir(), "gsd-symlink-real-")));
|
|
73
|
+
const linkParent = realpathSync(mkdtempSync(join(tmpdir(), "gsd-symlink-link-")));
|
|
74
|
+
const linkRoot = join(linkParent, "project");
|
|
75
|
+
try {
|
|
76
|
+
// Content-bearing legacy milestone dir so plan-milestone resolves the
|
|
77
|
+
// canonical (realpath) ROADMAP path under realRoot/.gsd/milestones/M001/.
|
|
78
|
+
const milestoneDir = join(realRoot, ".gsd", "milestones", "M001");
|
|
79
|
+
mkdirSync(milestoneDir, { recursive: true });
|
|
80
|
+
writeFileSync(join(milestoneDir, "M001-CONTEXT.md"), "# context\n");
|
|
81
|
+
// The ROADMAP intentionally does not exist — this hits the "not found on
|
|
82
|
+
// disk" branch, the exact symptom in #1238.
|
|
83
|
+
symlinkSync(realRoot, linkRoot);
|
|
84
|
+
|
|
85
|
+
_clearGsdRootCache();
|
|
86
|
+
clearPathCache();
|
|
87
|
+
|
|
88
|
+
const msg = _describeArtifactVerificationFailureForTest("plan-milestone", "M001", linkRoot);
|
|
89
|
+
|
|
90
|
+
assert.match(msg, /was not found on disk after unit execution/);
|
|
91
|
+
// The displayed path must be anchored at the real .gsd root, not a broken
|
|
92
|
+
// parent-walk out of the symlinked base.
|
|
93
|
+
assert.doesNotMatch(msg, /\.\.[/\\]/, "must not contain a ../ walk out of the symlinked base");
|
|
94
|
+
assert.match(
|
|
95
|
+
msg,
|
|
96
|
+
/\.gsd[/\\]milestones[/\\]M001[/\\]M001-ROADMAP\.md/,
|
|
97
|
+
"must show the clean .gsd-relative ROADMAP path",
|
|
98
|
+
);
|
|
99
|
+
} finally {
|
|
100
|
+
_clearGsdRootCache();
|
|
101
|
+
clearPathCache();
|
|
102
|
+
rmSync(linkParent, { recursive: true, force: true });
|
|
103
|
+
rmSync(realRoot, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
@@ -4,7 +4,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
|
|
7
|
-
import { closeDatabase, getAllMilestones, getMilestone, insertMilestone, isDbAvailable, openDatabase } from "../gsd-db.ts";
|
|
7
|
+
import { _getAdapter, closeDatabase, getAllMilestones, getMilestone, insertMilestone, isDbAvailable, openDatabase } from "../gsd-db.ts";
|
|
8
8
|
import { reconcileMergedMilestonesFromJournal, reconcileProjectMilestonesFromDisk } from "../auto-start.ts";
|
|
9
9
|
import { emitWorktreeMerged } from "../worktree-telemetry.ts";
|
|
10
10
|
|
|
@@ -69,3 +69,31 @@ test("#5389: bootstrap reconciles PROJECT.md milestones that are missing from DB
|
|
|
69
69
|
rmSync(base, { recursive: true, force: true });
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
|
+
|
|
73
|
+
test("#1236: bootstrap merged-milestone reconciliation degrades to a warning instead of aborting when the DB is degraded", () => {
|
|
74
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-merged-reconcile-degraded-"));
|
|
75
|
+
try {
|
|
76
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
77
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
78
|
+
insertMilestone({ id: "M001", title: "Merged Milestone", status: "active" });
|
|
79
|
+
|
|
80
|
+
emitWorktreeMerged(base, "M001", { reason: "milestone-complete", conflict: false });
|
|
81
|
+
|
|
82
|
+
// Simulate a degraded DB: the connection stays "available" (isDbAvailable()
|
|
83
|
+
// remains true because the handle is non-null), but the milestones table is
|
|
84
|
+
// gone, so the reconciler's DB access throws partway through bootstrap.
|
|
85
|
+
_getAdapter()!.exec("DROP TABLE milestones");
|
|
86
|
+
assert.equal(isDbAvailable(), true);
|
|
87
|
+
|
|
88
|
+
// Regression (#1236): this reconciler was previously unguarded, so a
|
|
89
|
+
// degraded-DB failure threw and aborted the rest of `/gsd auto` bootstrap.
|
|
90
|
+
// It must now catch, warn, and return 0, matching its sibling
|
|
91
|
+
// reconcileProjectMilestonesFromDisk. Reaching the assertion proves it did
|
|
92
|
+
// not throw.
|
|
93
|
+
const closed = reconcileMergedMilestonesFromJournal(base);
|
|
94
|
+
assert.equal(closed, 0);
|
|
95
|
+
} finally {
|
|
96
|
+
if (isDbAvailable()) closeDatabase();
|
|
97
|
+
rmSync(base, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
package/src/resources/extensions/gsd/tests/auto-timeout-recovery-supervisor-inflight-guard.test.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idle/hard timeout recovery honors the in-flight-tool guard (#1229 follow-up).
|
|
3
|
+
*
|
|
4
|
+
* `recoverTimedOutUnit` steers a still-running unit with `deliverAs: "steer"`
|
|
5
|
+
* and, before that, may swap the session model via
|
|
6
|
+
* `applySupervisorModelIfConfigured`. Swapping the model and triggering a turn
|
|
7
|
+
* while non-interactive tool calls are still open skips tool results and trips
|
|
8
|
+
* provider errors (#3512) — the same failure the soft wrap-up and continue-here
|
|
9
|
+
* timers guard against. This test pins that the idle and hard steering branches
|
|
10
|
+
* gate both the model swap and `triggerTurn` on `getInFlightToolCount() === 0`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
|
|
19
|
+
import { recoverTimedOutUnit, type RecoveryContext } from "../auto-timeout-recovery.ts";
|
|
20
|
+
import { clearGSDPreferencesCache } from "../preferences.ts";
|
|
21
|
+
import {
|
|
22
|
+
markToolStart,
|
|
23
|
+
markToolEnd,
|
|
24
|
+
clearInFlightTools,
|
|
25
|
+
} from "../auto-tool-tracking.ts";
|
|
26
|
+
|
|
27
|
+
const SUPERVISOR_PREFS = [
|
|
28
|
+
"auto_supervisor:",
|
|
29
|
+
" soft_timeout_minutes: 1",
|
|
30
|
+
" idle_timeout_minutes: 100",
|
|
31
|
+
" hard_timeout_minutes: 100",
|
|
32
|
+
" model:",
|
|
33
|
+
" model: supervisor-primary",
|
|
34
|
+
" fallbacks: [supervisor-fb]",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
interface Harness {
|
|
38
|
+
home: string;
|
|
39
|
+
base: string;
|
|
40
|
+
ctx: any;
|
|
41
|
+
pi: any;
|
|
42
|
+
rctx: RecoveryContext;
|
|
43
|
+
setModelCalls: unknown[][];
|
|
44
|
+
sendMessageCalls: unknown[][];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeHarness(): Harness {
|
|
48
|
+
const home = mkdtempSync(join(tmpdir(), "gsd-recovery-guard-home-"));
|
|
49
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-recovery-guard-base-"));
|
|
50
|
+
process.env.GSD_HOME = home;
|
|
51
|
+
writeFileSync(join(home, "preferences.md"), ["---", ...SUPERVISOR_PREFS, "---", ""].join("\n"));
|
|
52
|
+
clearGSDPreferencesCache();
|
|
53
|
+
|
|
54
|
+
const setModelCalls: unknown[][] = [];
|
|
55
|
+
const sendMessageCalls: unknown[][] = [];
|
|
56
|
+
|
|
57
|
+
const ctx = {
|
|
58
|
+
ui: { notify: () => {} },
|
|
59
|
+
model: { provider: "anthropic" },
|
|
60
|
+
modelRegistry: {
|
|
61
|
+
getAvailable: () => [
|
|
62
|
+
{ id: "supervisor-primary", provider: "anthropic" },
|
|
63
|
+
{ id: "supervisor-fb", provider: "anthropic" },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
} as any;
|
|
67
|
+
|
|
68
|
+
const pi = {
|
|
69
|
+
sendMessage: (...args: unknown[]) => {
|
|
70
|
+
sendMessageCalls.push(args);
|
|
71
|
+
},
|
|
72
|
+
setModel: async (...args: unknown[]) => {
|
|
73
|
+
setModelCalls.push(args);
|
|
74
|
+
return true;
|
|
75
|
+
},
|
|
76
|
+
getThinkingLevel: () => "off",
|
|
77
|
+
setThinkingLevel: () => {},
|
|
78
|
+
} as any;
|
|
79
|
+
|
|
80
|
+
const rctx: RecoveryContext = {
|
|
81
|
+
basePath: base,
|
|
82
|
+
verbose: false,
|
|
83
|
+
currentUnitStartedAt: 1234,
|
|
84
|
+
unitRecoveryCount: new Map(),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return { home, base, ctx, pi, rctx, setModelCalls, sendMessageCalls };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function cleanup(h: Harness): void {
|
|
91
|
+
clearInFlightTools();
|
|
92
|
+
clearGSDPreferencesCache();
|
|
93
|
+
delete process.env.GSD_HOME;
|
|
94
|
+
rmSync(h.home, { recursive: true, force: true });
|
|
95
|
+
rmSync(h.base, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// "discuss-project" resolves to a PROJECT.md that never exists in the temp base,
|
|
99
|
+
// so recovery skips the "already on disk" shortcut and reaches the steering
|
|
100
|
+
// branch with recoveryAttempts (0) < maxRecoveryAttempts.
|
|
101
|
+
for (const reason of ["idle", "hard"] as const) {
|
|
102
|
+
test(`${reason} recovery skips the supervisor model swap while a tool is in flight`, async () => {
|
|
103
|
+
const h = makeHarness();
|
|
104
|
+
try {
|
|
105
|
+
markToolStart("call-1", true, "some_tool");
|
|
106
|
+
|
|
107
|
+
const outcome = await recoverTimedOutUnit(
|
|
108
|
+
h.ctx,
|
|
109
|
+
h.pi,
|
|
110
|
+
"discuss-project",
|
|
111
|
+
"P01",
|
|
112
|
+
reason,
|
|
113
|
+
h.rctx,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
assert.equal(outcome, "recovered");
|
|
117
|
+
assert.equal(
|
|
118
|
+
h.setModelCalls.length,
|
|
119
|
+
0,
|
|
120
|
+
"model must NOT swap while a tool is in flight",
|
|
121
|
+
);
|
|
122
|
+
assert.equal(h.sendMessageCalls.length, 1, "steer message still sent (queued)");
|
|
123
|
+
const opts = h.sendMessageCalls[0][1] as { triggerTurn?: boolean; deliverAs?: string };
|
|
124
|
+
assert.equal(opts.triggerTurn, false, "turn is not triggered mid-tool-call");
|
|
125
|
+
assert.equal(opts.deliverAs, "steer");
|
|
126
|
+
|
|
127
|
+
markToolEnd("call-1");
|
|
128
|
+
} finally {
|
|
129
|
+
cleanup(h);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test(`${reason} recovery applies the supervisor model swap when no tools are in flight`, async () => {
|
|
134
|
+
const h = makeHarness();
|
|
135
|
+
try {
|
|
136
|
+
const outcome = await recoverTimedOutUnit(
|
|
137
|
+
h.ctx,
|
|
138
|
+
h.pi,
|
|
139
|
+
"discuss-project",
|
|
140
|
+
"P01",
|
|
141
|
+
reason,
|
|
142
|
+
h.rctx,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
assert.equal(outcome, "recovered");
|
|
146
|
+
assert.equal(h.setModelCalls.length, 1, "model swaps to the supervisor model");
|
|
147
|
+
assert.deepEqual(
|
|
148
|
+
h.setModelCalls[0][0],
|
|
149
|
+
{ id: "supervisor-primary", provider: "anthropic" },
|
|
150
|
+
"swaps to the configured supervisor primary",
|
|
151
|
+
);
|
|
152
|
+
assert.equal(h.sendMessageCalls.length, 1);
|
|
153
|
+
const opts = h.sendMessageCalls[0][1] as { triggerTurn?: boolean };
|
|
154
|
+
assert.equal(opts.triggerTurn, true, "turn is triggered when no tools are open");
|
|
155
|
+
} finally {
|
|
156
|
+
cleanup(h);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -18,6 +18,7 @@ import { randomUUID } from "node:crypto";
|
|
|
18
18
|
import { handleVerdict, parseValidationFile } from "../commands-verdict.ts";
|
|
19
19
|
import { openDatabase, closeDatabase, _getAdapter, insertAssessment } from "../gsd-db.ts";
|
|
20
20
|
import { invalidateStateCache } from "../state.ts";
|
|
21
|
+
import { resolveMilestoneFile } from "../paths.ts";
|
|
21
22
|
|
|
22
23
|
interface NotifyCall {
|
|
23
24
|
message: string;
|
|
@@ -363,7 +364,8 @@ test("handleVerdict pass override works when validation only exists in DB", asyn
|
|
|
363
364
|
const { ctx, calls } = makeMockCtx();
|
|
364
365
|
await handleVerdict('pass --milestone M001 --rationale "reviewed and accepted"', ctx, base);
|
|
365
366
|
|
|
366
|
-
const validationPath =
|
|
367
|
+
const validationPath = resolveMilestoneFile(base, "M001", "VALIDATION");
|
|
368
|
+
assert.ok(validationPath, "DB-backed validation should be materialized to disk");
|
|
367
369
|
const rewritten = readFileSync(validationPath, "utf-8");
|
|
368
370
|
assert.match(rewritten, /^verdict: pass$/m, "DB-backed validation should be rewritten as pass");
|
|
369
371
|
assert.match(rewritten, /reviewed and accepted/, "explicit rationale should be persisted");
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
writeCompatMarker,
|
|
13
13
|
normalizeForHash,
|
|
14
14
|
computeProjectionSha,
|
|
15
|
+
pruneOrphanedProjectionEntries,
|
|
15
16
|
EMPTY_MARKER,
|
|
16
17
|
compatMarkerPath,
|
|
17
18
|
} from "../compat/compat-marker.ts";
|
|
@@ -103,3 +104,87 @@ test("compatMarkerPath resolves under .gsd", () => {
|
|
|
103
104
|
const base = makeTmpBase();
|
|
104
105
|
assert.equal(compatMarkerPath(base), join(base, ".gsd", ".compat.json"));
|
|
105
106
|
});
|
|
107
|
+
|
|
108
|
+
test("pruneOrphanedProjectionEntries drops entries whose backing file is gone (#1257)", () => {
|
|
109
|
+
const base = makeTmpBase();
|
|
110
|
+
// Live projection: file exists on disk.
|
|
111
|
+
const liveRel = "phases/29-frontend-code-debt-cleanup/29-ROADMAP.md";
|
|
112
|
+
mkdirSync(join(base, ".gsd", "phases", "29-frontend-code-debt-cleanup"), { recursive: true });
|
|
113
|
+
writeFileSync(join(base, ".gsd", liveRel), "# M029\n", "utf-8");
|
|
114
|
+
// Phantom projection: directory was renamed, file no longer exists.
|
|
115
|
+
const ghostRel = "phases/29-new-milestone-m029/29-ROADMAP.md";
|
|
116
|
+
|
|
117
|
+
writeCompatMarker(base, {
|
|
118
|
+
schema: 2,
|
|
119
|
+
lastWriter: "gsd-pi",
|
|
120
|
+
lastProjectedAt: "2026-07-05T05:35:19.379Z",
|
|
121
|
+
projections: {
|
|
122
|
+
[liveRel]: { sha: "live000000000000", entities: ["M029"] },
|
|
123
|
+
[ghostRel]: { sha: "ghost00000000000", entities: ["M029"] },
|
|
124
|
+
},
|
|
125
|
+
piVersion: "1.4.0",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const removed = pruneOrphanedProjectionEntries(base);
|
|
129
|
+
assert.equal(removed, 1);
|
|
130
|
+
|
|
131
|
+
const marker = readCompatMarker(base);
|
|
132
|
+
assert.ok(marker.projections[liveRel], "live projection must survive");
|
|
133
|
+
assert.equal(marker.projections[ghostRel], undefined, "phantom projection must be pruned");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("pruneOrphanedProjectionEntries also prunes .planning projections/passthrough", () => {
|
|
137
|
+
const base = makeTmpBase();
|
|
138
|
+
mkdirSync(join(base, ".planning"), { recursive: true });
|
|
139
|
+
writeFileSync(join(base, ".planning", "ROADMAP.md"), "# roadmap\n", "utf-8");
|
|
140
|
+
|
|
141
|
+
writeCompatMarker(base, {
|
|
142
|
+
schema: 2,
|
|
143
|
+
lastWriter: "gsd-pi",
|
|
144
|
+
lastProjectedAt: "2026-07-05T05:35:19.379Z",
|
|
145
|
+
projections: {},
|
|
146
|
+
planning: {
|
|
147
|
+
active: true,
|
|
148
|
+
layout: "flat-phases",
|
|
149
|
+
projections: {
|
|
150
|
+
"ROADMAP.md": { sha: "live000000000000", entities: [] },
|
|
151
|
+
"gone/PLAN.md": { sha: "ghost00000000000", entities: [] },
|
|
152
|
+
},
|
|
153
|
+
passthrough: {
|
|
154
|
+
"gone/PATTERNS.md": { sha: "ghost11111111111", entities: [] },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
piVersion: "1.4.0",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const removed = pruneOrphanedProjectionEntries(base);
|
|
161
|
+
assert.equal(removed, 2);
|
|
162
|
+
|
|
163
|
+
const marker = readCompatMarker(base);
|
|
164
|
+
assert.ok(marker.planning!.projections["ROADMAP.md"], "live planning projection must survive");
|
|
165
|
+
assert.equal(marker.planning!.projections["gone/PLAN.md"], undefined);
|
|
166
|
+
assert.equal(marker.planning!.passthrough["gone/PATTERNS.md"], undefined);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("pruneOrphanedProjectionEntries is a no-op when nothing is orphaned", () => {
|
|
170
|
+
const base = makeTmpBase();
|
|
171
|
+
const rel = "phases/01-phase/01-01-PLAN.md";
|
|
172
|
+
mkdirSync(join(base, ".gsd", "phases", "01-phase"), { recursive: true });
|
|
173
|
+
writeFileSync(join(base, ".gsd", rel), "# plan\n", "utf-8");
|
|
174
|
+
writeCompatMarker(base, {
|
|
175
|
+
schema: 2,
|
|
176
|
+
lastWriter: "gsd-pi",
|
|
177
|
+
lastProjectedAt: "2026-07-05T05:35:19.379Z",
|
|
178
|
+
projections: { [rel]: { sha: "abc000000000000", entities: ["M001"] } },
|
|
179
|
+
piVersion: "1.4.0",
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
assert.equal(pruneOrphanedProjectionEntries(base), 0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("pruneOrphanedProjectionEntries returns 0 and writes nothing when marker is missing", () => {
|
|
186
|
+
const base = makeTmpBase();
|
|
187
|
+
assert.equal(pruneOrphanedProjectionEntries(base), 0);
|
|
188
|
+
const files = readdirSync(join(base, ".gsd"));
|
|
189
|
+
assert.ok(!files.includes(".compat.json"), "must not create a marker where none existed");
|
|
190
|
+
});
|
|
@@ -422,6 +422,51 @@ console.log('\n=== complete-milestone: existing SUMMARY.md preserved (#4598) ===
|
|
|
422
422
|
cleanup(dbPath);
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
426
|
+
// complete-milestone: flat-phase compatibility SUMMARY.md is not hidden
|
|
427
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
428
|
+
|
|
429
|
+
console.log('\n=== complete-milestone: flat-phase legacy-named SUMMARY.md preserved (#4598) ===');
|
|
430
|
+
{
|
|
431
|
+
const dbPath = tempDbPath();
|
|
432
|
+
openDatabase(dbPath);
|
|
433
|
+
const basePath = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-flat-complete-milestone-'));
|
|
434
|
+
const phaseDir = path.join(basePath, '.gsd', 'phases', '01-test-milestone');
|
|
435
|
+
fs.mkdirSync(path.join(phaseDir, 'slices', 'S01', 'tasks'), { recursive: true });
|
|
436
|
+
seedCompletedMilestone({ basePath, validationVerdict: 'pass' });
|
|
437
|
+
|
|
438
|
+
// Flat-phase projects may still contain legacy-named compatibility files.
|
|
439
|
+
// The no-overwrite guard must preserve those richer summaries instead of
|
|
440
|
+
// generating a canonical sibling that later readers would prefer.
|
|
441
|
+
const summaryPath = path.join(phaseDir, 'M001-SUMMARY.md');
|
|
442
|
+
const canonicalSummaryPath = path.join(phaseDir, '01-SUMMARY.md');
|
|
443
|
+
const sentinel = '# M001: Pre-existing flat-phase richer summary\n\nDO NOT OVERWRITE ME\n';
|
|
444
|
+
fs.writeFileSync(summaryPath, sentinel, 'utf-8');
|
|
445
|
+
|
|
446
|
+
const result = await handleCompleteMilestone(makeValidParams(), basePath);
|
|
447
|
+
assertTrue(!('error' in result), 'completion should still succeed when flat-phase compatibility SUMMARY.md exists');
|
|
448
|
+
if (!('error' in result)) {
|
|
449
|
+
assertEq(
|
|
450
|
+
result.summaryPath,
|
|
451
|
+
fs.realpathSync(summaryPath),
|
|
452
|
+
'completion should report the preserved existing SUMMARY path',
|
|
453
|
+
);
|
|
454
|
+
assertEq(
|
|
455
|
+
fs.readFileSync(summaryPath, 'utf-8'),
|
|
456
|
+
sentinel,
|
|
457
|
+
'flat-phase compatibility SUMMARY.md must be preserved, not hidden by a canonical rewrite',
|
|
458
|
+
);
|
|
459
|
+
assertTrue(
|
|
460
|
+
!fs.existsSync(canonicalSummaryPath),
|
|
461
|
+
'completion must not create a canonical sibling over an existing compatibility summary',
|
|
462
|
+
);
|
|
463
|
+
assertEq(getMilestone('M001')!.status, 'complete', 'milestone should still be marked complete in DB');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
cleanupDir(basePath);
|
|
467
|
+
cleanup(dbPath);
|
|
468
|
+
}
|
|
469
|
+
|
|
425
470
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
426
471
|
|
|
427
472
|
report();
|
|
@@ -33,6 +33,45 @@ test("normalizeDbRows normalizes every row", () => {
|
|
|
33
33
|
assert.deepEqual(normalizeDbRows([first, second]), [{ id: "S01" }, { id: "S02" }]);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
test("createDbAdapter strips named-parameter key prefixes for provider compatibility", () => {
|
|
37
|
+
const received: unknown[][] = [];
|
|
38
|
+
const rawStatement = {
|
|
39
|
+
run: (...params: unknown[]) => {
|
|
40
|
+
received.push(params);
|
|
41
|
+
return { changes: 1 };
|
|
42
|
+
},
|
|
43
|
+
get: (...params: unknown[]) => {
|
|
44
|
+
received.push(params);
|
|
45
|
+
return undefined;
|
|
46
|
+
},
|
|
47
|
+
all: (...params: unknown[]) => {
|
|
48
|
+
received.push(params);
|
|
49
|
+
return [];
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
const rawDb = {
|
|
53
|
+
exec: () => {},
|
|
54
|
+
prepare: () => rawStatement,
|
|
55
|
+
close: () => {},
|
|
56
|
+
};
|
|
57
|
+
const adapter = createDbAdapter(rawDb);
|
|
58
|
+
const stmt = adapter.prepare("INSERT INTO t (version, applied_at) VALUES (:version, :applied_at)");
|
|
59
|
+
|
|
60
|
+
stmt.run({ ":version": 1, "@name": "a", "$flag": true, bare: "kept" });
|
|
61
|
+
assert.deepEqual(received.pop(), [{ version: 1, name: "a", flag: true, bare: "kept" }]);
|
|
62
|
+
|
|
63
|
+
// Non-binding params pass through untouched: positional scalars, arrays,
|
|
64
|
+
// and binary blobs must not be treated as named-binding objects.
|
|
65
|
+
const blob = new Uint8Array([1, 2, 3]);
|
|
66
|
+
stmt.get("T01", blob);
|
|
67
|
+
const passthrough = received.pop() as unknown[];
|
|
68
|
+
assert.equal(passthrough[0], "T01");
|
|
69
|
+
assert.equal(passthrough[1], blob);
|
|
70
|
+
|
|
71
|
+
stmt.all({ plain: "unchanged" });
|
|
72
|
+
assert.deepEqual(received.pop(), [{ plain: "unchanged" }]);
|
|
73
|
+
});
|
|
74
|
+
|
|
36
75
|
test("createDbAdapter caches prepared statements and clears cache on close", () => {
|
|
37
76
|
const calls: unknown[] = [];
|
|
38
77
|
const rawStatement = {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// gsd-pi — DB writability probe regression tests (#1234).
|
|
2
|
+
//
|
|
3
|
+
// Auto-start's DB gate only checked that the provider opened, not that the
|
|
4
|
+
// handle can actually write. A schema-current DB does zero writes during open,
|
|
5
|
+
// so a read-only / DBMOVED handle passed the open-only check and failed much
|
|
6
|
+
// later at the first authoritative write with an opaque "readonly database"
|
|
7
|
+
// error. `probeDbWritable` forces a real page write so that failure class is
|
|
8
|
+
// caught at the natural checkpoint.
|
|
9
|
+
|
|
10
|
+
import { test } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
|
|
16
|
+
import { openDatabase, closeDatabase, probeDbWritable, _getAdapter } from "../gsd-db.ts";
|
|
17
|
+
import type { DbAdapter } from "../db-adapter.ts";
|
|
18
|
+
|
|
19
|
+
function makeBase(): string {
|
|
20
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-db-writable-probe-"));
|
|
21
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("probeDbWritable succeeds against a freshly opened writable database", () => {
|
|
26
|
+
const base = makeBase();
|
|
27
|
+
assert.equal(openDatabase(join(base, ".gsd", "gsd.db")), true);
|
|
28
|
+
try {
|
|
29
|
+
assert.deepEqual(probeDbWritable(), { ok: true });
|
|
30
|
+
} finally {
|
|
31
|
+
closeDatabase();
|
|
32
|
+
rmSync(base, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("probeDbWritable reports not-ok with detail when no database is open", () => {
|
|
37
|
+
closeDatabase();
|
|
38
|
+
const result = probeDbWritable();
|
|
39
|
+
assert.equal(result.ok, false);
|
|
40
|
+
assert.match(result.detail ?? "", /No database is open/u);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("probeDbWritable reports not-ok when the page write fails (simulating a read-only handle)", () => {
|
|
44
|
+
const base = makeBase();
|
|
45
|
+
assert.equal(openDatabase(join(base, ".gsd", "gsd.db")), true);
|
|
46
|
+
const adapter = _getAdapter() as DbAdapter;
|
|
47
|
+
assert.ok(adapter);
|
|
48
|
+
|
|
49
|
+
// The read (prepare/get) still works; only the page-dirtying write throws,
|
|
50
|
+
// mirroring SQLITE_READONLY_DBMOVED which fails once a page is dirtied.
|
|
51
|
+
const origExec = adapter.exec.bind(adapter);
|
|
52
|
+
adapter.exec = (sql: string): void => {
|
|
53
|
+
if (sql.includes("user_version =")) throw new Error("attempt to write a readonly database");
|
|
54
|
+
return origExec(sql);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const result = probeDbWritable();
|
|
59
|
+
assert.equal(result.ok, false);
|
|
60
|
+
assert.match(result.detail ?? "", /readonly database/u);
|
|
61
|
+
} finally {
|
|
62
|
+
adapter.exec = origExec;
|
|
63
|
+
closeDatabase();
|
|
64
|
+
rmSync(base, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
@@ -7,7 +7,7 @@ import { join } from "node:path";
|
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
|
|
10
|
-
import { renderPlanFromDb, renderRoadmapFromDb, renderAssessmentFromDb } from "../markdown-renderer.ts";
|
|
10
|
+
import { renderPlanFromDb, renderRoadmapFromDb, renderAssessmentFromDb, renderReplanFromDb } from "../markdown-renderer.ts";
|
|
11
11
|
import { clearPathCache } from "../paths.ts";
|
|
12
12
|
import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertTask } from "../gsd-db.ts";
|
|
13
13
|
|
|
@@ -83,6 +83,27 @@ test("renderPlanFromDb does NOT write per-task plan files", async () => {
|
|
|
83
83
|
assert.equal(result.taskPlanPaths.length, 0);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
|
|
87
|
+
// Replan uses the same slice-level target rules as assessment: flat-phase
|
|
88
|
+
// writes must stay at the phase root even if an old slices/SID/ dir exists.
|
|
89
|
+
test("renderReplanFromDb writes to the phase root even when a stray slices/SID/ dir exists", async () => {
|
|
90
|
+
const base = makeTmp();
|
|
91
|
+
const plan = await renderPlanFromDb(base, "M001", "S01");
|
|
92
|
+
const phaseDir = plan.planPath.slice(0, plan.planPath.lastIndexOf("01-01-PLAN.md"));
|
|
93
|
+
mkdirSync(join(phaseDir, "slices", "S01"), { recursive: true });
|
|
94
|
+
clearPathCache();
|
|
95
|
+
|
|
96
|
+
const result = await renderReplanFromDb(base, "M001", "S01", {
|
|
97
|
+
blockerTaskId: "T01",
|
|
98
|
+
blockerDescription: "Need a better path target.",
|
|
99
|
+
whatChanged: "Centralized slice writer path resolution.",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
assert.match(result.replanPath, /phases[/\\]01-[^/\\]+[/\\]01-01-REPLAN\.md$/);
|
|
103
|
+
assert.ok(!result.replanPath.includes(join("slices", "S01")), "must not write under slices/S01/");
|
|
104
|
+
assert.ok(existsSync(result.replanPath), "replan file should exist at the phase root");
|
|
105
|
+
});
|
|
106
|
+
|
|
86
107
|
// Regression for #1190: a stray slices/SID/ subdir inside a flat-phase milestone
|
|
87
108
|
// must not divert the ASSESSMENT write into slices/SID/ (a hybrid path that
|
|
88
109
|
// auto-mode verification cannot find). It must land at the phase root.
|