gsd-pi 2.44.0-dev.0b97ffd → 2.44.0-dev.73f2fd5
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/extensions/gsd/auto/infra-errors.js +3 -0
- package/dist/resources/extensions/gsd/auto/phases.js +36 -36
- package/dist/resources/extensions/gsd/auto-prompts.js +24 -1
- package/dist/resources/extensions/gsd/auto-timers.js +57 -3
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +4 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +9 -6
- package/dist/resources/extensions/gsd/auto.js +30 -3
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +156 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -12
- package/dist/resources/extensions/gsd/commands/catalog.js +6 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
- package/dist/resources/extensions/gsd/commands-mcp-status.js +187 -0
- package/dist/resources/extensions/gsd/db-writer.js +34 -16
- package/dist/resources/extensions/gsd/doctor.js +8 -0
- package/dist/resources/extensions/gsd/git-service.js +8 -3
- package/dist/resources/extensions/gsd/gsd-db.js +12 -1
- package/dist/resources/extensions/gsd/markdown-renderer.js +1 -1
- package/dist/resources/extensions/gsd/preferences.js +9 -1
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +3 -14
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
- package/dist/resources/extensions/gsd/provider-error-pause.js +7 -0
- package/dist/resources/extensions/gsd/state.js +19 -2
- package/dist/resources/extensions/gsd/tools/plan-slice.js +1 -0
- package/dist/resources/extensions/gsd/tools/plan-task.js +1 -0
- package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -0
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +88 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +6 -0
- package/dist/resources/extensions/mcp-client/index.js +14 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
- 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 +2 -2
- 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/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 +16 -16
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +3 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +15 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/local-model-check.d.ts +15 -0
- package/packages/pi-coding-agent/dist/core/local-model-check.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/local-model-check.js +41 -0
- package/packages/pi-coding-agent/dist/core/local-model-check.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +20 -1
- package/packages/pi-coding-agent/dist/core/model-registry.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 +6 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +17 -0
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js +32 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +8 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +12 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts +15 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js +40 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +4 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +5 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +13 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +17 -8
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.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 +7 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/auth-storage.ts +15 -1
- package/packages/pi-coding-agent/src/core/local-model-check.ts +45 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +21 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +9 -0
- package/packages/pi-coding-agent/src/main.ts +19 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/timestamp.test.ts +38 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +10 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +15 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/timestamp.ts +48 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +3 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +18 -3
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +16 -7
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +8 -1
- package/src/resources/extensions/gsd/auto/infra-errors.ts +3 -0
- package/src/resources/extensions/gsd/auto/phases.ts +45 -48
- package/src/resources/extensions/gsd/auto-prompts.ts +24 -1
- package/src/resources/extensions/gsd/auto-timers.ts +64 -3
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +5 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +9 -6
- package/src/resources/extensions/gsd/auto.ts +37 -3
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +148 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +48 -11
- package/src/resources/extensions/gsd/commands/catalog.ts +6 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
- package/src/resources/extensions/gsd/commands-mcp-status.ts +247 -0
- package/src/resources/extensions/gsd/db-writer.ts +39 -17
- package/src/resources/extensions/gsd/doctor.ts +7 -1
- package/src/resources/extensions/gsd/git-service.ts +6 -2
- package/src/resources/extensions/gsd/gsd-db.ts +16 -1
- package/src/resources/extensions/gsd/markdown-renderer.ts +1 -1
- package/src/resources/extensions/gsd/preferences.ts +11 -1
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
- package/src/resources/extensions/gsd/prompts/replan-slice.md +3 -14
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
- package/src/resources/extensions/gsd/provider-error-pause.ts +9 -0
- package/src/resources/extensions/gsd/state.ts +19 -1
- package/src/resources/extensions/gsd/tests/auto-pr-bugs.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +114 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +60 -0
- package/src/resources/extensions/gsd/tests/est-annotation-timeout.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/infra-error.test.ts +20 -2
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +89 -0
- package/src/resources/extensions/gsd/tests/mcp-status.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +11 -7
- package/src/resources/extensions/gsd/tests/stop-auto-merge-back.test.ts +67 -0
- package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +2 -1
- package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
- package/src/resources/extensions/gsd/tools/plan-task.ts +2 -0
- package/src/resources/extensions/gsd/tools/replan-slice.ts +3 -0
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +127 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +7 -0
- package/src/resources/extensions/mcp-client/index.ts +20 -0
- /package/dist/web/standalone/.next/static/{alS4hoANx0TK4UVZY27da → kxxAA66bah_yhPYqLBHE2}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{alS4hoANx0TK4UVZY27da → kxxAA66bah_yhPYqLBHE2}/_ssgManifest.js +0 -0
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
getSliceTasks,
|
|
49
49
|
getReplanHistory,
|
|
50
50
|
getSlice,
|
|
51
|
+
insertMilestone,
|
|
51
52
|
type MilestoneRow,
|
|
52
53
|
type SliceRow,
|
|
53
54
|
type TaskRow,
|
|
@@ -257,7 +258,24 @@ function isStatusDone(status: string): boolean {
|
|
|
257
258
|
export async function deriveStateFromDb(basePath: string): Promise<GSDState> {
|
|
258
259
|
const requirements = parseRequirementCounts(await loadFile(resolveGsdRootFile(basePath, "REQUIREMENTS")));
|
|
259
260
|
|
|
260
|
-
|
|
261
|
+
let allMilestones = getAllMilestones();
|
|
262
|
+
|
|
263
|
+
// Incremental disk→DB sync: milestone directories created outside the DB
|
|
264
|
+
// write path (via /gsd queue, manual mkdir, or complete-milestone writing the
|
|
265
|
+
// next CONTEXT.md) are never inserted by the initial migration guard in
|
|
266
|
+
// auto-start.ts because that guard only runs when gsd.db doesn't exist yet.
|
|
267
|
+
// Reconcile here so deriveStateFromDb never silently misses queued milestones.
|
|
268
|
+
// insertMilestone uses INSERT OR IGNORE, so this is safe to call every time.
|
|
269
|
+
const dbIdSet = new Set(allMilestones.map(m => m.id));
|
|
270
|
+
const diskIds = findMilestoneIds(basePath);
|
|
271
|
+
let synced = false;
|
|
272
|
+
for (const diskId of diskIds) {
|
|
273
|
+
if (!dbIdSet.has(diskId) && !isGhostMilestone(basePath, diskId)) {
|
|
274
|
+
insertMilestone({ id: diskId, status: 'active' });
|
|
275
|
+
synced = true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (synced) allMilestones = getAllMilestones();
|
|
261
279
|
|
|
262
280
|
// Parallel worker isolation: when locked, filter to just the locked milestone
|
|
263
281
|
const milestoneLock = process.env.GSD_MILESTONE_LOCK;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auto-pr-bugs.test.ts — Regression tests for #2302.
|
|
3
|
+
*
|
|
4
|
+
* Three interacting bugs prevented auto_pr from ever creating a PR:
|
|
5
|
+
* 1. auto_pr was gated on `pushed` (which requires auto_push)
|
|
6
|
+
* 2. Milestone branch was not pushed to remote before PR creation
|
|
7
|
+
* 3. createDraftPR in git-service.ts lacked --head/--base parameters
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
// ─── Bug 1: auto_pr should not depend on auto_push / pushed flag ────────────
|
|
16
|
+
|
|
17
|
+
const autoWorktreeSrcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
|
|
18
|
+
const autoWorktreeSrc = readFileSync(autoWorktreeSrcPath, "utf-8");
|
|
19
|
+
|
|
20
|
+
test("#2302 bug 1: auto_pr condition should not require pushed flag", () => {
|
|
21
|
+
// Find the auto_pr block in mergeMilestoneToMain
|
|
22
|
+
const autoPrIdx = autoWorktreeSrc.indexOf("auto_pr");
|
|
23
|
+
assert.ok(autoPrIdx !== -1, "auto_pr reference exists in auto-worktree.ts");
|
|
24
|
+
|
|
25
|
+
// Get context around the auto_pr check
|
|
26
|
+
const lineStart = autoWorktreeSrc.lastIndexOf("\n", autoPrIdx) + 1;
|
|
27
|
+
const lineEnd = autoWorktreeSrc.indexOf("\n", autoPrIdx);
|
|
28
|
+
const autoPrLine = autoWorktreeSrc.slice(lineStart, lineEnd);
|
|
29
|
+
|
|
30
|
+
// The condition should NOT include `&& pushed`
|
|
31
|
+
assert.ok(
|
|
32
|
+
!autoPrLine.includes("&& pushed"),
|
|
33
|
+
"auto_pr condition should not be gated on pushed flag (auto_push dependency)",
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ─── Bug 2: phases.ts should not duplicate PR creation ──────────────────────
|
|
38
|
+
|
|
39
|
+
const phasesSrcPath = join(import.meta.dirname, "..", "auto", "phases.ts");
|
|
40
|
+
const phasesSrc = readFileSync(phasesSrcPath, "utf-8");
|
|
41
|
+
|
|
42
|
+
test("#2302 bug 2: phases.ts should not call createDraftPR (handled by mergeMilestoneToMain)", () => {
|
|
43
|
+
// After fix, phases.ts should not import or call createDraftPR because
|
|
44
|
+
// PR creation is handled inside mergeMilestoneToMain in auto-worktree.ts
|
|
45
|
+
const createDraftPRCalls = phasesSrc.match(/createDraftPR\(/g) || [];
|
|
46
|
+
|
|
47
|
+
assert.equal(
|
|
48
|
+
createDraftPRCalls.length,
|
|
49
|
+
0,
|
|
50
|
+
"phases.ts should not call createDraftPR — it's handled by mergeMilestoneToMain",
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ─── Bug 3: createDraftPR should accept head and base branch parameters ─────
|
|
55
|
+
|
|
56
|
+
const gitServiceSrcPath = join(import.meta.dirname, "..", "git-service.ts");
|
|
57
|
+
const gitServiceSrc = readFileSync(gitServiceSrcPath, "utf-8");
|
|
58
|
+
|
|
59
|
+
test("#2302 bug 3: createDraftPR should accept head and base branch parameters", () => {
|
|
60
|
+
// Find the createDraftPR function signature
|
|
61
|
+
const fnIdx = gitServiceSrc.indexOf("function createDraftPR");
|
|
62
|
+
assert.ok(fnIdx !== -1, "createDraftPR function exists");
|
|
63
|
+
|
|
64
|
+
// Get the function signature (up to the closing paren)
|
|
65
|
+
const sigEnd = gitServiceSrc.indexOf(")", fnIdx);
|
|
66
|
+
const signature = gitServiceSrc.slice(fnIdx, sigEnd);
|
|
67
|
+
|
|
68
|
+
// Should have head and base parameters
|
|
69
|
+
assert.ok(
|
|
70
|
+
signature.includes("head") || signature.includes("branch"),
|
|
71
|
+
"createDraftPR should accept a head/branch parameter",
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("#2302 bug 3: createDraftPR should pass --head and --base to gh pr create", () => {
|
|
76
|
+
const fnIdx = gitServiceSrc.indexOf("function createDraftPR");
|
|
77
|
+
const fnEnd = gitServiceSrc.indexOf("\n}", fnIdx);
|
|
78
|
+
const fnBody = gitServiceSrc.slice(fnIdx, fnEnd);
|
|
79
|
+
|
|
80
|
+
assert.ok(
|
|
81
|
+
fnBody.includes("--head"),
|
|
82
|
+
"createDraftPR should pass --head to gh pr create",
|
|
83
|
+
);
|
|
84
|
+
assert.ok(
|
|
85
|
+
fnBody.includes("--base"),
|
|
86
|
+
"createDraftPR should pass --base to gh pr create",
|
|
87
|
+
);
|
|
88
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* completed-units-metrics-sync.test.ts — Regression tests for #2313.
|
|
3
|
+
*
|
|
4
|
+
* 1. completed-units.json should be archived (not wiped) on milestone transition
|
|
5
|
+
* 2. metrics.json should be in the worktree → project root sync file list
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, cpSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
|
|
14
|
+
// ─── Bug 1: completed-units.json should be archived, not wiped ─────────────
|
|
15
|
+
|
|
16
|
+
const phasesSrcPath = join(import.meta.dirname, "..", "auto", "phases.ts");
|
|
17
|
+
const phasesSrc = readFileSync(phasesSrcPath, "utf-8");
|
|
18
|
+
|
|
19
|
+
test("#2313: completed-units.json should not be blindly wiped to [] on milestone transition", () => {
|
|
20
|
+
// The milestone transition block should NOT write an empty array to completed-units.json
|
|
21
|
+
// without first archiving the existing data. Look for the archive/rename pattern.
|
|
22
|
+
const transitionIdx = phasesSrc.indexOf("Milestone transition");
|
|
23
|
+
assert.ok(transitionIdx !== -1, "Milestone transition section exists");
|
|
24
|
+
|
|
25
|
+
// Find the completed-units handling block
|
|
26
|
+
const completedUnitsIdx = phasesSrc.indexOf("completed-units", transitionIdx);
|
|
27
|
+
assert.ok(completedUnitsIdx !== -1, "completed-units handling exists in transition");
|
|
28
|
+
|
|
29
|
+
// Get a window around the completed-units handling (1200 chars to
|
|
30
|
+
// accommodate CRLF line endings on Windows which inflate byte offsets).
|
|
31
|
+
const windowStart = Math.max(0, completedUnitsIdx - 300);
|
|
32
|
+
const windowEnd = Math.min(phasesSrc.length, completedUnitsIdx + 900);
|
|
33
|
+
const window = phasesSrc.slice(windowStart, windowEnd).toLowerCase();
|
|
34
|
+
|
|
35
|
+
// Should archive/rename the old file before resetting
|
|
36
|
+
const hasArchive = window.includes("archive") ||
|
|
37
|
+
window.includes("rename") ||
|
|
38
|
+
window.includes("cpsync") ||
|
|
39
|
+
window.includes("safecopy") ||
|
|
40
|
+
window.includes("completed-units-");
|
|
41
|
+
|
|
42
|
+
assert.ok(
|
|
43
|
+
hasArchive,
|
|
44
|
+
"completed-units.json should be archived before reset during milestone transition",
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ─── Bug 2: metrics.json should be in the sync file lists ──────────────────
|
|
49
|
+
|
|
50
|
+
test("#2313: syncStateToProjectRoot should sync metrics.json", () => {
|
|
51
|
+
const syncSrcPath = join(import.meta.dirname, "..", "auto-worktree-sync.ts");
|
|
52
|
+
const syncSrc = readFileSync(syncSrcPath, "utf-8");
|
|
53
|
+
|
|
54
|
+
// syncStateToProjectRoot should copy metrics.json from worktree to project root
|
|
55
|
+
assert.ok(
|
|
56
|
+
syncSrc.includes("metrics.json"),
|
|
57
|
+
"auto-worktree-sync.ts should reference metrics.json for sync",
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("#2313: syncWorktreeStateBack should include metrics.json in root files list", () => {
|
|
62
|
+
const autoWorktreeSrcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
|
|
63
|
+
const autoWorktreeSrc = readFileSync(autoWorktreeSrcPath, "utf-8");
|
|
64
|
+
|
|
65
|
+
// Find the rootFiles array in syncWorktreeStateBack
|
|
66
|
+
const syncBackIdx = autoWorktreeSrc.indexOf("syncWorktreeStateBack");
|
|
67
|
+
assert.ok(syncBackIdx !== -1, "syncWorktreeStateBack exists");
|
|
68
|
+
|
|
69
|
+
const rootFilesIdx = autoWorktreeSrc.indexOf("rootFiles", syncBackIdx);
|
|
70
|
+
assert.ok(rootFilesIdx !== -1, "rootFiles list exists in syncWorktreeStateBack");
|
|
71
|
+
|
|
72
|
+
// Get the rootFiles array content
|
|
73
|
+
const arrayStart = autoWorktreeSrc.indexOf("[", rootFilesIdx);
|
|
74
|
+
const arrayEnd = autoWorktreeSrc.indexOf("]", arrayStart);
|
|
75
|
+
const rootFilesBlock = autoWorktreeSrc.slice(arrayStart, arrayEnd);
|
|
76
|
+
|
|
77
|
+
assert.ok(
|
|
78
|
+
rootFilesBlock.includes("metrics.json"),
|
|
79
|
+
"metrics.json should be in syncWorktreeStateBack rootFiles list",
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ─── Functional test: completed-units archive ────────────────────────────────
|
|
84
|
+
|
|
85
|
+
test("#2313: functional — completed-units archive creates milestone-specific file", () => {
|
|
86
|
+
const tmpBase = mkdtempSync(join(tmpdir(), "gsd-completed-units-"));
|
|
87
|
+
const gsdDir = join(tmpBase, ".gsd");
|
|
88
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
89
|
+
|
|
90
|
+
// Simulate existing completed-units.json with data
|
|
91
|
+
const existing = [
|
|
92
|
+
{ type: "task", id: "T01" },
|
|
93
|
+
{ type: "slice", id: "S01" },
|
|
94
|
+
];
|
|
95
|
+
const completedKeysPath = join(gsdDir, "completed-units.json");
|
|
96
|
+
writeFileSync(completedKeysPath, JSON.stringify(existing, null, 2));
|
|
97
|
+
|
|
98
|
+
// Simulate the archive behavior: copy to milestone-specific file
|
|
99
|
+
const milestoneId = "M001";
|
|
100
|
+
const archivePath = join(gsdDir, `completed-units-${milestoneId}.json`);
|
|
101
|
+
cpSync(completedKeysPath, archivePath);
|
|
102
|
+
|
|
103
|
+
// Reset the main file
|
|
104
|
+
writeFileSync(completedKeysPath, JSON.stringify([], null, 2));
|
|
105
|
+
|
|
106
|
+
// Verify archive exists with original data
|
|
107
|
+
assert.ok(existsSync(archivePath), "archive file should exist");
|
|
108
|
+
const archived = JSON.parse(readFileSync(archivePath, "utf-8"));
|
|
109
|
+
assert.deepEqual(archived, existing, "archived data should match original");
|
|
110
|
+
|
|
111
|
+
// Verify main file is reset
|
|
112
|
+
const current = JSON.parse(readFileSync(completedKeysPath, "utf-8"));
|
|
113
|
+
assert.deepEqual(current, [], "current completed-units should be empty after transition");
|
|
114
|
+
});
|
|
@@ -483,6 +483,85 @@ describe('db-writer', () => {
|
|
|
483
483
|
}
|
|
484
484
|
});
|
|
485
485
|
|
|
486
|
+
test('saveArtifactToDb — shrinkage guard preserves larger existing file', async () => {
|
|
487
|
+
const tmpDir = makeTmpDir();
|
|
488
|
+
const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
|
|
489
|
+
openDatabase(dbPath);
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
const fullContent = '# Full Research\n\n' + 'x'.repeat(20000) + '\n';
|
|
493
|
+
const abbreviatedContent = '# Summary\n\nShort version.\n';
|
|
494
|
+
|
|
495
|
+
// Pre-create the file with full content (simulating a prior `write` tool call)
|
|
496
|
+
const relPath = 'milestones/M001/M001-RESEARCH.md';
|
|
497
|
+
const filePath = path.join(tmpDir, '.gsd', relPath);
|
|
498
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
499
|
+
fs.writeFileSync(filePath, fullContent);
|
|
500
|
+
|
|
501
|
+
// Call saveArtifactToDb with abbreviated content — should trigger shrinkage guard
|
|
502
|
+
await saveArtifactToDb({
|
|
503
|
+
path: relPath,
|
|
504
|
+
artifact_type: 'RESEARCH',
|
|
505
|
+
content: abbreviatedContent,
|
|
506
|
+
milestone_id: 'M001',
|
|
507
|
+
}, tmpDir);
|
|
508
|
+
|
|
509
|
+
// Disk file should be preserved (not overwritten)
|
|
510
|
+
assert.deepStrictEqual(
|
|
511
|
+
fs.readFileSync(filePath, 'utf-8'),
|
|
512
|
+
fullContent,
|
|
513
|
+
'disk file preserved — shrinkage guard prevented overwrite',
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
// DB should contain the full disk content, not the abbreviated content
|
|
517
|
+
const adapter = _getAdapter();
|
|
518
|
+
const row = adapter!
|
|
519
|
+
.prepare('SELECT full_content FROM artifacts WHERE path = ?')
|
|
520
|
+
.get(relPath);
|
|
521
|
+
assert.deepStrictEqual(
|
|
522
|
+
row!['full_content'],
|
|
523
|
+
fullContent,
|
|
524
|
+
'DB stores the richer disk content instead of abbreviated content',
|
|
525
|
+
);
|
|
526
|
+
} finally {
|
|
527
|
+
closeDatabase();
|
|
528
|
+
cleanupDir(tmpDir);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test('saveArtifactToDb — allows overwrite when new content is similar size', async () => {
|
|
533
|
+
const tmpDir = makeTmpDir();
|
|
534
|
+
const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
|
|
535
|
+
openDatabase(dbPath);
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
const oldContent = '# Summary v1\n\nOriginal content here.\n';
|
|
539
|
+
const newContent = '# Summary v2\n\nUpdated content here with more details.\n';
|
|
540
|
+
|
|
541
|
+
const relPath = 'milestones/M001/M001-SUMMARY.md';
|
|
542
|
+
const filePath = path.join(tmpDir, '.gsd', relPath);
|
|
543
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
544
|
+
fs.writeFileSync(filePath, oldContent);
|
|
545
|
+
|
|
546
|
+
await saveArtifactToDb({
|
|
547
|
+
path: relPath,
|
|
548
|
+
artifact_type: 'SUMMARY',
|
|
549
|
+
content: newContent,
|
|
550
|
+
milestone_id: 'M001',
|
|
551
|
+
}, tmpDir);
|
|
552
|
+
|
|
553
|
+
// Disk file should be updated (new content is >=50% of old size)
|
|
554
|
+
assert.deepStrictEqual(
|
|
555
|
+
fs.readFileSync(filePath, 'utf-8'),
|
|
556
|
+
newContent,
|
|
557
|
+
'disk file updated when new content is similar size',
|
|
558
|
+
);
|
|
559
|
+
} finally {
|
|
560
|
+
closeDatabase();
|
|
561
|
+
cleanupDir(tmpDir);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
486
565
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
487
566
|
// Full Round-Trip: DB → Markdown → Parse → Compare
|
|
488
567
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
insertArtifact,
|
|
12
12
|
isDbAvailable,
|
|
13
13
|
insertMilestone,
|
|
14
|
+
getAllMilestones,
|
|
14
15
|
insertSlice,
|
|
15
16
|
insertTask,
|
|
16
17
|
} from '../gsd-db.ts';
|
|
@@ -962,4 +963,63 @@ describe('derive-state-db', async () => {
|
|
|
962
963
|
cleanup(base);
|
|
963
964
|
}
|
|
964
965
|
});
|
|
966
|
+
|
|
967
|
+
// ─── Regression: disk-only milestones synced into DB (#2416) ─────────
|
|
968
|
+
test('derive-state-db: disk-only milestone auto-synced into DB (#2416)', async () => {
|
|
969
|
+
const base = createFixtureBase();
|
|
970
|
+
try {
|
|
971
|
+
// M001 is complete and exists in DB. M002 was queued on disk only — no DB row.
|
|
972
|
+
writeFile(base, 'milestones/M001/M001-SUMMARY.md', '# M001 Summary\n\nDone.');
|
|
973
|
+
writeFile(base, 'milestones/M002/M002-CONTEXT.md', '# M002: Queued\n\nQueued milestone.');
|
|
974
|
+
|
|
975
|
+
openDatabase(':memory:');
|
|
976
|
+
// Only insert M001 — simulates the state after migration guard ran then /gsd queue added M002
|
|
977
|
+
insertMilestone({ id: 'M001', title: 'First', status: 'complete' });
|
|
978
|
+
|
|
979
|
+
invalidateStateCache();
|
|
980
|
+
const state = await deriveStateFromDb(base);
|
|
981
|
+
|
|
982
|
+
// Before the fix, M002 was invisible: getAllMilestones() returned only M001
|
|
983
|
+
// (complete) → phase='complete' → auto-mode stopped.
|
|
984
|
+
// After the fix, deriveStateFromDb reconciles disk dirs and inserts M002.
|
|
985
|
+
assert.deepStrictEqual(state.phase, 'pre-planning', 'disk-sync-2416: phase is pre-planning, not complete');
|
|
986
|
+
assert.deepStrictEqual(state.registry.length, 2, 'disk-sync-2416: both milestones visible in registry');
|
|
987
|
+
assert.deepStrictEqual(state.registry[0]?.id, 'M001', 'disk-sync-2416: registry[0] is M001');
|
|
988
|
+
assert.deepStrictEqual(state.registry[0]?.status, 'complete', 'disk-sync-2416: M001 is complete');
|
|
989
|
+
assert.deepStrictEqual(state.registry[1]?.id, 'M002', 'disk-sync-2416: registry[1] is M002');
|
|
990
|
+
assert.deepStrictEqual(state.registry[1]?.status, 'active', 'disk-sync-2416: M002 is active');
|
|
991
|
+
assert.deepStrictEqual(state.activeMilestone?.id, 'M002', 'disk-sync-2416: activeMilestone is M002');
|
|
992
|
+
|
|
993
|
+
closeDatabase();
|
|
994
|
+
} finally {
|
|
995
|
+
closeDatabase();
|
|
996
|
+
cleanup(base);
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
// ─── Queued milestone row not clobbered by later plan (#2416 root cause) ──
|
|
1001
|
+
test('derive-state-db: queued milestone row survives gsd_plan_milestone INSERT OR IGNORE', async () => {
|
|
1002
|
+
try {
|
|
1003
|
+
openDatabase(':memory:');
|
|
1004
|
+
|
|
1005
|
+
// Simulates gsd_milestone_generate_id inserting a minimal queued row
|
|
1006
|
+
insertMilestone({ id: 'M001', status: 'queued' });
|
|
1007
|
+
|
|
1008
|
+
const before = getAllMilestones();
|
|
1009
|
+
assert.equal(before.length, 1, 'queued-row: one row after generate_id');
|
|
1010
|
+
assert.equal(before[0]!.status, 'queued', 'queued-row: status is queued');
|
|
1011
|
+
|
|
1012
|
+
// Simulates gsd_plan_milestone calling insertMilestone (INSERT OR IGNORE)
|
|
1013
|
+
insertMilestone({ id: 'M001', title: 'Planned Title', status: 'active' });
|
|
1014
|
+
|
|
1015
|
+
const after = getAllMilestones();
|
|
1016
|
+
assert.equal(after.length, 1, 'queued-row: still one row after plan');
|
|
1017
|
+
// INSERT OR IGNORE keeps the original row — status stays 'queued'
|
|
1018
|
+
assert.equal(after[0]!.status, 'queued', 'queued-row: INSERT OR IGNORE preserves original status');
|
|
1019
|
+
|
|
1020
|
+
closeDatabase();
|
|
1021
|
+
} finally {
|
|
1022
|
+
closeDatabase();
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
965
1025
|
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* est-annotation-timeout.test.ts — Regression tests for #2243.
|
|
3
|
+
*
|
|
4
|
+
* Tasks with `est: 30m` or `est: 2h` annotations should get extended
|
|
5
|
+
* supervision timeouts. The parseEstimateMinutes helper should parse
|
|
6
|
+
* estimate strings, and startUnitSupervision should use them.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
const timersSrcPath = join(import.meta.dirname, "..", "auto-timers.ts");
|
|
15
|
+
const timersSrc = readFileSync(timersSrcPath, "utf-8");
|
|
16
|
+
|
|
17
|
+
// ─── Source analysis: parseEstimateMinutes exists and is exported ────────────
|
|
18
|
+
|
|
19
|
+
test("#2243: auto-timers.ts should export parseEstimateMinutes", () => {
|
|
20
|
+
assert.ok(
|
|
21
|
+
timersSrc.includes("export function parseEstimateMinutes"),
|
|
22
|
+
"parseEstimateMinutes should be exported from auto-timers.ts",
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// ─── Inline unit test of parseEstimateMinutes logic ─────────────────────────
|
|
27
|
+
// Since importing the module pulls in heavy deps, test the parsing logic inline.
|
|
28
|
+
|
|
29
|
+
function parseEstimateMinutes(estimate: string): number | null {
|
|
30
|
+
if (!estimate || typeof estimate !== "string") return null;
|
|
31
|
+
const trimmed = estimate.trim();
|
|
32
|
+
if (!trimmed) return null;
|
|
33
|
+
|
|
34
|
+
let totalMinutes = 0;
|
|
35
|
+
let matched = false;
|
|
36
|
+
|
|
37
|
+
const hoursMatch = trimmed.match(/(\d+)\s*h/i);
|
|
38
|
+
if (hoursMatch) {
|
|
39
|
+
totalMinutes += Number(hoursMatch[1]) * 60;
|
|
40
|
+
matched = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const minutesMatch = trimmed.match(/(\d+)\s*m/i);
|
|
44
|
+
if (minutesMatch) {
|
|
45
|
+
totalMinutes += Number(minutesMatch[1]);
|
|
46
|
+
matched = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return matched ? totalMinutes : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("#2243: parseEstimateMinutes parses '30m' correctly", () => {
|
|
53
|
+
assert.equal(parseEstimateMinutes("30m"), 30);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("#2243: parseEstimateMinutes parses '2h' correctly", () => {
|
|
57
|
+
assert.equal(parseEstimateMinutes("2h"), 120);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("#2243: parseEstimateMinutes parses '1h30m' correctly", () => {
|
|
61
|
+
assert.equal(parseEstimateMinutes("1h30m"), 90);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("#2243: parseEstimateMinutes parses '15m' correctly", () => {
|
|
65
|
+
assert.equal(parseEstimateMinutes("15m"), 15);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("#2243: parseEstimateMinutes returns null for empty string", () => {
|
|
69
|
+
assert.equal(parseEstimateMinutes(""), null);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("#2243: parseEstimateMinutes returns null for invalid string", () => {
|
|
73
|
+
assert.equal(parseEstimateMinutes("not a time"), null);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ─── Source analysis: startUnitSupervision uses task estimates ───────────────
|
|
77
|
+
|
|
78
|
+
test("#2243: startUnitSupervision should reference task estimates for timeout scaling", () => {
|
|
79
|
+
const usesEstimate =
|
|
80
|
+
timersSrc.includes("parseEstimateMinutes") &&
|
|
81
|
+
timersSrc.includes("estimateMinutes") &&
|
|
82
|
+
timersSrc.includes("taskEstimate");
|
|
83
|
+
|
|
84
|
+
assert.ok(
|
|
85
|
+
usesEstimate,
|
|
86
|
+
"startUnitSupervision should use task estimate annotations for timeout scaling",
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("#2243: SupervisionContext should accept an optional taskEstimate field", () => {
|
|
91
|
+
const ctxIdx = timersSrc.indexOf("SupervisionContext");
|
|
92
|
+
assert.ok(ctxIdx !== -1, "SupervisionContext interface exists");
|
|
93
|
+
|
|
94
|
+
const ctxEnd = timersSrc.indexOf("}", ctxIdx);
|
|
95
|
+
const ctxBlock = timersSrc.slice(ctxIdx, ctxEnd);
|
|
96
|
+
|
|
97
|
+
assert.ok(
|
|
98
|
+
ctxBlock.includes("taskEstimate"),
|
|
99
|
+
"SupervisionContext should include a taskEstimate field",
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("#2243: timeouts should be scaled by estimate (timeoutScale in source)", () => {
|
|
104
|
+
assert.ok(
|
|
105
|
+
timersSrc.includes("timeoutScale"),
|
|
106
|
+
"auto-timers.ts should use a timeoutScale factor derived from est: annotations",
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("#2243: idle timeout should NOT be scaled (idle is idle regardless of estimate)", () => {
|
|
111
|
+
// Find the idleTimeoutMs line
|
|
112
|
+
const idleIdx = timersSrc.indexOf("const idleTimeoutMs");
|
|
113
|
+
assert.ok(idleIdx !== -1, "idleTimeoutMs variable exists");
|
|
114
|
+
|
|
115
|
+
const idleLine = timersSrc.slice(idleIdx, timersSrc.indexOf("\n", idleIdx));
|
|
116
|
+
assert.ok(
|
|
117
|
+
!idleLine.includes("timeoutScale"),
|
|
118
|
+
"idleTimeoutMs should NOT be scaled — idle is idle",
|
|
119
|
+
);
|
|
120
|
+
});
|
|
@@ -7,10 +7,13 @@ import { isInfrastructureError, INFRA_ERROR_CODES } from "../auto/infra-errors.j
|
|
|
7
7
|
// ── INFRA_ERROR_CODES constant ───────────────────────────────────────────────
|
|
8
8
|
|
|
9
9
|
test("INFRA_ERROR_CODES contains the expected codes", () => {
|
|
10
|
-
for (const code of [
|
|
10
|
+
for (const code of [
|
|
11
|
+
"ENOSPC", "ENOMEM", "EROFS", "EDQUOT", "EMFILE", "ENFILE",
|
|
12
|
+
"ECONNREFUSED", "ENOTFOUND", "ENETUNREACH",
|
|
13
|
+
]) {
|
|
11
14
|
assert.ok(INFRA_ERROR_CODES.has(code), `missing ${code}`);
|
|
12
15
|
}
|
|
13
|
-
assert.equal(INFRA_ERROR_CODES.size,
|
|
16
|
+
assert.equal(INFRA_ERROR_CODES.size, 9, "unexpected extra codes");
|
|
14
17
|
});
|
|
15
18
|
|
|
16
19
|
// ── isInfrastructureError: code property detection ───────────────────────────
|
|
@@ -45,6 +48,21 @@ test("detects ENFILE via code property", () => {
|
|
|
45
48
|
assert.equal(isInfrastructureError(err), "ENFILE");
|
|
46
49
|
});
|
|
47
50
|
|
|
51
|
+
test("detects ECONNREFUSED via code property", () => {
|
|
52
|
+
const err = Object.assign(new Error("connect ECONNREFUSED 127.0.0.1:3000"), { code: "ECONNREFUSED" });
|
|
53
|
+
assert.equal(isInfrastructureError(err), "ECONNREFUSED");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("detects ENOTFOUND via code property", () => {
|
|
57
|
+
const err = Object.assign(new Error("getaddrinfo ENOTFOUND api.example.com"), { code: "ENOTFOUND" });
|
|
58
|
+
assert.equal(isInfrastructureError(err), "ENOTFOUND");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("detects ENETUNREACH via code property", () => {
|
|
62
|
+
const err = Object.assign(new Error("connect ENETUNREACH 2607:f8b0:4004::"), { code: "ENETUNREACH" });
|
|
63
|
+
assert.equal(isInfrastructureError(err), "ENETUNREACH");
|
|
64
|
+
});
|
|
65
|
+
|
|
48
66
|
// ── isInfrastructureError: message fallback ──────────────────────────────────
|
|
49
67
|
|
|
50
68
|
test("falls back to message scanning when no code property", () => {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - resolveGsdRootFile resolves KNOWLEDGE paths correctly
|
|
7
7
|
* - inlineGsdRootFile works with the KNOWLEDGE key
|
|
8
8
|
* - before_agent_start hook includes/omits knowledge block appropriately
|
|
9
|
+
* - loadKnowledgeBlock merges global and project knowledge correctly
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import test from 'node:test';
|
|
@@ -16,6 +17,7 @@ import { tmpdir } from 'node:os';
|
|
|
16
17
|
import { GSD_ROOT_FILES, resolveGsdRootFile } from '../paths.ts';
|
|
17
18
|
import { inlineGsdRootFile } from '../auto-prompts.ts';
|
|
18
19
|
import { appendKnowledge } from '../files.ts';
|
|
20
|
+
import { loadKnowledgeBlock } from '../bootstrap/system-context.ts';
|
|
19
21
|
|
|
20
22
|
// ─── KNOWLEDGE is registered in GSD_ROOT_FILES ─────────────────────────────
|
|
21
23
|
|
|
@@ -159,3 +161,90 @@ test('knowledge: appendKnowledge handles lesson type', async () => {
|
|
|
159
161
|
|
|
160
162
|
rmSync(tmp, { recursive: true, force: true });
|
|
161
163
|
});
|
|
164
|
+
|
|
165
|
+
// ─── loadKnowledgeBlock — global + project merge ────────────────────────────
|
|
166
|
+
|
|
167
|
+
test('loadKnowledgeBlock: returns empty block when neither file exists', () => {
|
|
168
|
+
const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
|
|
169
|
+
const gsdHome = join(tmp, 'home');
|
|
170
|
+
const cwd = join(tmp, 'project');
|
|
171
|
+
mkdirSync(join(cwd, '.gsd'), { recursive: true });
|
|
172
|
+
mkdirSync(join(gsdHome, 'agent'), { recursive: true });
|
|
173
|
+
|
|
174
|
+
const result = loadKnowledgeBlock(gsdHome, cwd);
|
|
175
|
+
assert.strictEqual(result.block, '');
|
|
176
|
+
assert.strictEqual(result.globalSizeKb, 0);
|
|
177
|
+
|
|
178
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('loadKnowledgeBlock: uses project knowledge alone when no global file', () => {
|
|
182
|
+
const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
|
|
183
|
+
const gsdHome = join(tmp, 'home');
|
|
184
|
+
const cwd = join(tmp, 'project');
|
|
185
|
+
mkdirSync(join(cwd, '.gsd'), { recursive: true });
|
|
186
|
+
mkdirSync(join(gsdHome, 'agent'), { recursive: true });
|
|
187
|
+
writeFileSync(join(cwd, '.gsd', 'KNOWLEDGE.md'), 'K001: Use real DB');
|
|
188
|
+
|
|
189
|
+
const result = loadKnowledgeBlock(gsdHome, cwd);
|
|
190
|
+
assert.ok(result.block.includes('[KNOWLEDGE — Rules, patterns, and lessons learned]'));
|
|
191
|
+
assert.ok(result.block.includes('## Project Knowledge'));
|
|
192
|
+
assert.ok(result.block.includes('K001: Use real DB'));
|
|
193
|
+
assert.ok(!result.block.includes('## Global Knowledge'));
|
|
194
|
+
assert.strictEqual(result.globalSizeKb, 0);
|
|
195
|
+
|
|
196
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('loadKnowledgeBlock: uses global knowledge alone when no project file', () => {
|
|
200
|
+
const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
|
|
201
|
+
const gsdHome = join(tmp, 'home');
|
|
202
|
+
const cwd = join(tmp, 'project');
|
|
203
|
+
mkdirSync(join(cwd, '.gsd'), { recursive: true });
|
|
204
|
+
mkdirSync(join(gsdHome, 'agent'), { recursive: true });
|
|
205
|
+
writeFileSync(join(gsdHome, 'agent', 'KNOWLEDGE.md'), 'G001: Respond in English');
|
|
206
|
+
|
|
207
|
+
const result = loadKnowledgeBlock(gsdHome, cwd);
|
|
208
|
+
assert.ok(result.block.includes('[KNOWLEDGE — Rules, patterns, and lessons learned]'));
|
|
209
|
+
assert.ok(result.block.includes('## Global Knowledge'));
|
|
210
|
+
assert.ok(result.block.includes('G001: Respond in English'));
|
|
211
|
+
assert.ok(!result.block.includes('## Project Knowledge'));
|
|
212
|
+
assert.ok(result.globalSizeKb > 0);
|
|
213
|
+
|
|
214
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('loadKnowledgeBlock: merges global before project when both exist', () => {
|
|
218
|
+
const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
|
|
219
|
+
const gsdHome = join(tmp, 'home');
|
|
220
|
+
const cwd = join(tmp, 'project');
|
|
221
|
+
mkdirSync(join(cwd, '.gsd'), { recursive: true });
|
|
222
|
+
mkdirSync(join(gsdHome, 'agent'), { recursive: true });
|
|
223
|
+
writeFileSync(join(gsdHome, 'agent', 'KNOWLEDGE.md'), 'G001: Global rule');
|
|
224
|
+
writeFileSync(join(cwd, '.gsd', 'KNOWLEDGE.md'), 'K001: Project rule');
|
|
225
|
+
|
|
226
|
+
const result = loadKnowledgeBlock(gsdHome, cwd);
|
|
227
|
+
assert.ok(result.block.includes('## Global Knowledge'));
|
|
228
|
+
assert.ok(result.block.includes('## Project Knowledge'));
|
|
229
|
+
assert.ok(result.block.includes('G001: Global rule'));
|
|
230
|
+
assert.ok(result.block.includes('K001: Project rule'));
|
|
231
|
+
// Global section appears before project section
|
|
232
|
+
assert.ok(result.block.indexOf('## Global Knowledge') < result.block.indexOf('## Project Knowledge'));
|
|
233
|
+
|
|
234
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('loadKnowledgeBlock: reports globalSizeKb above 4KB threshold', () => {
|
|
238
|
+
const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
|
|
239
|
+
const gsdHome = join(tmp, 'home');
|
|
240
|
+
const cwd = join(tmp, 'project');
|
|
241
|
+
mkdirSync(join(cwd, '.gsd'), { recursive: true });
|
|
242
|
+
mkdirSync(join(gsdHome, 'agent'), { recursive: true });
|
|
243
|
+
// Write > 4KB of content
|
|
244
|
+
writeFileSync(join(gsdHome, 'agent', 'KNOWLEDGE.md'), 'x'.repeat(5000));
|
|
245
|
+
|
|
246
|
+
const result = loadKnowledgeBlock(gsdHome, cwd);
|
|
247
|
+
assert.ok(result.globalSizeKb > 4, `expected > 4KB, got ${result.globalSizeKb}`);
|
|
248
|
+
|
|
249
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
250
|
+
});
|