@opengsd/gsd-pi 1.1.1-dev.2034b16 → 1.1.1-dev.595401e
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/gsd/auto-post-unit.js +21 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +15 -6
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +2 -2
- package/dist/resources/extensions/gsd/browser-evidence.js +29 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +8 -0
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +2 -2
- package/dist/resources/extensions/gsd/post-unit-hooks.js +9 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +39 -0
- package/dist/resources/extensions/gsd/prompt-loader.js +7 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +40 -22
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +3 -3
- package/dist/resources/extensions/gsd/rule-registry.js +428 -52
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +46 -16
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +29 -14
- package/dist/resources/extensions/gsd/verdict-parser.js +59 -15
- package/dist/rtk.d.ts +7 -1
- package/dist/rtk.js +27 -11
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
- 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 +7 -7
- 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/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/dist/session/agent-session-compaction.d.ts +2 -0
- package/packages/gsd-agent-core/dist/session/agent-session-compaction.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/session/agent-session-compaction.js +8 -2
- package/packages/gsd-agent-core/dist/session/agent-session-compaction.js.map +1 -1
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
- package/packages/mcp-server/dist/remote-questions.js +23 -9
- package/packages/mcp-server/dist/remote-questions.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +17 -17
- package/packages/pi-ai/dist/models.generated.js +19 -19
- package/packages/pi-ai/dist/models.generated.js.map +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 +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +28 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +16 -6
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +2 -2
- package/src/resources/extensions/gsd/browser-evidence.ts +26 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +8 -0
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +2 -2
- package/src/resources/extensions/gsd/post-unit-hooks.ts +14 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +36 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +8 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +40 -22
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +3 -3
- package/src/resources/extensions/gsd/rule-registry.ts +558 -58
- package/src/resources/extensions/gsd/rule-types.ts +2 -0
- package/src/resources/extensions/gsd/tests/browser-evidence.test.ts +142 -0
- package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/doctor-runtime-checks.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +66 -10
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +157 -0
- package/src/resources/extensions/gsd/tests/post-unit-retry-on-orchestrator-bridge.test.ts +179 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +29 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +22 -1
- package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/rule-registry.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +133 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +74 -0
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +46 -15
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +31 -14
- package/src/resources/extensions/gsd/types.ts +63 -0
- package/src/resources/extensions/gsd/verdict-parser.ts +54 -13
- /package/dist/web/standalone/.next/static/{StOMnvtgGiBHrBOZJZ1Gr → IDKjyRHLIaumjgonPcYiX}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{StOMnvtgGiBHrBOZJZ1Gr → IDKjyRHLIaumjgonPcYiX}/_ssgManifest.js +0 -0
|
@@ -32,6 +32,8 @@ export interface RuleLifecycle {
|
|
|
32
32
|
retry_on?: string;
|
|
33
33
|
/** Max times this hook can fire for the same trigger unit. */
|
|
34
34
|
max_cycles?: number;
|
|
35
|
+
/** Whether this hook is advisory or blocking. */
|
|
36
|
+
criticality?: PostUnitHookConfig["criticality"];
|
|
35
37
|
/** Idempotency key pattern for this hook. */
|
|
36
38
|
idempotency_key?: string;
|
|
37
39
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Project/App: gsd-pi
|
|
2
|
+
// File Purpose: Unit tests for hasBrowserRequiredText heading-depth section guard.
|
|
3
|
+
|
|
4
|
+
import { describe, test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
|
|
7
|
+
import { hasBrowserRequiredText } from '../browser-evidence.ts';
|
|
8
|
+
|
|
9
|
+
describe('hasBrowserRequiredText', () => {
|
|
10
|
+
test('detects browser requirement in a plain test-cases section', () => {
|
|
11
|
+
const text = [
|
|
12
|
+
'## Test Cases',
|
|
13
|
+
'',
|
|
14
|
+
'1. Open index.html in a browser and navigate to /dashboard.',
|
|
15
|
+
'',
|
|
16
|
+
].join('\n');
|
|
17
|
+
assert.ok(hasBrowserRequiredText(text), 'plain browser step should be detected');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('ignores browser mention under a top-level non-requirement heading', () => {
|
|
21
|
+
const text = [
|
|
22
|
+
'## Not Proven',
|
|
23
|
+
'',
|
|
24
|
+
'- Keyboard usability through a real browser.',
|
|
25
|
+
'- Browser console cleanliness.',
|
|
26
|
+
'',
|
|
27
|
+
].join('\n');
|
|
28
|
+
assert.ok(!hasBrowserRequiredText(text), 'browser mention under "Not Proven" should be ignored');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('sub-heading inside a non-requirement section does not re-enable detection', () => {
|
|
32
|
+
// BUG (pre-fix): ### sub-heading under ## Not Proven resets inNonRequirementSection
|
|
33
|
+
// to false, causing subsequent lines to be detected as browser requirements.
|
|
34
|
+
const text = [
|
|
35
|
+
'## Not Proven By This UAT',
|
|
36
|
+
'',
|
|
37
|
+
'- No live browser session was used.',
|
|
38
|
+
'',
|
|
39
|
+
'### Visual Checks',
|
|
40
|
+
'',
|
|
41
|
+
'- Browser visual polish deferred to next slice.',
|
|
42
|
+
'- Keyboard interaction in a real browser is not proven here.',
|
|
43
|
+
'',
|
|
44
|
+
].join('\n');
|
|
45
|
+
assert.ok(
|
|
46
|
+
!hasBrowserRequiredText(text),
|
|
47
|
+
'sub-heading under a non-requirement section must not re-enable browser detection',
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('requirement-level heading after non-requirement section re-enables detection', () => {
|
|
52
|
+
const text = [
|
|
53
|
+
'## Not Proven',
|
|
54
|
+
'',
|
|
55
|
+
'- Browser polish deferred.',
|
|
56
|
+
'',
|
|
57
|
+
'## Test Cases',
|
|
58
|
+
'',
|
|
59
|
+
'1. Launch browser and open localhost.',
|
|
60
|
+
'',
|
|
61
|
+
].join('\n');
|
|
62
|
+
assert.ok(
|
|
63
|
+
hasBrowserRequiredText(text),
|
|
64
|
+
'browser step under "Test Cases" (same depth as "Not Proven") must still be detected',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('deferred sub-heading inside a requirement section scopes exclusion to its own block', () => {
|
|
69
|
+
const text = [
|
|
70
|
+
'## Test Cases',
|
|
71
|
+
'',
|
|
72
|
+
'1. Open browser at localhost.',
|
|
73
|
+
'',
|
|
74
|
+
'### Deferred: keyboard check',
|
|
75
|
+
'',
|
|
76
|
+
'- Keyboard UAT deferred to next slice.',
|
|
77
|
+
'',
|
|
78
|
+
'### Step 2: Verify DOM',
|
|
79
|
+
'',
|
|
80
|
+
'1. Navigate to /dashboard in the browser.',
|
|
81
|
+
'',
|
|
82
|
+
].join('\n');
|
|
83
|
+
assert.ok(
|
|
84
|
+
hasBrowserRequiredText(text),
|
|
85
|
+
'browser step under "Step 2" sub-heading must be detected after a sibling "Deferred" sub-heading',
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('deferred sub-heading at same depth as test cases does not escape to parent', () => {
|
|
90
|
+
const text = [
|
|
91
|
+
'## Test Cases',
|
|
92
|
+
'',
|
|
93
|
+
'### Deferred: responsive layout',
|
|
94
|
+
'',
|
|
95
|
+
'- Responsive layout check is deferred to S02.',
|
|
96
|
+
'',
|
|
97
|
+
].join('\n');
|
|
98
|
+
assert.ok(
|
|
99
|
+
!hasBrowserRequiredText(text),
|
|
100
|
+
'content under a "Deferred" sub-heading should be excluded from detection',
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('detects browser requirement written only in a heading', () => {
|
|
105
|
+
// Regression: the line-by-line scan previously skip-continued past headings,
|
|
106
|
+
// missing browser obligations expressed only in heading text.
|
|
107
|
+
const text = '## Open browser session at localhost\n';
|
|
108
|
+
assert.ok(hasBrowserRequiredText(text), 'browser requirement in heading text must be detected');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('heading that opens a non-requirement section is not itself detected as a requirement', () => {
|
|
112
|
+
const text = '## Not Proven\n\n- Some note.\n';
|
|
113
|
+
assert.ok(
|
|
114
|
+
!hasBrowserRequiredText(text),
|
|
115
|
+
'a non-requirement section heading should not trigger browser detection',
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('returns false for empty text', () => {
|
|
120
|
+
assert.ok(!hasBrowserRequiredText(''), 'empty string returns false');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('notes-for-tester heading with sub-headings stays non-requirement', () => {
|
|
124
|
+
const text = [
|
|
125
|
+
'## Notes for Tester',
|
|
126
|
+
'',
|
|
127
|
+
'### Browser Setup',
|
|
128
|
+
'',
|
|
129
|
+
'- Run this spec without a browser; a DOM harness is sufficient.',
|
|
130
|
+
'- Browser-based visual checks are deferred.',
|
|
131
|
+
'',
|
|
132
|
+
'### Follow-up Items',
|
|
133
|
+
'',
|
|
134
|
+
'- Track browser session evidence in S02.',
|
|
135
|
+
'',
|
|
136
|
+
].join('\n');
|
|
137
|
+
assert.ok(
|
|
138
|
+
!hasBrowserRequiredText(text),
|
|
139
|
+
'sub-headings under "Notes for Tester" should not re-enable browser detection',
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -12,6 +12,7 @@ import { tmpdir } from "node:os";
|
|
|
12
12
|
|
|
13
13
|
import { buildSliceSummaryExcerpt, buildCompleteMilestonePrompt, buildValidateMilestonePrompt } from "../auto-prompts.ts";
|
|
14
14
|
import { invalidateAllCaches } from "../cache.ts";
|
|
15
|
+
import { closeDatabase, insertMilestone, openDatabase } from "../gsd-db.ts";
|
|
15
16
|
|
|
16
17
|
// ─── Fixture helpers ──────────────────────────────────────────────────────
|
|
17
18
|
|
|
@@ -364,3 +365,32 @@ test("validate-milestone prompt uses slice excerpts and on-demand paths instead
|
|
|
364
365
|
"validate prompt must not inline full assessment traces",
|
|
365
366
|
);
|
|
366
367
|
});
|
|
368
|
+
|
|
369
|
+
test("validate-milestone prompt inlines planned verification classes as canonical rows", async (t) => {
|
|
370
|
+
const base = createBase();
|
|
371
|
+
t.after(() => {
|
|
372
|
+
try { closeDatabase(); } catch { /* ignore */ }
|
|
373
|
+
cleanup(base);
|
|
374
|
+
});
|
|
375
|
+
invalidateAllCaches();
|
|
376
|
+
|
|
377
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
378
|
+
insertMilestone({
|
|
379
|
+
id: "M001",
|
|
380
|
+
planning: {
|
|
381
|
+
verificationContract: "Local command exits 0.",
|
|
382
|
+
verificationOperational: "No long-running child process remains.",
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
writeRoadmap(base, makeRoadmap());
|
|
386
|
+
writeSummary(base, "S01", makeFatSummary("S01"));
|
|
387
|
+
writeSummary(base, "S02", makeFatSummary("S02"));
|
|
388
|
+
|
|
389
|
+
const prompt = await buildValidateMilestonePrompt("M001", "Test Milestone", base);
|
|
390
|
+
|
|
391
|
+
assert.match(prompt, /### Verification Classes \(from planning\)/);
|
|
392
|
+
assert.match(prompt, /Every row in this table must appear in `verificationClasses`/);
|
|
393
|
+
assert.match(prompt, /\| Class \| Planned Check \|/);
|
|
394
|
+
assert.match(prompt, /\| Contract \| Local command exits 0\. \|/);
|
|
395
|
+
assert.match(prompt, /\| Operational \| No long-running child process remains\. \|/);
|
|
396
|
+
});
|
|
@@ -45,3 +45,30 @@ test("doctor fix respects git.manage_gitignore false (#4161)", async (t) => {
|
|
|
45
45
|
assert.equal(readFileSync(join(dir, ".gitignore"), "utf-8"), "node_modules/\n");
|
|
46
46
|
assert.equal(existsSync(join(dir, ".gsd", "PREFERENCES.md")), true);
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
test("doctor fix resets run-uat counters at the dispatch cap", async (t) => {
|
|
50
|
+
const dir = createGitProject();
|
|
51
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
52
|
+
|
|
53
|
+
const runtimeDir = join(dir, ".gsd", "runtime");
|
|
54
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
55
|
+
const counterPath = join(runtimeDir, "uat-count-M002-S01.json");
|
|
56
|
+
writeFileSync(
|
|
57
|
+
counterPath,
|
|
58
|
+
JSON.stringify({ count: 3, updatedAt: "2026-06-02T19:40:23.289Z" }) + "\n",
|
|
59
|
+
"utf-8",
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const detect = await runGSDDoctor(dir);
|
|
63
|
+
const issue = detect.issues.find((candidate) => candidate.code === "uat_retry_exhausted");
|
|
64
|
+
assert.ok(issue, "doctor reports the exhausted UAT retry counter at the dispatch cap");
|
|
65
|
+
assert.equal(issue.unitId, "M002/S01");
|
|
66
|
+
assert.match(issue.message, /3 attempt\(s\)/);
|
|
67
|
+
|
|
68
|
+
const fixed = await runGSDDoctor(dir, { fix: true, scope: "M002/S02" });
|
|
69
|
+
assert.ok(
|
|
70
|
+
fixed.fixesApplied.some((fix) => fix.includes("reset exhausted run-uat retry counter for M002/S01")),
|
|
71
|
+
"doctor --fix resets the blocked counter even when the current displayed scope has advanced",
|
|
72
|
+
);
|
|
73
|
+
assert.equal(existsSync(counterPath), false);
|
|
74
|
+
});
|
|
@@ -119,8 +119,8 @@ test("resolveExpectedArtifactPath returns correct path for all slice-level types
|
|
|
119
119
|
// ─── run-uat artifact path contract (#2873) ──────────────────────────────
|
|
120
120
|
|
|
121
121
|
test("resolveExpectedArtifactPath for run-uat returns ASSESSMENT path, not UAT (#2873)", (t) => {
|
|
122
|
-
// The run-uat prompt instructs the agent to call
|
|
123
|
-
//
|
|
122
|
+
// The run-uat prompt instructs the agent to call gsd_uat_result_save, which
|
|
123
|
+
// writes S##-ASSESSMENT.md through the workflow persistence path. The artifact
|
|
124
124
|
// verification path must match — otherwise verification fails and auto-mode
|
|
125
125
|
// retries the unit in an infinite loop.
|
|
126
126
|
const base = makeTmpBase();
|
|
@@ -147,12 +147,12 @@ test("diagnoseExpectedArtifact for run-uat references ASSESSMENT (#2873)", (t) =
|
|
|
147
147
|
});
|
|
148
148
|
|
|
149
149
|
test("verifyExpectedArtifact passes for run-uat when ASSESSMENT file exists (#2873)", (t) => {
|
|
150
|
-
// Regression test: run-uat writes S##-ASSESSMENT.md via
|
|
150
|
+
// Regression test: run-uat writes S##-ASSESSMENT.md via gsd_uat_result_save,
|
|
151
151
|
// but verification looked for S##-UAT.md, causing false stuck retries.
|
|
152
152
|
const base = makeTmpBase();
|
|
153
153
|
t.after(() => cleanup(base));
|
|
154
154
|
|
|
155
|
-
// Write the ASSESSMENT file (what
|
|
155
|
+
// Write the ASSESSMENT file (what gsd_uat_result_save actually produces)
|
|
156
156
|
const assessPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-ASSESSMENT.md");
|
|
157
157
|
writeFileSync(assessPath, "---\nverdict: PASS\n---\n# UAT Assessment\n");
|
|
158
158
|
|
|
@@ -72,6 +72,38 @@ function makeBrowserObservableUatContent(mode = 'artifact-driven'): string {
|
|
|
72
72
|
].join('\n');
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function makeDeferredBrowserUatContent(): string {
|
|
76
|
+
return [
|
|
77
|
+
'# UAT File',
|
|
78
|
+
'',
|
|
79
|
+
'## UAT Type',
|
|
80
|
+
'',
|
|
81
|
+
'- UAT mode: artifact-driven',
|
|
82
|
+
'- Why this mode is sufficient: Node interaction tests exercise the real app.js render/event/localStorage loop through a DOM harness. Live browser, keyboard, responsive, and visual-polish UAT remain intentionally deferred to S02.',
|
|
83
|
+
'',
|
|
84
|
+
'## Smoke Test',
|
|
85
|
+
'',
|
|
86
|
+
'Run `node --test tests/s01-static-interactions.test.js` and confirm all tests pass.',
|
|
87
|
+
'',
|
|
88
|
+
'## Test Cases',
|
|
89
|
+
'',
|
|
90
|
+
'1. Click the todo row edit control in the DOM harness.',
|
|
91
|
+
'2. Save changed text and reload/recreate the app from persisted localStorage.',
|
|
92
|
+
'3. Expected: the stored record shape remains unchanged.',
|
|
93
|
+
'',
|
|
94
|
+
'## Not Proven By This UAT',
|
|
95
|
+
'',
|
|
96
|
+
'- Final visual polish of edit controls.',
|
|
97
|
+
'- Keyboard usability through a real browser.',
|
|
98
|
+
'- Browser console and local network cleanliness.',
|
|
99
|
+
'',
|
|
100
|
+
'## Notes for Tester',
|
|
101
|
+
'',
|
|
102
|
+
'S02 should capture browser evidence for the full loop rather than changing this persisted model.',
|
|
103
|
+
'',
|
|
104
|
+
].join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
75
107
|
describe('run-uat', () => {
|
|
76
108
|
test('(a) artifact-driven', () => {
|
|
77
109
|
assert.deepStrictEqual(
|
|
@@ -232,8 +264,8 @@ test('(k) run-uat prompt template', () => {
|
|
|
232
264
|
`prompt contains detected dynamic uatType value "${uatType}" after substitution`,
|
|
233
265
|
);
|
|
234
266
|
assert.ok(
|
|
235
|
-
promptResult?.includes(`uatType: ${uatType}`) ?? false,
|
|
236
|
-
`prompt contains dynamic uatType
|
|
267
|
+
promptResult?.includes(`uatType: "${uatType}"`) ?? false,
|
|
268
|
+
`prompt contains dynamic uatType field "${uatType}" after substitution`,
|
|
237
269
|
);
|
|
238
270
|
assert.ok(
|
|
239
271
|
!/\{\{[^}]+\}\}/.test(promptResult ?? ''),
|
|
@@ -249,7 +281,7 @@ test('(k) run-uat prompt template', () => {
|
|
|
249
281
|
);
|
|
250
282
|
});
|
|
251
283
|
|
|
252
|
-
test('(k2) run-uat prompt references
|
|
284
|
+
test('(k2) run-uat prompt references gsd_uat_result_save, not direct write', () => {
|
|
253
285
|
const promptResult = loadPromptFromWorktree('run-uat', {
|
|
254
286
|
workingDirectory: '/tmp/test-project',
|
|
255
287
|
milestoneId: 'M001',
|
|
@@ -261,17 +293,25 @@ test('(k2) run-uat prompt references gsd_summary_save, not direct write', () =>
|
|
|
261
293
|
});
|
|
262
294
|
|
|
263
295
|
assert.ok(
|
|
264
|
-
promptResult.includes('
|
|
265
|
-
'run-uat prompt should reference
|
|
296
|
+
promptResult.includes('gsd_uat_result_save'),
|
|
297
|
+
'run-uat prompt should reference gsd_uat_result_save tool',
|
|
298
|
+
);
|
|
299
|
+
assert.ok(
|
|
300
|
+
promptResult.includes('presentedTools') && promptResult.includes('blockedTools'),
|
|
301
|
+
'run-uat prompt should specify the tool presentation contract',
|
|
266
302
|
);
|
|
267
303
|
assert.ok(
|
|
268
|
-
promptResult.includes('
|
|
269
|
-
'run-uat prompt should
|
|
304
|
+
!promptResult.includes('Call `gsd_summary_save`'),
|
|
305
|
+
'run-uat prompt should not instruct direct summary-save UAT persistence',
|
|
270
306
|
);
|
|
271
307
|
assert.ok(
|
|
272
308
|
!promptResult.includes('MUST write'),
|
|
273
309
|
'run-uat prompt should not instruct direct file write in footer',
|
|
274
310
|
);
|
|
311
|
+
assert.ok(
|
|
312
|
+
!promptResult.includes('Call `gsd_summary_save` with `artifact_type: "ASSESSMENT"`'),
|
|
313
|
+
'run-uat prompt should not instruct the legacy summary-save UAT path',
|
|
314
|
+
);
|
|
275
315
|
});
|
|
276
316
|
|
|
277
317
|
test('(l) dispatch preconditions via resolveSliceFile', () => {
|
|
@@ -482,8 +522,8 @@ test('(n) stale replay guard', async () => {
|
|
|
482
522
|
});
|
|
483
523
|
|
|
484
524
|
test('(q) verdict in ASSESSMENT file skips UAT dispatch (file-based path)', async () => {
|
|
485
|
-
// Regression test for #2644: run-uat
|
|
486
|
-
// S{sid}-ASSESSMENT.md
|
|
525
|
+
// Regression test for #2644: run-uat writes the verdict to
|
|
526
|
+
// S{sid}-ASSESSMENT.md through the structured UAT save path,
|
|
487
527
|
// but checkNeedsRunUat only checked S{sid}-UAT.md — causing a stuck loop.
|
|
488
528
|
const base = createFixtureBase();
|
|
489
529
|
try {
|
|
@@ -679,11 +719,27 @@ test('(u) run-uat prompt promotes artifact-driven browser specs to browser-execu
|
|
|
679
719
|
const prompt = await buildRunUatPrompt('M001', 'S01', uatRel, uatContent, base);
|
|
680
720
|
|
|
681
721
|
assert.match(prompt, /\*\*Detected UAT mode:\*\*\s*`browser-executable`/);
|
|
682
|
-
assert.match(prompt, /uatType: browser-executable/);
|
|
722
|
+
assert.match(prompt, /uatType: "browser-executable"/);
|
|
683
723
|
assert.match(prompt, /use gsd-browser tools/i);
|
|
684
724
|
} finally {
|
|
685
725
|
cleanup(base);
|
|
686
726
|
}
|
|
687
727
|
});
|
|
688
728
|
|
|
729
|
+
test('(v) run-uat prompt keeps deferred browser work artifact-driven', async () => {
|
|
730
|
+
const base = createFixtureBase();
|
|
731
|
+
try {
|
|
732
|
+
const uatRel = '.gsd/milestones/M001/slices/S01/S01-UAT.md';
|
|
733
|
+
const uatContent = makeDeferredBrowserUatContent();
|
|
734
|
+
writeSliceFile(base, 'M001', 'S01', 'UAT', uatContent);
|
|
735
|
+
|
|
736
|
+
const prompt = await buildRunUatPrompt('M001', 'S01', uatRel, uatContent, base);
|
|
737
|
+
|
|
738
|
+
assert.match(prompt, /\*\*Detected UAT mode:\*\*\s*`artifact-driven`/);
|
|
739
|
+
assert.match(prompt, /uatType: "artifact-driven"/);
|
|
740
|
+
assert.doesNotMatch(prompt, /uatType: "browser-executable"/);
|
|
741
|
+
} finally {
|
|
742
|
+
cleanup(base);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
689
745
|
});
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
resetHookState,
|
|
12
12
|
isRetryPending,
|
|
13
13
|
consumeRetryTrigger,
|
|
14
|
+
consumeGateBlock,
|
|
14
15
|
resolveHookArtifactPath,
|
|
15
16
|
runPreDispatchHooks,
|
|
16
17
|
persistHookState,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
formatHookStatus,
|
|
21
22
|
triggerHookManually,
|
|
22
23
|
} from "../post-unit-hooks.ts";
|
|
24
|
+
import { invalidateAllCaches } from "../cache.ts";
|
|
23
25
|
|
|
24
26
|
// ─── Fixture Helpers ───────────────────────────────────────────────────────
|
|
25
27
|
|
|
@@ -29,6 +31,11 @@ function createFixtureBase(): string {
|
|
|
29
31
|
return base;
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
function writeHookPreferences(base: string, hookYaml: string): void {
|
|
35
|
+
writeFileSync(join(base, ".gsd", "PREFERENCES.md"), `---\npost_unit_hooks:\n${hookYaml}\n---\n`, "utf-8");
|
|
36
|
+
invalidateAllCaches();
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
33
40
|
// Phase 1: Post-Unit Hook Tests
|
|
34
41
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -104,6 +111,156 @@ test('consumeRetryTrigger clears state', () => {
|
|
|
104
111
|
assert.ok(!isRetryPending(), "no retry initially");
|
|
105
112
|
});
|
|
106
113
|
|
|
114
|
+
test('Advisory hook keeps artifact idempotency without verdict frontmatter', () => {
|
|
115
|
+
resetHookState();
|
|
116
|
+
const base = createFixtureBase();
|
|
117
|
+
try {
|
|
118
|
+
writeHookPreferences(base, ` - name: docs-hint
|
|
119
|
+
after:
|
|
120
|
+
- execute-task
|
|
121
|
+
prompt: Review docs
|
|
122
|
+
artifact: DOCS-HINT.md
|
|
123
|
+
`);
|
|
124
|
+
writeFileSync(resolveHookArtifactPath(base, "M001/S01/T01", "DOCS-HINT.md"), "plain advisory note", "utf-8");
|
|
125
|
+
|
|
126
|
+
const result = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
|
|
127
|
+
assert.deepStrictEqual(result, null, "existing advisory artifact remains idempotent");
|
|
128
|
+
assert.deepStrictEqual(consumeGateBlock(), null, "advisory hook does not create gate block");
|
|
129
|
+
} finally {
|
|
130
|
+
resetHookState();
|
|
131
|
+
invalidateAllCaches();
|
|
132
|
+
rmSync(base, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('Blocking hook skips only after passing frontmatter verdict', () => {
|
|
137
|
+
resetHookState();
|
|
138
|
+
const base = createFixtureBase();
|
|
139
|
+
try {
|
|
140
|
+
writeHookPreferences(base, ` - name: security-review
|
|
141
|
+
after:
|
|
142
|
+
- execute-task
|
|
143
|
+
prompt: Review security
|
|
144
|
+
artifact: SECURITY-REVIEW.md
|
|
145
|
+
criticality: blocking
|
|
146
|
+
`);
|
|
147
|
+
writeFileSync(
|
|
148
|
+
resolveHookArtifactPath(base, "M001/S01/T01", "SECURITY-REVIEW.md"),
|
|
149
|
+
"---\nverdict: pass\n---\n\nNo blocking findings.\n",
|
|
150
|
+
"utf-8",
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const result = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
|
|
154
|
+
assert.deepStrictEqual(result, null, "passing gate artifact is idempotent");
|
|
155
|
+
assert.deepStrictEqual(consumeGateBlock(), null, "passing gate does not block");
|
|
156
|
+
} finally {
|
|
157
|
+
resetHookState();
|
|
158
|
+
invalidateAllCaches();
|
|
159
|
+
rmSync(base, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('Blocking hook reruns invalid artifact once then blocks at cycle budget', () => {
|
|
164
|
+
resetHookState();
|
|
165
|
+
const base = createFixtureBase();
|
|
166
|
+
try {
|
|
167
|
+
writeHookPreferences(base, ` - name: security-review
|
|
168
|
+
after:
|
|
169
|
+
- execute-task
|
|
170
|
+
prompt: Review security
|
|
171
|
+
artifact: SECURITY-REVIEW.md
|
|
172
|
+
criticality: blocking
|
|
173
|
+
`);
|
|
174
|
+
writeFileSync(resolveHookArtifactPath(base, "M001/S01/T01", "SECURITY-REVIEW.md"), "partial output", "utf-8");
|
|
175
|
+
|
|
176
|
+
const dispatch = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
|
|
177
|
+
assert.ok(dispatch, "invalid gate artifact dispatches the blocking hook");
|
|
178
|
+
assert.equal(dispatch.unitType, "hook/security-review");
|
|
179
|
+
|
|
180
|
+
const afterHook = checkPostUnitHooks("hook/security-review", "M001/S01/T01", base);
|
|
181
|
+
assert.deepStrictEqual(afterHook, null, "no further hook dispatch after max_cycles=1");
|
|
182
|
+
const block = consumeGateBlock();
|
|
183
|
+
assert.ok(block, "gate block is recorded");
|
|
184
|
+
assert.equal(block.hookName, "security-review");
|
|
185
|
+
assert.match(block.reason, /missing frontmatter verdict/);
|
|
186
|
+
} finally {
|
|
187
|
+
resetHookState();
|
|
188
|
+
invalidateAllCaches();
|
|
189
|
+
rmSync(base, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('Blocking hook restored from disk does not trust artifact without clean hook completion', () => {
|
|
194
|
+
resetHookState();
|
|
195
|
+
const base = createFixtureBase();
|
|
196
|
+
try {
|
|
197
|
+
writeHookPreferences(base, ` - name: security-review
|
|
198
|
+
after:
|
|
199
|
+
- execute-task
|
|
200
|
+
prompt: Review security
|
|
201
|
+
artifact: SECURITY-REVIEW.md
|
|
202
|
+
criticality: blocking
|
|
203
|
+
max_cycles: 2
|
|
204
|
+
`);
|
|
205
|
+
const firstDispatch = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
|
|
206
|
+
assert.ok(firstDispatch, "gate dispatches first cycle");
|
|
207
|
+
persistHookState(base);
|
|
208
|
+
|
|
209
|
+
writeFileSync(
|
|
210
|
+
resolveHookArtifactPath(base, "M001/S01/T01", "SECURITY-REVIEW.md"),
|
|
211
|
+
"---\noutcome:\n verdict: pass\n---\n",
|
|
212
|
+
"utf-8",
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
resetHookState();
|
|
216
|
+
restoreHookState(base);
|
|
217
|
+
|
|
218
|
+
const resumed = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
|
|
219
|
+
assert.ok(resumed, "persisted active gate reruns when clean hook completion was not observed");
|
|
220
|
+
assert.equal(resumed.unitType, "hook/security-review");
|
|
221
|
+
} finally {
|
|
222
|
+
resetHookState();
|
|
223
|
+
invalidateAllCaches();
|
|
224
|
+
rmSync(base, { recursive: true, force: true });
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('Blocking hook needs-rework verdict requests trigger unit retry', () => {
|
|
229
|
+
resetHookState();
|
|
230
|
+
const base = createFixtureBase();
|
|
231
|
+
try {
|
|
232
|
+
writeHookPreferences(base, ` - name: review-arbiter
|
|
233
|
+
after:
|
|
234
|
+
- execute-task
|
|
235
|
+
prompt: Review task
|
|
236
|
+
artifact: REVIEW-DEBATE.md
|
|
237
|
+
criticality: blocking
|
|
238
|
+
max_cycles: 2
|
|
239
|
+
on_block:
|
|
240
|
+
action: retry-unit
|
|
241
|
+
`);
|
|
242
|
+
const dispatch = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
|
|
243
|
+
assert.ok(dispatch, "gate dispatches");
|
|
244
|
+
writeFileSync(
|
|
245
|
+
resolveHookArtifactPath(base, "M001/S01/T01", "REVIEW-DEBATE.md"),
|
|
246
|
+
"---\nverdict: needs-rework\n---\n\nRework required.\n",
|
|
247
|
+
"utf-8",
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const afterHook = checkPostUnitHooks("hook/review-arbiter", "M001/S01/T01", base);
|
|
251
|
+
assert.deepStrictEqual(afterHook, null, "needs-rework routes via retry signal");
|
|
252
|
+
assert.ok(isRetryPending(), "retry is pending");
|
|
253
|
+
assert.deepStrictEqual(consumeRetryTrigger(), {
|
|
254
|
+
unitType: "execute-task",
|
|
255
|
+
unitId: "M001/S01/T01",
|
|
256
|
+
});
|
|
257
|
+
} finally {
|
|
258
|
+
resetHookState();
|
|
259
|
+
invalidateAllCaches();
|
|
260
|
+
rmSync(base, { recursive: true, force: true });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
107
264
|
// ─── Variable substitution in prompts ──────────────────────────────────────
|
|
108
265
|
test('Variable substitution', () => {
|
|
109
266
|
const base = "/project";
|