@nathapp/nax 0.27.0 → 0.28.0
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/CLAUDE.md +38 -8
- package/docs/ROADMAP.md +42 -17
- package/nax/features/prompt-builder/prd.json +152 -0
- package/nax/features/prompt-builder/progress.txt +3 -0
- package/nax/status.json +14 -14
- package/package.json +1 -1
- package/src/cli/config.ts +40 -1
- package/src/cli/prompts.ts +18 -6
- package/src/config/defaults.ts +1 -0
- package/src/config/schemas.ts +10 -0
- package/src/config/types.ts +7 -0
- package/src/pipeline/runner.ts +2 -1
- package/src/pipeline/stages/autofix.ts +5 -0
- package/src/pipeline/stages/execution.ts +5 -0
- package/src/pipeline/stages/prompt.ts +13 -4
- package/src/pipeline/stages/rectify.ts +5 -0
- package/src/pipeline/stages/regression.ts +6 -1
- package/src/pipeline/stages/verify.ts +2 -1
- package/src/pipeline/types.ts +9 -0
- package/src/precheck/checks-warnings.ts +37 -0
- package/src/precheck/checks.ts +1 -0
- package/src/precheck/index.ts +14 -7
- package/src/prompts/builder.ts +178 -0
- package/src/prompts/index.ts +2 -0
- package/src/prompts/loader.ts +43 -0
- package/src/prompts/sections/conventions.ts +15 -0
- package/src/prompts/sections/index.ts +11 -0
- package/src/prompts/sections/isolation.ts +24 -0
- package/src/prompts/sections/role-task.ts +32 -0
- package/src/prompts/sections/story.ts +13 -0
- package/src/prompts/sections/verdict.ts +70 -0
- package/src/prompts/templates/implementer.ts +6 -0
- package/src/prompts/templates/single-session.ts +6 -0
- package/src/prompts/templates/test-writer.ts +6 -0
- package/src/prompts/templates/verifier.ts +6 -0
- package/src/prompts/types.ts +21 -0
- package/src/tdd/orchestrator.ts +11 -1
- package/src/tdd/rectification-gate.ts +18 -13
- package/src/tdd/session-runner.ts +12 -12
- package/src/tdd/types.ts +2 -0
- package/test/integration/cli/cli-config-prompts-explain.test.ts +74 -0
- package/test/integration/prompts/pb-004-migration.test.ts +523 -0
- package/test/unit/precheck/checks-warnings.test.ts +114 -0
- package/test/unit/prompts/builder.test.ts +258 -0
- package/test/unit/prompts/loader.test.ts +355 -0
- package/test/unit/prompts/sections/conventions.test.ts +30 -0
- package/test/unit/prompts/sections/isolation.test.ts +35 -0
- package/test/unit/prompts/sections/role-task.test.ts +40 -0
- package/test/unit/prompts/sections/sections.test.ts +238 -0
- package/test/unit/prompts/sections/story.test.ts +45 -0
- package/test/unit/prompts/sections/verdict.test.ts +58 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PB-004: Migrate call sites to PromptBuilder — integration tests
|
|
3
|
+
*
|
|
4
|
+
* These tests are expected to FAIL until:
|
|
5
|
+
* 1. PromptBuilder gains a .withLoader(workdir, config) method
|
|
6
|
+
* 2. The 6 user-facing prompt functions are replaced with PromptBuilder calls
|
|
7
|
+
* 3. Call sites in session-runner.ts and prompt.ts stage are updated
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
11
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join, dirname } from "node:path";
|
|
14
|
+
import type { NaxConfig } from "../../../src/config/types";
|
|
15
|
+
import { PromptBuilder } from "../../../src/prompts/builder";
|
|
16
|
+
import type { UserStory } from "../../../src/prd";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Fixtures
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
function makeStory(overrides: Partial<UserStory> = {}): UserStory {
|
|
23
|
+
return {
|
|
24
|
+
id: "PB-004",
|
|
25
|
+
title: "Migrate call sites to PromptBuilder",
|
|
26
|
+
description: "Replace 6 user-facing prompt functions with PromptBuilder calls.",
|
|
27
|
+
acceptanceCriteria: [
|
|
28
|
+
"All 6 user-facing prompt functions replaced with PromptBuilder calls",
|
|
29
|
+
"Internal prompts remain unchanged",
|
|
30
|
+
"No regression in generated prompt text",
|
|
31
|
+
],
|
|
32
|
+
tags: [],
|
|
33
|
+
dependencies: [],
|
|
34
|
+
status: "pending",
|
|
35
|
+
passes: false,
|
|
36
|
+
escalations: [],
|
|
37
|
+
attempts: 0,
|
|
38
|
+
...overrides,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeConfig(overrides: Partial<NaxConfig> = {}): NaxConfig {
|
|
43
|
+
return {
|
|
44
|
+
version: 1,
|
|
45
|
+
models: {
|
|
46
|
+
fast: { provider: "anthropic", model: "haiku" },
|
|
47
|
+
balanced: { provider: "anthropic", model: "sonnet" },
|
|
48
|
+
powerful: { provider: "anthropic", model: "opus" },
|
|
49
|
+
},
|
|
50
|
+
autoMode: {
|
|
51
|
+
enabled: true,
|
|
52
|
+
defaultAgent: "claude",
|
|
53
|
+
fallbackOrder: ["claude"],
|
|
54
|
+
complexityRouting: { simple: "fast", medium: "balanced", complex: "powerful", expert: "powerful" },
|
|
55
|
+
escalation: { enabled: true, tierOrder: [{ tier: "fast", attempts: 3 }] },
|
|
56
|
+
},
|
|
57
|
+
routing: { strategy: "keyword" },
|
|
58
|
+
execution: {
|
|
59
|
+
maxIterations: 10,
|
|
60
|
+
iterationDelayMs: 2000,
|
|
61
|
+
costLimit: 5,
|
|
62
|
+
sessionTimeoutSeconds: 600,
|
|
63
|
+
verificationTimeoutSeconds: 300,
|
|
64
|
+
maxStoriesPerFeature: 500,
|
|
65
|
+
rectification: {
|
|
66
|
+
enabled: true,
|
|
67
|
+
maxRetries: 2,
|
|
68
|
+
fullSuiteTimeoutSeconds: 120,
|
|
69
|
+
maxFailureSummaryChars: 2000,
|
|
70
|
+
abortOnIncreasingFailures: true,
|
|
71
|
+
},
|
|
72
|
+
regressionGate: { enabled: true, timeoutSeconds: 120 },
|
|
73
|
+
contextProviderTokenBudget: 2000,
|
|
74
|
+
},
|
|
75
|
+
quality: {
|
|
76
|
+
requireTypecheck: true,
|
|
77
|
+
requireLint: true,
|
|
78
|
+
requireTests: true,
|
|
79
|
+
commands: {},
|
|
80
|
+
forceExit: false,
|
|
81
|
+
detectOpenHandles: true,
|
|
82
|
+
detectOpenHandlesRetries: 1,
|
|
83
|
+
gracePeriodMs: 5000,
|
|
84
|
+
dangerouslySkipPermissions: true,
|
|
85
|
+
drainTimeoutMs: 2000,
|
|
86
|
+
shell: "/bin/sh",
|
|
87
|
+
stripEnvVars: [],
|
|
88
|
+
environmentalEscalationDivisor: 2,
|
|
89
|
+
},
|
|
90
|
+
tdd: {
|
|
91
|
+
maxRetries: 2,
|
|
92
|
+
autoVerifyIsolation: true,
|
|
93
|
+
strategy: "auto",
|
|
94
|
+
autoApproveVerifier: true,
|
|
95
|
+
},
|
|
96
|
+
constitution: { enabled: false, path: "constitution.md", maxTokens: 2000 },
|
|
97
|
+
analyze: { llmEnhanced: false, model: "balanced", fallbackToKeywords: true, maxCodebaseSummaryTokens: 5000 },
|
|
98
|
+
review: { enabled: false, checks: [], commands: {} },
|
|
99
|
+
plan: { model: "balanced", outputPath: "spec.md" },
|
|
100
|
+
acceptance: { enabled: false, maxRetries: 2, generateTests: false, testPath: "acceptance.test.ts" },
|
|
101
|
+
context: {
|
|
102
|
+
testCoverage: {
|
|
103
|
+
enabled: false,
|
|
104
|
+
detail: "names-only",
|
|
105
|
+
maxTokens: 500,
|
|
106
|
+
testPattern: "**/*.test.ts",
|
|
107
|
+
scopeToStory: false,
|
|
108
|
+
},
|
|
109
|
+
autoDetect: { enabled: false, maxFiles: 5, traceImports: false },
|
|
110
|
+
},
|
|
111
|
+
...overrides,
|
|
112
|
+
} as NaxConfig;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let tmpDir: string;
|
|
116
|
+
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
tmpDir = mkdtempSync(join(tmpdir(), "nax-pb004-test-"));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
afterEach(() => {
|
|
122
|
+
try {
|
|
123
|
+
// best-effort cleanup
|
|
124
|
+
Bun.spawnSync(["rm", "-rf", tmpDir]);
|
|
125
|
+
} catch {
|
|
126
|
+
// ignore
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// 1. PromptBuilder.withLoader API — fails until withLoader is implemented
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
describe("PromptBuilder.withLoader(workdir, config)", () => {
|
|
135
|
+
test("withLoader is chainable and returns a PromptBuilder", () => {
|
|
136
|
+
const config = makeConfig();
|
|
137
|
+
// FAILS: withLoader does not exist on PromptBuilder
|
|
138
|
+
const pb = (PromptBuilder.for("test-writer") as any).withLoader(tmpDir, config);
|
|
139
|
+
expect(pb).toBeInstanceOf(PromptBuilder);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("withLoader + no override in config: build succeeds and uses default", async () => {
|
|
143
|
+
const config = makeConfig(); // no prompts.overrides
|
|
144
|
+
const story = makeStory();
|
|
145
|
+
// FAILS: withLoader does not exist on PromptBuilder
|
|
146
|
+
const prompt = await (PromptBuilder.for("test-writer") as any)
|
|
147
|
+
.withLoader(tmpDir, config)
|
|
148
|
+
.story(story)
|
|
149
|
+
.build();
|
|
150
|
+
expect(prompt).toContain(story.title);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("withLoader reads override file when config.prompts.overrides is set", async () => {
|
|
154
|
+
const overrideContent = "# CUSTOM_TEST_WRITER_OVERRIDE\nCustom role body from user override.";
|
|
155
|
+
const relPath = ".nax/prompts/test-writer.md";
|
|
156
|
+
const absPath = join(tmpDir, relPath);
|
|
157
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
158
|
+
writeFileSync(absPath, overrideContent);
|
|
159
|
+
|
|
160
|
+
const config = makeConfig({ prompts: { overrides: { "test-writer": relPath } } });
|
|
161
|
+
const story = makeStory();
|
|
162
|
+
|
|
163
|
+
// FAILS: withLoader does not exist on PromptBuilder
|
|
164
|
+
const prompt = await (PromptBuilder.for("test-writer") as any)
|
|
165
|
+
.withLoader(tmpDir, config)
|
|
166
|
+
.story(story)
|
|
167
|
+
.build();
|
|
168
|
+
|
|
169
|
+
expect(prompt).toContain("CUSTOM_TEST_WRITER_OVERRIDE");
|
|
170
|
+
// Story context (non-overridable) must still appear
|
|
171
|
+
expect(prompt).toContain(story.title);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("withLoader falls back to default when override file is absent", async () => {
|
|
175
|
+
const config = makeConfig({
|
|
176
|
+
prompts: { overrides: { "test-writer": ".nax/prompts/nonexistent.md" } },
|
|
177
|
+
});
|
|
178
|
+
const story = makeStory({ title: "FALLBACK_STORY_TITLE" });
|
|
179
|
+
|
|
180
|
+
// FAILS: withLoader does not exist on PromptBuilder
|
|
181
|
+
const prompt = await (PromptBuilder.for("test-writer") as any)
|
|
182
|
+
.withLoader(tmpDir, config)
|
|
183
|
+
.story(story)
|
|
184
|
+
.build();
|
|
185
|
+
|
|
186
|
+
expect(prompt).toContain("FALLBACK_STORY_TITLE");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// 2. Integration — 6 roles produce semantically correct output (no override)
|
|
192
|
+
// Uses withLoader so it fails until migration is complete
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
describe("Integration: 6 roles with no override — story title and AC present", () => {
|
|
196
|
+
const story = makeStory({
|
|
197
|
+
title: "ROLE_INTEGRATION_TEST_STORY",
|
|
198
|
+
acceptanceCriteria: ["CRITERIA_ONE", "CRITERIA_TWO"],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("test-writer (strict isolation) contains story title and acceptance criteria", async () => {
|
|
202
|
+
const config = makeConfig();
|
|
203
|
+
// FAILS: withLoader does not exist
|
|
204
|
+
const prompt = await (PromptBuilder.for("test-writer", { isolation: "strict" }) as any)
|
|
205
|
+
.withLoader(tmpDir, config)
|
|
206
|
+
.story(story)
|
|
207
|
+
.build();
|
|
208
|
+
|
|
209
|
+
expect(prompt).toContain("ROLE_INTEGRATION_TEST_STORY");
|
|
210
|
+
expect(prompt).toContain("CRITERIA_ONE");
|
|
211
|
+
expect(prompt).toContain("CRITERIA_TWO");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("test-writer (strict) includes test-only isolation instructions", async () => {
|
|
215
|
+
const config = makeConfig();
|
|
216
|
+
// FAILS: withLoader does not exist
|
|
217
|
+
const prompt = await (PromptBuilder.for("test-writer", { isolation: "strict" }) as any)
|
|
218
|
+
.withLoader(tmpDir, config)
|
|
219
|
+
.story(story)
|
|
220
|
+
.build();
|
|
221
|
+
|
|
222
|
+
const lower = prompt.toLowerCase();
|
|
223
|
+
// Must mention writing tests or test/ directory restriction
|
|
224
|
+
const hasTestInstruction =
|
|
225
|
+
lower.includes("test") &&
|
|
226
|
+
(lower.includes("only") || lower.includes("do not") || lower.includes("don't") || lower.includes("src/"));
|
|
227
|
+
expect(hasTestInstruction).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("test-writer (lite) contains story title and acceptance criteria", async () => {
|
|
231
|
+
const config = makeConfig();
|
|
232
|
+
// FAILS: withLoader does not exist
|
|
233
|
+
const prompt = await (PromptBuilder.for("test-writer", { isolation: "lite" }) as any)
|
|
234
|
+
.withLoader(tmpDir, config)
|
|
235
|
+
.story(story)
|
|
236
|
+
.build();
|
|
237
|
+
|
|
238
|
+
expect(prompt).toContain("ROLE_INTEGRATION_TEST_STORY");
|
|
239
|
+
expect(prompt).toContain("CRITERIA_ONE");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("test-writer (lite) mentions allowing src/ reads or stubs", async () => {
|
|
243
|
+
const config = makeConfig();
|
|
244
|
+
// FAILS: withLoader does not exist
|
|
245
|
+
const prompt = await (PromptBuilder.for("test-writer", { isolation: "lite" }) as any)
|
|
246
|
+
.withLoader(tmpDir, config)
|
|
247
|
+
.story(story)
|
|
248
|
+
.build();
|
|
249
|
+
|
|
250
|
+
const lower = prompt.toLowerCase();
|
|
251
|
+
// Lite mode allows reading source files or creating stubs
|
|
252
|
+
const hasLiteInstruction =
|
|
253
|
+
lower.includes("stub") ||
|
|
254
|
+
lower.includes("may read") ||
|
|
255
|
+
lower.includes("read source") ||
|
|
256
|
+
lower.includes("import from source");
|
|
257
|
+
expect(hasLiteInstruction).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("implementer (standard) contains story title and acceptance criteria", async () => {
|
|
261
|
+
const config = makeConfig();
|
|
262
|
+
// FAILS: withLoader does not exist
|
|
263
|
+
const prompt = await (PromptBuilder.for("implementer", { variant: "standard" }) as any)
|
|
264
|
+
.withLoader(tmpDir, config)
|
|
265
|
+
.story(story)
|
|
266
|
+
.build();
|
|
267
|
+
|
|
268
|
+
expect(prompt).toContain("ROLE_INTEGRATION_TEST_STORY");
|
|
269
|
+
expect(prompt).toContain("CRITERIA_ONE");
|
|
270
|
+
expect(prompt).toContain("CRITERIA_TWO");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("implementer (standard) includes implementation instructions", async () => {
|
|
274
|
+
const config = makeConfig();
|
|
275
|
+
// FAILS: withLoader does not exist
|
|
276
|
+
const prompt = await (PromptBuilder.for("implementer", { variant: "standard" }) as any)
|
|
277
|
+
.withLoader(tmpDir, config)
|
|
278
|
+
.story(story)
|
|
279
|
+
.build();
|
|
280
|
+
|
|
281
|
+
const lower = prompt.toLowerCase();
|
|
282
|
+
const hasImplInstruction =
|
|
283
|
+
lower.includes("implement") ||
|
|
284
|
+
lower.includes("make") ||
|
|
285
|
+
lower.includes("pass");
|
|
286
|
+
expect(hasImplInstruction).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("implementer (lite) contains story title and acceptance criteria", async () => {
|
|
290
|
+
const config = makeConfig();
|
|
291
|
+
// FAILS: withLoader does not exist
|
|
292
|
+
const prompt = await (PromptBuilder.for("implementer", { variant: "lite" }) as any)
|
|
293
|
+
.withLoader(tmpDir, config)
|
|
294
|
+
.story(story)
|
|
295
|
+
.build();
|
|
296
|
+
|
|
297
|
+
expect(prompt).toContain("ROLE_INTEGRATION_TEST_STORY");
|
|
298
|
+
expect(prompt).toContain("CRITERIA_ONE");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("implementer (lite) mentions writing tests AND implementing", async () => {
|
|
302
|
+
const config = makeConfig();
|
|
303
|
+
// FAILS: withLoader does not exist
|
|
304
|
+
const prompt = await (PromptBuilder.for("implementer", { variant: "lite" }) as any)
|
|
305
|
+
.withLoader(tmpDir, config)
|
|
306
|
+
.story(story)
|
|
307
|
+
.build();
|
|
308
|
+
|
|
309
|
+
const lower = prompt.toLowerCase();
|
|
310
|
+
const hasTests = lower.includes("test");
|
|
311
|
+
const hasImpl = lower.includes("implement") || lower.includes("feature");
|
|
312
|
+
expect(hasTests && hasImpl).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("verifier contains story title and acceptance criteria", async () => {
|
|
316
|
+
const config = makeConfig();
|
|
317
|
+
// FAILS: withLoader does not exist
|
|
318
|
+
const prompt = await (PromptBuilder.for("verifier") as any)
|
|
319
|
+
.withLoader(tmpDir, config)
|
|
320
|
+
.story(story)
|
|
321
|
+
.build();
|
|
322
|
+
|
|
323
|
+
expect(prompt).toContain("ROLE_INTEGRATION_TEST_STORY");
|
|
324
|
+
expect(prompt).toContain("CRITERIA_ONE");
|
|
325
|
+
expect(prompt).toContain("CRITERIA_TWO");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("verifier includes verification instructions", async () => {
|
|
329
|
+
const config = makeConfig();
|
|
330
|
+
// FAILS: withLoader does not exist
|
|
331
|
+
const prompt = await (PromptBuilder.for("verifier") as any)
|
|
332
|
+
.withLoader(tmpDir, config)
|
|
333
|
+
.story(story)
|
|
334
|
+
.build();
|
|
335
|
+
|
|
336
|
+
const lower = prompt.toLowerCase();
|
|
337
|
+
const hasVerifyInstruction = lower.includes("verify") || lower.includes("check") || lower.includes("ensure");
|
|
338
|
+
expect(hasVerifyInstruction).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("single-session contains story title and acceptance criteria", async () => {
|
|
342
|
+
const config = makeConfig();
|
|
343
|
+
// FAILS: withLoader does not exist
|
|
344
|
+
const prompt = await (PromptBuilder.for("single-session") as any)
|
|
345
|
+
.withLoader(tmpDir, config)
|
|
346
|
+
.story(story)
|
|
347
|
+
.build();
|
|
348
|
+
|
|
349
|
+
expect(prompt).toContain("ROLE_INTEGRATION_TEST_STORY");
|
|
350
|
+
expect(prompt).toContain("CRITERIA_ONE");
|
|
351
|
+
expect(prompt).toContain("CRITERIA_TWO");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("single-session includes both test and implementation instructions", async () => {
|
|
355
|
+
const config = makeConfig();
|
|
356
|
+
// FAILS: withLoader does not exist
|
|
357
|
+
const prompt = await (PromptBuilder.for("single-session") as any)
|
|
358
|
+
.withLoader(tmpDir, config)
|
|
359
|
+
.story(story)
|
|
360
|
+
.build();
|
|
361
|
+
|
|
362
|
+
const lower = prompt.toLowerCase();
|
|
363
|
+
const hasTests = lower.includes("test");
|
|
364
|
+
const hasImpl = lower.includes("implement") || lower.includes("feature");
|
|
365
|
+
expect(hasTests && hasImpl).toBe(true);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// 3. Structural: call sites no longer import the 6 old functions
|
|
371
|
+
// FAILS until migration removes/replaces imports in call sites
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
describe("Structural: call sites migrated away from old prompt functions", () => {
|
|
375
|
+
test("src/tdd/session-runner.ts does not import buildTestWriterPrompt from ./prompts", async () => {
|
|
376
|
+
const source = await Bun.file(
|
|
377
|
+
new URL("../../../src/tdd/session-runner.ts", import.meta.url).pathname,
|
|
378
|
+
).text();
|
|
379
|
+
|
|
380
|
+
// After migration, session-runner should NOT import these old functions
|
|
381
|
+
expect(source).not.toContain("buildTestWriterPrompt");
|
|
382
|
+
expect(source).not.toContain("buildTestWriterLitePrompt");
|
|
383
|
+
expect(source).not.toContain("buildImplementerPrompt");
|
|
384
|
+
expect(source).not.toContain("buildImplementerLitePrompt");
|
|
385
|
+
expect(source).not.toContain("buildVerifierPrompt");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("src/tdd/session-runner.ts imports PromptBuilder after migration", async () => {
|
|
389
|
+
const source = await Bun.file(
|
|
390
|
+
new URL("../../../src/tdd/session-runner.ts", import.meta.url).pathname,
|
|
391
|
+
).text();
|
|
392
|
+
|
|
393
|
+
// After migration, session-runner should use PromptBuilder
|
|
394
|
+
expect(source).toContain("PromptBuilder");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("src/pipeline/stages/prompt.ts does not import buildSingleSessionPrompt after migration", async () => {
|
|
398
|
+
const source = await Bun.file(
|
|
399
|
+
new URL("../../../src/pipeline/stages/prompt.ts", import.meta.url).pathname,
|
|
400
|
+
).text();
|
|
401
|
+
|
|
402
|
+
// After migration, prompt stage should NOT use the old function
|
|
403
|
+
expect(source).not.toContain("buildSingleSessionPrompt");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("src/pipeline/stages/prompt.ts imports PromptBuilder after migration", async () => {
|
|
407
|
+
const source = await Bun.file(
|
|
408
|
+
new URL("../../../src/pipeline/stages/prompt.ts", import.meta.url).pathname,
|
|
409
|
+
).text();
|
|
410
|
+
|
|
411
|
+
// After migration, prompt stage should use PromptBuilder
|
|
412
|
+
expect(source).toContain("PromptBuilder");
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("src/cli/prompts.ts does not dynamically import buildTestWriterPrompt after migration", async () => {
|
|
416
|
+
const source = await Bun.file(
|
|
417
|
+
new URL("../../../src/cli/prompts.ts", import.meta.url).pathname,
|
|
418
|
+
).text();
|
|
419
|
+
|
|
420
|
+
// cli/prompts.ts has a dynamic import of tdd/prompts — after migration it should use PromptBuilder
|
|
421
|
+
expect(source).not.toContain("buildTestWriterPrompt");
|
|
422
|
+
expect(source).not.toContain("buildImplementerPrompt");
|
|
423
|
+
expect(source).not.toContain("buildVerifierPrompt");
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// 4. Internal prompts remain unchanged (regression guard — expected to PASS)
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
describe("Internal prompts: not migrated, still accessible", () => {
|
|
432
|
+
test("buildImplementerRectificationPrompt still exported from src/tdd/prompts", async () => {
|
|
433
|
+
const mod = await import("../../../src/tdd/prompts");
|
|
434
|
+
expect(typeof mod.buildImplementerRectificationPrompt).toBe("function");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("buildRectificationPrompt still exported from src/tdd/prompts", async () => {
|
|
438
|
+
const mod = await import("../../../src/tdd/prompts");
|
|
439
|
+
expect(typeof mod.buildRectificationPrompt).toBe("function");
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("buildBatchPrompt still exported from src/execution/prompts", async () => {
|
|
443
|
+
const mod = await import("../../../src/execution/prompts");
|
|
444
|
+
expect(typeof mod.buildBatchPrompt).toBe("function");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("buildRoutingPrompt still exported from src/routing/strategies/llm-prompts", async () => {
|
|
448
|
+
const mod = await import("../../../src/routing/strategies/llm-prompts");
|
|
449
|
+
expect(typeof mod.buildRoutingPrompt).toBe("function");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("buildBatchPrompt still exported from src/routing/strategies/llm-prompts", async () => {
|
|
453
|
+
const mod = await import("../../../src/routing/strategies/llm-prompts");
|
|
454
|
+
expect(typeof mod.buildBatchPrompt).toBe("function");
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
// 5. withLoader override: context passed through correctly
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
describe("PromptBuilder.withLoader override content integration", () => {
|
|
463
|
+
test("override for implementer role replaces role body", async () => {
|
|
464
|
+
const overrideBody = "IMPLEMENTER_CUSTOM_ROLE_BODY_MARKER";
|
|
465
|
+
const relPath = ".nax/prompts/implementer.md";
|
|
466
|
+
const absPath = join(tmpDir, relPath);
|
|
467
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
468
|
+
writeFileSync(absPath, overrideBody);
|
|
469
|
+
|
|
470
|
+
const config = makeConfig({ prompts: { overrides: { implementer: relPath } } });
|
|
471
|
+
const story = makeStory({ title: "OVERRIDE_STORY_TITLE" });
|
|
472
|
+
|
|
473
|
+
// FAILS: withLoader does not exist
|
|
474
|
+
const prompt = await (PromptBuilder.for("implementer", { variant: "standard" }) as any)
|
|
475
|
+
.withLoader(tmpDir, config)
|
|
476
|
+
.story(story)
|
|
477
|
+
.build();
|
|
478
|
+
|
|
479
|
+
expect(prompt).toContain(overrideBody);
|
|
480
|
+
// Story context still present (non-overridable)
|
|
481
|
+
expect(prompt).toContain("OVERRIDE_STORY_TITLE");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("override for verifier role replaces role body", async () => {
|
|
485
|
+
const overrideBody = "VERIFIER_CUSTOM_ROLE_BODY_MARKER";
|
|
486
|
+
const relPath = ".nax/prompts/verifier.md";
|
|
487
|
+
const absPath = join(tmpDir, relPath);
|
|
488
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
489
|
+
writeFileSync(absPath, overrideBody);
|
|
490
|
+
|
|
491
|
+
const config = makeConfig({ prompts: { overrides: { verifier: relPath } } });
|
|
492
|
+
const story = makeStory({ title: "VERIFIER_OVERRIDE_TITLE" });
|
|
493
|
+
|
|
494
|
+
// FAILS: withLoader does not exist
|
|
495
|
+
const prompt = await (PromptBuilder.for("verifier") as any)
|
|
496
|
+
.withLoader(tmpDir, config)
|
|
497
|
+
.story(story)
|
|
498
|
+
.build();
|
|
499
|
+
|
|
500
|
+
expect(prompt).toContain(overrideBody);
|
|
501
|
+
expect(prompt).toContain("VERIFIER_OVERRIDE_TITLE");
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("override for single-session role replaces role body", async () => {
|
|
505
|
+
const overrideBody = "SINGLE_SESSION_CUSTOM_ROLE_BODY_MARKER";
|
|
506
|
+
const relPath = ".nax/prompts/single-session.md";
|
|
507
|
+
const absPath = join(tmpDir, relPath);
|
|
508
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
509
|
+
writeFileSync(absPath, overrideBody);
|
|
510
|
+
|
|
511
|
+
const config = makeConfig({ prompts: { overrides: { "single-session": relPath } } });
|
|
512
|
+
const story = makeStory({ title: "SINGLE_SESSION_OVERRIDE_TITLE" });
|
|
513
|
+
|
|
514
|
+
// FAILS: withLoader does not exist
|
|
515
|
+
const prompt = await (PromptBuilder.for("single-session") as any)
|
|
516
|
+
.withLoader(tmpDir, config)
|
|
517
|
+
.story(story)
|
|
518
|
+
.build();
|
|
519
|
+
|
|
520
|
+
expect(prompt).toContain(overrideBody);
|
|
521
|
+
expect(prompt).toContain("SINGLE_SESSION_OVERRIDE_TITLE");
|
|
522
|
+
});
|
|
523
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for checks-warnings.ts — prompt override file checks (PB-005)
|
|
3
|
+
*
|
|
4
|
+
* Tests the new checkPromptOverrideFiles check which warns when a configured
|
|
5
|
+
* override file path does not exist. Non-blocking: run continues regardless.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdtempSync, writeFileSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
+
import type { NaxConfig } from "../../../src/config/types";
|
|
13
|
+
import { checkPromptOverrideFiles } from "../../../src/precheck/checks-warnings";
|
|
14
|
+
|
|
15
|
+
function makeTmpDir(): string {
|
|
16
|
+
return mkdtempSync(join(tmpdir(), "nax-test-"));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makeMinimalConfig(overrides?: Record<string, string>): NaxConfig {
|
|
20
|
+
return {
|
|
21
|
+
prompts: overrides ? { overrides } : undefined,
|
|
22
|
+
} as unknown as NaxConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("checkPromptOverrideFiles", () => {
|
|
26
|
+
let workdir: string;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
workdir = makeTmpDir();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("no warning when config.prompts is absent", async () => {
|
|
33
|
+
const config = makeMinimalConfig(undefined);
|
|
34
|
+
const checks = await checkPromptOverrideFiles(config, workdir);
|
|
35
|
+
expect(checks).toHaveLength(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("no warning when config.prompts.overrides is empty", async () => {
|
|
39
|
+
const config = makeMinimalConfig({});
|
|
40
|
+
const checks = await checkPromptOverrideFiles(config, workdir);
|
|
41
|
+
expect(checks).toHaveLength(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("no warning when override file exists", async () => {
|
|
45
|
+
// Create the override file
|
|
46
|
+
const promptsDir = join(workdir, ".nax", "prompts");
|
|
47
|
+
mkdirSync(promptsDir, { recursive: true });
|
|
48
|
+
const filePath = join(promptsDir, "test-writer.md");
|
|
49
|
+
writeFileSync(filePath, "# Test Writer Prompt");
|
|
50
|
+
|
|
51
|
+
const config = makeMinimalConfig({
|
|
52
|
+
"test-writer": ".nax/prompts/test-writer.md",
|
|
53
|
+
});
|
|
54
|
+
const checks = await checkPromptOverrideFiles(config, workdir);
|
|
55
|
+
expect(checks).toHaveLength(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("emits warning when override file is missing", async () => {
|
|
59
|
+
const config = makeMinimalConfig({
|
|
60
|
+
"test-writer": ".nax/prompts/test-writer.md",
|
|
61
|
+
});
|
|
62
|
+
const checks = await checkPromptOverrideFiles(config, workdir);
|
|
63
|
+
|
|
64
|
+
expect(checks).toHaveLength(1);
|
|
65
|
+
expect(checks[0].tier).toBe("warning");
|
|
66
|
+
expect(checks[0].passed).toBe(false);
|
|
67
|
+
expect(checks[0].message).toContain("test-writer");
|
|
68
|
+
expect(checks[0].message).toContain("test-writer.md");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("warning message contains resolved absolute path", async () => {
|
|
72
|
+
const config = makeMinimalConfig({
|
|
73
|
+
"implementer": ".nax/prompts/implementer.md",
|
|
74
|
+
});
|
|
75
|
+
const checks = await checkPromptOverrideFiles(config, workdir);
|
|
76
|
+
|
|
77
|
+
expect(checks[0].message).toContain(workdir);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("emits one warning per missing role", async () => {
|
|
81
|
+
const config = makeMinimalConfig({
|
|
82
|
+
"test-writer": ".nax/prompts/test-writer.md",
|
|
83
|
+
"implementer": ".nax/prompts/implementer.md",
|
|
84
|
+
});
|
|
85
|
+
const checks = await checkPromptOverrideFiles(config, workdir);
|
|
86
|
+
|
|
87
|
+
expect(checks).toHaveLength(2);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("only warns for missing files, not existing ones", async () => {
|
|
91
|
+
const promptsDir = join(workdir, ".nax", "prompts");
|
|
92
|
+
mkdirSync(promptsDir, { recursive: true });
|
|
93
|
+
writeFileSync(join(promptsDir, "test-writer.md"), "# exists");
|
|
94
|
+
|
|
95
|
+
const config = makeMinimalConfig({
|
|
96
|
+
"test-writer": ".nax/prompts/test-writer.md",
|
|
97
|
+
"implementer": ".nax/prompts/implementer.md", // does not exist
|
|
98
|
+
});
|
|
99
|
+
const checks = await checkPromptOverrideFiles(config, workdir);
|
|
100
|
+
|
|
101
|
+
expect(checks).toHaveLength(1);
|
|
102
|
+
expect(checks[0].message).toContain("implementer");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("warning check name identifies the role", async () => {
|
|
106
|
+
const config = makeMinimalConfig({
|
|
107
|
+
"verifier": ".nax/prompts/verifier.md",
|
|
108
|
+
});
|
|
109
|
+
const checks = await checkPromptOverrideFiles(config, workdir);
|
|
110
|
+
|
|
111
|
+
expect(checks[0].name).toContain("prompt-override");
|
|
112
|
+
expect(checks[0].name).toContain("verifier");
|
|
113
|
+
});
|
|
114
|
+
});
|