@quinteroac/agents-coding-toolkit 0.1.1-preview.0 → 0.2.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/README.md +29 -15
- package/package.json +2 -1
- package/scaffold/.agents/flow/tmpl_it_000001_progress.example.json +20 -0
- package/scaffold/.agents/skills/execute-refactor-item/tmpl_SKILL.md +5 -5
- package/scaffold/schemas/tmpl_prototype-progress.ts +22 -0
- package/scaffold/schemas/tmpl_test-execution-progress.ts +17 -0
- package/schemas/issues.ts +19 -0
- package/schemas/prototype-progress.ts +22 -0
- package/schemas/test-execution-progress.ts +17 -0
- package/schemas/validate-progress.ts +1 -1
- package/schemas/validate-state.ts +1 -1
- package/src/cli.ts +51 -6
- package/src/commands/approve-prototype.test.ts +427 -0
- package/src/commands/approve-prototype.ts +185 -0
- package/src/commands/create-prototype.test.ts +459 -7
- package/src/commands/create-prototype.ts +168 -56
- package/src/commands/execute-automated-fix.test.ts +78 -33
- package/src/commands/execute-automated-fix.ts +34 -101
- package/src/commands/execute-refactor.test.ts +3 -3
- package/src/commands/execute-refactor.ts +8 -12
- package/src/commands/execute-test-plan.test.ts +20 -19
- package/src/commands/execute-test-plan.ts +19 -52
- package/src/commands/flow-config.ts +79 -0
- package/src/commands/flow.test.ts +755 -0
- package/src/commands/flow.ts +405 -0
- package/src/commands/start-iteration.test.ts +52 -0
- package/src/commands/start-iteration.ts +5 -0
- package/src/flow-cli.test.ts +18 -0
- package/src/guardrail.ts +2 -24
- package/src/progress-utils.ts +34 -0
- package/src/readline.ts +23 -0
- package/src/write-json-artifact.ts +33 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { mkdtemp } from "node:fs/promises";
|
|
6
5
|
|
|
6
|
+
import type { AgentResult } from "../agent";
|
|
7
7
|
import { readState, writeState } from "../state";
|
|
8
8
|
import type { State } from "../../scaffold/schemas/tmpl_state";
|
|
9
|
-
import { runCreatePrototype } from "./create-prototype";
|
|
9
|
+
import { promptForDirtyTreeCommit, runCreatePrototype, runPrePrototypeCommit } from "./create-prototype";
|
|
10
10
|
|
|
11
11
|
async function createProjectRoot(): Promise<string> {
|
|
12
12
|
return mkdtemp(join(tmpdir(), "nvst-create-prototype-"));
|
|
@@ -77,6 +77,22 @@ async function seedPrd(projectRoot: string, iteration: string): Promise<void> {
|
|
|
77
77
|
);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
async function seedProjectContext(projectRoot: string): Promise<void> {
|
|
81
|
+
await mkdir(join(projectRoot, ".agents"), { recursive: true });
|
|
82
|
+
await writeFile(join(projectRoot, ".agents", "PROJECT_CONTEXT.md"), "# Project Context\n", "utf8");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function initGitRepo(projectRoot: string): Promise<void> {
|
|
86
|
+
const { $ } = await import("bun");
|
|
87
|
+
await $`git init`.cwd(projectRoot).nothrow().quiet();
|
|
88
|
+
await $`git config user.email "test@test" && git config user.name "Test"`.cwd(projectRoot).nothrow().quiet();
|
|
89
|
+
await $`git add -A && git commit -m init`.cwd(projectRoot).nothrow().quiet();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function makeAgentResult(exitCode: number): AgentResult {
|
|
93
|
+
return { exitCode, stdout: "", stderr: "" };
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
const createdRoots: string[] = [];
|
|
81
97
|
|
|
82
98
|
afterEach(async () => {
|
|
@@ -112,7 +128,7 @@ describe("create prototype phase validation", () => {
|
|
|
112
128
|
});
|
|
113
129
|
});
|
|
114
130
|
|
|
115
|
-
test("
|
|
131
|
+
test("prompts and returns when dirty during define-to-prototype transition and confirmation is denied", async () => {
|
|
116
132
|
const root = await createProjectRoot();
|
|
117
133
|
createdRoots.push(root);
|
|
118
134
|
const iteration = "000009";
|
|
@@ -130,14 +146,70 @@ describe("create prototype phase validation", () => {
|
|
|
130
146
|
await $`git init`.cwd(root).nothrow().quiet();
|
|
131
147
|
await $`git config user.email "test@test" && git config user.name "Test"`.cwd(root).nothrow().quiet();
|
|
132
148
|
await $`git add -A && git commit -m init`.cwd(root).nothrow().quiet();
|
|
149
|
+
await writeFile(join(root, "dirty.txt"), "dirty\n", "utf8");
|
|
150
|
+
|
|
151
|
+
const prompts: string[] = [];
|
|
152
|
+
const logs: string[] = [];
|
|
133
153
|
|
|
134
154
|
await withCwd(root, async () => {
|
|
135
|
-
await expect(runCreatePrototype(
|
|
136
|
-
|
|
155
|
+
await expect(runCreatePrototype(
|
|
156
|
+
{ provider: "claude" },
|
|
157
|
+
{
|
|
158
|
+
promptDirtyTreeCommitFn: async (question) => {
|
|
159
|
+
prompts.push(question);
|
|
160
|
+
return false;
|
|
161
|
+
},
|
|
162
|
+
logFn: (message) => {
|
|
163
|
+
logs.push(message);
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
)).resolves.toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(prompts).toEqual([
|
|
170
|
+
"Working tree has uncommitted changes. Stage and commit them now to proceed? [y/N]",
|
|
171
|
+
]);
|
|
172
|
+
expect(logs).toContain("Aborted. Commit or discard your changes and re-run `bun nvst create prototype`.");
|
|
173
|
+
|
|
174
|
+
// No transition happens when confirmation is denied.
|
|
175
|
+
const updatedState = await readState(root);
|
|
176
|
+
expect(updatedState.current_phase).toBe("define");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("commits dirty tree on confirmation during define-to-prototype transition and transitions state to prototype", async () => {
|
|
180
|
+
const root = await createProjectRoot();
|
|
181
|
+
createdRoots.push(root);
|
|
182
|
+
const iteration = "000009";
|
|
183
|
+
await seedState(root, makeState({ currentPhase: "define", prdStatus: "completed", projectContextStatus: "created", iteration }));
|
|
184
|
+
await seedPrd(root, iteration);
|
|
185
|
+
await seedProjectContext(root);
|
|
186
|
+
await initGitRepo(root);
|
|
187
|
+
await writeFile(join(root, "dirty.txt"), "dirty\n", "utf8");
|
|
188
|
+
|
|
189
|
+
const commitMessages: string[] = [];
|
|
190
|
+
|
|
191
|
+
await withCwd(root, async () => {
|
|
192
|
+
await runCreatePrototype(
|
|
193
|
+
{ provider: "claude" },
|
|
194
|
+
{
|
|
195
|
+
promptDirtyTreeCommitFn: async (_question) => true,
|
|
196
|
+
gitAddAndCommitFn: async (projectRoot, commitMessage) => {
|
|
197
|
+
commitMessages.push(commitMessage);
|
|
198
|
+
const { $ } = await import("bun");
|
|
199
|
+
await $`git add -A && git commit -m ${commitMessage}`.cwd(projectRoot).nothrow().quiet();
|
|
200
|
+
},
|
|
201
|
+
loadSkillFn: async () => "Implement story",
|
|
202
|
+
invokeAgentFn: async ({ cwd }) => {
|
|
203
|
+
if (!cwd) throw new Error("Expected cwd");
|
|
204
|
+
await writeFile(join(cwd, "story.txt"), "implemented\n", "utf8");
|
|
205
|
+
return makeAgentResult(0);
|
|
206
|
+
},
|
|
207
|
+
checkGhAvailableFn: async () => false,
|
|
208
|
+
},
|
|
137
209
|
);
|
|
138
210
|
});
|
|
139
211
|
|
|
140
|
-
|
|
212
|
+
expect(commitMessages).toEqual(["chore: pre-prototype commit it_000009"]);
|
|
141
213
|
const updatedState = await readState(root);
|
|
142
214
|
expect(updatedState.current_phase).toBe("prototype");
|
|
143
215
|
});
|
|
@@ -168,4 +240,384 @@ describe("create prototype phase validation", () => {
|
|
|
168
240
|
);
|
|
169
241
|
});
|
|
170
242
|
});
|
|
243
|
+
|
|
244
|
+
test("prompts at the post-transition dirty-tree check when phase is already prototype", async () => {
|
|
245
|
+
const root = await createProjectRoot();
|
|
246
|
+
createdRoots.push(root);
|
|
247
|
+
const iteration = "000009";
|
|
248
|
+
await seedState(root, makeState({ currentPhase: "prototype", prdStatus: "completed", projectContextStatus: "created", iteration }));
|
|
249
|
+
await seedPrd(root, iteration);
|
|
250
|
+
await seedProjectContext(root);
|
|
251
|
+
await initGitRepo(root);
|
|
252
|
+
await writeFile(join(root, "dirty.txt"), "dirty\n", "utf8");
|
|
253
|
+
|
|
254
|
+
const prompts: string[] = [];
|
|
255
|
+
const logs: string[] = [];
|
|
256
|
+
let gitAddAndCommitCalled = false;
|
|
257
|
+
const statePath = join(root, ".agents", "state.json");
|
|
258
|
+
const stateBefore = await readFile(statePath, "utf8");
|
|
259
|
+
process.exitCode = 0;
|
|
260
|
+
|
|
261
|
+
await withCwd(root, async () => {
|
|
262
|
+
await expect(runCreatePrototype(
|
|
263
|
+
{ provider: "claude" },
|
|
264
|
+
{
|
|
265
|
+
promptDirtyTreeCommitFn: async (question) => {
|
|
266
|
+
prompts.push(question);
|
|
267
|
+
return false;
|
|
268
|
+
},
|
|
269
|
+
logFn: (message) => {
|
|
270
|
+
logs.push(message);
|
|
271
|
+
},
|
|
272
|
+
gitAddAndCommitFn: async () => {
|
|
273
|
+
gitAddAndCommitCalled = true;
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
)).resolves.toBeUndefined();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(prompts).toEqual([
|
|
280
|
+
"Working tree has uncommitted changes. Stage and commit them now to proceed? [y/N]",
|
|
281
|
+
]);
|
|
282
|
+
expect(logs).toContain("Aborted. Commit or discard your changes and re-run `bun nvst create prototype`.");
|
|
283
|
+
expect(gitAddAndCommitCalled).toBe(false);
|
|
284
|
+
expect(process.exitCode).not.toBe(1);
|
|
285
|
+
const stateAfter = await readFile(statePath, "utf8");
|
|
286
|
+
expect(stateAfter).toBe(stateBefore);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("continues build when dirty-tree prompt is confirmed and calls gitAddAndCommitFn with commit message", async () => {
|
|
290
|
+
const root = await createProjectRoot();
|
|
291
|
+
createdRoots.push(root);
|
|
292
|
+
const iteration = "000018";
|
|
293
|
+
await seedState(root, makeState({ currentPhase: "prototype", prdStatus: "completed", projectContextStatus: "created", iteration }));
|
|
294
|
+
await seedPrd(root, iteration);
|
|
295
|
+
await seedProjectContext(root);
|
|
296
|
+
await initGitRepo(root);
|
|
297
|
+
await writeFile(join(root, "dirty.txt"), "dirty\n", "utf8");
|
|
298
|
+
|
|
299
|
+
const prompts: string[] = [];
|
|
300
|
+
const commitMessages: string[] = [];
|
|
301
|
+
|
|
302
|
+
await withCwd(root, async () => {
|
|
303
|
+
await expect(runCreatePrototype(
|
|
304
|
+
{ provider: "claude" },
|
|
305
|
+
{
|
|
306
|
+
promptDirtyTreeCommitFn: async (question) => {
|
|
307
|
+
prompts.push(question);
|
|
308
|
+
return true;
|
|
309
|
+
},
|
|
310
|
+
gitAddAndCommitFn: async (_projectRoot, commitMessage) => {
|
|
311
|
+
commitMessages.push(commitMessage);
|
|
312
|
+
},
|
|
313
|
+
loadSkillFn: async () => "Implement story",
|
|
314
|
+
invokeAgentFn: async ({ cwd }) => {
|
|
315
|
+
if (!cwd) {
|
|
316
|
+
throw new Error("Expected cwd");
|
|
317
|
+
}
|
|
318
|
+
await writeFile(join(cwd, "story.txt"), "implemented\n", "utf8");
|
|
319
|
+
return makeAgentResult(0);
|
|
320
|
+
},
|
|
321
|
+
checkGhAvailableFn: async () => false,
|
|
322
|
+
},
|
|
323
|
+
)).resolves.toBeUndefined();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
expect(prompts).toEqual([
|
|
327
|
+
"Working tree has uncommitted changes. Stage and commit them now to proceed? [y/N]",
|
|
328
|
+
]);
|
|
329
|
+
expect(commitMessages).toEqual(["chore: pre-prototype commit it_000018"]);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("commits dirty tree on confirmation and proceeds with prototype build", async () => {
|
|
333
|
+
const root = await createProjectRoot();
|
|
334
|
+
createdRoots.push(root);
|
|
335
|
+
const iteration = "000018";
|
|
336
|
+
await seedState(root, makeState({ currentPhase: "prototype", prdStatus: "completed", projectContextStatus: "created", iteration }));
|
|
337
|
+
await seedPrd(root, iteration);
|
|
338
|
+
await seedProjectContext(root);
|
|
339
|
+
await initGitRepo(root);
|
|
340
|
+
await writeFile(join(root, "dirty.txt"), "dirty\n", "utf8");
|
|
341
|
+
|
|
342
|
+
let promptCount = 0;
|
|
343
|
+
|
|
344
|
+
await withCwd(root, async () => {
|
|
345
|
+
await runCreatePrototype(
|
|
346
|
+
{ provider: "claude" },
|
|
347
|
+
{
|
|
348
|
+
promptDirtyTreeCommitFn: async () => {
|
|
349
|
+
promptCount += 1;
|
|
350
|
+
return true;
|
|
351
|
+
},
|
|
352
|
+
loadSkillFn: async () => "Implement story",
|
|
353
|
+
invokeAgentFn: async ({ cwd }) => {
|
|
354
|
+
if (!cwd) {
|
|
355
|
+
throw new Error("Expected cwd");
|
|
356
|
+
}
|
|
357
|
+
await writeFile(join(cwd, "story.txt"), "implemented\n", "utf8");
|
|
358
|
+
return makeAgentResult(0);
|
|
359
|
+
},
|
|
360
|
+
checkGhAvailableFn: async () => false,
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const { $ } = await import("bun");
|
|
366
|
+
const preCommitCountResult = await $`git log --oneline --grep "chore: pre-prototype commit it_000018"`.cwd(root).nothrow().quiet();
|
|
367
|
+
expect(preCommitCountResult.exitCode).toBe(0);
|
|
368
|
+
expect(preCommitCountResult.stdout.toString().trim().length).toBeGreaterThan(0);
|
|
369
|
+
|
|
370
|
+
expect(promptCount).toBe(1);
|
|
371
|
+
|
|
372
|
+
const updatedState = await readState(root);
|
|
373
|
+
expect(updatedState.phases.prototype.prototype_build.status).toBe("created");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("throws pre-prototype commit failure and does not continue build", async () => {
|
|
377
|
+
const root = await createProjectRoot();
|
|
378
|
+
createdRoots.push(root);
|
|
379
|
+
const iteration = "000018";
|
|
380
|
+
await seedState(root, makeState({ currentPhase: "prototype", prdStatus: "completed", projectContextStatus: "created", iteration }));
|
|
381
|
+
await seedPrd(root, iteration);
|
|
382
|
+
await seedProjectContext(root);
|
|
383
|
+
await initGitRepo(root);
|
|
384
|
+
await writeFile(join(root, "dirty.txt"), "dirty\n", "utf8");
|
|
385
|
+
|
|
386
|
+
let agentCalled = false;
|
|
387
|
+
|
|
388
|
+
await withCwd(root, async () => {
|
|
389
|
+
await expect(runCreatePrototype(
|
|
390
|
+
{ provider: "claude" },
|
|
391
|
+
{
|
|
392
|
+
promptDirtyTreeCommitFn: async () => true,
|
|
393
|
+
gitAddAndCommitFn: async () => {
|
|
394
|
+
throw new Error("hook rejected");
|
|
395
|
+
},
|
|
396
|
+
invokeAgentFn: async () => {
|
|
397
|
+
agentCalled = true;
|
|
398
|
+
return makeAgentResult(0);
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
)).rejects.toThrow("Pre-prototype commit failed:\nhook rejected");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
expect(agentCalled).toBe(false);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
describe("promptForDirtyTreeCommit", () => {
|
|
409
|
+
test("returns true for lowercase y and uppercase Y", async () => {
|
|
410
|
+
expect(
|
|
411
|
+
await promptForDirtyTreeCommit(
|
|
412
|
+
"Working tree has uncommitted changes. Stage and commit them now to proceed? [y/N]",
|
|
413
|
+
async () => "y",
|
|
414
|
+
() => {},
|
|
415
|
+
() => true,
|
|
416
|
+
),
|
|
417
|
+
).toBe(true);
|
|
418
|
+
|
|
419
|
+
expect(
|
|
420
|
+
await promptForDirtyTreeCommit(
|
|
421
|
+
"Working tree has uncommitted changes. Stage and commit them now to proceed? [y/N]",
|
|
422
|
+
async () => "Y",
|
|
423
|
+
() => {},
|
|
424
|
+
() => true,
|
|
425
|
+
),
|
|
426
|
+
).toBe(true);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("treats other input, empty input, and no input as no", async () => {
|
|
430
|
+
expect(
|
|
431
|
+
await promptForDirtyTreeCommit(
|
|
432
|
+
"Working tree has uncommitted changes. Stage and commit them now to proceed? [y/N]",
|
|
433
|
+
async () => "n",
|
|
434
|
+
() => {},
|
|
435
|
+
() => true,
|
|
436
|
+
),
|
|
437
|
+
).toBe(false);
|
|
438
|
+
|
|
439
|
+
expect(
|
|
440
|
+
await promptForDirtyTreeCommit(
|
|
441
|
+
"Working tree has uncommitted changes. Stage and commit them now to proceed? [y/N]",
|
|
442
|
+
async () => "",
|
|
443
|
+
() => {},
|
|
444
|
+
() => true,
|
|
445
|
+
),
|
|
446
|
+
).toBe(false);
|
|
447
|
+
|
|
448
|
+
expect(
|
|
449
|
+
await promptForDirtyTreeCommit(
|
|
450
|
+
"Working tree has uncommitted changes. Stage and commit them now to proceed? [y/N]",
|
|
451
|
+
async () => null,
|
|
452
|
+
() => {},
|
|
453
|
+
() => true,
|
|
454
|
+
),
|
|
455
|
+
).toBe(false);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("TC-005: returns false immediately without reading input when stdin is not a TTY", async () => {
|
|
459
|
+
let readLineCalled = false;
|
|
460
|
+
let writeCalled = false;
|
|
461
|
+
|
|
462
|
+
const result = await promptForDirtyTreeCommit(
|
|
463
|
+
"Working tree has uncommitted changes. Stage and commit them now to proceed? [y/N]",
|
|
464
|
+
async () => {
|
|
465
|
+
readLineCalled = true;
|
|
466
|
+
return "y";
|
|
467
|
+
},
|
|
468
|
+
() => {
|
|
469
|
+
writeCalled = true;
|
|
470
|
+
},
|
|
471
|
+
() => false,
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
expect(result).toBe(false);
|
|
475
|
+
expect(readLineCalled).toBe(false);
|
|
476
|
+
expect(writeCalled).toBe(false);
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
describe("runPrePrototypeCommit", () => {
|
|
481
|
+
test("runs git add and creates commit with required message", async () => {
|
|
482
|
+
const root = await createProjectRoot();
|
|
483
|
+
createdRoots.push(root);
|
|
484
|
+
await initGitRepo(root);
|
|
485
|
+
await writeFile(join(root, "dirty.txt"), "dirty\n", "utf8");
|
|
486
|
+
|
|
487
|
+
await runPrePrototypeCommit(root, "000018");
|
|
488
|
+
|
|
489
|
+
const { $ } = await import("bun");
|
|
490
|
+
const commitMsg = await $`git log -1 --pretty=%s`.cwd(root).nothrow().quiet();
|
|
491
|
+
expect(commitMsg.exitCode).toBe(0);
|
|
492
|
+
expect(commitMsg.stdout.toString().trim()).toBe("chore: pre-prototype commit it_000018");
|
|
493
|
+
|
|
494
|
+
const status = await $`git status --porcelain`.cwd(root).nothrow().quiet();
|
|
495
|
+
expect(status.exitCode).toBe(0);
|
|
496
|
+
expect(status.stdout.toString().trim()).toBe("");
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
describe("create prototype gh PR creation", () => {
|
|
501
|
+
test("runs gh pr create with generated title/body when gh is available", async () => {
|
|
502
|
+
const root = await createProjectRoot();
|
|
503
|
+
createdRoots.push(root);
|
|
504
|
+
const iteration = "000016";
|
|
505
|
+
await seedState(root, makeState({ currentPhase: "prototype", projectContextStatus: "created", iteration }));
|
|
506
|
+
await seedPrd(root, iteration);
|
|
507
|
+
await seedProjectContext(root);
|
|
508
|
+
await initGitRepo(root);
|
|
509
|
+
|
|
510
|
+
let prTitle = "";
|
|
511
|
+
let prBody = "";
|
|
512
|
+
|
|
513
|
+
await withCwd(root, async () => {
|
|
514
|
+
await runCreatePrototype(
|
|
515
|
+
{ provider: "claude" },
|
|
516
|
+
{
|
|
517
|
+
promptDirtyTreeCommitFn: async () => true,
|
|
518
|
+
loadSkillFn: async () => "Implement story",
|
|
519
|
+
invokeAgentFn: async ({ cwd }) => {
|
|
520
|
+
if (!cwd) {
|
|
521
|
+
throw new Error("Expected cwd");
|
|
522
|
+
}
|
|
523
|
+
await writeFile(join(cwd, "story.txt"), "implemented\n", "utf8");
|
|
524
|
+
return makeAgentResult(0);
|
|
525
|
+
},
|
|
526
|
+
checkGhAvailableFn: async () => true,
|
|
527
|
+
createPullRequestFn: async (_projectRoot, title, body) => {
|
|
528
|
+
prTitle = title;
|
|
529
|
+
prBody = body;
|
|
530
|
+
return { exitCode: 0, stderr: "" };
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
expect(prTitle).toBe("feat: prototype it_000016");
|
|
537
|
+
expect(prBody).toBe("Prototype for iteration it_000016");
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("logs skip message and exits cleanly when gh is unavailable", async () => {
|
|
541
|
+
const root = await createProjectRoot();
|
|
542
|
+
createdRoots.push(root);
|
|
543
|
+
const iteration = "000016";
|
|
544
|
+
await seedState(root, makeState({ currentPhase: "prototype", projectContextStatus: "created", iteration }));
|
|
545
|
+
await seedPrd(root, iteration);
|
|
546
|
+
await seedProjectContext(root);
|
|
547
|
+
await initGitRepo(root);
|
|
548
|
+
|
|
549
|
+
let createPrCalled = false;
|
|
550
|
+
const logs: string[] = [];
|
|
551
|
+
|
|
552
|
+
await withCwd(root, async () => {
|
|
553
|
+
await runCreatePrototype(
|
|
554
|
+
{ provider: "claude" },
|
|
555
|
+
{
|
|
556
|
+
promptDirtyTreeCommitFn: async () => true,
|
|
557
|
+
loadSkillFn: async () => "Implement story",
|
|
558
|
+
invokeAgentFn: async ({ cwd }) => {
|
|
559
|
+
if (!cwd) {
|
|
560
|
+
throw new Error("Expected cwd");
|
|
561
|
+
}
|
|
562
|
+
await writeFile(join(cwd, "story.txt"), "implemented\n", "utf8");
|
|
563
|
+
return makeAgentResult(0);
|
|
564
|
+
},
|
|
565
|
+
checkGhAvailableFn: async () => false,
|
|
566
|
+
createPullRequestFn: async () => {
|
|
567
|
+
createPrCalled = true;
|
|
568
|
+
return { exitCode: 0, stderr: "" };
|
|
569
|
+
},
|
|
570
|
+
logFn: (message) => {
|
|
571
|
+
logs.push(message);
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
expect(createPrCalled).toBe(false);
|
|
578
|
+
expect(logs).toContain("gh CLI not found — skipping PR creation");
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("treats gh pr create failures as non-fatal warnings and still updates state", async () => {
|
|
582
|
+
const root = await createProjectRoot();
|
|
583
|
+
createdRoots.push(root);
|
|
584
|
+
const iteration = "000016";
|
|
585
|
+
await seedState(root, makeState({ currentPhase: "prototype", projectContextStatus: "created", iteration }));
|
|
586
|
+
await seedPrd(root, iteration);
|
|
587
|
+
await seedProjectContext(root);
|
|
588
|
+
await initGitRepo(root);
|
|
589
|
+
|
|
590
|
+
const warnings: string[] = [];
|
|
591
|
+
|
|
592
|
+
await withCwd(root, async () => {
|
|
593
|
+
await runCreatePrototype(
|
|
594
|
+
{ provider: "claude" },
|
|
595
|
+
{
|
|
596
|
+
promptDirtyTreeCommitFn: async () => true,
|
|
597
|
+
loadSkillFn: async () => "Implement story",
|
|
598
|
+
invokeAgentFn: async ({ cwd }) => {
|
|
599
|
+
if (!cwd) {
|
|
600
|
+
throw new Error("Expected cwd");
|
|
601
|
+
}
|
|
602
|
+
await writeFile(join(cwd, "story.txt"), "implemented\n", "utf8");
|
|
603
|
+
return makeAgentResult(0);
|
|
604
|
+
},
|
|
605
|
+
checkGhAvailableFn: async () => true,
|
|
606
|
+
createPullRequestFn: async () => ({
|
|
607
|
+
exitCode: 1,
|
|
608
|
+
stderr: "a pull request for branch already exists",
|
|
609
|
+
}),
|
|
610
|
+
warnFn: (message) => {
|
|
611
|
+
warnings.push(message);
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
expect(warnings).toHaveLength(1);
|
|
618
|
+
expect(warnings[0]).toContain("gh pr create failed (non-fatal)");
|
|
619
|
+
|
|
620
|
+
const updatedState = await readState(root);
|
|
621
|
+
expect(updatedState.phases.prototype.prototype_build.status).toBe("created");
|
|
622
|
+
});
|
|
171
623
|
});
|