ai-spec-dev 0.31.0 → 0.35.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.
Files changed (70) hide show
  1. package/.claude/commands/add-lesson.md +34 -0
  2. package/.claude/commands/check-layers.md +65 -0
  3. package/.claude/commands/installed-deps.md +35 -0
  4. package/.claude/commands/recall-lessons.md +40 -0
  5. package/.claude/commands/scan-singletons.md +45 -0
  6. package/.claude/commands/verify-imports.md +48 -0
  7. package/.claude/settings.local.json +15 -1
  8. package/README.md +531 -213
  9. package/RELEASE_LOG.md +460 -0
  10. package/cli/commands/config.ts +93 -0
  11. package/cli/commands/create.ts +1233 -0
  12. package/cli/commands/dashboard.ts +62 -0
  13. package/cli/commands/export.ts +66 -0
  14. package/cli/commands/init.ts +190 -0
  15. package/cli/commands/learn.ts +30 -0
  16. package/cli/commands/logs.ts +106 -0
  17. package/cli/commands/mock.ts +175 -0
  18. package/cli/commands/model.ts +156 -0
  19. package/cli/commands/restore.ts +22 -0
  20. package/cli/commands/review.ts +63 -0
  21. package/cli/commands/scan.ts +99 -0
  22. package/cli/commands/trend.ts +36 -0
  23. package/cli/commands/types.ts +69 -0
  24. package/cli/commands/update.ts +178 -0
  25. package/cli/commands/vcr.ts +70 -0
  26. package/cli/commands/workspace.ts +219 -0
  27. package/cli/index.ts +34 -2240
  28. package/cli/utils.ts +83 -0
  29. package/core/combined-generator.ts +13 -3
  30. package/core/dashboard-generator.ts +340 -0
  31. package/core/design-dialogue.ts +124 -0
  32. package/core/dsl-feedback.ts +285 -0
  33. package/core/error-feedback.ts +46 -2
  34. package/core/project-index.ts +301 -0
  35. package/core/reviewer.ts +84 -6
  36. package/core/run-logger.ts +109 -3
  37. package/core/run-trend.ts +261 -0
  38. package/core/self-evaluator.ts +139 -7
  39. package/core/spec-generator.ts +14 -8
  40. package/core/task-generator.ts +17 -0
  41. package/core/types-generator.ts +219 -0
  42. package/core/vcr.ts +210 -0
  43. package/dist/cli/index.js +6692 -4512
  44. package/dist/cli/index.js.map +1 -1
  45. package/dist/cli/index.mjs +6692 -4512
  46. package/dist/cli/index.mjs.map +1 -1
  47. package/dist/index.d.mts +19 -5
  48. package/dist/index.d.ts +19 -5
  49. package/dist/index.js +420 -224
  50. package/dist/index.js.map +1 -1
  51. package/dist/index.mjs +418 -224
  52. package/dist/index.mjs.map +1 -1
  53. package/docs-assets/purpose/architecture-overview.svg +64 -0
  54. package/docs-assets/purpose/create-pipeline.svg +113 -0
  55. package/docs-assets/purpose/task-layering.svg +74 -0
  56. package/package.json +6 -3
  57. package/prompts/codegen.prompt.ts +97 -9
  58. package/prompts/design.prompt.ts +59 -0
  59. package/prompts/spec.prompt.ts +8 -1
  60. package/prompts/tasks.prompt.ts +27 -2
  61. package/purpose.md +600 -174
  62. package/tests/dsl-extractor.test.ts +264 -0
  63. package/tests/dsl-feedback.test.ts +266 -0
  64. package/tests/dsl-validator.test.ts +283 -0
  65. package/tests/error-feedback.test.ts +292 -0
  66. package/tests/provider-utils.test.ts +173 -0
  67. package/tests/run-trend.test.ts +186 -0
  68. package/tests/self-evaluator.test.ts +339 -0
  69. package/tests/spec-assessor.test.ts +142 -0
  70. package/tests/task-generator.test.ts +230 -0
@@ -0,0 +1,283 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { validateDsl } from "../core/dsl-validator";
3
+
4
+ // ─── Fixtures ─────────────────────────────────────────────────────────────────
5
+
6
+ const VALID_DSL = {
7
+ version: "1.0",
8
+ feature: {
9
+ id: "user-login",
10
+ title: "User Login",
11
+ description: "Allows users to authenticate with email and password",
12
+ },
13
+ models: [
14
+ {
15
+ name: "User",
16
+ description: "Application user",
17
+ fields: [
18
+ { name: "id", type: "String", required: true },
19
+ { name: "email", type: "String", required: true, unique: true },
20
+ { name: "password", type: "String", required: true },
21
+ ],
22
+ },
23
+ ],
24
+ endpoints: [
25
+ {
26
+ id: "EP-001",
27
+ method: "POST",
28
+ path: "/api/auth/login",
29
+ description: "Authenticate user and return JWT token",
30
+ auth: false,
31
+ successStatus: 200,
32
+ successDescription: "JWT token returned",
33
+ request: {
34
+ body: { email: "string (email format)", password: "string (min 8 chars)" },
35
+ },
36
+ errors: [
37
+ { status: 400, code: "INVALID_EMAIL", description: "Email format is invalid" },
38
+ { status: 401, code: "INVALID_CREDENTIALS", description: "Password is incorrect" },
39
+ ],
40
+ },
41
+ ],
42
+ behaviors: [
43
+ {
44
+ id: "BHV-001",
45
+ description: "Failed login attempts are rate-limited to 5 per minute",
46
+ },
47
+ ],
48
+ };
49
+
50
+ // ─── Valid DSL ────────────────────────────────────────────────────────────────
51
+
52
+ describe("validateDsl — valid input", () => {
53
+ it("accepts a well-formed DSL", () => {
54
+ const result = validateDsl(VALID_DSL);
55
+ expect(result.valid).toBe(true);
56
+ });
57
+
58
+ it("accepts empty models array", () => {
59
+ const result = validateDsl({ ...VALID_DSL, models: [] });
60
+ expect(result.valid).toBe(true);
61
+ });
62
+
63
+ it("accepts empty endpoints array", () => {
64
+ const result = validateDsl({ ...VALID_DSL, endpoints: [] });
65
+ expect(result.valid).toBe(true);
66
+ });
67
+
68
+ it("accepts empty behaviors array", () => {
69
+ const result = validateDsl({ ...VALID_DSL, behaviors: [] });
70
+ expect(result.valid).toBe(true);
71
+ });
72
+
73
+ it("accepts DSL without optional components field", () => {
74
+ const { ...dsl } = VALID_DSL;
75
+ const result = validateDsl(dsl);
76
+ expect(result.valid).toBe(true);
77
+ });
78
+
79
+ it("returns the typed DSL on success", () => {
80
+ const result = validateDsl(VALID_DSL);
81
+ if (result.valid) {
82
+ expect(result.dsl.feature.id).toBe("user-login");
83
+ expect(result.dsl.endpoints[0].method).toBe("POST");
84
+ }
85
+ });
86
+
87
+ it("accepts all valid HTTP methods", () => {
88
+ for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE"] as const) {
89
+ const ep = { ...VALID_DSL.endpoints[0], method };
90
+ const result = validateDsl({ ...VALID_DSL, endpoints: [ep] });
91
+ expect(result.valid).toBe(true);
92
+ }
93
+ });
94
+ });
95
+
96
+ // ─── Invalid root structure ───────────────────────────────────────────────────
97
+
98
+ describe("validateDsl — root structure errors", () => {
99
+ it("rejects null", () => {
100
+ const result = validateDsl(null);
101
+ expect(result.valid).toBe(false);
102
+ });
103
+
104
+ it("rejects a plain array", () => {
105
+ const result = validateDsl([]);
106
+ expect(result.valid).toBe(false);
107
+ });
108
+
109
+ it("rejects a string", () => {
110
+ const result = validateDsl("not-an-object");
111
+ expect(result.valid).toBe(false);
112
+ });
113
+
114
+ it("rejects wrong version", () => {
115
+ const result = validateDsl({ ...VALID_DSL, version: "2.0" });
116
+ expect(result.valid).toBe(false);
117
+ if (!result.valid) {
118
+ expect(result.errors.some((e) => e.path === "version")).toBe(true);
119
+ }
120
+ });
121
+ });
122
+
123
+ // ─── Feature validation ───────────────────────────────────────────────────────
124
+
125
+ describe("validateDsl — feature validation", () => {
126
+ it("rejects missing feature.id", () => {
127
+ const result = validateDsl({ ...VALID_DSL, feature: { ...VALID_DSL.feature, id: "" } });
128
+ expect(result.valid).toBe(false);
129
+ });
130
+
131
+ it("rejects missing feature.title", () => {
132
+ const result = validateDsl({ ...VALID_DSL, feature: { ...VALID_DSL.feature, title: "" } });
133
+ expect(result.valid).toBe(false);
134
+ });
135
+
136
+ it("rejects missing feature.description", () => {
137
+ const result = validateDsl({ ...VALID_DSL, feature: { ...VALID_DSL.feature, description: "" } });
138
+ expect(result.valid).toBe(false);
139
+ });
140
+
141
+ it("rejects non-object feature", () => {
142
+ const result = validateDsl({ ...VALID_DSL, feature: "login" });
143
+ expect(result.valid).toBe(false);
144
+ });
145
+ });
146
+
147
+ // ─── Endpoint validation ──────────────────────────────────────────────────────
148
+
149
+ describe("validateDsl — endpoint validation", () => {
150
+ it("rejects invalid HTTP method", () => {
151
+ const ep = { ...VALID_DSL.endpoints[0], method: "CONNECT" };
152
+ const result = validateDsl({ ...VALID_DSL, endpoints: [ep] });
153
+ expect(result.valid).toBe(false);
154
+ if (!result.valid) {
155
+ expect(result.errors.some((e) => e.path.includes("method"))).toBe(true);
156
+ }
157
+ });
158
+
159
+ it("rejects path not starting with /", () => {
160
+ const ep = { ...VALID_DSL.endpoints[0], path: "api/login" };
161
+ const result = validateDsl({ ...VALID_DSL, endpoints: [ep] });
162
+ expect(result.valid).toBe(false);
163
+ if (!result.valid) {
164
+ expect(result.errors.some((e) => e.path.includes("path"))).toBe(true);
165
+ }
166
+ });
167
+
168
+ it("rejects non-boolean auth", () => {
169
+ const ep = { ...VALID_DSL.endpoints[0], auth: "yes" };
170
+ const result = validateDsl({ ...VALID_DSL, endpoints: [ep] });
171
+ expect(result.valid).toBe(false);
172
+ if (!result.valid) {
173
+ expect(result.errors.some((e) => e.path.includes("auth"))).toBe(true);
174
+ }
175
+ });
176
+
177
+ it("rejects out-of-range successStatus", () => {
178
+ const ep = { ...VALID_DSL.endpoints[0], successStatus: 99 };
179
+ const result = validateDsl({ ...VALID_DSL, endpoints: [ep] });
180
+ expect(result.valid).toBe(false);
181
+ });
182
+
183
+ it("rejects successStatus > 599", () => {
184
+ const ep = { ...VALID_DSL.endpoints[0], successStatus: 600 };
185
+ const result = validateDsl({ ...VALID_DSL, endpoints: [ep] });
186
+ expect(result.valid).toBe(false);
187
+ });
188
+
189
+ it("rejects missing endpoint description", () => {
190
+ const ep = { ...VALID_DSL.endpoints[0], description: "" };
191
+ const result = validateDsl({ ...VALID_DSL, endpoints: [ep] });
192
+ expect(result.valid).toBe(false);
193
+ });
194
+
195
+ it("rejects non-string FieldMap value", () => {
196
+ const ep = {
197
+ ...VALID_DSL.endpoints[0],
198
+ request: { body: { email: { nested: "object" } } },
199
+ };
200
+ const result = validateDsl({ ...VALID_DSL, endpoints: [ep] });
201
+ expect(result.valid).toBe(false);
202
+ });
203
+
204
+ it("rejects error entry with invalid status code", () => {
205
+ const ep = {
206
+ ...VALID_DSL.endpoints[0],
207
+ errors: [{ status: 999, code: "BAD", description: "Bad" }],
208
+ };
209
+ const result = validateDsl({ ...VALID_DSL, endpoints: [ep] });
210
+ expect(result.valid).toBe(false);
211
+ });
212
+
213
+ it("rejects endpoints array exceeding MAX_ENDPOINTS (100)", () => {
214
+ const eps = Array.from({ length: 101 }, (_, i) => ({
215
+ ...VALID_DSL.endpoints[0],
216
+ id: `EP-${i.toString().padStart(3, "0")}`,
217
+ path: `/api/route-${i}`,
218
+ }));
219
+ const result = validateDsl({ ...VALID_DSL, endpoints: eps });
220
+ expect(result.valid).toBe(false);
221
+ if (!result.valid) {
222
+ expect(result.errors.some((e) => e.path === "endpoints")).toBe(true);
223
+ }
224
+ });
225
+ });
226
+
227
+ // ─── Model validation ─────────────────────────────────────────────────────────
228
+
229
+ describe("validateDsl — model validation", () => {
230
+ it("rejects model with missing name", () => {
231
+ const model = { ...VALID_DSL.models[0], name: "" };
232
+ const result = validateDsl({ ...VALID_DSL, models: [model] });
233
+ expect(result.valid).toBe(false);
234
+ });
235
+
236
+ it("rejects model field with non-boolean required", () => {
237
+ const model = {
238
+ ...VALID_DSL.models[0],
239
+ fields: [{ name: "id", type: "String", required: "yes" }],
240
+ };
241
+ const result = validateDsl({ ...VALID_DSL, models: [model] });
242
+ expect(result.valid).toBe(false);
243
+ });
244
+
245
+ it("rejects model with non-array fields", () => {
246
+ const model = { ...VALID_DSL.models[0], fields: "not-an-array" };
247
+ const result = validateDsl({ ...VALID_DSL, models: [model] });
248
+ expect(result.valid).toBe(false);
249
+ });
250
+
251
+ it("rejects models array exceeding MAX_MODELS (50)", () => {
252
+ const models = Array.from({ length: 51 }, (_, i) => ({
253
+ ...VALID_DSL.models[0],
254
+ name: `Model${i}`,
255
+ }));
256
+ const result = validateDsl({ ...VALID_DSL, models });
257
+ expect(result.valid).toBe(false);
258
+ });
259
+
260
+ it("rejects non-string relation entry", () => {
261
+ const model = { ...VALID_DSL.models[0], relations: [42] };
262
+ const result = validateDsl({ ...VALID_DSL, models: [model] });
263
+ expect(result.valid).toBe(false);
264
+ });
265
+ });
266
+
267
+ // ─── Error collection (all errors in one pass) ───────────────────────────────
268
+
269
+ describe("validateDsl — error accumulation", () => {
270
+ it("collects multiple errors in a single validation run", () => {
271
+ const result = validateDsl({
272
+ version: "2.0", // wrong version
273
+ feature: { id: "", title: "", description: "" }, // all empty
274
+ models: [],
275
+ endpoints: [],
276
+ behaviors: [],
277
+ });
278
+ expect(result.valid).toBe(false);
279
+ if (!result.valid) {
280
+ expect(result.errors.length).toBeGreaterThan(1);
281
+ }
282
+ });
283
+ });
@@ -0,0 +1,292 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import * as fs from "fs-extra";
5
+ import { execSync } from "child_process";
6
+ import { runErrorFeedback } from "../core/error-feedback";
7
+ import type { AIProvider } from "../core/spec-generator";
8
+
9
+ // ─── Module-level mock for child_process ──────────────────────────────────────
10
+ // execSync is non-configurable so it must be mocked at module level, not with spyOn.
11
+
12
+ vi.mock("child_process", () => ({
13
+ execSync: vi.fn(() => Buffer.from("")),
14
+ }));
15
+
16
+ const mockExecSync = vi.mocked(execSync);
17
+
18
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
19
+
20
+ async function makeTempDir(): Promise<string> {
21
+ const dir = path.join(os.tmpdir(), `ai-spec-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
22
+ await fs.mkdirp(dir);
23
+ return dir;
24
+ }
25
+
26
+ const NOOP_PROVIDER: AIProvider = { generate: vi.fn().mockResolvedValue("// fixed") };
27
+
28
+ // ─── All checks skipped ───────────────────────────────────────────────────────
29
+
30
+ describe("runErrorFeedback — all checks skipped", () => {
31
+ it("returns true immediately when skipTests + skipLint + skipBuild are all true", async () => {
32
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
33
+ const result = await runErrorFeedback(NOOP_PROVIDER, os.tmpdir(), null, {
34
+ skipTests: true,
35
+ skipLint: true,
36
+ skipBuild: true,
37
+ });
38
+ spy.mockRestore();
39
+ expect(result).toBe(true);
40
+ });
41
+
42
+ it("does not call execSync when all checks are skipped", async () => {
43
+ mockExecSync.mockClear();
44
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
45
+ await runErrorFeedback(NOOP_PROVIDER, os.tmpdir(), null, {
46
+ skipTests: true,
47
+ skipLint: true,
48
+ skipBuild: true,
49
+ });
50
+ spy.mockRestore();
51
+ expect(mockExecSync).not.toHaveBeenCalled();
52
+ });
53
+ });
54
+
55
+ // ─── Detection: empty directory ───────────────────────────────────────────────
56
+
57
+ describe("runErrorFeedback — empty project directory", () => {
58
+ let tmpDir: string;
59
+
60
+ beforeEach(async () => {
61
+ tmpDir = await makeTempDir();
62
+ mockExecSync.mockClear();
63
+ });
64
+
65
+ afterEach(async () => {
66
+ await fs.remove(tmpDir);
67
+ });
68
+
69
+ it("returns true when no commands can be detected", async () => {
70
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
71
+ const result = await runErrorFeedback(NOOP_PROVIDER, tmpDir);
72
+ spy.mockRestore();
73
+ expect(result).toBe(true);
74
+ expect(mockExecSync).not.toHaveBeenCalled();
75
+ });
76
+ });
77
+
78
+ // ─── Detection: Go project ────────────────────────────────────────────────────
79
+
80
+ describe("runErrorFeedback — Go project detection", () => {
81
+ let tmpDir: string;
82
+
83
+ beforeEach(async () => {
84
+ tmpDir = await makeTempDir();
85
+ await fs.writeFile(path.join(tmpDir, "go.mod"), "module example.com/myapp\n\ngo 1.21\n");
86
+ mockExecSync.mockClear();
87
+ mockExecSync.mockReturnValue(Buffer.from("ok"));
88
+ });
89
+
90
+ afterEach(async () => {
91
+ await fs.remove(tmpDir);
92
+ });
93
+
94
+ it("detects go.mod and runs go test", async () => {
95
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
96
+ await runErrorFeedback(NOOP_PROVIDER, tmpDir, null, {
97
+ skipBuild: true,
98
+ maxCycles: 1,
99
+ });
100
+ spy.mockRestore();
101
+
102
+ const calls = mockExecSync.mock.calls.map((c) => String(c[0]));
103
+ expect(calls.some((cmd) => cmd.includes("go test") || cmd.includes("go vet"))).toBe(true);
104
+ });
105
+ });
106
+
107
+ // ─── Detection: Rust project ──────────────────────────────────────────────────
108
+
109
+ describe("runErrorFeedback — Rust project detection", () => {
110
+ let tmpDir: string;
111
+
112
+ beforeEach(async () => {
113
+ tmpDir = await makeTempDir();
114
+ await fs.writeFile(path.join(tmpDir, "Cargo.toml"), "[package]\nname = \"my-crate\"\n");
115
+ mockExecSync.mockClear();
116
+ mockExecSync.mockReturnValue(Buffer.from("test result: ok"));
117
+ });
118
+
119
+ afterEach(async () => {
120
+ await fs.remove(tmpDir);
121
+ });
122
+
123
+ it("detects Cargo.toml and runs cargo test", async () => {
124
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
125
+ await runErrorFeedback(NOOP_PROVIDER, tmpDir, null, {
126
+ skipBuild: true,
127
+ maxCycles: 1,
128
+ });
129
+ spy.mockRestore();
130
+
131
+ const calls = mockExecSync.mock.calls.map((c) => String(c[0]));
132
+ expect(calls.some((cmd) => cmd.includes("cargo test"))).toBe(true);
133
+ });
134
+ });
135
+
136
+ // ─── Detection: Node.js project ──────────────────────────────────────────────
137
+
138
+ describe("runErrorFeedback — Node.js project detection", () => {
139
+ let tmpDir: string;
140
+
141
+ beforeEach(async () => {
142
+ tmpDir = await makeTempDir();
143
+ mockExecSync.mockClear();
144
+ mockExecSync.mockReturnValue(Buffer.from(""));
145
+ });
146
+
147
+ afterEach(async () => {
148
+ await fs.remove(tmpDir);
149
+ });
150
+
151
+ it("uses npm test when package.json has a test script", async () => {
152
+ await fs.writeJson(path.join(tmpDir, "package.json"), {
153
+ scripts: { test: "vitest run" },
154
+ });
155
+
156
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
157
+ await runErrorFeedback(NOOP_PROVIDER, tmpDir, null, {
158
+ skipLint: true,
159
+ skipBuild: true,
160
+ maxCycles: 1,
161
+ });
162
+ spy.mockRestore();
163
+
164
+ const calls = mockExecSync.mock.calls.map((c) => String(c[0]));
165
+ expect(calls.some((cmd) => cmd === "npm test")).toBe(true);
166
+ });
167
+
168
+ it("uses npx vitest run when vitest.config.ts exists (no test script)", async () => {
169
+ await fs.writeJson(path.join(tmpDir, "package.json"), { scripts: {} });
170
+ await fs.writeFile(path.join(tmpDir, "vitest.config.ts"), "export default {}");
171
+
172
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
173
+ await runErrorFeedback(NOOP_PROVIDER, tmpDir, null, {
174
+ skipLint: true,
175
+ skipBuild: true,
176
+ maxCycles: 1,
177
+ });
178
+ spy.mockRestore();
179
+
180
+ const calls = mockExecSync.mock.calls.map((c) => String(c[0]));
181
+ expect(calls.some((cmd) => cmd.includes("vitest run"))).toBe(true);
182
+ });
183
+
184
+ it("uses npm run lint when package.json has a lint script", async () => {
185
+ await fs.writeJson(path.join(tmpDir, "package.json"), {
186
+ scripts: { lint: "eslint ." },
187
+ });
188
+
189
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
190
+ await runErrorFeedback(NOOP_PROVIDER, tmpDir, null, {
191
+ skipTests: true,
192
+ skipBuild: true,
193
+ maxCycles: 1,
194
+ });
195
+ spy.mockRestore();
196
+
197
+ const calls = mockExecSync.mock.calls.map((c) => String(c[0]));
198
+ expect(calls.some((cmd) => cmd === "npm run lint")).toBe(true);
199
+ });
200
+
201
+ it("uses npx tsc --noEmit when tsconfig.json exists", async () => {
202
+ await fs.writeJson(path.join(tmpDir, "package.json"), { scripts: {} });
203
+ await fs.writeFile(path.join(tmpDir, "tsconfig.json"), "{}");
204
+
205
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
206
+ await runErrorFeedback(NOOP_PROVIDER, tmpDir, null, {
207
+ skipTests: true,
208
+ skipLint: true,
209
+ maxCycles: 1,
210
+ });
211
+ spy.mockRestore();
212
+
213
+ const calls = mockExecSync.mock.calls.map((c) => String(c[0]));
214
+ expect(calls.some((cmd) => cmd.includes("tsc --noEmit"))).toBe(true);
215
+ });
216
+
217
+ it("prefers vue-tsc when vue-tsc is in devDependencies", async () => {
218
+ await fs.writeJson(path.join(tmpDir, "package.json"), {
219
+ scripts: {},
220
+ devDependencies: { "vue-tsc": "^1.0.0" },
221
+ });
222
+ await fs.writeFile(path.join(tmpDir, "tsconfig.json"), "{}");
223
+
224
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
225
+ await runErrorFeedback(NOOP_PROVIDER, tmpDir, null, {
226
+ skipTests: true,
227
+ skipLint: true,
228
+ maxCycles: 1,
229
+ });
230
+ spy.mockRestore();
231
+
232
+ const calls = mockExecSync.mock.calls.map((c) => String(c[0]));
233
+ expect(calls.some((cmd) => cmd.includes("vue-tsc"))).toBe(true);
234
+ });
235
+ });
236
+
237
+ // ─── Pass/fail outcomes ───────────────────────────────────────────────────────
238
+
239
+ describe("runErrorFeedback — pass/fail outcomes", () => {
240
+ let tmpDir: string;
241
+
242
+ beforeEach(async () => {
243
+ tmpDir = await makeTempDir();
244
+ await fs.writeJson(path.join(tmpDir, "package.json"), {
245
+ scripts: { test: "vitest run" },
246
+ });
247
+ mockExecSync.mockClear();
248
+ });
249
+
250
+ afterEach(async () => {
251
+ await fs.remove(tmpDir);
252
+ });
253
+
254
+ it("returns true when all checks pass", async () => {
255
+ mockExecSync.mockReturnValue(Buffer.from("All tests passed"));
256
+
257
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
258
+ const result = await runErrorFeedback(NOOP_PROVIDER, tmpDir, null, {
259
+ skipLint: true,
260
+ skipBuild: true,
261
+ maxCycles: 1,
262
+ });
263
+ spy.mockRestore();
264
+
265
+ expect(result).toBe(true);
266
+ });
267
+
268
+ it("returns false when checks fail after all fix cycles", async () => {
269
+ // Always throw to simulate test failures with a file reference in the output
270
+ mockExecSync.mockImplementation(() => {
271
+ const err: Error & { stdout?: string; stderr?: string } = new Error("Tests failed");
272
+ err.stdout = "src/auth.ts:10:5 - error TS2345: Type mismatch\n";
273
+ throw err;
274
+ });
275
+
276
+ // Create the file so attemptFix can read it
277
+ await fs.mkdirp(path.join(tmpDir, "src"));
278
+ await fs.writeFile(path.join(tmpDir, "src/auth.ts"), "// broken code");
279
+
280
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
281
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
282
+ const result = await runErrorFeedback(NOOP_PROVIDER, tmpDir, null, {
283
+ skipLint: true,
284
+ skipBuild: true,
285
+ maxCycles: 2,
286
+ });
287
+ consoleSpy.mockRestore();
288
+ warnSpy.mockRestore();
289
+
290
+ expect(result).toBe(false);
291
+ });
292
+ });