swallowkit 2.0.0-alpha.1 → 2.0.0-alpha.10

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 (224) hide show
  1. package/README.ja.md +14 -0
  2. package/README.md +14 -0
  3. package/dist/cli/commands/add-auth.js +37 -6
  4. package/dist/cli/commands/add-auth.js.map +1 -1
  5. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  6. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  7. package/dist/cli/commands/configure-mcp.js +42 -0
  8. package/dist/cli/commands/configure-mcp.js.map +1 -0
  9. package/dist/cli/commands/dev.d.ts +2 -2
  10. package/dist/cli/commands/dev.d.ts.map +1 -1
  11. package/dist/cli/commands/dev.js +39 -27
  12. package/dist/cli/commands/dev.js.map +1 -1
  13. package/dist/cli/commands/init.d.ts +8 -2
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/init.js +67 -132
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/cli/commands/migrations.d.ts.map +1 -1
  18. package/dist/cli/commands/migrations.js +25 -0
  19. package/dist/cli/commands/migrations.js.map +1 -1
  20. package/dist/cli/commands/provision.d.ts +5 -0
  21. package/dist/cli/commands/provision.d.ts.map +1 -1
  22. package/dist/cli/commands/provision.js +278 -55
  23. package/dist/cli/commands/provision.js.map +1 -1
  24. package/dist/cli/commands/scaffold.d.ts +4 -0
  25. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  26. package/dist/cli/commands/scaffold.js +85 -3
  27. package/dist/cli/commands/scaffold.js.map +1 -1
  28. package/dist/cli/index.d.ts +2 -0
  29. package/dist/cli/index.d.ts.map +1 -1
  30. package/dist/cli/index.js +43 -5
  31. package/dist/cli/index.js.map +1 -1
  32. package/dist/core/operations/create-model.d.ts +2 -0
  33. package/dist/core/operations/create-model.d.ts.map +1 -1
  34. package/dist/core/operations/create-model.js +60 -8
  35. package/dist/core/operations/create-model.js.map +1 -1
  36. package/dist/core/operations/migration-generator.d.ts.map +1 -1
  37. package/dist/core/operations/migration-generator.js +12 -1
  38. package/dist/core/operations/migration-generator.js.map +1 -1
  39. package/dist/core/operations/model-migration.d.ts +22 -0
  40. package/dist/core/operations/model-migration.d.ts.map +1 -0
  41. package/dist/core/operations/model-migration.js +36 -0
  42. package/dist/core/operations/model-migration.js.map +1 -0
  43. package/dist/core/operations/provision-deployment.d.ts +52 -0
  44. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  45. package/dist/core/operations/provision-deployment.js +164 -0
  46. package/dist/core/operations/provision-deployment.js.map +1 -0
  47. package/dist/core/operations/provision-operations.d.ts +62 -6
  48. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  49. package/dist/core/operations/provision-operations.js +653 -115
  50. package/dist/core/operations/provision-operations.js.map +1 -1
  51. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  52. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  53. package/dist/core/operations/scaffold-apply.js +1 -0
  54. package/dist/core/operations/scaffold-apply.js.map +1 -1
  55. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  56. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  57. package/dist/core/operations/scaffold-machine.js.map +1 -1
  58. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  59. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  60. package/dist/core/operations/scaffold-plan.js +3 -1
  61. package/dist/core/operations/scaffold-plan.js.map +1 -1
  62. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  63. package/dist/core/project/agent-instructions.js +4 -1
  64. package/dist/core/project/agent-instructions.js.map +1 -1
  65. package/dist/core/project/boundaries.d.ts +5 -2
  66. package/dist/core/project/boundaries.d.ts.map +1 -1
  67. package/dist/core/project/boundaries.js +51 -20
  68. package/dist/core/project/boundaries.js.map +1 -1
  69. package/dist/core/project/capabilities.d.ts +10 -0
  70. package/dist/core/project/capabilities.d.ts.map +1 -1
  71. package/dist/core/project/capabilities.js +38 -6
  72. package/dist/core/project/capabilities.js.map +1 -1
  73. package/dist/core/project/home-page.d.ts +4 -0
  74. package/dist/core/project/home-page.d.ts.map +1 -0
  75. package/dist/core/project/home-page.js +113 -0
  76. package/dist/core/project/home-page.js.map +1 -0
  77. package/dist/core/project/mcp-config.d.ts +26 -0
  78. package/dist/core/project/mcp-config.d.ts.map +1 -0
  79. package/dist/core/project/mcp-config.js +117 -0
  80. package/dist/core/project/mcp-config.js.map +1 -0
  81. package/dist/core/project/react-project.d.ts +1 -1
  82. package/dist/core/project/react-project.d.ts.map +1 -1
  83. package/dist/core/project/react-project.js +46 -25
  84. package/dist/core/project/react-project.js.map +1 -1
  85. package/dist/core/project/state.d.ts +7 -0
  86. package/dist/core/project/state.d.ts.map +1 -1
  87. package/dist/core/project/state.js +86 -0
  88. package/dist/core/project/state.js.map +1 -1
  89. package/dist/core/project/workflows.d.ts.map +1 -1
  90. package/dist/core/project/workflows.js +53 -7
  91. package/dist/core/project/workflows.js.map +1 -1
  92. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  93. package/dist/core/scaffold/auth-generator.js +26 -27
  94. package/dist/core/scaffold/auth-generator.js.map +1 -1
  95. package/dist/core/scaffold/model-parser.d.ts +23 -0
  96. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  97. package/dist/core/scaffold/model-parser.js +407 -137
  98. package/dist/core/scaffold/model-parser.js.map +1 -1
  99. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  100. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  101. package/dist/core/scaffold/native-schema-generator.js +294 -91
  102. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  103. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  104. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  105. package/dist/core/scaffold/openapi-generator.js +53 -3
  106. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  107. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  108. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  109. package/dist/core/scaffold/ui-generator.js +68 -7
  110. package/dist/core/scaffold/ui-generator.js.map +1 -1
  111. package/dist/core/verify/index.d.ts +4 -3
  112. package/dist/core/verify/index.d.ts.map +1 -1
  113. package/dist/core/verify/index.js +122 -5
  114. package/dist/core/verify/index.js.map +1 -1
  115. package/dist/machine/contracts.d.ts +1 -0
  116. package/dist/machine/contracts.d.ts.map +1 -1
  117. package/dist/machine/errors.d.ts +3 -2
  118. package/dist/machine/errors.d.ts.map +1 -1
  119. package/dist/machine/errors.js +2 -1
  120. package/dist/machine/errors.js.map +1 -1
  121. package/dist/machine/index.d.ts.map +1 -1
  122. package/dist/machine/index.js +59 -28
  123. package/dist/machine/index.js.map +1 -1
  124. package/dist/mcp/index.d.ts +6 -4
  125. package/dist/mcp/index.d.ts.map +1 -1
  126. package/dist/mcp/index.js +210 -25
  127. package/dist/mcp/index.js.map +1 -1
  128. package/dist/mcp/version-selection.d.ts +55 -0
  129. package/dist/mcp/version-selection.d.ts.map +1 -0
  130. package/dist/mcp/version-selection.js +152 -0
  131. package/dist/mcp/version-selection.js.map +1 -0
  132. package/dist/utils/azure-cli.d.ts +56 -0
  133. package/dist/utils/azure-cli.d.ts.map +1 -0
  134. package/dist/utils/azure-cli.js +326 -0
  135. package/dist/utils/azure-cli.js.map +1 -0
  136. package/dist/utils/azure-diagnostics.d.ts +16 -0
  137. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  138. package/dist/utils/azure-diagnostics.js +68 -0
  139. package/dist/utils/azure-diagnostics.js.map +1 -0
  140. package/dist/utils/package-manager.js +2 -2
  141. package/dist/utils/package-manager.js.map +1 -1
  142. package/dist/utils/python-uv.d.ts +5 -7
  143. package/dist/utils/python-uv.d.ts.map +1 -1
  144. package/dist/utils/python-uv.js +50 -24
  145. package/dist/utils/python-uv.js.map +1 -1
  146. package/dist/utils/react-dev.d.ts +4 -0
  147. package/dist/utils/react-dev.d.ts.map +1 -0
  148. package/dist/utils/react-dev.js +35 -0
  149. package/dist/utils/react-dev.js.map +1 -0
  150. package/dist/utils/semver.d.ts +14 -0
  151. package/dist/utils/semver.d.ts.map +1 -0
  152. package/dist/utils/semver.js +61 -0
  153. package/dist/utils/semver.js.map +1 -0
  154. package/package.json +6 -3
  155. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  156. package/src/__tests__/agent-instructions.test.ts +5 -0
  157. package/src/__tests__/agent-loop-phase34.test.ts +143 -8
  158. package/src/__tests__/auth.test.ts +9 -0
  159. package/src/__tests__/azure-cli.test.ts +202 -0
  160. package/src/__tests__/boundaries.test.ts +42 -0
  161. package/src/__tests__/csharp-schema.test.ts +261 -0
  162. package/src/__tests__/dev.test.ts +13 -6
  163. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  164. package/src/__tests__/fixtures/azure-cli.cjs +153 -0
  165. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  166. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  167. package/src/__tests__/home-page.test.ts +50 -0
  168. package/src/__tests__/init.test.ts +116 -0
  169. package/src/__tests__/mcp-version.test.ts +262 -0
  170. package/src/__tests__/mcp.test.ts +120 -3
  171. package/src/__tests__/multi-auth.test.ts +115 -4
  172. package/src/__tests__/package-manager.test.ts +17 -2
  173. package/src/__tests__/plan-apply.test.ts +179 -0
  174. package/src/__tests__/provision-runtime.test.ts +722 -0
  175. package/src/__tests__/python-uv.test.ts +50 -14
  176. package/src/__tests__/python-venv.test.ts +48 -0
  177. package/src/__tests__/react-dev.test.ts +72 -0
  178. package/src/__tests__/react.test.ts +19 -0
  179. package/src/__tests__/schema-analysis.test.ts +329 -0
  180. package/src/cli/commands/add-auth.ts +36 -6
  181. package/src/cli/commands/configure-mcp.ts +54 -0
  182. package/src/cli/commands/dev.ts +40 -31
  183. package/src/cli/commands/init.ts +73 -138
  184. package/src/cli/commands/migrations.ts +22 -0
  185. package/src/cli/commands/provision.ts +294 -56
  186. package/src/cli/commands/scaffold.ts +105 -3
  187. package/src/cli/index.ts +40 -7
  188. package/src/core/operations/create-model.ts +51 -13
  189. package/src/core/operations/migration-generator.ts +14 -1
  190. package/src/core/operations/model-migration.ts +52 -0
  191. package/src/core/operations/provision-deployment.ts +183 -0
  192. package/src/core/operations/provision-operations.ts +662 -154
  193. package/src/core/operations/scaffold-apply.ts +2 -0
  194. package/src/core/operations/scaffold-machine.ts +1 -0
  195. package/src/core/operations/scaffold-plan.ts +5 -1
  196. package/src/core/project/agent-instructions.ts +4 -1
  197. package/src/core/project/agent-templates/AGENTS.md +89 -6
  198. package/src/core/project/agent-templates/CLAUDE.md +14 -1
  199. package/src/core/project/agent-templates/copilot-instructions.md +15 -1
  200. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  201. package/src/core/project/boundaries.ts +62 -20
  202. package/src/core/project/capabilities.ts +49 -6
  203. package/src/core/project/home-page.ts +114 -0
  204. package/src/core/project/mcp-config.ts +84 -0
  205. package/src/core/project/react-project.ts +50 -28
  206. package/src/core/project/state.ts +70 -0
  207. package/src/core/project/workflows.ts +53 -7
  208. package/src/core/scaffold/auth-generator.ts +26 -27
  209. package/src/core/scaffold/model-parser.ts +413 -129
  210. package/src/core/scaffold/native-schema-generator.ts +319 -103
  211. package/src/core/scaffold/openapi-generator.ts +63 -5
  212. package/src/core/scaffold/ui-generator.ts +70 -8
  213. package/src/core/verify/index.ts +118 -6
  214. package/src/machine/contracts.ts +1 -0
  215. package/src/machine/errors.ts +4 -2
  216. package/src/machine/index.ts +65 -34
  217. package/src/mcp/index.ts +221 -31
  218. package/src/mcp/version-selection.ts +163 -0
  219. package/src/utils/azure-cli.ts +315 -0
  220. package/src/utils/azure-diagnostics.ts +75 -0
  221. package/src/utils/package-manager.ts +2 -2
  222. package/src/utils/python-uv.ts +53 -33
  223. package/src/utils/react-dev.ts +31 -0
  224. package/src/utils/semver.ts +59 -0
@@ -0,0 +1,261 @@
1
+ import { spawnSync } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+ import * as z from "zod/v4";
6
+ import { FileOperationSession } from "../core/operations/file-session";
7
+ import { parseModelFile } from "../core/scaffold/model-parser";
8
+ import { generateLanguageSchemaArtifacts } from "../core/scaffold/native-schema-generator";
9
+ import { generateOpenApiDocument } from "../core/scaffold/openapi-generator";
10
+
11
+ const MODEL_SOURCE = `import { z } from 'zod/v4';
12
+ export const ProbeItem = z.object({
13
+ id: z.string(),
14
+ userId: z.string(),
15
+ title: z.string().min(1).max(40),
16
+ code: z.string().regex(/^[a-z]+-\\d+$/i).optional(),
17
+ tags: z.array(z.string().min(2)).max(3),
18
+ score: z.number().min(0).max(1),
19
+ count: z.number().int().nonnegative(),
20
+ kind: z.enum(['a', 'b']),
21
+ nested: z.object({ x: z.string(), depth: z.object({ level: z.number().int() }).optional() }).optional(),
22
+ publishedAt: z.iso.datetime().optional(),
23
+ note: z.string().nullable(),
24
+ rating: z.number().gt(0).min(0).lt(5).max(5).nullable(),
25
+ items: z.array(z.object({ x: z.string().min(1) })).optional(),
26
+ kinds: z.array(z.enum(['a', 'b'])).optional(),
27
+ metadata: z.object({}).optional(),
28
+ createdAt: z.string().optional(),
29
+ updatedAt: z.string().optional(),
30
+ });
31
+ export type ProbeItem = z.infer<typeof ProbeItem>;
32
+ export const partitionKey = '/userId';
33
+ export const displayName = 'Probe';
34
+ `;
35
+
36
+ // Same schema as MODEL_SOURCE; used to validate C# output exactly as the BFF responseSchema would.
37
+ const ProbeItem = z.object({
38
+ id: z.string(),
39
+ userId: z.string(),
40
+ title: z.string().min(1).max(40),
41
+ code: z.string().regex(/^[a-z]+-\d+$/i).optional(),
42
+ tags: z.array(z.string().min(2)).max(3),
43
+ score: z.number().min(0).max(1),
44
+ count: z.number().int().nonnegative(),
45
+ kind: z.enum(["a", "b"]),
46
+ nested: z.object({ x: z.string(), depth: z.object({ level: z.number().int() }).optional() }).optional(),
47
+ publishedAt: z.iso.datetime().optional(),
48
+ note: z.string().nullable(),
49
+ rating: z.number().gt(0).min(0).lt(5).max(5).nullable(),
50
+ items: z.array(z.object({ x: z.string().min(1) })).optional(),
51
+ kinds: z.array(z.enum(["a", "b"])).optional(),
52
+ metadata: z.object({}).optional(),
53
+ createdAt: z.string().optional(),
54
+ updatedAt: z.string().optional(),
55
+ });
56
+
57
+ const VALID_INPUTS = [
58
+ {
59
+ id: "1",
60
+ userId: "user-1",
61
+ title: "Probe",
62
+ code: "ABC-12",
63
+ tags: ["aa", "bb"],
64
+ score: 0.25,
65
+ count: 3,
66
+ kind: "b",
67
+ nested: { x: "value", depth: { level: 2 } },
68
+ publishedAt: "2024-05-06T07:08:09.123Z",
69
+ note: "note",
70
+ rating: 2.5,
71
+ items: [{ x: "a" }, { x: "b" }],
72
+ kinds: ["a", "b"],
73
+ metadata: {},
74
+ createdAt: "2024-05-06T07:08:09.123456+09:00",
75
+ updatedAt: "not a timestamp but still a string",
76
+ },
77
+ { id: "2", userId: "user-2", title: "Minimal", tags: [], score: 1, count: 0, kind: "a", note: null, rating: null },
78
+ ];
79
+
80
+ const INVALID_INPUTS: Array<{ input: Record<string, unknown>; members: string[] }> = [
81
+ { input: { ...VALID_INPUTS[1], title: "" }, members: ["title"] },
82
+ { input: { ...VALID_INPUTS[1], title: "x".repeat(41) }, members: ["title"] },
83
+ { input: { ...VALID_INPUTS[1], code: "no-digits" }, members: ["code"] },
84
+ { input: { ...VALID_INPUTS[1], tags: ["a"] }, members: ["tags[0]"] },
85
+ { input: { ...VALID_INPUTS[1], tags: ["aa", "bb", "cc", "dd"] }, members: ["tags"] },
86
+ { input: { ...VALID_INPUTS[1], score: 1.5 }, members: ["score"] },
87
+ { input: { ...VALID_INPUTS[1], count: -1 }, members: ["count"] },
88
+ { input: { ...VALID_INPUTS[1], nested: { depth: { level: 1 } } }, members: ["nested.x"] },
89
+ { input: { ...VALID_INPUTS[1], rating: 0 }, members: ["rating"] },
90
+ { input: { ...VALID_INPUTS[1], rating: 5 }, members: ["rating"] },
91
+ { input: { ...VALID_INPUTS[1], items: [{ x: "ok" }, { x: "" }] }, members: ["items[1].x"] },
92
+ ];
93
+
94
+ function createModel(): { root: string; modelPath: string } {
95
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-csharp-schema-"));
96
+ const modelPath = path.join(root, "shared", "models", "probe-item.ts");
97
+ fs.mkdirSync(path.dirname(modelPath), { recursive: true });
98
+ fs.writeFileSync(modelPath, MODEL_SOURCE);
99
+ return { root, modelPath };
100
+ }
101
+
102
+ describe("Zod -> OpenAPI -> C# conversion", () => {
103
+ let root: string;
104
+ let modelPath: string;
105
+ const originalCwd = process.cwd();
106
+
107
+ beforeEach(() => {
108
+ ({ root, modelPath } = createModel());
109
+ });
110
+
111
+ afterEach(() => {
112
+ process.chdir(originalCwd);
113
+ fs.rmSync(root, { recursive: true, force: true });
114
+ });
115
+
116
+ it("records formats, integers, constraints, and inline object shapes from Zod", async () => {
117
+ const model = await parseModelFile(modelPath);
118
+ const field = (name: string) => model.fields.find((candidate) => candidate.name === name)!;
119
+ expect(model.parserMode).toBe("static-ast");
120
+ expect(field("createdAt")).toMatchObject({ type: "string", isOptional: true });
121
+ expect(field("createdAt").format).toBeUndefined();
122
+ expect(field("publishedAt").format).toBe("date-time");
123
+ expect(field("title").constraints).toEqual({ minLength: 1, maxLength: 40 });
124
+ expect(field("code").constraints).toEqual({ pattern: "^[a-z]+-\\d+$", patternFlags: "i" });
125
+ expect(field("tags").constraints).toEqual({ minLength: 2, maxItems: 3 });
126
+ expect(field("score")).toMatchObject({ constraints: { minimum: 0, maximum: 1 } });
127
+ expect(field("score").isInteger).toBeUndefined();
128
+ expect(field("count")).toMatchObject({ isInteger: true, constraints: { minimum: 0 } });
129
+ expect(field("nested").objectFields?.map((child) => child.name)).toEqual(["x", "depth"]);
130
+ expect(field("nested").objectFields?.[1].objectFields?.[0]).toMatchObject({ name: "level", isInteger: true });
131
+ expect(field("rating")).toMatchObject({
132
+ isNullable: true,
133
+ constraints: { minimum: 0, exclusiveMinimum: true, maximum: 5, exclusiveMaximum: true },
134
+ });
135
+ expect(field("metadata").objectFields).toEqual([]);
136
+ });
137
+
138
+ it("emits typed OpenAPI components without guessing formats from names", async () => {
139
+ const model = await parseModelFile(modelPath);
140
+ const schemas = JSON.parse(generateOpenApiDocument([model], model)).components.schemas;
141
+ const properties = schemas.ProbeItem.properties;
142
+ expect(properties.createdAt).toEqual({ type: "string" });
143
+ expect(properties.publishedAt).toEqual({ type: "string", format: "date-time" });
144
+ expect(properties.score).toEqual({ type: "number", format: "double", minimum: 0, maximum: 1 });
145
+ expect(properties.count).toEqual({ type: "integer", format: "int64", minimum: 0 });
146
+ expect(properties.title).toEqual({ type: "string", minLength: 1, maxLength: 40 });
147
+ expect(properties.code).toEqual({ type: "string", pattern: "^[a-z]+-\\d+$" });
148
+ expect(properties.tags).toEqual({ type: "array", items: { type: "string", minLength: 2 }, maxItems: 3 });
149
+ expect(properties.nested).toEqual({ $ref: "#/components/schemas/ProbeItemNested" });
150
+ expect(schemas.ProbeItemNested).toEqual({
151
+ type: "object",
152
+ properties: { x: { type: "string" }, depth: { $ref: "#/components/schemas/ProbeItemNestedDepth" } },
153
+ required: ["x"],
154
+ });
155
+ expect(schemas.ProbeItemNestedDepth.properties.level).toEqual({ type: "integer", format: "int64" });
156
+ });
157
+
158
+ it("generates C# types and validation from the Zod contract", async () => {
159
+ const model = await parseModelFile(modelPath);
160
+ process.chdir(root);
161
+ await generateLanguageSchemaArtifacts([model], model, "functions", "csharp", new FileOperationSession("commit", root));
162
+ const modelDir = path.join(root, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model");
163
+ const source = fs.readFileSync(path.join(modelDir, "ProbeItem.cs"), "utf8");
164
+
165
+ expect(source).toContain("public string? CreatedAt");
166
+ expect(source).toContain("public DateTime? PublishedAt");
167
+ expect(source).toContain("public double Score");
168
+ expect(source).toContain("public long Count");
169
+ expect(source).toContain("public ProbeItemNested? Nested");
170
+ expect(source).not.toMatch(/Dictionary<string, object>|decimal|DateTime\? CreatedAt/);
171
+ expect(source).not.toContain("yield break;");
172
+ expect(source).toContain('Title.Length < 1');
173
+ expect(source).toContain('Regex.IsMatch(Code, "^[a-z]+-\\\\d+$", RegexOptions.IgnoreCase)');
174
+ expect(source).toContain("Score > 1");
175
+ expect(source).toContain("Count < 0");
176
+ expect(fs.readFileSync(path.join(modelDir, "ProbeItemNested.cs"), "utf8")).toContain("public ProbeItemNestedDepth? Depth");
177
+ expect(fs.readFileSync(path.join(modelDir, "ProbeItemNestedDepth.cs"), "utf8")).toContain("public long Level");
178
+ expect(source).toContain("public string? Note");
179
+ expect(source).not.toContain("if (Note is null)");
180
+ expect(source).toContain("public double? Rating");
181
+ expect(source).toContain("Rating.HasValue && Rating.Value <= 0");
182
+ expect(source).toContain("public List<ProbeItemItems>? Items");
183
+ expect(source).toContain("public List<KindsEnum>? Kinds");
184
+ const metadata = fs.readFileSync(path.join(modelDir, "ProbeItemMetadata.cs"), "utf8");
185
+ expect(metadata.match(/public ProbeItemMetadata\(\)/g)).toHaveLength(1);
186
+ });
187
+ });
188
+
189
+ const dotnetVersion = spawnSync("dotnet", ["--version"], { encoding: "utf8" });
190
+ const itWithDotnet = dotnetVersion.status === 0 ? it : it.skip;
191
+
192
+ describe("generated C# JSON round trip", () => {
193
+ itWithDotnet("serializes back to JSON that passes the same Zod schema, and rejects what Zod rejects", async () => {
194
+ const { root, modelPath } = createModel();
195
+ const originalCwd = process.cwd();
196
+ try {
197
+ const model = await parseModelFile(modelPath);
198
+ process.chdir(root);
199
+ await generateLanguageSchemaArtifacts([model], model, "functions", "csharp", new FileOperationSession("commit", root));
200
+ process.chdir(originalCwd);
201
+
202
+ const app = path.join(root, "roundtrip");
203
+ fs.mkdirSync(app);
204
+ const major = String(dotnetVersion.stdout).trim().split(".")[0];
205
+ fs.writeFileSync(path.join(app, "RoundTrip.csproj"), `<Project Sdk="Microsoft.NET.Sdk">
206
+ <PropertyGroup>
207
+ <OutputType>Exe</OutputType>
208
+ <TargetFramework>net${major}.0</TargetFramework>
209
+ <Nullable>enable</Nullable>
210
+ <ImplicitUsings>enable</ImplicitUsings>
211
+ </PropertyGroup>
212
+ <ItemGroup>
213
+ <Compile Include="../functions/generated/csharp-models/**/*.cs" />
214
+ </ItemGroup>
215
+ </Project>
216
+ `);
217
+ fs.writeFileSync(path.join(app, "Program.cs"), `using System.ComponentModel.DataAnnotations;
218
+ using System.Text.Json;
219
+ using SwallowKitBackendModels.Model;
220
+
221
+ var results = new List<object>();
222
+ foreach (var file in args)
223
+ {
224
+ var item = JsonSerializer.Deserialize<ProbeItem>(File.ReadAllText(file))!;
225
+ var errors = new List<ValidationResult>();
226
+ Validator.TryValidateObject(item, new ValidationContext(item), errors, true);
227
+ results.Add(new { json = JsonSerializer.Serialize(item), members = errors.SelectMany(error => error.MemberNames).ToArray() });
228
+ }
229
+ Console.WriteLine(JsonSerializer.Serialize(results));
230
+ `);
231
+ const inputs = [...VALID_INPUTS, ...INVALID_INPUTS.map((entry) => entry.input)];
232
+ const files = inputs.map((input, index) => {
233
+ const file = path.join(root, `input-${index}.json`);
234
+ fs.writeFileSync(file, JSON.stringify(input));
235
+ return file;
236
+ });
237
+ const run = spawnSync("dotnet", ["run", "--project", app, "--", ...files], {
238
+ encoding: "utf8",
239
+ timeout: 240000,
240
+ env: { ...process.env, DOTNET_CLI_TELEMETRY_OPTOUT: "1", DOTNET_NOLOGO: "1" },
241
+ });
242
+ if (run.status !== 0) throw new Error(`dotnet run failed (${run.status}):\n${run.stdout}\n${run.stderr}`);
243
+ const output = JSON.parse(run.stdout.trim().split(/\r?\n/).pop()!) as Array<{ json: string; members: string[] }>;
244
+
245
+ VALID_INPUTS.forEach((input, index) => {
246
+ expect(ProbeItem.safeParse(input).success).toBe(true);
247
+ expect(output[index].members).toEqual([]);
248
+ const roundTripped = JSON.parse(output[index].json);
249
+ expect(ProbeItem.safeParse(roundTripped).success).toBe(true);
250
+ expect(roundTripped).toEqual(input);
251
+ });
252
+ INVALID_INPUTS.forEach(({ input, members }, offset) => {
253
+ expect(ProbeItem.safeParse(input).success).toBe(false);
254
+ expect(output[VALID_INPUTS.length + offset].members).toEqual(members);
255
+ });
256
+ } finally {
257
+ process.chdir(originalCwd);
258
+ fs.rmSync(root, { recursive: true, force: true });
259
+ }
260
+ }, 300000);
261
+ });
@@ -32,6 +32,7 @@ describe("dev command helpers", () => {
32
32
 
33
33
  it("passes the requested port to Azure Functions Core Tools", () => {
34
34
  expect(buildFunctionsStartArgs("7076")).toEqual(["start", "--port", "7076"]);
35
+ expect(buildFunctionsStartArgs("7076", "csharp")).toEqual(["--port", "7076"]);
35
36
  });
36
37
 
37
38
  it("parses the Core Tools version from CLI output", () => {
@@ -47,16 +48,22 @@ describe("dev command helpers", () => {
47
48
  it("falls back to npm Core Tools for older C# isolated hosts", () => {
48
49
  expect(buildFunctionsCoreToolsCommand("csharp", "4.0.5198")).toEqual({
49
50
  command: "npm",
50
- argsPrefix: ["exec", "--yes", "azure-functions-core-tools@4", "--"],
51
- label: "npm exec azure-functions-core-tools@4 (installed func 4.0.5198 is too old for C# isolated)",
51
+ argsPrefix: ["exec", "--yes", "--package", "azure-functions-core-tools@4", "--", "dotnet", "run", "--no-launch-profile", "--"],
52
+ label: "dotnet run with npm azure-functions-core-tools@4 (installed func 4.0.5198 is too old for C# isolated)",
52
53
  });
53
54
  });
54
55
 
55
- it("keeps the installed func command for supported Core Tools versions", () => {
56
+ it("uses the .NET SDK launch target for C# so extensions load from the output directory", () => {
56
57
  expect(buildFunctionsCoreToolsCommand("csharp", "4.6.0")).toEqual({
57
- command: "func",
58
- argsPrefix: [],
59
- label: "func 4.6.0",
58
+ command: "dotnet",
59
+ argsPrefix: ["run", "--no-launch-profile", "--"],
60
+ label: "dotnet run",
61
+ });
62
+ });
63
+
64
+ it.each(["typescript", "python"] as const)("keeps func start for %s", language => {
65
+ expect(buildFunctionsCoreToolsCommand(language, "4.14.0")).toEqual({
66
+ command: "func", argsPrefix: [], label: "func 4.14.0",
60
67
  });
61
68
  });
62
69
 
@@ -0,0 +1,2 @@
1
+ @echo off
2
+ "%SWALLOWKIT_FIXTURE_NODE%" "%~dp0azure-cli.cjs" %*
@@ -0,0 +1,153 @@
1
+ const fs = require("node:fs");
2
+ const args = process.argv.slice(2);
3
+ const emit = (value) => process.stdout.write(JSON.stringify(value));
4
+
5
+ if (args[0] === "process") {
6
+ const mode = args[1];
7
+ if (mode === "warning" || mode === "slow") {
8
+ console.error("WARNING: fixture Bicep warning");
9
+ if (mode === "slow") setTimeout(() => emit({ done: true }), Number(args[2] ?? 600));
10
+ } else if (mode === "flood") {
11
+ process.stdout.write("x".repeat(2 * 1024 * 1024));
12
+ process.stderr.write("y".repeat(2 * 1024 * 1024));
13
+ } else if (mode === "communication") {
14
+ console.error("ConnectionResetError: fixture communication interrupted");
15
+ process.exitCode = 1;
16
+ } else if (mode === "nonzero") {
17
+ console.error("ERROR: fixture az failure");
18
+ process.exitCode = 7;
19
+ } else if (mode === "echo") {
20
+ emit(args.slice(2));
21
+ } else if (mode === "terminate") {
22
+ process.kill(process.pid, "SIGTERM");
23
+ }
24
+ return;
25
+ }
26
+
27
+ const file = process.env.SWALLOWKIT_AZ_FIXTURE_STATE;
28
+ if (!file) throw new Error("This fixture requires its isolated state file; it never invokes Azure.");
29
+ const state = JSON.parse(fs.readFileSync(file, "utf8"));
30
+ state.calls.push(args);
31
+ const save = () => {
32
+ const temporary = `${file}.tmp-${process.pid}`;
33
+ fs.writeFileSync(temporary, JSON.stringify(state));
34
+ fs.renameSync(temporary, file);
35
+ };
36
+ const value = (flag) => args[args.indexOf(flag) + 1];
37
+ const subscription = state.subscription;
38
+ const resourceGroup = state.resourceGroup;
39
+ const groupId = `/subscriptions/${subscription}/resourceGroups/${resourceGroup}`;
40
+ save();
41
+
42
+ if (args[0] === "--version") {
43
+ process.stdout.write("azure-cli fixture");
44
+ } else if (args[0] === "account" && args[1] === "show") {
45
+ process.stdout.write(subscription);
46
+ } else if (args[0] === "bicep" && args[1] === "build") {
47
+ emit(state.template);
48
+ } else if (args[0] === "group" && args[1] === "exists") {
49
+ process.stdout.write(String(state.groupExists));
50
+ } else if (args[0] === "group" && args[1] === "show") {
51
+ emit({ id: groupId, tags: state.tags });
52
+ } else if (args[0] === "group" && args[1] === "create") {
53
+ state.groupExists = true;
54
+ state.groupCreates++;
55
+ save();
56
+ emit(groupId);
57
+ } else if ((args[0] === "tag" && args[1] === "update") || (args[0] === "group" && args[1] === "update")) {
58
+ state.tagAttempts++;
59
+ const assignment = value(args[0] === "tag" ? "--tags" : "--set").replace(/^tags\./, "");
60
+ const index = assignment.indexOf("=");
61
+ const apply = () => { state.tags[assignment.slice(0, index)] = assignment.slice(index + 1); };
62
+ if (state.failTags > 0) {
63
+ state.failTags--;
64
+ if (state.applyTagBeforeFailure) apply();
65
+ save();
66
+ console.error("ERROR: fixture tag update failed");
67
+ process.exitCode = 9;
68
+ } else {
69
+ apply();
70
+ save();
71
+ }
72
+ } else if (args[0] === "staticwebapp" && args[1] === "secrets" && args[2] === "list") {
73
+ if (state.secretQueryFails) {
74
+ console.error("ERROR: fixture secret query failed");
75
+ process.exitCode = 1;
76
+ } else {
77
+ process.stdout.write(`fixture-swa-token-for-${value("--name")}\n`);
78
+ }
79
+ } else if (args[0] === "webapp" && args[1] === "deployment" && args[2] === "list-publishing-profiles") {
80
+ if (state.secretQueryFails) {
81
+ console.error("ERROR: fixture publish profile query failed");
82
+ process.exitCode = 1;
83
+ } else {
84
+ process.stdout.write(`<publishData><publishProfile publishUrl="${value("--name")}.scm.azurewebsites.net" /></publishData>\n`);
85
+ }
86
+ } else if (args[0] === "deployment" && args[1] === "operation") {
87
+ emit(state.operations ?? []);
88
+ } else if (args[0] === "deployment" && args[2] === "export") {
89
+ emit(state.remoteTemplate ?? state.template);
90
+ } else if (args[0] === "deployment" && args[2] === "what-if") {
91
+ emit({ changes: [] });
92
+ } else if (args[0] === "deployment" && args[2] === "create") {
93
+ const name = value("--name");
94
+ state.deploymentCreates++;
95
+ state.deployments[name] = {
96
+ correlationId: `fixture-correlation-${state.deploymentCreates}`,
97
+ templateHash: "fixture-template-hash",
98
+ };
99
+ save();
100
+ if (state.warning) console.error("WARNING: The configuration value of bicep.use_binary_from_path has been set to 'false'.");
101
+ if (state.submitStderr) console.error(state.submitStderr);
102
+ if (state.submitMode === "communication") {
103
+ console.error("ConnectionResetError: fixture connection closed after acceptance");
104
+ process.exitCode = 1;
105
+ } else if (state.submitMode === "nonzero") {
106
+ console.error("ERROR: fixture submit exited nonzero");
107
+ process.exitCode = 7;
108
+ } else if (state.submitMode === "flood") {
109
+ process.stdout.write(JSON.stringify({ properties: { outputs: { apiKey: { value: "fixture-secret-do-not-print" } } } }));
110
+ process.stdout.write("x".repeat(2 * 1024 * 1024));
111
+ process.stderr.write("y".repeat(2 * 1024 * 1024));
112
+ } else if (state.submitMode === "slow") {
113
+ setTimeout(() => emit({ accepted: true }), state.submitDelay ?? 2000);
114
+ } else {
115
+ emit({ accepted: true });
116
+ }
117
+ } else if (args[0] === "deployment" && args[2] === "show") {
118
+ const name = value("--name");
119
+ const deployment = state.deployments[name];
120
+ if (!deployment || state.queryMode === "not-found") {
121
+ console.error("ERROR: (DeploymentNotFound) fixture deployment is not visible");
122
+ process.exitCode = 3;
123
+ } else if (state.queryMode === "communication" || (state.transientQueryFailures ?? 0) > 0) {
124
+ if (state.transientQueryFailures) { state.transientQueryFailures -= 1; save(); }
125
+ console.error("ConnectionResetError: fixture status query interrupted");
126
+ process.exitCode = 1;
127
+ } else if (state.queryMode === "invalid") {
128
+ process.stdout.write("{invalid JSON");
129
+ } else if (value("--query") === "properties.parameters") {
130
+ emit(state.parameters);
131
+ } else if (value("--query").includes("properties.outputs.")) {
132
+ if (state.outputQueryFails) {
133
+ console.error("ERROR: resource output query failed");
134
+ process.exitCode = 1;
135
+ } else {
136
+ const outputs = state.outputs ?? {};
137
+ emit(Object.fromEntries(["staticWebAppName", "staticWebAppUrl", "functionsAppName", "functionsAppUrl", "cosmosDbAccountName", "cosmosDatabaseName"].map(key => [key, outputs[key] ?? null])));
138
+ }
139
+ } else {
140
+ const states = state.states ?? ["Succeeded"];
141
+ const provisioningState = states.length > 1 ? states.shift() : states[0];
142
+ save();
143
+ emit({
144
+ id: `${groupId}/providers/Microsoft.Resources/deployments/${name}`,
145
+ name, state: provisioningState,
146
+ correlationId: deployment.correlationId, templateHash: deployment.templateHash,
147
+ error: state.error ?? null,
148
+ });
149
+ }
150
+ } else {
151
+ console.error("ERROR: fixture does not support this command");
152
+ process.exitCode = 4;
153
+ }
@@ -0,0 +1,28 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const ts = require("typescript");
4
+ const root = path.resolve(__dirname, "..", "..", "..");
5
+ require.extensions[".ts"] = (module, file) => {
6
+ module._compile(ts.transpileModule(fs.readFileSync(file, "utf8"), {
7
+ compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true },
8
+ fileName: file,
9
+ }).outputText, file);
10
+ };
11
+ const azure = require(path.join(root, "src", "utils", "azure-cli.ts"));
12
+ azure.setAzCliInvocationForTesting({
13
+ command: process.execPath,
14
+ prefixArgs: [path.join(__dirname, "azure-cli.cjs")],
15
+ source: "isolated fixture",
16
+ });
17
+ const provision = require(path.join(root, "src", "core", "operations", "provision-operations.ts"));
18
+ const state = JSON.parse(fs.readFileSync(process.env.SWALLOWKIT_AZ_FIXTURE_STATE, "utf8"));
19
+ void (async () => {
20
+ const plan = await provision.planProvisionOperation({
21
+ resourceGroup: state.resourceGroup, location: "japaneast", swaLocation: "eastasia",
22
+ });
23
+ const result = await provision.executeProvisionPlan(plan, { approve: true });
24
+ console.log(JSON.stringify(result));
25
+ })().catch((error) => {
26
+ console.error(error.message);
27
+ process.exitCode = 1;
28
+ });