swallowkit 1.0.0-beta.9 → 1.0.1

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 (272) hide show
  1. package/README.ja.md +87 -181
  2. package/README.md +87 -238
  3. package/dist/__tests__/fixtures.d.ts +12 -0
  4. package/dist/__tests__/fixtures.d.ts.map +1 -1
  5. package/dist/__tests__/fixtures.js +101 -0
  6. package/dist/__tests__/fixtures.js.map +1 -1
  7. package/dist/cli/commands/add-auth.d.ts +15 -0
  8. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  9. package/dist/cli/commands/add-auth.js +790 -0
  10. package/dist/cli/commands/add-auth.js.map +1 -0
  11. package/dist/cli/commands/add-connector.d.ts +20 -0
  12. package/dist/cli/commands/add-connector.d.ts.map +1 -0
  13. package/dist/cli/commands/add-connector.js +164 -0
  14. package/dist/cli/commands/add-connector.js.map +1 -0
  15. package/dist/cli/commands/cosmos-worktree-isolation.d.ts +25 -0
  16. package/dist/cli/commands/cosmos-worktree-isolation.d.ts.map +1 -0
  17. package/dist/cli/commands/cosmos-worktree-isolation.js +124 -0
  18. package/dist/cli/commands/cosmos-worktree-isolation.js.map +1 -0
  19. package/dist/cli/commands/create-model.d.ts +1 -4
  20. package/dist/cli/commands/create-model.d.ts.map +1 -1
  21. package/dist/cli/commands/create-model.js +20 -82
  22. package/dist/cli/commands/create-model.js.map +1 -1
  23. package/dist/cli/commands/dev-seeds.d.ts +27 -0
  24. package/dist/cli/commands/dev-seeds.d.ts.map +1 -1
  25. package/dist/cli/commands/dev-seeds.js +188 -6
  26. package/dist/cli/commands/dev-seeds.js.map +1 -1
  27. package/dist/cli/commands/dev.d.ts +30 -0
  28. package/dist/cli/commands/dev.d.ts.map +1 -1
  29. package/dist/cli/commands/dev.js +495 -129
  30. package/dist/cli/commands/dev.js.map +1 -1
  31. package/dist/cli/commands/init.d.ts +69 -1
  32. package/dist/cli/commands/init.d.ts.map +1 -1
  33. package/dist/cli/commands/init.js +788 -766
  34. package/dist/cli/commands/init.js.map +1 -1
  35. package/dist/cli/commands/scaffold.d.ts +24 -3
  36. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  37. package/dist/cli/commands/scaffold.js +408 -189
  38. package/dist/cli/commands/scaffold.js.map +1 -1
  39. package/dist/cli/index.d.ts +5 -1
  40. package/dist/cli/index.d.ts.map +1 -1
  41. package/dist/cli/index.js +335 -58
  42. package/dist/cli/index.js.map +1 -1
  43. package/dist/core/config.d.ts +13 -3
  44. package/dist/core/config.d.ts.map +1 -1
  45. package/dist/core/config.js +151 -8
  46. package/dist/core/config.js.map +1 -1
  47. package/dist/core/mock/connector-mock-server.d.ts +101 -0
  48. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  49. package/dist/core/mock/connector-mock-server.js +480 -0
  50. package/dist/core/mock/connector-mock-server.js.map +1 -0
  51. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  52. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  53. package/dist/core/mock/zod-mock-generator.js +163 -0
  54. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  55. package/dist/core/operations/auth-operations.d.ts +61 -0
  56. package/dist/core/operations/auth-operations.d.ts.map +1 -0
  57. package/dist/core/operations/auth-operations.js +272 -0
  58. package/dist/core/operations/auth-operations.js.map +1 -0
  59. package/dist/core/operations/create-model.d.ts +15 -0
  60. package/dist/core/operations/create-model.d.ts.map +1 -0
  61. package/dist/core/operations/create-model.js +185 -0
  62. package/dist/core/operations/create-model.js.map +1 -0
  63. package/dist/core/operations/file-session.d.ts +56 -0
  64. package/dist/core/operations/file-session.d.ts.map +1 -0
  65. package/dist/core/operations/file-session.js +203 -0
  66. package/dist/core/operations/file-session.js.map +1 -0
  67. package/dist/core/operations/provision-operations.d.ts +52 -0
  68. package/dist/core/operations/provision-operations.d.ts.map +1 -0
  69. package/dist/core/operations/provision-operations.js +211 -0
  70. package/dist/core/operations/provision-operations.js.map +1 -0
  71. package/dist/core/operations/runtime.d.ts +37 -0
  72. package/dist/core/operations/runtime.d.ts.map +1 -0
  73. package/dist/core/operations/runtime.js +248 -0
  74. package/dist/core/operations/runtime.js.map +1 -0
  75. package/dist/core/operations/scaffold-apply.d.ts +21 -0
  76. package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
  77. package/dist/core/operations/scaffold-apply.js +57 -0
  78. package/dist/core/operations/scaffold-apply.js.map +1 -0
  79. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  80. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  81. package/dist/core/operations/scaffold-machine.js +51 -0
  82. package/dist/core/operations/scaffold-machine.js.map +1 -0
  83. package/dist/core/operations/scaffold-plan.d.ts +43 -0
  84. package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
  85. package/dist/core/operations/scaffold-plan.js +187 -0
  86. package/dist/core/operations/scaffold-plan.js.map +1 -0
  87. package/dist/core/project/agent-instructions.d.ts +29 -0
  88. package/dist/core/project/agent-instructions.d.ts.map +1 -0
  89. package/dist/core/project/agent-instructions.js +160 -0
  90. package/dist/core/project/agent-instructions.js.map +1 -0
  91. package/dist/core/project/artifacts.d.ts +58 -0
  92. package/dist/core/project/artifacts.d.ts.map +1 -0
  93. package/dist/core/project/artifacts.js +179 -0
  94. package/dist/core/project/artifacts.js.map +1 -0
  95. package/dist/core/project/boundaries.d.ts +50 -0
  96. package/dist/core/project/boundaries.d.ts.map +1 -0
  97. package/dist/core/project/boundaries.js +117 -0
  98. package/dist/core/project/boundaries.js.map +1 -0
  99. package/dist/core/project/capabilities.d.ts +58 -0
  100. package/dist/core/project/capabilities.d.ts.map +1 -0
  101. package/dist/core/project/capabilities.js +135 -0
  102. package/dist/core/project/capabilities.js.map +1 -0
  103. package/dist/core/project/drift.d.ts +29 -0
  104. package/dist/core/project/drift.d.ts.map +1 -0
  105. package/dist/core/project/drift.js +190 -0
  106. package/dist/core/project/drift.js.map +1 -0
  107. package/dist/core/project/infra.d.ts +49 -0
  108. package/dist/core/project/infra.d.ts.map +1 -0
  109. package/dist/core/project/infra.js +167 -0
  110. package/dist/core/project/infra.js.map +1 -0
  111. package/dist/core/project/manifest.d.ts +92 -0
  112. package/dist/core/project/manifest.d.ts.map +1 -0
  113. package/dist/core/project/manifest.js +321 -0
  114. package/dist/core/project/manifest.js.map +1 -0
  115. package/dist/core/project/state.d.ts +15 -0
  116. package/dist/core/project/state.d.ts.map +1 -0
  117. package/dist/core/project/state.js +106 -0
  118. package/dist/core/project/state.js.map +1 -0
  119. package/dist/core/project/validation.d.ts +20 -0
  120. package/dist/core/project/validation.d.ts.map +1 -0
  121. package/dist/core/project/validation.js +274 -0
  122. package/dist/core/project/validation.js.map +1 -0
  123. package/dist/core/project/workflows.d.ts +43 -0
  124. package/dist/core/project/workflows.d.ts.map +1 -0
  125. package/dist/core/project/workflows.js +299 -0
  126. package/dist/core/project/workflows.js.map +1 -0
  127. package/dist/core/scaffold/auth-generator.d.ts +63 -0
  128. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  129. package/dist/core/scaffold/auth-generator.js +1774 -0
  130. package/dist/core/scaffold/auth-generator.js.map +1 -0
  131. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  132. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  133. package/dist/core/scaffold/connector-functions-generator.js +1029 -0
  134. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  135. package/dist/core/scaffold/functions-generator.d.ts +4 -3
  136. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  137. package/dist/core/scaffold/functions-generator.js +405 -134
  138. package/dist/core/scaffold/functions-generator.js.map +1 -1
  139. package/dist/core/scaffold/model-parser.d.ts +20 -0
  140. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  141. package/dist/core/scaffold/model-parser.js +229 -78
  142. package/dist/core/scaffold/model-parser.js.map +1 -1
  143. package/dist/core/scaffold/native-schema-generator.d.ts +19 -0
  144. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
  145. package/dist/core/scaffold/native-schema-generator.js +753 -0
  146. package/dist/core/scaffold/native-schema-generator.js.map +1 -0
  147. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  148. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  149. package/dist/core/scaffold/nextjs-generator.js +152 -8
  150. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  151. package/dist/core/scaffold/ui-generator.d.ts +17 -4
  152. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  153. package/dist/core/scaffold/ui-generator.js +174 -34
  154. package/dist/core/scaffold/ui-generator.js.map +1 -1
  155. package/dist/core/verify/index.d.ts +69 -0
  156. package/dist/core/verify/index.d.ts.map +1 -0
  157. package/dist/core/verify/index.js +412 -0
  158. package/dist/core/verify/index.js.map +1 -0
  159. package/dist/machine/contracts.d.ts +24 -0
  160. package/dist/machine/contracts.d.ts.map +1 -0
  161. package/dist/machine/contracts.js +3 -0
  162. package/dist/machine/contracts.js.map +1 -0
  163. package/dist/machine/errors.d.ts +14 -0
  164. package/dist/machine/errors.d.ts.map +1 -0
  165. package/dist/machine/errors.js +42 -0
  166. package/dist/machine/errors.js.map +1 -0
  167. package/dist/machine/index.d.ts +3 -0
  168. package/dist/machine/index.d.ts.map +1 -0
  169. package/dist/machine/index.js +566 -0
  170. package/dist/machine/index.js.map +1 -0
  171. package/dist/mcp/index.d.ts +25 -0
  172. package/dist/mcp/index.d.ts.map +1 -0
  173. package/dist/mcp/index.js +425 -0
  174. package/dist/mcp/index.js.map +1 -0
  175. package/dist/types/index.d.ts +105 -0
  176. package/dist/types/index.d.ts.map +1 -1
  177. package/dist/utils/package-manager.d.ts +7 -11
  178. package/dist/utils/package-manager.d.ts.map +1 -1
  179. package/dist/utils/package-manager.js +42 -17
  180. package/dist/utils/package-manager.js.map +1 -1
  181. package/dist/utils/python-uv.d.ts +21 -0
  182. package/dist/utils/python-uv.d.ts.map +1 -0
  183. package/dist/utils/python-uv.js +111 -0
  184. package/dist/utils/python-uv.js.map +1 -0
  185. package/dist/version.d.ts +2 -0
  186. package/dist/version.d.ts.map +1 -0
  187. package/dist/version.js +49 -0
  188. package/dist/version.js.map +1 -0
  189. package/package.json +15 -17
  190. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +716 -22
  191. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +19 -7
  192. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +20 -12
  193. package/src/__tests__/agent-instructions.test.ts +237 -0
  194. package/src/__tests__/agent-loop-phase34.test.ts +619 -0
  195. package/src/__tests__/auth.test.ts +770 -0
  196. package/src/__tests__/bff-swa-auth.test.ts +275 -0
  197. package/src/__tests__/config.test.ts +176 -3
  198. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  199. package/src/__tests__/connector-mock-server.test.ts +459 -0
  200. package/src/__tests__/connector-model-bff.test.ts +162 -0
  201. package/src/__tests__/cosmos-worktree-isolation.test.ts +149 -0
  202. package/src/__tests__/dev-seeds.test.ts +124 -0
  203. package/src/__tests__/dev.test.ts +254 -0
  204. package/src/__tests__/file-session.test.ts +98 -0
  205. package/src/__tests__/fixtures.ts +101 -0
  206. package/src/__tests__/functions-generator.test.ts +137 -1
  207. package/src/__tests__/init.test.ts +366 -2
  208. package/src/__tests__/jest-globals.d.ts +8 -0
  209. package/src/__tests__/machine.test.ts +371 -0
  210. package/src/__tests__/mcp.test.ts +253 -0
  211. package/src/__tests__/model-parser.test.ts +142 -0
  212. package/src/__tests__/multi-auth.test.ts +160 -0
  213. package/src/__tests__/nextjs-generator.test.ts +8 -0
  214. package/src/__tests__/package-manager.test.ts +59 -4
  215. package/src/__tests__/plan-apply.test.ts +499 -0
  216. package/src/__tests__/python-uv.test.ts +48 -0
  217. package/src/__tests__/scaffold.test.ts +287 -24
  218. package/src/__tests__/tsconfig.json +15 -0
  219. package/src/__tests__/ui-generator.test.ts +104 -0
  220. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  221. package/src/cli/commands/add-auth.ts +853 -0
  222. package/src/cli/commands/add-connector.ts +159 -0
  223. package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
  224. package/src/cli/commands/create-model.ts +21 -100
  225. package/src/cli/commands/dev-seeds.ts +278 -7
  226. package/src/cli/commands/dev.ts +607 -160
  227. package/src/cli/commands/init.ts +851 -786
  228. package/src/cli/commands/scaffold.ts +531 -236
  229. package/src/cli/index.ts +338 -60
  230. package/src/core/config.ts +145 -9
  231. package/src/core/mock/connector-mock-server.ts +555 -0
  232. package/src/core/mock/zod-mock-generator.ts +205 -0
  233. package/src/core/operations/auth-operations.ts +360 -0
  234. package/src/core/operations/create-model.ts +191 -0
  235. package/src/core/operations/file-session.ts +222 -0
  236. package/src/core/operations/provision-operations.ts +297 -0
  237. package/src/core/operations/runtime.ts +258 -0
  238. package/src/core/operations/scaffold-apply.ts +106 -0
  239. package/src/core/operations/scaffold-machine.ts +77 -0
  240. package/src/core/operations/scaffold-plan.ts +215 -0
  241. package/src/core/project/agent-instructions.ts +167 -0
  242. package/src/core/project/agent-templates/AGENTS.md +504 -0
  243. package/src/core/project/agent-templates/CLAUDE.md +31 -0
  244. package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
  245. package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
  246. package/src/core/project/agent-templates/copilot-instructions.md +27 -0
  247. package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
  248. package/src/core/project/artifacts.ts +205 -0
  249. package/src/core/project/boundaries.ts +170 -0
  250. package/src/core/project/capabilities.ts +193 -0
  251. package/src/core/project/drift.ts +203 -0
  252. package/src/core/project/infra.ts +196 -0
  253. package/src/core/project/manifest.ts +402 -0
  254. package/src/core/project/state.ts +77 -0
  255. package/src/core/project/validation.ts +295 -0
  256. package/src/core/project/workflows.ts +295 -0
  257. package/src/core/scaffold/auth-generator.ts +1824 -0
  258. package/src/core/scaffold/connector-functions-generator.ts +1133 -0
  259. package/src/core/scaffold/functions-generator.ts +432 -134
  260. package/src/core/scaffold/model-parser.ts +244 -90
  261. package/src/core/scaffold/native-schema-generator.ts +904 -0
  262. package/src/core/scaffold/nextjs-generator.ts +174 -9
  263. package/src/core/scaffold/ui-generator.ts +191 -36
  264. package/src/core/verify/index.ts +468 -0
  265. package/src/machine/contracts.ts +33 -0
  266. package/src/machine/errors.ts +45 -0
  267. package/src/machine/index.ts +690 -0
  268. package/src/mcp/index.ts +423 -0
  269. package/src/types/index.ts +137 -0
  270. package/src/utils/package-manager.ts +41 -19
  271. package/src/utils/python-uv.ts +96 -0
  272. package/src/version.ts +13 -0
@@ -0,0 +1,619 @@
1
+ /**
2
+ * Phase 3/4 agent-loop tests:
3
+ * - inspect boundaries / inspect infra
4
+ * - plan/apply auth(承認フロー含む)
5
+ * - plan/apply provision(az 実行なしの安全パスのみ)
6
+ * - verify のカスタムチェック読込
7
+ */
8
+
9
+ import * as fs from "fs";
10
+ import * as os from "os";
11
+ import * as path from "path";
12
+ import * as childProcess from "child_process";
13
+ import { runMachineCli } from "../machine";
14
+ import { hashContent } from "../core/operations/file-session";
15
+ import { detectDrift } from "../core/project/drift";
16
+ import { inspectInfra } from "../core/project/infra";
17
+ import { loadCustomVerifyChecks, compactVerifyResult, runVerify, VerifyResult } from "../core/verify";
18
+ import { buildAgentSkills, buildWorkflowDocs, writeAgentSkills, writeWorkflowDocs } from "../core/project/workflows";
19
+
20
+ jest.mock("child_process", () => {
21
+ const actual = jest.requireActual<typeof import("child_process")>("child_process");
22
+ return {
23
+ ...actual,
24
+ spawn: jest.fn(),
25
+ spawnSync: jest.fn(),
26
+ };
27
+ });
28
+
29
+ const repoRoot = process.cwd();
30
+
31
+ function writeFile(filePath: string, content: string): void {
32
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
33
+ fs.writeFileSync(filePath, content, "utf-8");
34
+ }
35
+
36
+ function createProjectFixture(rootDir: string): void {
37
+ writeFile(path.join(rootDir, "package.json"), JSON.stringify({ name: "sample-app" }, null, 2));
38
+ writeFile(
39
+ path.join(rootDir, "swallowkit.config.js"),
40
+ `module.exports = {
41
+ database: {
42
+ connectionString: 'AccountEndpoint=https://example.local;',
43
+ },
44
+ backend: {
45
+ language: 'typescript',
46
+ },
47
+ api: {
48
+ endpoint: '/api/_swallowkit',
49
+ },
50
+ };
51
+ `
52
+ );
53
+ writeFile(path.join(rootDir, "shared", "package.json"), JSON.stringify({ name: "@sample-app/shared" }, null, 2));
54
+ writeFile(path.join(rootDir, "shared", "index.ts"), "export {};\n");
55
+ writeFile(path.join(rootDir, "functions", "package.json"), JSON.stringify({ name: "functions" }, null, 2));
56
+ fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
57
+ fs.symlinkSync(
58
+ path.join(repoRoot, "node_modules", "zod"),
59
+ path.join(rootDir, "node_modules", "zod"),
60
+ "junction"
61
+ );
62
+ }
63
+
64
+ const SAMPLE_MAIN_BICEP = `targetScope = 'resourceGroup'
65
+
66
+ @description('Project name')
67
+ param projectName string
68
+
69
+ @description('Cosmos DB mode')
70
+ @allowed(['freetier', 'serverless'])
71
+ param cosmosDbMode string = 'serverless'
72
+
73
+ param enableVNet bool = false
74
+
75
+ module logAnalytics 'modules/loganalytics.bicep' = {
76
+ name: 'logAnalytics'
77
+ params: {
78
+ name: 'log-x'
79
+ }
80
+ }
81
+
82
+ module cosmosDbServerless 'modules/cosmosdb-serverless.bicep' = if (cosmosDbMode == 'serverless') {
83
+ name: 'cosmosDb'
84
+ }
85
+
86
+ module todoContainer 'containers/todo-container.bicep' = {
87
+ name: 'todoContainer'
88
+ }
89
+
90
+ output cosmosDbAccountName string = 'x'
91
+ output vnetEnabled bool = enableVNet
92
+ `;
93
+
94
+ async function runMachine(argv: string[]): Promise<{ response: any; exitCode: number }> {
95
+ const writes: string[] = [];
96
+ const originalWrite = process.stdout.write.bind(process.stdout);
97
+ const originalExitCode = process.exitCode;
98
+
99
+ (process.stdout.write as unknown as (chunk: string | Uint8Array) => boolean) = ((chunk: string | Uint8Array) => {
100
+ writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf-8"));
101
+ return true;
102
+ }) as typeof process.stdout.write;
103
+
104
+ process.exitCode = 0;
105
+
106
+ try {
107
+ await runMachineCli(argv);
108
+ return {
109
+ response: JSON.parse(writes.join("")),
110
+ exitCode: process.exitCode || 0,
111
+ };
112
+ } finally {
113
+ process.stdout.write = originalWrite;
114
+ process.exitCode = originalExitCode;
115
+ }
116
+ }
117
+
118
+ describe("agent loop phase 3/4", () => {
119
+ const originalCwd = process.cwd();
120
+ let tempDir: string;
121
+
122
+ beforeEach(() => {
123
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-p34-"));
124
+ process.chdir(tempDir);
125
+ (childProcess.spawnSync as unknown as any).mockReturnValue({ status: 1, stdout: "", stderr: "" });
126
+ });
127
+
128
+ afterEach(() => {
129
+ process.chdir(originalCwd);
130
+ fs.rmSync(tempDir, { recursive: true, force: true });
131
+ jest.clearAllMocks();
132
+ });
133
+
134
+ it("returns the responsibility boundary contract via inspect boundaries", async () => {
135
+ createProjectFixture(tempDir);
136
+ const { response, exitCode } = await runMachine(["node", "swallowkit", "machine", "inspect", "boundaries"]);
137
+
138
+ expect(exitCode).toBe(0);
139
+ expect(response.ok).toBe(true);
140
+ expect(response.data.contractVersion).toBe(1);
141
+ expect(response.data.ownershipPolicies.map((p: any) => p.ownership)).toEqual(
142
+ expect.arrayContaining(["managed", "generated-once", "user-owned", "extension-point", "metadata"])
143
+ );
144
+ const managed = response.data.ownershipPolicies.find((p: any) => p.ownership === "managed");
145
+ expect(managed.zone).toBe("deterministic");
146
+ expect(managed.editPolicy).toBe("regenerate-via-plan-apply");
147
+ expect(response.data.conventionRules.some((r: any) => r.pattern === "*")).toBe(true);
148
+ });
149
+
150
+ it("inspects Bicep infra assets deterministically", () => {
151
+ createProjectFixture(tempDir);
152
+ writeFile(path.join(tempDir, "infra", "main.bicep"), SAMPLE_MAIN_BICEP);
153
+ writeFile(path.join(tempDir, "infra", "main.parameters.json"), "{}\n");
154
+ writeFile(path.join(tempDir, "infra", "modules", "loganalytics.bicep"), "// module\n");
155
+ writeFile(path.join(tempDir, "infra", "containers", "todo-container.bicep"), "// wired\n");
156
+ writeFile(path.join(tempDir, "infra", "containers", "orphan-container.bicep"), "// not wired\n");
157
+
158
+ const inspection = inspectInfra(tempDir);
159
+
160
+ expect(inspection.mainBicep.exists).toBe(true);
161
+ expect(inspection.mainBicep.params.map((p) => p.name)).toEqual(["projectName", "cosmosDbMode", "enableVNet"]);
162
+ const cosmosMode = inspection.mainBicep.params.find((p) => p.name === "cosmosDbMode");
163
+ expect(cosmosMode?.allowed).toEqual(["freetier", "serverless"]);
164
+ expect(cosmosMode?.description).toBe("Cosmos DB mode");
165
+
166
+ const serverless = inspection.mainBicep.modules.find((m) => m.symbolicName === "cosmosDbServerless");
167
+ expect(serverless?.condition).toBe("cosmosDbMode == 'serverless'");
168
+ expect(serverless?.deploymentName).toBe("cosmosDb");
169
+
170
+ expect(inspection.mainBicep.outputs).toEqual([
171
+ { name: "cosmosDbAccountName", type: "string" },
172
+ { name: "vnetEnabled", type: "bool" },
173
+ ]);
174
+
175
+ expect(inspection.containers).toEqual([
176
+ { file: "infra/containers/orphan-container.bicep", wired: false },
177
+ { file: "infra/containers/todo-container.bicep", wired: true },
178
+ ]);
179
+ expect(inspection.warnings.some((w) => w.startsWith("container-not-wired"))).toBe(true);
180
+ });
181
+
182
+ it("plans auth without writing files, applies it, and enforces approval on conflicts", async () => {
183
+ createProjectFixture(tempDir);
184
+
185
+ // 1. Plan — ディスクへの書き込みなし
186
+ const plan1 = await runMachine(["node", "swallowkit", "machine", "plan", "auth", "--provider", "custom-jwt"]);
187
+ expect(plan1.exitCode).toBe(0);
188
+ expect(plan1.response.ok).toBe(true);
189
+ expect(plan1.response.data.planType).toBe("auth");
190
+ expect(plan1.response.data.requiresApproval).toBe(false);
191
+ expect(plan1.response.status).toBe("complete");
192
+ const plannedPaths = plan1.response.data.operations.map((op: any) => op.path);
193
+ expect(plannedPaths).toEqual(expect.arrayContaining(["shared/models/auth.ts", "proxy.ts", "app/login/page.tsx"]));
194
+ expect(fs.existsSync(path.join(tempDir, "proxy.ts"))).toBe(false);
195
+ expect(fs.existsSync(path.join(tempDir, "shared", "models", "auth.ts"))).toBe(false);
196
+
197
+ // 2. Apply — 実際に書き込み、台帳に記録される
198
+ const apply1 = await runMachine([
199
+ "node", "swallowkit", "machine", "apply", "auth", "--plan", plan1.response.data.planId,
200
+ ]);
201
+ expect(apply1.exitCode).toBe(0);
202
+ expect(apply1.response.ok).toBe(true);
203
+ expect(apply1.response.status).toBe("complete");
204
+ expect(fs.existsSync(path.join(tempDir, "proxy.ts"))).toBe(true);
205
+ expect(fs.existsSync(path.join(tempDir, "shared", "models", "auth.ts"))).toBe(true);
206
+ const ledger = JSON.parse(fs.readFileSync(path.join(tempDir, ".swallowkit", "artifacts.json"), "utf-8"));
207
+ expect(ledger.artifacts.some((a: any) => a.path === "proxy.ts" && a.generator === "add-auth")).toBe(true);
208
+
209
+ // 3. managed ファイルを手編集後の plan は競合として検出される
210
+ fs.appendFileSync(path.join(tempDir, "lib", "auth", "auth-context.tsx"), "\n// hand edit\n");
211
+ const plan2 = await runMachine(["node", "swallowkit", "machine", "plan", "auth", "--provider", "custom-jwt"]);
212
+ expect(plan2.response.ok).toBe(true);
213
+ expect(plan2.response.data.requiresApproval).toBe(true);
214
+ expect(plan2.response.status).toBe("requires-human");
215
+ const conflict = plan2.response.data.conflicts.find((c: any) => c.path === "lib/auth/auth-context.tsx");
216
+ expect(conflict?.conflictReason).toBe("modified-after-generation");
217
+
218
+ // 4. 承認なしの apply は requires-human で停止
219
+ const applyNoApprove = await runMachine(["node", "swallowkit", "machine", "apply", "auth", "--provider", "custom-jwt"]);
220
+ expect(applyNoApprove.response.ok).toBe(false);
221
+ expect(applyNoApprove.response.error.code).toBe("approval-required");
222
+ expect(applyNoApprove.response.status).toBe("requires-human");
223
+ expect(fs.readFileSync(path.join(tempDir, "lib", "auth", "auth-context.tsx"), "utf-8")).toContain("// hand edit");
224
+
225
+ // 5. --approve で上書き適用できる
226
+ const applyApproved = await runMachine([
227
+ "node", "swallowkit", "machine", "apply", "auth", "--provider", "custom-jwt", "--approve",
228
+ ]);
229
+ expect(applyApproved.response.ok).toBe(true);
230
+ expect(applyApproved.response.data.approvedConflicts.length).toBeGreaterThan(0);
231
+ expect(fs.readFileSync(path.join(tempDir, "lib", "auth", "auth-context.tsx"), "utf-8")).not.toContain("// hand edit");
232
+ });
233
+
234
+ it("threads --allowed-providers into the config and generated SWA artifacts for a named scheme", async () => {
235
+ createProjectFixture(tempDir);
236
+
237
+ const plan = await runMachine([
238
+ "node", "swallowkit", "machine", "plan", "auth",
239
+ "--provider", "swa", "--scheme", "admin", "--allowed-providers", "github",
240
+ ]);
241
+ expect(plan.response.ok).toBe(true);
242
+ expect(plan.response.data.allowedProviders).toEqual(["github"]);
243
+
244
+ const apply = await runMachine([
245
+ "node", "swallowkit", "machine", "apply", "auth", "--plan", plan.response.data.planId,
246
+ ]);
247
+ expect(apply.response.ok).toBe(true);
248
+
249
+ const config = fs.readFileSync(path.join(tempDir, "swallowkit.config.js"), "utf-8");
250
+ expect(config).toContain("swa: { allowedProviders: ['github'] }");
251
+
252
+ const authContext = fs.readFileSync(
253
+ path.join(tempDir, "lib", "auth", "schemes", "admin", "auth-context.tsx"),
254
+ "utf-8"
255
+ );
256
+ expect(authContext).toContain("/.auth/login/github?");
257
+ expect(authContext).not.toContain("/.auth/login/aad");
258
+
259
+ // apply auth は「policies を定義せよ」等の後続手順を nextActions で返す
260
+ const descriptions = apply.response.nextActions.map((a: any) => a.description).join(" ");
261
+ expect(descriptions).toContain("auth.authorization.policies");
262
+ expect(descriptions).toContain("allowedProviders");
263
+ });
264
+
265
+ it("plans provision locally and always requires human approval to apply", async () => {
266
+ createProjectFixture(tempDir);
267
+ writeFile(path.join(tempDir, "infra", "main.bicep"), SAMPLE_MAIN_BICEP);
268
+ writeFile(path.join(tempDir, "infra", "main.parameters.json"), "{}\n");
269
+
270
+ const plan = await runMachine([
271
+ "node", "swallowkit", "machine", "plan", "provision",
272
+ "--resource-group", "rg-sample",
273
+ "--location", "japaneast",
274
+ "--swa-location", "eastasia",
275
+ ]);
276
+
277
+ expect(plan.exitCode).toBe(0);
278
+ expect(plan.response.ok).toBe(true);
279
+ expect(plan.response.status).toBe("requires-human");
280
+ expect(plan.response.data.requiresApproval).toBe(true);
281
+ // spawnSync がモックで失敗を返すため az CLI は「利用不可」として警告される
282
+ expect(plan.response.data.azCliAvailable).toBe(false);
283
+ expect(plan.response.data.warnings.some((w: string) => w.startsWith("az-cli-not-found"))).toBe(true);
284
+ expect(plan.response.data.commands.some((c: string) => c.includes("az deployment group create"))).toBe(true);
285
+
286
+ // 承認なしの apply は requires-human
287
+ const applyNoApprove = await runMachine([
288
+ "node", "swallowkit", "machine", "apply", "provision", "--plan", plan.response.data.planId,
289
+ ]);
290
+ expect(applyNoApprove.response.ok).toBe(false);
291
+ expect(applyNoApprove.response.error.code).toBe("approval-required");
292
+ expect(applyNoApprove.response.status).toBe("requires-human");
293
+
294
+ // 存在しない plan は blocked
295
+ const applyMissing = await runMachine([
296
+ "node", "swallowkit", "machine", "apply", "provision", "--plan", "does-not-exist", "--approve",
297
+ ]);
298
+ expect(applyMissing.response.ok).toBe(false);
299
+ expect(applyMissing.response.error.code).toBe("plan-not-found");
300
+ expect(applyMissing.response.status).toBe("blocked");
301
+ });
302
+
303
+ it("rejects invalid provision inputs", async () => {
304
+ createProjectFixture(tempDir);
305
+ writeFile(path.join(tempDir, "infra", "main.bicep"), SAMPLE_MAIN_BICEP);
306
+
307
+ const badRg = await runMachine([
308
+ "node", "swallowkit", "machine", "plan", "provision",
309
+ "--resource-group", "rg; rm -rf /",
310
+ "--location", "japaneast",
311
+ "--swa-location", "eastasia",
312
+ ]);
313
+ expect(badRg.response.ok).toBe(false);
314
+ expect(badRg.response.error.code).toBe("invalid-arguments");
315
+
316
+ const badLocation = await runMachine([
317
+ "node", "swallowkit", "machine", "plan", "provision",
318
+ "--resource-group", "rg-sample",
319
+ "--location", "Japan East",
320
+ "--swa-location", "eastasia",
321
+ ]);
322
+ expect(badLocation.response.ok).toBe(false);
323
+ expect(badLocation.response.error.code).toBe("invalid-arguments");
324
+ });
325
+
326
+ it("loads custom verify checks from swallowkit.config.json and rejects invalid ids", () => {
327
+ writeFile(
328
+ path.join(tempDir, "swallowkit.config.json"),
329
+ JSON.stringify({
330
+ verify: {
331
+ checks: [
332
+ { id: "smoke-api", title: "API smoke", command: "node scripts/smoke.js" },
333
+ { id: "Bad_Id", command: "echo bad" },
334
+ { id: "typecheck", command: "echo clash-with-builtin" },
335
+ ],
336
+ },
337
+ })
338
+ );
339
+
340
+ const checks = loadCustomVerifyChecks(tempDir);
341
+ expect(checks).toHaveLength(1);
342
+ expect(checks[0]).toMatchObject({ id: "smoke-api", command: "node scripts/smoke.js" });
343
+ });
344
+
345
+ it.each<[string, string[]]>([
346
+ ["pnpm", ["exec", "tsc", "--noEmit"]],
347
+ ["npm", ["exec", "tsc", "--", "--noEmit"]],
348
+ ])("uses %s for the typecheck fallback", async (packageManager, expectedArgs) => {
349
+ writeFile(
350
+ path.join(tempDir, "package.json"),
351
+ JSON.stringify({
352
+ name: "sample-app",
353
+ devEngines: { packageManager: { name: packageManager, version: ">=11 <13", onFail: "error" } },
354
+ })
355
+ );
356
+ writeFile(path.join(tempDir, "tsconfig.json"), JSON.stringify({ compilerOptions: { noEmit: true } }));
357
+ (childProcess.spawnSync as unknown as ReturnType<typeof jest.fn>).mockReturnValue({ status: 0, stdout: "", stderr: "" });
358
+
359
+ const result = await runVerify(["typecheck"], tempDir);
360
+
361
+ expect(result.checks[0]).toMatchObject({
362
+ status: "pass",
363
+ evidence: { command: `${packageManager} ${expectedArgs.join(" ")}` },
364
+ });
365
+ expect(childProcess.spawnSync).toHaveBeenCalledWith(
366
+ packageManager,
367
+ expectedArgs,
368
+ expect.objectContaining({ cwd: tempDir })
369
+ );
370
+ });
371
+
372
+ it("uses the existing typecheck script when present", async () => {
373
+ writeFile(
374
+ path.join(tempDir, "package.json"),
375
+ JSON.stringify({
376
+ name: "sample-app",
377
+ scripts: { typecheck: "tsc --noEmit" },
378
+ devEngines: { packageManager: { name: "pnpm", version: ">=11 <13", onFail: "error" } },
379
+ })
380
+ );
381
+ writeFile(path.join(tempDir, "tsconfig.json"), JSON.stringify({ compilerOptions: { noEmit: true } }));
382
+ (childProcess.spawnSync as unknown as ReturnType<typeof jest.fn>).mockReturnValue({ status: 0, stdout: "", stderr: "" });
383
+
384
+ const result = await runVerify(["typecheck"], tempDir);
385
+
386
+ expect(result.checks[0]).toMatchObject({ status: "pass", evidence: { command: "pnpm run typecheck" } });
387
+ expect(childProcess.spawnSync).toHaveBeenCalledWith(
388
+ "pnpm",
389
+ ["run", "typecheck"],
390
+ expect.objectContaining({ cwd: tempDir })
391
+ );
392
+ });
393
+
394
+ it("counts only stale schema-generated artifacts in a mixed-hash ledger", async () => {
395
+ const modelSource = "export const Todo = {};\n";
396
+ const currentSchemaHash = hashContent(modelSource);
397
+ writeFile(path.join(tempDir, "shared", "models", "todo.ts"), modelSource);
398
+ writeFile(
399
+ path.join(tempDir, ".swallowkit", "artifacts.json"),
400
+ JSON.stringify({
401
+ version: 1,
402
+ swallowkitVersion: "1.0.0",
403
+ artifacts: [
404
+ {
405
+ path: "functions/src/todo.ts",
406
+ ownership: "managed",
407
+ generator: "scaffold",
408
+ generatorVersion: "1.0.0",
409
+ sourceModel: "Todo",
410
+ schemaHash: currentSchemaHash,
411
+ contentHash: "current-managed-content",
412
+ generatedAt: "2026-01-01T00:00:00.000Z",
413
+ lastOperation: "update",
414
+ },
415
+ {
416
+ path: "app/api/todo/route.ts",
417
+ ownership: "managed",
418
+ generator: "scaffold",
419
+ generatorVersion: "1.0.0",
420
+ sourceModel: "Todo",
421
+ schemaHash: "stale-managed-hash",
422
+ contentHash: "stale-managed-content",
423
+ generatedAt: "2026-01-01T00:00:00.000Z",
424
+ lastOperation: "update",
425
+ },
426
+ {
427
+ path: "functions/src/extensions/todo.ts",
428
+ ownership: "extension-point",
429
+ generator: "scaffold",
430
+ generatorVersion: "1.0.0",
431
+ sourceModel: "Todo",
432
+ schemaHash: "old-extension-hash",
433
+ contentHash: "extension-content",
434
+ generatedAt: "2026-01-01T00:00:00.000Z",
435
+ lastOperation: "update",
436
+ },
437
+ ],
438
+ })
439
+ );
440
+
441
+ const result = await detectDrift(tempDir);
442
+ const schemaFindings = result.findings.filter((finding) => finding.kind === "schema-drift");
443
+
444
+ expect(schemaFindings).toHaveLength(1);
445
+ expect(schemaFindings[0]).toMatchObject({ entity: "Todo", expected: "stale-managed-hash", actual: currentSchemaHash });
446
+ expect(schemaFindings[0].message).toContain("(1 artifact(s) are stale)");
447
+ });
448
+
449
+ it("compactVerifyResult suppresses info findings without changing the summary", () => {
450
+ const result: VerifyResult = {
451
+ verifiedAt: new Date().toISOString(),
452
+ summary: { passed: 1, failed: 0, skipped: 0, errors: 0, done: true },
453
+ checks: [
454
+ {
455
+ id: "drift",
456
+ title: "Generated artifact drift",
457
+ status: "pass",
458
+ durationMs: 1,
459
+ fixable: true,
460
+ evidence: {
461
+ findings: [
462
+ { kind: "artifact-modified", severity: "info", path: "shared/models/todo.ts", message: "expected edit", repairAction: "none" },
463
+ { kind: "schema-drift", severity: "warning", path: "functions/src/todo.ts", message: "stale", repairAction: "re-plan" },
464
+ ] as never,
465
+ },
466
+ suggestedActions: [],
467
+ },
468
+ ],
469
+ };
470
+
471
+ const compact = compactVerifyResult(result);
472
+ expect(compact.summary).toEqual(result.summary);
473
+ expect(compact.checks[0].evidence.findings).toHaveLength(1);
474
+ expect(compact.checks[0].evidence.findings![0].severity).toBe("warning");
475
+ expect(compact.checks[0].evidence.suppressedInfoFindings).toBe(1);
476
+ // 元の結果は変更しない
477
+ expect(result.checks[0].evidence.findings).toHaveLength(2);
478
+ });
479
+
480
+ describe("workflow docs & agent skills", () => {
481
+ it("builds workflow docs including provision and an index referencing skills", () => {
482
+ const docs = buildWorkflowDocs("pnpm exec");
483
+ const fileNames = docs.map((d) => d.fileName);
484
+ expect(fileNames).toEqual(
485
+ expect.arrayContaining(["README.md", "add-model.md", "modify-model.md", "add-auth.md", "verify-and-repair.md", "provision.md"])
486
+ );
487
+ const index = docs.find((d) => d.fileName === "README.md")!;
488
+ expect(index.content).toContain(".github/skills/");
489
+ expect(index.content).toContain("swallowkit-provision");
490
+ });
491
+
492
+ it("builds agent skills that follow the agentskills.io spec", () => {
493
+ const skills = buildAgentSkills("pnpm exec");
494
+ expect(skills.map((s) => s.skillName)).toEqual([
495
+ "swallowkit-add-model",
496
+ "swallowkit-modify-model",
497
+ "swallowkit-add-auth",
498
+ "swallowkit-verify-repair",
499
+ "swallowkit-provision",
500
+ ]);
501
+ for (const skill of skills) {
502
+ // name: 小文字英数字とハイフンのみ・連続ハイフンなし・64文字以内
503
+ expect(skill.skillName).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
504
+ expect(skill.skillName.length).toBeLessThanOrEqual(64);
505
+ // frontmatter: name はディレクトリ名と一致、description は 1024 文字以内
506
+ const frontmatter = skill.content.match(/^---\n([\s\S]*?)\n---\n/);
507
+ expect(frontmatter).not.toBeNull();
508
+ expect(frontmatter![1]).toContain(`name: ${skill.skillName}`);
509
+ const description = frontmatter![1].match(/description: (.+)/)![1];
510
+ expect(description.length).toBeGreaterThan(0);
511
+ expect(description.length).toBeLessThanOrEqual(1024);
512
+ // 本文にランブックが含まれる
513
+ expect(skill.content).toContain("# Workflow:");
514
+ expect(skill.content).toContain("pnpm exec swallowkit machine");
515
+ }
516
+ });
517
+
518
+ it("writes skills to .github/skills/<name>/SKILL.md and runbooks to .swallowkit/workflows/", () => {
519
+ const skillPaths = writeAgentSkills(tempDir, "npx");
520
+ expect(skillPaths).toEqual([
521
+ ".github/skills/swallowkit-add-model/SKILL.md",
522
+ ".github/skills/swallowkit-modify-model/SKILL.md",
523
+ ".github/skills/swallowkit-add-auth/SKILL.md",
524
+ ".github/skills/swallowkit-verify-repair/SKILL.md",
525
+ ".github/skills/swallowkit-provision/SKILL.md",
526
+ ]);
527
+ for (const rel of skillPaths) {
528
+ expect(fs.existsSync(path.join(tempDir, rel))).toBe(true);
529
+ }
530
+
531
+ const workflowPaths = writeWorkflowDocs(tempDir, "npx");
532
+ expect(workflowPaths).toContain(".swallowkit/workflows/provision.md");
533
+ for (const rel of workflowPaths) {
534
+ expect(fs.existsSync(path.join(tempDir, rel))).toBe(true);
535
+ }
536
+
537
+ // provision スキルは承認ゲートを明記する
538
+ const provisionSkill = fs.readFileSync(
539
+ path.join(tempDir, ".github", "skills", "swallowkit-provision", "SKILL.md"),
540
+ "utf-8"
541
+ );
542
+ expect(provisionSkill).toContain("requires-human");
543
+ expect(provisionSkill).toContain("--approve");
544
+ });
545
+ });
546
+
547
+ describe("robustness (irregular inputs)", () => {
548
+ it("returns not-a-swallowkit-project (blocked) for write commands outside a project", async () => {
549
+ // fixture なし = プロジェクトマーカーなし
550
+ const planResult = await runMachine(["node", "swallowkit", "machine", "plan", "scaffold", "todo"]);
551
+ expect(planResult.response.ok).toBe(false);
552
+ expect(planResult.response.status).toBe("blocked");
553
+ expect(planResult.response.error.code).toBe("not-a-swallowkit-project");
554
+
555
+ const generateResult = await runMachine(["node", "swallowkit", "machine", "generate", "model", "todo"]);
556
+ expect(generateResult.response.ok).toBe(false);
557
+ expect(generateResult.response.status).toBe("blocked");
558
+ expect(generateResult.response.error.code).toBe("not-a-swallowkit-project");
559
+ });
560
+
561
+ it("degrades corrupted plan state to plan-not-found instead of crashing", async () => {
562
+ createProjectFixture(tempDir);
563
+ const planFile = path.join(tempDir, ".swallowkit", "state", "plans", "corrupt99.json");
564
+ writeFile(planFile, "{not valid json!!!");
565
+
566
+ const result = await runMachine([
567
+ "node", "swallowkit", "machine", "apply", "scaffold", "--plan", "corrupt99", "--approve",
568
+ ]);
569
+ expect(result.response.ok).toBe(false);
570
+ expect(result.response.status).toBe("blocked");
571
+ expect(result.response.error.code).toBe("plan-not-found");
572
+ });
573
+
574
+ it("degrades corrupted last-verify state to no-verification-result", async () => {
575
+ createProjectFixture(tempDir);
576
+ writeFile(path.join(tempDir, ".swallowkit", "state", "last-verify.json"), "not json");
577
+
578
+ const result = await runMachine(["node", "swallowkit", "machine", "explain", "failure"]);
579
+ expect(result.response.ok).toBe(false);
580
+ expect(result.response.status).toBe("blocked");
581
+ expect(result.response.error.code).toBe("no-verification-result");
582
+ });
583
+
584
+ it("returns help text as a success envelope for --help", async () => {
585
+ const result = await runMachine(["node", "swallowkit", "machine", "--help"]);
586
+ expect(result.response.ok).toBe(true);
587
+ expect(result.response.command).toBe("machine-help");
588
+ expect(result.response.data.help).toContain("Usage:");
589
+ expect(result.exitCode).toBe(0);
590
+
591
+ const subResult = await runMachine(["node", "swallowkit", "machine", "plan", "scaffold", "--help"]);
592
+ expect(subResult.response.ok).toBe(true);
593
+ expect(subResult.response.data.help).toContain("plan scaffold");
594
+ });
595
+
596
+ it("returns invalid-command with usage details for unknown commands and missing required options", async () => {
597
+ const unknown = await runMachine(["node", "swallowkit", "machine", "frobnicate"]);
598
+ expect(unknown.response.ok).toBe(false);
599
+ expect(unknown.response.error.code).toBe("invalid-command");
600
+ expect(unknown.exitCode).toBe(1);
601
+
602
+ createProjectFixture(tempDir);
603
+ const missingOption = await runMachine(["node", "swallowkit", "machine", "plan", "provision"]);
604
+ expect(missingOption.response.ok).toBe(false);
605
+ expect(missingOption.response.error.code).toBe("invalid-command");
606
+ expect(missingOption.response.error.message).toContain("--resource-group");
607
+ expect(missingOption.exitCode).toBe(1);
608
+ });
609
+
610
+ it("preserves the full multi-line error message from legacy console.error paths", async () => {
611
+ createProjectFixture(tempDir);
612
+ // 存在しないモデル → scaffold 内部の console.error + process.exit 経路
613
+ const result = await runMachine(["node", "swallowkit", "machine", "plan", "scaffold", "no-such-model"]);
614
+ expect(result.response.ok).toBe(false);
615
+ // 断片ではなく "Model file not found" を含む全文が返ること
616
+ expect(result.response.error.message).toContain("Model file not found");
617
+ });
618
+ });
619
+ });