gsd-pi 2.74.0-dev.703eabc → 2.74.0-dev.ffbcc03
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-recovery.js +24 -10
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +16 -5
- package/dist/resources/extensions/gsd/cache.js +16 -5
- package/dist/resources/extensions/gsd/guided-flow.js +1 -1
- package/dist/resources/extensions/gsd/safety/evidence-collector.js +15 -30
- 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 +14 -14
- 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/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 +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- 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/package.json +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +88 -6
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.ts +95 -10
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +8 -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 +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- 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 +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +17 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +19 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto-recovery.ts +29 -9
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +16 -5
- package/src/resources/extensions/gsd/cache.ts +16 -5
- package/src/resources/extensions/gsd/guided-flow.ts +1 -1
- package/src/resources/extensions/gsd/safety/evidence-collector.ts +15 -31
- package/src/resources/extensions/gsd/tests/artifacts-table-preserved-on-cache-invalidate.test.ts +177 -0
- package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +272 -0
- /package/dist/web/standalone/.next/static/{3U-oZ5FT59BM7sm2GInic → kn6xzWKYnogsxp2b6RpDD}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{3U-oZ5FT59BM7sm2GInic → kn6xzWKYnogsxp2b6RpDD}/_ssgManifest.js +0 -0
|
@@ -264,18 +264,30 @@ export function verifyExpectedArtifact(
|
|
|
264
264
|
const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
|
|
265
265
|
// For unit types with no verifiable artifact (null path), the parent directory
|
|
266
266
|
// is missing on disk — treat as stale completion state so the key gets evicted (#313).
|
|
267
|
-
if (!absPath)
|
|
268
|
-
|
|
267
|
+
if (!absPath) {
|
|
268
|
+
logWarning("recovery", `verify-fail ${unitType} ${unitId}: resolveExpectedArtifactPath returned null (parent dir missing)`);
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
if (!existsSync(absPath)) {
|
|
272
|
+
logWarning("recovery", `verify-fail ${unitType} ${unitId}: existsSync false for ${absPath}`);
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
269
275
|
|
|
270
276
|
if (unitType === "validate-milestone") {
|
|
271
277
|
const validationContent = readFileSync(absPath, "utf-8");
|
|
272
|
-
if (!isValidationTerminal(validationContent))
|
|
278
|
+
if (!isValidationTerminal(validationContent)) {
|
|
279
|
+
logWarning("recovery", `verify-fail ${unitType} ${unitId}: validation not terminal (len=${validationContent.length}) at ${absPath}`);
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
273
282
|
}
|
|
274
283
|
|
|
275
284
|
if (unitType === "plan-milestone") {
|
|
276
285
|
try {
|
|
277
286
|
const roadmap = parseLegacyRoadmap(readFileSync(absPath, "utf-8"));
|
|
278
|
-
if (roadmap.slices.length === 0)
|
|
287
|
+
if (roadmap.slices.length === 0) {
|
|
288
|
+
logWarning("recovery", `verify-fail ${unitType} ${unitId}: roadmap has zero slices at ${absPath}`);
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
279
291
|
} catch (err) {
|
|
280
292
|
logWarning("recovery", `plan-milestone roadmap verification failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
281
293
|
return false;
|
|
@@ -292,7 +304,10 @@ export function verifyExpectedArtifact(
|
|
|
292
304
|
// Accept checkbox-style (- [x] **T01: ...) or heading-style (### T01 -- / ### T01: / ### T01 —)
|
|
293
305
|
const hasCheckboxTask = /^- \[[xX ]\] \*\*T\d+:/m.test(planContent);
|
|
294
306
|
const hasHeadingTask = /^#{2,4}\s+T\d+\s*(?:--|—|:)/m.test(planContent);
|
|
295
|
-
if (!hasCheckboxTask && !hasHeadingTask)
|
|
307
|
+
if (!hasCheckboxTask && !hasHeadingTask) {
|
|
308
|
+
logWarning("recovery", `verify-fail ${unitType} ${unitId}: plan has no task checkbox/heading (len=${planContent.length}) at ${absPath}`);
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
296
311
|
}
|
|
297
312
|
|
|
298
313
|
// execute-task: DB status is authoritative. Fall back to checked-checkbox
|
|
@@ -349,10 +364,15 @@ export function verifyExpectedArtifact(
|
|
|
349
364
|
|
|
350
365
|
if (taskIds && taskIds.length > 0) {
|
|
351
366
|
const tasksDir = resolveTasksDir(base, mid, sid);
|
|
352
|
-
if (tasksDir) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
367
|
+
if (!tasksDir) {
|
|
368
|
+
logWarning("recovery", `verify-fail ${unitType} ${unitId}: resolveTasksDir returned null for ${mid}/${sid}`);
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
for (const tid of taskIds) {
|
|
372
|
+
const taskPlanFile = join(tasksDir, `${tid}-PLAN.md`);
|
|
373
|
+
if (!existsSync(taskPlanFile)) {
|
|
374
|
+
logWarning("recovery", `verify-fail ${unitType} ${unitId}: task plan missing ${taskPlanFile}`);
|
|
375
|
+
return false;
|
|
356
376
|
}
|
|
357
377
|
}
|
|
358
378
|
}
|
|
@@ -52,8 +52,12 @@ export function registerHooks(
|
|
|
52
52
|
resetToolCallLoopGuard();
|
|
53
53
|
resetAskUserQuestionsCache();
|
|
54
54
|
await syncServiceTierStatus(ctx);
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// Skip MCP auto-prep when running inside an auto-worktree (see session_switch below).
|
|
56
|
+
const { isInAutoWorktree } = await import("../auto-worktree.js");
|
|
57
|
+
if (!isInAutoWorktree(process.cwd())) {
|
|
58
|
+
const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
|
|
59
|
+
prepareWorkflowMcpForProject(ctx, process.cwd());
|
|
60
|
+
}
|
|
57
61
|
|
|
58
62
|
// Apply show_token_cost preference (#1515)
|
|
59
63
|
try {
|
|
@@ -94,8 +98,15 @@ export function registerHooks(
|
|
|
94
98
|
resetAskUserQuestionsCache();
|
|
95
99
|
clearDiscussionFlowState();
|
|
96
100
|
await syncServiceTierStatus(ctx);
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
// Skip MCP auto-prep when running inside an auto-worktree. The worktree
|
|
102
|
+
// already has .mcp.json from createAutoWorktree, and re-running the writer
|
|
103
|
+
// post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
|
|
104
|
+
// CLI path resolution), dirtying the tree and breaking the milestone merge.
|
|
105
|
+
const { isInAutoWorktree } = await import("../auto-worktree.js");
|
|
106
|
+
if (!isInAutoWorktree(process.cwd())) {
|
|
107
|
+
const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
|
|
108
|
+
prepareWorkflowMcpForProject(ctx, process.cwd());
|
|
109
|
+
}
|
|
99
110
|
loadToolApiKeys();
|
|
100
111
|
});
|
|
101
112
|
|
|
@@ -314,7 +325,7 @@ export function registerHooks(
|
|
|
314
325
|
// ── Safety harness: evidence collection + destructive command warnings ──
|
|
315
326
|
pi.on("tool_call", async (event, ctx) => {
|
|
316
327
|
if (!isAutoActive()) return;
|
|
317
|
-
safetyRecordToolCall(event.toolName, event.input as Record<string, unknown>);
|
|
328
|
+
safetyRecordToolCall(event.toolCallId, event.toolName, event.input as Record<string, unknown>);
|
|
318
329
|
|
|
319
330
|
// Destructive command classification (warn only, never block)
|
|
320
331
|
if (isToolCallEventType("bash", event)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// GSD Extension — Unified Cache Invalidation
|
|
2
2
|
//
|
|
3
|
-
// Three module-scoped caches exist across the GSD extension:
|
|
3
|
+
// Three module-scoped read caches exist across the GSD extension:
|
|
4
4
|
// 1. State cache (state.ts) — memoized deriveState() result
|
|
5
5
|
// 2. Path cache (paths.ts) — directory listing results (readdirSync)
|
|
6
6
|
// 3. Parse cache (files.ts) — parsed markdown file results
|
|
@@ -8,22 +8,33 @@
|
|
|
8
8
|
// After any file write that changes .gsd/ contents, all three must be
|
|
9
9
|
// invalidated together to prevent stale reads. This module provides a
|
|
10
10
|
// single function that clears all three atomically.
|
|
11
|
+
//
|
|
12
|
+
// NOTE: The DB `artifacts` table is NOT included here. Earlier versions
|
|
13
|
+
// called clearArtifacts() as part of this bundle (#793), intending to
|
|
14
|
+
// force deriveState() to re-parse from disk when files were edited
|
|
15
|
+
// out-of-band. But invalidateAllCaches() fires on every post-unit pass,
|
|
16
|
+
// so bundling a DESTRUCTIVE `DELETE FROM artifacts` with routine cache
|
|
17
|
+
// invalidation meant every row written by saveArtifactToDb / writeAndStore
|
|
18
|
+
// was wiped within seconds — leaving the milestone completed on disk but
|
|
19
|
+
// the `artifacts` table empty and the agent looping on "file exists but
|
|
20
|
+
// DB record missing" recovery calls. If a call site genuinely needs the
|
|
21
|
+
// artifact table cleared after an out-of-band file mutation, it should
|
|
22
|
+
// invoke clearArtifacts() from gsd-db.js explicitly — do not add it back
|
|
23
|
+
// here.
|
|
11
24
|
|
|
12
25
|
import { invalidateStateCache } from './state.js';
|
|
13
26
|
import { clearPathCache } from './paths.js';
|
|
14
27
|
import { clearParseCache } from './files.js';
|
|
15
|
-
import { clearArtifacts } from './gsd-db.js';
|
|
16
28
|
|
|
17
29
|
/**
|
|
18
|
-
* Invalidate all GSD runtime caches in one call.
|
|
30
|
+
* Invalidate all GSD runtime read caches in one call.
|
|
19
31
|
*
|
|
20
32
|
* Call this after file writes, milestone transitions, merge reconciliation,
|
|
21
33
|
* or any operation that changes .gsd/ contents on disk. Forgetting to clear
|
|
22
|
-
* any single cache causes stale reads (see #431
|
|
34
|
+
* any single cache causes stale reads (see #431).
|
|
23
35
|
*/
|
|
24
36
|
export function invalidateAllCaches(): void {
|
|
25
37
|
invalidateStateCache();
|
|
26
38
|
clearPathCache();
|
|
27
39
|
clearParseCache();
|
|
28
|
-
clearArtifacts();
|
|
29
40
|
}
|
|
@@ -272,7 +272,7 @@ export function checkAutoStartAfterDiscuss(): boolean {
|
|
|
272
272
|
try { unlinkSync(manifestPath); } catch (e) { logWarning("guided", `manifest unlink failed: ${(e as Error).message}`); }
|
|
273
273
|
|
|
274
274
|
pendingAutoStartMap.delete(basePath);
|
|
275
|
-
ctx.ui.notify(`Milestone ${milestoneId} ready.`, "
|
|
275
|
+
ctx.ui.notify(`Milestone ${milestoneId} ready.`, "success");
|
|
276
276
|
startAutoDetached(ctx, pi, basePath, false, { step });
|
|
277
277
|
return true;
|
|
278
278
|
}
|
|
@@ -68,11 +68,11 @@ export function getFilePaths(): string[] {
|
|
|
68
68
|
* Record a tool call at dispatch time (before execution).
|
|
69
69
|
* Exit codes and output are filled in by recordToolResult after execution.
|
|
70
70
|
*/
|
|
71
|
-
export function recordToolCall(toolName: string, input: Record<string, unknown>): void {
|
|
71
|
+
export function recordToolCall(toolCallId: string, toolName: string, input: Record<string, unknown>): void {
|
|
72
72
|
if (toolName === "bash" || toolName === "Bash") {
|
|
73
73
|
unitEvidence.push({
|
|
74
74
|
kind: "bash",
|
|
75
|
-
toolCallId
|
|
75
|
+
toolCallId,
|
|
76
76
|
command: String(input.command ?? ""),
|
|
77
77
|
exitCode: -1,
|
|
78
78
|
outputSnippet: "",
|
|
@@ -81,14 +81,14 @@ export function recordToolCall(toolName: string, input: Record<string, unknown>)
|
|
|
81
81
|
} else if (toolName === "write" || toolName === "Write") {
|
|
82
82
|
unitEvidence.push({
|
|
83
83
|
kind: "write",
|
|
84
|
-
toolCallId
|
|
84
|
+
toolCallId,
|
|
85
85
|
path: String(input.file_path ?? input.path ?? ""),
|
|
86
86
|
timestamp: Date.now(),
|
|
87
87
|
});
|
|
88
88
|
} else if (toolName === "edit" || toolName === "Edit") {
|
|
89
89
|
unitEvidence.push({
|
|
90
90
|
kind: "edit",
|
|
91
|
-
toolCallId
|
|
91
|
+
toolCallId,
|
|
92
92
|
path: String(input.file_path ?? input.path ?? ""),
|
|
93
93
|
timestamp: Date.now(),
|
|
94
94
|
});
|
|
@@ -96,8 +96,9 @@ export function recordToolCall(toolName: string, input: Record<string, unknown>)
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
* Record a tool execution result. Matches the
|
|
100
|
-
*
|
|
99
|
+
* Record a tool execution result. Matches the entry by toolCallId (assigned
|
|
100
|
+
* at dispatch time) and fills in exit code + output. Prior versions matched
|
|
101
|
+
* by `kind + empty-string` which corrupted parallel tool calls.
|
|
101
102
|
*/
|
|
102
103
|
export function recordToolResult(
|
|
103
104
|
toolCallId: string,
|
|
@@ -105,36 +106,19 @@ export function recordToolResult(
|
|
|
105
106
|
result: unknown,
|
|
106
107
|
isError: boolean,
|
|
107
108
|
): void {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const exitMatch = text.match(/Command exited with code (\d+)/);
|
|
117
|
-
entry.exitCode = exitMatch ? Number(exitMatch[1]) : (isError ? 1 : 0);
|
|
118
|
-
}
|
|
119
|
-
} else if (normalizedName === "write" || normalizedName === "edit") {
|
|
120
|
-
const entry = findLastUnresolved(normalizedName as "write" | "edit");
|
|
121
|
-
if (entry) {
|
|
122
|
-
entry.toolCallId = toolCallId;
|
|
123
|
-
}
|
|
109
|
+
const entry = unitEvidence.find(e => e.toolCallId === toolCallId);
|
|
110
|
+
if (!entry) return;
|
|
111
|
+
|
|
112
|
+
if (entry.kind === "bash") {
|
|
113
|
+
const text = extractResultText(result);
|
|
114
|
+
entry.outputSnippet = text.slice(0, 500);
|
|
115
|
+
const exitMatch = text.match(/Command exited with code (\d+)/);
|
|
116
|
+
entry.exitCode = exitMatch ? Number(exitMatch[1]) : (isError ? 1 : 0);
|
|
124
117
|
}
|
|
125
118
|
}
|
|
126
119
|
|
|
127
120
|
// ─── Internals ──────────────────────────────────────────────────────────────
|
|
128
121
|
|
|
129
|
-
function findLastUnresolved(kind: string): EvidenceEntry | undefined {
|
|
130
|
-
for (let i = unitEvidence.length - 1; i >= 0; i--) {
|
|
131
|
-
if (unitEvidence[i].kind === kind && unitEvidence[i].toolCallId === "") {
|
|
132
|
-
return unitEvidence[i];
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
122
|
function extractResultText(result: unknown): string {
|
|
139
123
|
if (typeof result === "string") return result;
|
|
140
124
|
if (result && typeof result === "object") {
|
package/src/resources/extensions/gsd/tests/artifacts-table-preserved-on-cache-invalidate.test.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test: invalidateAllCaches() must NOT wipe the artifacts table.
|
|
3
|
+
*
|
|
4
|
+
* Prior to this fix, `cache.ts` bundled `clearArtifacts()` (which runs
|
|
5
|
+
* `DELETE FROM artifacts`) into `invalidateAllCaches()`. That helper fires
|
|
6
|
+
* on every post-unit pass, so rows written by `saveArtifactToDb` and
|
|
7
|
+
* `writeAndStore` (RESEARCH, CONTEXT, VALIDATION, ASSESSMENT, PLAN,
|
|
8
|
+
* ROADMAP, task PLAN, task SUMMARY) got deleted within seconds of being
|
|
9
|
+
* written. The milestone completed on disk, but `SELECT COUNT(*) FROM
|
|
10
|
+
* artifacts` returned 0, and the agent fell into a "file exists but DB
|
|
11
|
+
* record missing" recovery loop.
|
|
12
|
+
*
|
|
13
|
+
* The artifacts table is a write-through store, not a read cache. Routine
|
|
14
|
+
* cache invalidation must preserve its contents.
|
|
15
|
+
*
|
|
16
|
+
* Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, test, afterEach } from "node:test";
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import { resolve } from "node:path";
|
|
23
|
+
|
|
24
|
+
import { invalidateAllCaches } from "../cache.ts";
|
|
25
|
+
import {
|
|
26
|
+
openDatabase,
|
|
27
|
+
closeDatabase,
|
|
28
|
+
insertArtifact,
|
|
29
|
+
isDbAvailable,
|
|
30
|
+
_getAdapter,
|
|
31
|
+
} from "../gsd-db.ts";
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
if (isDbAvailable()) {
|
|
35
|
+
try {
|
|
36
|
+
closeDatabase();
|
|
37
|
+
} catch {
|
|
38
|
+
/* best-effort teardown */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("invalidateAllCaches() must preserve the artifacts table", () => {
|
|
44
|
+
test("rows survive a single invalidate call", () => {
|
|
45
|
+
const opened = openDatabase(":memory:");
|
|
46
|
+
assert.equal(opened, true, "in-memory DB must open");
|
|
47
|
+
|
|
48
|
+
insertArtifact({
|
|
49
|
+
path: "milestones/M001/slices/S01/S01-RESEARCH.md",
|
|
50
|
+
artifact_type: "RESEARCH",
|
|
51
|
+
milestone_id: "M001",
|
|
52
|
+
slice_id: "S01",
|
|
53
|
+
task_id: null,
|
|
54
|
+
full_content: "# Research\n\nFindings go here.\n",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
invalidateAllCaches();
|
|
58
|
+
|
|
59
|
+
const adapter = _getAdapter();
|
|
60
|
+
assert.ok(adapter, "adapter should be available");
|
|
61
|
+
const row = adapter!
|
|
62
|
+
.prepare(
|
|
63
|
+
"SELECT path, artifact_type, length(full_content) AS len FROM artifacts WHERE path = :path",
|
|
64
|
+
)
|
|
65
|
+
.get({ ":path": "milestones/M001/slices/S01/S01-RESEARCH.md" }) as
|
|
66
|
+
| { path: string; artifact_type: string; len: number }
|
|
67
|
+
| undefined;
|
|
68
|
+
|
|
69
|
+
assert.ok(
|
|
70
|
+
row,
|
|
71
|
+
"artifact row must still exist after invalidateAllCaches — this is the Phase B bug",
|
|
72
|
+
);
|
|
73
|
+
assert.equal(row!.artifact_type, "RESEARCH");
|
|
74
|
+
assert.ok((row!.len ?? 0) > 0, "full_content must not be truncated");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("multiple rows for a full milestone survive repeated invalidates", () => {
|
|
78
|
+
openDatabase(":memory:");
|
|
79
|
+
|
|
80
|
+
const inserts = [
|
|
81
|
+
{
|
|
82
|
+
path: "milestones/M001/M001-ROADMAP.md",
|
|
83
|
+
artifact_type: "ROADMAP",
|
|
84
|
+
milestone_id: "M001",
|
|
85
|
+
slice_id: null,
|
|
86
|
+
task_id: null,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
path: "milestones/M001/slices/S01/S01-RESEARCH.md",
|
|
90
|
+
artifact_type: "RESEARCH",
|
|
91
|
+
milestone_id: "M001",
|
|
92
|
+
slice_id: "S01",
|
|
93
|
+
task_id: null,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
path: "milestones/M001/slices/S01/S01-PLAN.md",
|
|
97
|
+
artifact_type: "PLAN",
|
|
98
|
+
milestone_id: "M001",
|
|
99
|
+
slice_id: "S01",
|
|
100
|
+
task_id: null,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
path: "milestones/M001/slices/S01/tasks/T01-PLAN.md",
|
|
104
|
+
artifact_type: "PLAN",
|
|
105
|
+
milestone_id: "M001",
|
|
106
|
+
slice_id: "S01",
|
|
107
|
+
task_id: "T01",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
path: "milestones/M001/slices/S01/tasks/T01-SUMMARY.md",
|
|
111
|
+
artifact_type: "SUMMARY",
|
|
112
|
+
milestone_id: "M001",
|
|
113
|
+
slice_id: "S01",
|
|
114
|
+
task_id: "T01",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
path: "milestones/M001/M001-SUMMARY.md",
|
|
118
|
+
artifact_type: "SUMMARY",
|
|
119
|
+
milestone_id: "M001",
|
|
120
|
+
slice_id: null,
|
|
121
|
+
task_id: null,
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
for (const i of inserts) {
|
|
126
|
+
insertArtifact({ ...i, full_content: `# ${i.artifact_type} content\n` });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Simulate a full milestone's worth of post-unit cycles.
|
|
130
|
+
for (let i = 0; i < 10; i++) {
|
|
131
|
+
invalidateAllCaches();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const adapter = _getAdapter()!;
|
|
135
|
+
const count = (
|
|
136
|
+
adapter.prepare("SELECT COUNT(*) AS n FROM artifacts").get() as { n: number }
|
|
137
|
+
).n;
|
|
138
|
+
|
|
139
|
+
assert.equal(
|
|
140
|
+
count,
|
|
141
|
+
inserts.length,
|
|
142
|
+
`all ${inserts.length} artifact rows must survive repeated invalidate calls; got ${count}`,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("cache.ts must not re-import clearArtifacts into invalidateAllCaches", () => {
|
|
148
|
+
const src = readFileSync(
|
|
149
|
+
resolve(process.cwd(), "src", "resources", "extensions", "gsd", "cache.ts"),
|
|
150
|
+
"utf-8",
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
test("clearArtifacts is not imported from gsd-db", () => {
|
|
154
|
+
assert.ok(
|
|
155
|
+
!/import\s*\{[^}]*clearArtifacts[^}]*\}\s*from\s*['"]\.\/gsd-db/.test(src),
|
|
156
|
+
"cache.ts must not import clearArtifacts — it causes the artifacts-table-wipe regression",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("invalidateAllCaches does not call clearArtifacts", () => {
|
|
161
|
+
const fnIdx = src.indexOf("function invalidateAllCaches");
|
|
162
|
+
assert.ok(fnIdx !== -1);
|
|
163
|
+
const body = src.slice(fnIdx, fnIdx + 1000);
|
|
164
|
+
assert.ok(
|
|
165
|
+
!/\bclearArtifacts\s*\(/.test(body),
|
|
166
|
+
"invalidateAllCaches must not call clearArtifacts() — it wipes the write-through store",
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("cache.ts documents why clearArtifacts is not bundled here", () => {
|
|
171
|
+
// Future reviewers need to see the rationale or they'll re-add it.
|
|
172
|
+
assert.ok(
|
|
173
|
+
/artifacts.*NOT included|write-through store/i.test(src),
|
|
174
|
+
"cache.ts must explain why the artifacts table is NOT invalidated here",
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
});
|