swallowkit 1.0.0-beta.55 → 1.0.0-beta.57

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 (141) hide show
  1. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  2. package/dist/cli/commands/add-auth.js +154 -157
  3. package/dist/cli/commands/add-auth.js.map +1 -1
  4. package/dist/cli/commands/init.d.ts +5 -0
  5. package/dist/cli/commands/init.d.ts.map +1 -1
  6. package/dist/cli/commands/init.js +178 -25
  7. package/dist/cli/commands/init.js.map +1 -1
  8. package/dist/cli/commands/scaffold.d.ts +7 -2
  9. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  10. package/dist/cli/commands/scaffold.js +113 -123
  11. package/dist/cli/commands/scaffold.js.map +1 -1
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +127 -2
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/core/config.d.ts +1 -1
  16. package/dist/core/config.d.ts.map +1 -1
  17. package/dist/core/config.js +8 -4
  18. package/dist/core/config.js.map +1 -1
  19. package/dist/core/operations/auth-operations.d.ts +57 -0
  20. package/dist/core/operations/auth-operations.d.ts.map +1 -0
  21. package/dist/core/operations/auth-operations.js +269 -0
  22. package/dist/core/operations/auth-operations.js.map +1 -0
  23. package/dist/core/operations/create-model.d.ts.map +1 -1
  24. package/dist/core/operations/create-model.js +23 -13
  25. package/dist/core/operations/create-model.js.map +1 -1
  26. package/dist/core/operations/file-session.d.ts +56 -0
  27. package/dist/core/operations/file-session.d.ts.map +1 -0
  28. package/dist/core/operations/file-session.js +203 -0
  29. package/dist/core/operations/file-session.js.map +1 -0
  30. package/dist/core/operations/provision-operations.d.ts +52 -0
  31. package/dist/core/operations/provision-operations.d.ts.map +1 -0
  32. package/dist/core/operations/provision-operations.js +211 -0
  33. package/dist/core/operations/provision-operations.js.map +1 -0
  34. package/dist/core/operations/runtime.d.ts +5 -0
  35. package/dist/core/operations/runtime.d.ts.map +1 -1
  36. package/dist/core/operations/runtime.js +23 -0
  37. package/dist/core/operations/runtime.js.map +1 -1
  38. package/dist/core/operations/scaffold-apply.d.ts +21 -0
  39. package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
  40. package/dist/core/operations/scaffold-apply.js +57 -0
  41. package/dist/core/operations/scaffold-apply.js.map +1 -0
  42. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  43. package/dist/core/operations/scaffold-machine.js +1 -13
  44. package/dist/core/operations/scaffold-machine.js.map +1 -1
  45. package/dist/core/operations/scaffold-plan.d.ts +43 -0
  46. package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
  47. package/dist/core/operations/scaffold-plan.js +187 -0
  48. package/dist/core/operations/scaffold-plan.js.map +1 -0
  49. package/dist/core/project/artifacts.d.ts +58 -0
  50. package/dist/core/project/artifacts.d.ts.map +1 -0
  51. package/dist/core/project/artifacts.js +179 -0
  52. package/dist/core/project/artifacts.js.map +1 -0
  53. package/dist/core/project/boundaries.d.ts +50 -0
  54. package/dist/core/project/boundaries.d.ts.map +1 -0
  55. package/dist/core/project/boundaries.js +117 -0
  56. package/dist/core/project/boundaries.js.map +1 -0
  57. package/dist/core/project/drift.d.ts +29 -0
  58. package/dist/core/project/drift.d.ts.map +1 -0
  59. package/dist/core/project/drift.js +188 -0
  60. package/dist/core/project/drift.js.map +1 -0
  61. package/dist/core/project/infra.d.ts +49 -0
  62. package/dist/core/project/infra.d.ts.map +1 -0
  63. package/dist/core/project/infra.js +167 -0
  64. package/dist/core/project/infra.js.map +1 -0
  65. package/dist/core/project/state.d.ts +15 -0
  66. package/dist/core/project/state.d.ts.map +1 -0
  67. package/dist/core/project/state.js +106 -0
  68. package/dist/core/project/state.js.map +1 -0
  69. package/dist/core/project/workflows.d.ts +43 -0
  70. package/dist/core/project/workflows.d.ts.map +1 -0
  71. package/dist/core/project/workflows.js +255 -0
  72. package/dist/core/project/workflows.js.map +1 -0
  73. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  74. package/dist/core/scaffold/auth-generator.js +24 -27
  75. package/dist/core/scaffold/auth-generator.js.map +1 -1
  76. package/dist/core/scaffold/functions-generator.js +9 -9
  77. package/dist/core/scaffold/native-schema-generator.d.ts +7 -1
  78. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  79. package/dist/core/scaffold/native-schema-generator.js +104 -47
  80. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  81. package/dist/core/scaffold/nextjs-generator.js +6 -6
  82. package/dist/core/scaffold/ui-generator.js +19 -19
  83. package/dist/core/scaffold/ui-generator.js.map +1 -1
  84. package/dist/core/verify/index.d.ts +62 -0
  85. package/dist/core/verify/index.d.ts.map +1 -0
  86. package/dist/core/verify/index.js +343 -0
  87. package/dist/core/verify/index.js.map +1 -0
  88. package/dist/machine/contracts.d.ts +8 -0
  89. package/dist/machine/contracts.d.ts.map +1 -1
  90. package/dist/machine/errors.d.ts +4 -1
  91. package/dist/machine/errors.d.ts.map +1 -1
  92. package/dist/machine/errors.js +9 -1
  93. package/dist/machine/errors.js.map +1 -1
  94. package/dist/machine/index.d.ts.map +1 -1
  95. package/dist/machine/index.js +350 -16
  96. package/dist/machine/index.js.map +1 -1
  97. package/dist/mcp/index.d.ts.map +1 -1
  98. package/dist/mcp/index.js +202 -1
  99. package/dist/mcp/index.js.map +1 -1
  100. package/dist/types/index.d.ts +14 -0
  101. package/dist/types/index.d.ts.map +1 -1
  102. package/package.json +1 -1
  103. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +17 -17
  104. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +6 -6
  105. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +11 -11
  106. package/src/__tests__/agent-loop-phase34.test.ts +449 -0
  107. package/src/__tests__/file-session.test.ts +98 -0
  108. package/src/__tests__/init.test.ts +50 -0
  109. package/src/__tests__/mcp.test.ts +113 -5
  110. package/src/__tests__/plan-apply.test.ts +461 -0
  111. package/src/__tests__/scaffold.test.ts +2 -2
  112. package/src/cli/commands/add-auth.ts +164 -164
  113. package/src/cli/commands/init.ts +186 -24
  114. package/src/cli/commands/scaffold.ts +136 -130
  115. package/src/cli/index.ts +148 -2
  116. package/src/core/config.ts +5 -4
  117. package/src/core/operations/auth-operations.ts +353 -0
  118. package/src/core/operations/create-model.ts +25 -13
  119. package/src/core/operations/file-session.ts +222 -0
  120. package/src/core/operations/provision-operations.ts +297 -0
  121. package/src/core/operations/runtime.ts +23 -0
  122. package/src/core/operations/scaffold-apply.ts +106 -0
  123. package/src/core/operations/scaffold-machine.ts +2 -16
  124. package/src/core/operations/scaffold-plan.ts +215 -0
  125. package/src/core/project/artifacts.ts +205 -0
  126. package/src/core/project/boundaries.ts +170 -0
  127. package/src/core/project/drift.ts +202 -0
  128. package/src/core/project/infra.ts +196 -0
  129. package/src/core/project/state.ts +77 -0
  130. package/src/core/project/workflows.ts +249 -0
  131. package/src/core/scaffold/auth-generator.ts +24 -27
  132. package/src/core/scaffold/functions-generator.ts +9 -9
  133. package/src/core/scaffold/native-schema-generator.ts +152 -70
  134. package/src/core/scaffold/nextjs-generator.ts +6 -6
  135. package/src/core/scaffold/ui-generator.ts +19 -19
  136. package/src/core/verify/index.ts +392 -0
  137. package/src/machine/contracts.ts +16 -0
  138. package/src/machine/errors.ts +12 -1
  139. package/src/machine/index.ts +444 -15
  140. package/src/mcp/index.ts +198 -2
  141. package/src/types/index.ts +17 -0
@@ -81,6 +81,110 @@ describe("SwallowKit MCP tool definitions", () => {
81
81
  expect(JSON.parse(result.content[0].text)).toEqual({ createdFiles: ["functions/src/todo.ts"] });
82
82
  });
83
83
 
84
+ it("exposes the agent-loop tools (plan/apply/artifacts/drift/verify/explain)", () => {
85
+ const names = buildSwallowKitToolDefinitions(jest.fn() as unknown as MachineCliRunner).map((tool) => tool.name);
86
+ expect(names).toEqual(
87
+ expect.arrayContaining([
88
+ "swallowkit_plan_scaffold",
89
+ "swallowkit_apply_scaffold",
90
+ "swallowkit_inspect_artifacts",
91
+ "swallowkit_inspect_drift",
92
+ "swallowkit_verify_project",
93
+ "swallowkit_explain_failure",
94
+ ])
95
+ );
96
+ });
97
+
98
+ it("delegates plan_scaffold and passes through status and nextActions", async () => {
99
+ const runner = jest.fn(async () => ({
100
+ stdout: JSON.stringify({
101
+ ok: true,
102
+ command: "plan-scaffold",
103
+ status: "requires-human",
104
+ nextActions: [{ command: "swallowkit machine apply scaffold --plan abc --approve", description: "Apply." }],
105
+ data: { planId: "abc", requiresApproval: true, operations: [] },
106
+ }),
107
+ stderr: "",
108
+ exitCode: 0,
109
+ }));
110
+
111
+ const tool = buildSwallowKitToolDefinitions(runner as MachineCliRunner).find(
112
+ (candidate) => candidate.name === "swallowkit_plan_scaffold"
113
+ );
114
+ const result = await tool!.handler({ model: "todo", apiOnly: true });
115
+
116
+ expect(runner).toHaveBeenCalledWith(["plan", "scaffold", "todo", "--api-only"]);
117
+ const parsed = JSON.parse(result.content[0].text);
118
+ expect(parsed.status).toBe("requires-human");
119
+ expect(parsed.planId).toBe("abc");
120
+ expect(parsed.nextActions).toHaveLength(1);
121
+ });
122
+
123
+ it("delegates apply_scaffold with plan and approve flags", async () => {
124
+ const runner = jest.fn(async () => ({
125
+ stdout: JSON.stringify({
126
+ ok: true,
127
+ command: "apply-scaffold",
128
+ status: "complete",
129
+ data: { createdFiles: [] },
130
+ }),
131
+ stderr: "",
132
+ exitCode: 0,
133
+ }));
134
+
135
+ const tool = buildSwallowKitToolDefinitions(runner as MachineCliRunner).find(
136
+ (candidate) => candidate.name === "swallowkit_apply_scaffold"
137
+ );
138
+ await tool!.handler({ planId: "abc", approve: true });
139
+
140
+ expect(runner).toHaveBeenCalledWith(["apply", "scaffold", "--plan", "abc", "--approve"]);
141
+ });
142
+
143
+ it("surfaces machine error code, status, and details to the agent", async () => {
144
+ const runner = jest.fn(async () => ({
145
+ stdout: JSON.stringify({
146
+ ok: false,
147
+ command: "apply-scaffold",
148
+ status: "blocked",
149
+ error: {
150
+ code: "stale-plan",
151
+ message: "Plan is stale.",
152
+ details: { changedFiles: ["shared/models/todo.ts"] },
153
+ },
154
+ }),
155
+ stderr: "",
156
+ exitCode: 1,
157
+ }));
158
+
159
+ const tool = buildSwallowKitToolDefinitions(runner as MachineCliRunner).find(
160
+ (candidate) => candidate.name === "swallowkit_apply_scaffold"
161
+ );
162
+
163
+ await expect(tool!.handler({ planId: "abc" })).rejects.toThrow(
164
+ /\[stale-plan\] Plan is stale\. \(status: blocked\)[\s\S]*changedFiles/
165
+ );
166
+ });
167
+
168
+ it("delegates verify_project with selected checks", async () => {
169
+ const runner = jest.fn(async () => ({
170
+ stdout: JSON.stringify({
171
+ ok: true,
172
+ command: "verify-project",
173
+ status: "complete",
174
+ data: { summary: { done: true } },
175
+ }),
176
+ stderr: "",
177
+ exitCode: 0,
178
+ }));
179
+
180
+ const tool = buildSwallowKitToolDefinitions(runner as MachineCliRunner).find(
181
+ (candidate) => candidate.name === "swallowkit_verify_project"
182
+ );
183
+ await tool!.handler({ checks: ["structure", "drift"] });
184
+
185
+ expect(runner).toHaveBeenCalledWith(["verify", "project", "--checks", "structure,drift"]);
186
+ });
187
+
84
188
  itWhenBuiltEntrypointExists("keeps the built MCP entrypoint alive long enough to complete the handshake", async () => {
85
189
  await new Promise<void>((resolve, reject) => {
86
190
  const child = spawn(process.execPath, [builtMcpEntrypoint], {
@@ -105,13 +209,17 @@ describe("SwallowKit MCP tool definitions", () => {
105
209
  }
106
210
  };
107
211
 
108
- const timer = setTimeout(() => {
109
- expectedShutdown = true;
110
- child.kill();
111
- }, 1000);
212
+ let timer: NodeJS.Timeout | undefined;
112
213
 
113
214
  child.stderr.on("data", (chunk) => {
114
215
  stderr += chunk.toString();
216
+ // Start the aliveness window once the entrypoint has actually booted.
217
+ if (!timer && /\[swallowkit-mcp\] version: /.test(stderr)) {
218
+ timer = setTimeout(() => {
219
+ expectedShutdown = true;
220
+ child.kill();
221
+ }, 1000);
222
+ }
115
223
  });
116
224
 
117
225
  child.on("error", (error) => {
@@ -135,7 +243,7 @@ describe("SwallowKit MCP tool definitions", () => {
135
243
  );
136
244
  });
137
245
  });
138
- });
246
+ }, 20000);
139
247
 
140
248
  itWhenBuiltEntrypointExists("preserves a runtime dynamic execa import in the built MCP entrypoint", () => {
141
249
  const source = fs.readFileSync(builtMcpEntrypoint, "utf8");
@@ -0,0 +1,461 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import * as childProcess from "child_process";
5
+ import { EventEmitter } from "events";
6
+ import { runMachineCli } from "../machine";
7
+
8
+ jest.mock("child_process", () => {
9
+ const actual = jest.requireActual<typeof import("child_process")>("child_process");
10
+ return {
11
+ ...actual,
12
+ spawn: jest.fn(),
13
+ spawnSync: jest.fn(),
14
+ };
15
+ });
16
+
17
+ const repoRoot = process.cwd();
18
+
19
+ function writeFile(filePath: string, content: string): void {
20
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
21
+ fs.writeFileSync(filePath, content, "utf-8");
22
+ }
23
+
24
+ function createModelSource(name: string, extraField = ""): string {
25
+ return `import { z } from 'zod/v4';
26
+
27
+ export const ${name} = z.object({
28
+ id: z.string(),
29
+ name: z.string().min(1),${extraField}
30
+ createdAt: z.string().optional(),
31
+ updatedAt: z.string().optional(),
32
+ });
33
+
34
+ export type ${name} = z.infer<typeof ${name}>;
35
+
36
+ export const displayName = '${name}';
37
+ `;
38
+ }
39
+
40
+ function createProjectFixture(rootDir: string): void {
41
+ writeFile(path.join(rootDir, "package.json"), JSON.stringify({ name: "sample-app" }, null, 2));
42
+ writeFile(
43
+ path.join(rootDir, "swallowkit.config.js"),
44
+ `module.exports = {
45
+ database: {
46
+ connectionString: 'AccountEndpoint=https://example.local;',
47
+ },
48
+ backend: {
49
+ language: 'typescript',
50
+ },
51
+ api: {
52
+ endpoint: '/api/_swallowkit',
53
+ },
54
+ };
55
+ `
56
+ );
57
+ writeFile(path.join(rootDir, "shared", "package.json"), JSON.stringify({ name: "@sample-app/shared" }, null, 2));
58
+ writeFile(path.join(rootDir, "shared", "index.ts"), "export {};\n");
59
+ writeFile(path.join(rootDir, "shared", "models", "todo.ts"), createModelSource("Todo"));
60
+ fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
61
+ fs.symlinkSync(
62
+ path.join(repoRoot, "node_modules", "zod"),
63
+ path.join(rootDir, "node_modules", "zod"),
64
+ "junction"
65
+ );
66
+ }
67
+
68
+ function createNativeBackendFixture(rootDir: string, backendLanguage: "csharp" | "python"): void {
69
+ createProjectFixture(rootDir);
70
+ writeFile(
71
+ path.join(rootDir, "swallowkit.config.js"),
72
+ `module.exports = {
73
+ database: {
74
+ connectionString: 'AccountEndpoint=https://example.local;',
75
+ },
76
+ backend: {
77
+ language: '${backendLanguage}',
78
+ },
79
+ api: {
80
+ endpoint: '/api/_swallowkit',
81
+ },
82
+ };
83
+ `
84
+ );
85
+
86
+ if (backendLanguage === "csharp") {
87
+ writeFile(path.join(rootDir, "functions", "SwallowKit.Functions.csproj"), "<Project />\n");
88
+ } else {
89
+ writeFile(
90
+ path.join(rootDir, "functions", "function_app.py"),
91
+ "from azure.functions import FunctionApp\n\napp = FunctionApp()\n\n# SwallowKit scaffold registrations\n"
92
+ );
93
+ }
94
+ }
95
+
96
+ function mockSuccessfulNativeCodegen(): () => void {
97
+ const spawnSyncMock = childProcess.spawnSync as unknown as any;
98
+ const spawnMock = childProcess.spawn as unknown as any;
99
+ spawnSyncMock.mockReturnValue({ status: 0 });
100
+ spawnMock.mockImplementation(() => {
101
+ const child = new EventEmitter() as childProcess.ChildProcess;
102
+ process.nextTick(() => child.emit("close", 0));
103
+ return child;
104
+ });
105
+ return () => {
106
+ spawnMock.mockReset();
107
+ spawnSyncMock.mockReset();
108
+ };
109
+ }
110
+
111
+ async function runMachine(argv: string[]): Promise<{ response: any; exitCode: number }> {
112
+ const writes: string[] = [];
113
+ const originalWrite = process.stdout.write.bind(process.stdout);
114
+ const originalExitCode = process.exitCode;
115
+
116
+ (process.stdout.write as unknown as (chunk: string | Uint8Array) => boolean) = ((chunk: string | Uint8Array) => {
117
+ writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf-8"));
118
+ return true;
119
+ }) as typeof process.stdout.write;
120
+
121
+ process.exitCode = 0;
122
+
123
+ try {
124
+ await runMachineCli(argv);
125
+ return {
126
+ response: JSON.parse(writes.join("")),
127
+ exitCode: process.exitCode || 0,
128
+ };
129
+ } finally {
130
+ process.stdout.write = originalWrite;
131
+ process.exitCode = originalExitCode;
132
+ }
133
+ }
134
+
135
+ function machineArgs(...args: string[]): string[] {
136
+ return ["node", "swallowkit", "machine", ...args];
137
+ }
138
+
139
+ describe("plan / apply / drift / verify machine commands", () => {
140
+ const originalCwd = process.cwd();
141
+ let tempDir: string;
142
+
143
+ beforeEach(() => {
144
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-agent-"));
145
+ process.chdir(tempDir);
146
+ });
147
+
148
+ afterEach(() => {
149
+ process.chdir(originalCwd);
150
+ fs.rmSync(tempDir, { recursive: true, force: true });
151
+ });
152
+
153
+ it("plans a scaffold without writing files and reports operations", async () => {
154
+ createProjectFixture(tempDir);
155
+
156
+ const { response, exitCode } = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only"));
157
+
158
+ expect(exitCode).toBe(0);
159
+ expect(response.ok).toBe(true);
160
+ expect(response.command).toBe("plan-scaffold");
161
+ expect(response.status).toBe("complete");
162
+ expect(response.data.planId).toMatch(/^[0-9a-f]{12}$/);
163
+ expect(response.data.requiresApproval).toBe(false);
164
+ expect(response.data.operations).toEqual(
165
+ expect.arrayContaining([
166
+ expect.objectContaining({ path: "app/api/todo/route.ts", action: "create", ownership: "managed" }),
167
+ expect.objectContaining({ path: "functions/src/todo.ts", action: "create" }),
168
+ ])
169
+ );
170
+ expect(response.nextActions[0].command).toContain(`apply scaffold --plan ${response.data.planId}`);
171
+
172
+ // No generated files were written
173
+ expect(fs.existsSync(path.join(tempDir, "app"))).toBe(false);
174
+ expect(fs.existsSync(path.join(tempDir, "functions"))).toBe(false);
175
+ // But the plan state was persisted
176
+ expect(
177
+ fs.existsSync(path.join(tempDir, ".swallowkit", "state", "plans", `${response.data.planId}.json`))
178
+ ).toBe(true);
179
+ });
180
+
181
+ it("applies a previously computed plan and records the artifact ledger", async () => {
182
+ createProjectFixture(tempDir);
183
+
184
+ const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only"));
185
+ const planId = plan.response.data.planId;
186
+
187
+ const { response, exitCode } = await runMachine(machineArgs("apply", "scaffold", "--plan", planId));
188
+
189
+ expect(exitCode).toBe(0);
190
+ expect(response.ok).toBe(true);
191
+ expect(response.command).toBe("apply-scaffold");
192
+ expect(response.status).toBe("complete");
193
+ expect(response.data.createdFiles).toEqual(
194
+ expect.arrayContaining(["app/api/todo/route.ts", "functions/src/todo.ts"])
195
+ );
196
+
197
+ // Artifact ledger created and tracks the generated files
198
+ const ledgerPath = path.join(tempDir, ".swallowkit", "artifacts.json");
199
+ expect(fs.existsSync(ledgerPath)).toBe(true);
200
+ const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf-8"));
201
+ expect(ledger.artifacts).toEqual(
202
+ expect.arrayContaining([
203
+ expect.objectContaining({ path: "app/api/todo/route.ts", ownership: "managed", sourceModel: "Todo" }),
204
+ ])
205
+ );
206
+
207
+ // Applied plan state is cleaned up
208
+ expect(fs.existsSync(path.join(tempDir, ".swallowkit", "state", "plans", `${planId}.json`))).toBe(false);
209
+ });
210
+
211
+ it.each([
212
+ {
213
+ backendLanguage: "csharp" as const,
214
+ modelPath: "functions/generated/csharp-models/src/SwallowKitBackendModels/Model/Todo.cs",
215
+ },
216
+ {
217
+ backendLanguage: "python" as const,
218
+ modelPath: "functions/generated/python-models/backend_models/models/todo.py",
219
+ },
220
+ ])(
221
+ "includes $backendLanguage native schema artifacts in plan, ledger, and approval checks",
222
+ async ({ backendLanguage, modelPath }) => {
223
+ createNativeBackendFixture(tempDir, backendLanguage);
224
+ const restoreCodegenMocks = mockSuccessfulNativeCodegen();
225
+
226
+ try {
227
+ const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only"));
228
+
229
+ expect(plan.response).toEqual(expect.objectContaining({ ok: true, status: "complete" }));
230
+ expect(plan.exitCode).toBe(0);
231
+ expect(plan.response.data.warnings).not.toEqual(
232
+ expect.arrayContaining([expect.stringContaining("external-codegen")])
233
+ );
234
+ expect(plan.response.data.operations).toEqual(
235
+ expect.arrayContaining([
236
+ expect.objectContaining({ path: "functions/openapi/todo.openapi.json", action: "create" }),
237
+ expect.objectContaining({ path: modelPath, action: "create", ownership: "managed" }),
238
+ ])
239
+ );
240
+ expect(fs.existsSync(path.join(tempDir, "functions", "openapi"))).toBe(false);
241
+ expect(fs.existsSync(path.join(tempDir, modelPath))).toBe(false);
242
+ expect(childProcess.spawn).not.toHaveBeenCalled();
243
+
244
+ const applied = await runMachine(
245
+ machineArgs("apply", "scaffold", "--plan", plan.response.data.planId)
246
+ );
247
+ expect(applied.exitCode).toBe(0);
248
+ expect(fs.existsSync(path.join(tempDir, modelPath))).toBe(true);
249
+
250
+ const ledger = JSON.parse(
251
+ fs.readFileSync(path.join(tempDir, ".swallowkit", "artifacts.json"), "utf-8")
252
+ );
253
+ expect(ledger.artifacts).toEqual(
254
+ expect.arrayContaining([
255
+ expect.objectContaining({ path: modelPath, ownership: "managed", generator: "native-schema" }),
256
+ ])
257
+ );
258
+
259
+ fs.appendFileSync(path.join(tempDir, modelPath), "\n// customized\n");
260
+ const denied = await runMachine(machineArgs("apply", "scaffold", "todo", "--api-only"));
261
+ expect(denied.exitCode).toBe(1);
262
+ expect(denied.response.error.code).toBe("approval-required");
263
+ expect(denied.response.error.details.conflicts).toEqual(
264
+ expect.arrayContaining([
265
+ expect.objectContaining({ path: modelPath, conflictReason: "modified-after-generation" }),
266
+ ])
267
+ );
268
+ } finally {
269
+ restoreCodegenMocks();
270
+ }
271
+ }
272
+ );
273
+
274
+ it("leaves the project untouched when the native toolchain validation fails on apply", async () => {
275
+ createNativeBackendFixture(tempDir, "csharp");
276
+ const spawnSyncMock = childProcess.spawnSync as unknown as any;
277
+ // dotnet --version が失敗 → .NET SDK 不在としてプリフライトで拒否される
278
+ spawnSyncMock.mockReturnValue({ status: 1 });
279
+
280
+ try {
281
+ const { response, exitCode } = await runMachine(machineArgs("apply", "scaffold", "todo", "--api-only"));
282
+
283
+ expect(exitCode).toBe(1);
284
+ expect(response.ok).toBe(false);
285
+ expect(response.error.message).toContain(".NET SDK is required");
286
+
287
+ expect(fs.existsSync(path.join(tempDir, "app"))).toBe(false);
288
+ expect(fs.existsSync(path.join(tempDir, "lib"))).toBe(false);
289
+ expect(fs.existsSync(path.join(tempDir, "functions", "openapi"))).toBe(false);
290
+ expect(fs.existsSync(path.join(tempDir, "functions", "generated"))).toBe(false);
291
+ expect(fs.existsSync(path.join(tempDir, "functions", "Crud"))).toBe(false);
292
+ expect(fs.existsSync(path.join(tempDir, ".swallowkit", "artifacts.json"))).toBe(false);
293
+ } finally {
294
+ spawnSyncMock.mockReset();
295
+ }
296
+ });
297
+
298
+ it("rejects a stale plan when files change after planning", async () => {
299
+ createProjectFixture(tempDir);
300
+
301
+ const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only"));
302
+ const planId = plan.response.data.planId;
303
+
304
+ // Model schema changes after the plan was computed
305
+ writeFile(
306
+ path.join(tempDir, "shared", "models", "todo.ts"),
307
+ createModelSource("Todo", "\n priority: z.number().optional(),")
308
+ );
309
+
310
+ const { response, exitCode } = await runMachine(machineArgs("apply", "scaffold", "--plan", planId));
311
+
312
+ expect(exitCode).toBe(1);
313
+ expect(response.ok).toBe(false);
314
+ expect(response.error.code).toBe("stale-plan");
315
+ expect(response.status).toBe("blocked");
316
+ expect(response.error.details.changedFiles).toContain("shared/models/todo.ts");
317
+ // Nothing was applied
318
+ expect(fs.existsSync(path.join(tempDir, "app"))).toBe(false);
319
+ });
320
+
321
+ it("rejects a native backend plan when a nested model changes after planning", async () => {
322
+ createNativeBackendFixture(tempDir, "csharp");
323
+ writeFile(path.join(tempDir, "shared", "models", "member.ts"), createModelSource("Member"));
324
+ writeFile(
325
+ path.join(tempDir, "shared", "models", "todo.ts"),
326
+ `import { z } from 'zod/v4';
327
+ import { Member } from './member';
328
+
329
+ export const Todo = z.object({
330
+ id: z.string(),
331
+ name: z.string().min(1),
332
+ member: Member,
333
+ });
334
+
335
+ export type Todo = z.infer<typeof Todo>;
336
+ `
337
+ );
338
+
339
+ const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only"));
340
+ expect(plan.response).toEqual(expect.objectContaining({ ok: true, status: "complete" }));
341
+ expect(plan.response.data.fingerprints["shared/models/member.ts"]).toMatch(/^[0-9a-f]{64}$/);
342
+
343
+ writeFile(
344
+ path.join(tempDir, "shared", "models", "member.ts"),
345
+ createModelSource("Member", "\n role: z.string().optional(),")
346
+ );
347
+
348
+ const applied = await runMachine(
349
+ machineArgs("apply", "scaffold", "--plan", plan.response.data.planId)
350
+ );
351
+ expect(applied.exitCode).toBe(1);
352
+ expect(applied.response.error.code).toBe("stale-plan");
353
+ expect(applied.response.error.details.changedFiles).toContain("shared/models/member.ts");
354
+ });
355
+
356
+ it("requires approval to overwrite files modified after generation, then applies with --approve", async () => {
357
+ createProjectFixture(tempDir);
358
+
359
+ await runMachine(machineArgs("apply", "scaffold", "todo", "--api-only"));
360
+
361
+ // User modifies a managed artifact
362
+ const routePath = path.join(tempDir, "app", "api", "todo", "route.ts");
363
+ const customized = `// customized by user\n${fs.readFileSync(routePath, "utf-8")}`;
364
+ fs.writeFileSync(routePath, customized, "utf-8");
365
+
366
+ const denied = await runMachine(machineArgs("apply", "scaffold", "todo", "--api-only"));
367
+
368
+ expect(denied.exitCode).toBe(1);
369
+ expect(denied.response.ok).toBe(false);
370
+ expect(denied.response.error.code).toBe("approval-required");
371
+ expect(denied.response.status).toBe("requires-human");
372
+ expect(denied.response.error.details.conflicts).toEqual(
373
+ expect.arrayContaining([
374
+ expect.objectContaining({
375
+ path: "app/api/todo/route.ts",
376
+ action: "overwrite",
377
+ conflictReason: "modified-after-generation",
378
+ }),
379
+ ])
380
+ );
381
+ // The customization is untouched
382
+ expect(fs.readFileSync(routePath, "utf-8")).toBe(customized);
383
+
384
+ const approved = await runMachine(machineArgs("apply", "scaffold", "todo", "--api-only", "--approve"));
385
+
386
+ expect(approved.exitCode).toBe(0);
387
+ expect(approved.response.ok).toBe(true);
388
+ expect(approved.response.data.approvedConflicts).toEqual(
389
+ expect.arrayContaining([expect.objectContaining({ path: "app/api/todo/route.ts" })])
390
+ );
391
+ expect(fs.readFileSync(routePath, "utf-8")).not.toContain("customized by user");
392
+ });
393
+
394
+ it("inspects artifacts and detects drift after manual edits and schema changes", async () => {
395
+ createProjectFixture(tempDir);
396
+ await runMachine(machineArgs("apply", "scaffold", "todo", "--api-only"));
397
+
398
+ const artifacts = await runMachine(machineArgs("inspect", "artifacts"));
399
+ expect(artifacts.response.ok).toBe(true);
400
+ expect(artifacts.response.data.ledgerFound).toBe(true);
401
+ expect(artifacts.response.data.artifacts).toEqual(
402
+ expect.arrayContaining([
403
+ expect.objectContaining({ path: "functions/src/todo.ts", exists: true, modified: false }),
404
+ ])
405
+ );
406
+
407
+ // Clean project → no error/warning drift about artifacts
408
+ const cleanDrift = await runMachine(machineArgs("inspect", "drift"));
409
+ expect(cleanDrift.response.ok).toBe(true);
410
+ expect(
411
+ cleanDrift.response.data.findings.filter((f: any) => f.kind === "artifact-modified" && f.severity !== "info")
412
+ ).toEqual([]);
413
+
414
+ // Manual edit to a managed artifact + schema change
415
+ fs.appendFileSync(path.join(tempDir, "functions", "src", "todo.ts"), "\n// manual edit\n");
416
+ writeFile(
417
+ path.join(tempDir, "shared", "models", "todo.ts"),
418
+ createModelSource("Todo", "\n priority: z.number().optional(),")
419
+ );
420
+
421
+ const drift = await runMachine(machineArgs("inspect", "drift"));
422
+ expect(drift.response.ok).toBe(true);
423
+ expect(drift.response.data.findings).toEqual(
424
+ expect.arrayContaining([
425
+ expect.objectContaining({ kind: "artifact-modified", path: "functions/src/todo.ts", severity: "warning" }),
426
+ expect.objectContaining({ kind: "schema-drift", entity: "Todo" }),
427
+ ])
428
+ );
429
+ expect(drift.response.data.findings.every((f: any) => typeof f.repairAction === "string")).toBe(true);
430
+ });
431
+
432
+ it("verifies the project and explains failures", async () => {
433
+ createProjectFixture(tempDir);
434
+ await runMachine(machineArgs("apply", "scaffold", "todo", "--api-only"));
435
+
436
+ const verify = await runMachine(machineArgs("verify", "project", "--checks", "structure,drift"));
437
+
438
+ expect(verify.exitCode).toBe(0);
439
+ expect(verify.response.ok).toBe(true);
440
+ expect(verify.response.command).toBe("verify-project");
441
+ expect(verify.response.status).toBe("complete");
442
+ expect(verify.response.data.summary.done).toBe(true);
443
+ expect(verify.response.data.checks.map((check: any) => check.id)).toEqual(["structure", "drift"]);
444
+
445
+ // explain failure with no failing checks returns an empty list
446
+ const explain = await runMachine(machineArgs("explain", "failure"));
447
+ expect(explain.response.ok).toBe(true);
448
+ expect(explain.response.data.failures).toEqual([]);
449
+ });
450
+
451
+ it("returns no-verification-result when explain failure runs before verify", async () => {
452
+ createProjectFixture(tempDir);
453
+
454
+ const { response, exitCode } = await runMachine(machineArgs("explain", "failure"));
455
+
456
+ expect(exitCode).toBe(1);
457
+ expect(response.ok).toBe(false);
458
+ expect(response.error.code).toBe("no-verification-result");
459
+ expect(response.status).toBe("blocked");
460
+ });
461
+ });
@@ -167,7 +167,7 @@ describe("native schema generators", () => {
167
167
  });
168
168
 
169
169
  it("builds NSwag arguments for model-only C# generation", () => {
170
- const outputPath = path.join("C:\\temp\\generated\\csharp-models", ".native-temp", "Contracts.cs");
170
+ const outputPath = path.join(os.tmpdir(), "swallowkit-csharp-codegen-example", "Contracts.cs");
171
171
  const args = getCSharpNativeGeneratorArgs("C:\\temp\\todo.openapi.json", outputPath);
172
172
 
173
173
  expect(args.slice(0, 4)).toEqual(["tool", "run", "nswag", "openapi2csclient"]);
@@ -178,7 +178,7 @@ describe("native schema generators", () => {
178
178
  });
179
179
 
180
180
  it("builds datamodel-code-generator arguments for python assets", () => {
181
- const outputPath = path.join("C:\\temp\\generated\\python-models", ".native-temp", "models.py");
181
+ const outputPath = path.join(os.tmpdir(), "swallowkit-python-codegen-example", "models.py");
182
182
  const args = getPythonNativeGeneratorArgs("C:\\temp\\todo.openapi.json", outputPath);
183
183
 
184
184
  expect(args).toEqual(