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