swallowkit 2.0.0-alpha.3 → 2.0.0-alpha.5

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 (159) hide show
  1. package/dist/cli/commands/add-auth.js +37 -6
  2. package/dist/cli/commands/add-auth.js.map +1 -1
  3. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  4. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  5. package/dist/cli/commands/configure-mcp.js +42 -0
  6. package/dist/cli/commands/configure-mcp.js.map +1 -0
  7. package/dist/cli/commands/init.d.ts +6 -1
  8. package/dist/cli/commands/init.d.ts.map +1 -1
  9. package/dist/cli/commands/init.js +19 -5
  10. package/dist/cli/commands/init.js.map +1 -1
  11. package/dist/cli/commands/migrations.d.ts.map +1 -1
  12. package/dist/cli/commands/migrations.js +25 -0
  13. package/dist/cli/commands/migrations.js.map +1 -1
  14. package/dist/cli/commands/provision.d.ts +2 -0
  15. package/dist/cli/commands/provision.d.ts.map +1 -1
  16. package/dist/cli/commands/provision.js +45 -16
  17. package/dist/cli/commands/provision.js.map +1 -1
  18. package/dist/cli/commands/scaffold.d.ts +4 -0
  19. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  20. package/dist/cli/commands/scaffold.js +85 -3
  21. package/dist/cli/commands/scaffold.js.map +1 -1
  22. package/dist/cli/index.d.ts +2 -0
  23. package/dist/cli/index.d.ts.map +1 -1
  24. package/dist/cli/index.js +29 -4
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/core/operations/create-model.d.ts +2 -0
  27. package/dist/core/operations/create-model.d.ts.map +1 -1
  28. package/dist/core/operations/create-model.js +60 -8
  29. package/dist/core/operations/create-model.js.map +1 -1
  30. package/dist/core/operations/model-migration.d.ts +22 -0
  31. package/dist/core/operations/model-migration.d.ts.map +1 -0
  32. package/dist/core/operations/model-migration.js +36 -0
  33. package/dist/core/operations/model-migration.js.map +1 -0
  34. package/dist/core/operations/provision-operations.d.ts +19 -1
  35. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  36. package/dist/core/operations/provision-operations.js +62 -35
  37. package/dist/core/operations/provision-operations.js.map +1 -1
  38. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  39. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  40. package/dist/core/operations/scaffold-apply.js +1 -0
  41. package/dist/core/operations/scaffold-apply.js.map +1 -1
  42. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  43. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  44. package/dist/core/operations/scaffold-machine.js.map +1 -1
  45. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  46. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  47. package/dist/core/operations/scaffold-plan.js +3 -1
  48. package/dist/core/operations/scaffold-plan.js.map +1 -1
  49. package/dist/core/project/boundaries.d.ts +5 -2
  50. package/dist/core/project/boundaries.d.ts.map +1 -1
  51. package/dist/core/project/boundaries.js +51 -20
  52. package/dist/core/project/boundaries.js.map +1 -1
  53. package/dist/core/project/capabilities.d.ts +2 -0
  54. package/dist/core/project/capabilities.d.ts.map +1 -1
  55. package/dist/core/project/capabilities.js +29 -5
  56. package/dist/core/project/capabilities.js.map +1 -1
  57. package/dist/core/project/mcp-config.d.ts +26 -0
  58. package/dist/core/project/mcp-config.d.ts.map +1 -0
  59. package/dist/core/project/mcp-config.js +117 -0
  60. package/dist/core/project/mcp-config.js.map +1 -0
  61. package/dist/core/project/react-project.js +1 -1
  62. package/dist/core/project/workflows.d.ts.map +1 -1
  63. package/dist/core/project/workflows.js +23 -5
  64. package/dist/core/project/workflows.js.map +1 -1
  65. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  66. package/dist/core/scaffold/auth-generator.js +18 -19
  67. package/dist/core/scaffold/auth-generator.js.map +1 -1
  68. package/dist/core/scaffold/model-parser.d.ts +23 -0
  69. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  70. package/dist/core/scaffold/model-parser.js +407 -137
  71. package/dist/core/scaffold/model-parser.js.map +1 -1
  72. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  73. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  74. package/dist/core/scaffold/native-schema-generator.js +289 -76
  75. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  76. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  77. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  78. package/dist/core/scaffold/openapi-generator.js +53 -3
  79. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  80. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  81. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  82. package/dist/core/scaffold/ui-generator.js +68 -7
  83. package/dist/core/scaffold/ui-generator.js.map +1 -1
  84. package/dist/core/verify/index.d.ts +4 -3
  85. package/dist/core/verify/index.d.ts.map +1 -1
  86. package/dist/core/verify/index.js +122 -5
  87. package/dist/core/verify/index.js.map +1 -1
  88. package/dist/machine/contracts.d.ts +1 -0
  89. package/dist/machine/contracts.d.ts.map +1 -1
  90. package/dist/machine/errors.d.ts +3 -2
  91. package/dist/machine/errors.d.ts.map +1 -1
  92. package/dist/machine/errors.js +2 -1
  93. package/dist/machine/errors.js.map +1 -1
  94. package/dist/machine/index.d.ts.map +1 -1
  95. package/dist/machine/index.js +42 -17
  96. package/dist/machine/index.js.map +1 -1
  97. package/dist/mcp/index.d.ts +4 -2
  98. package/dist/mcp/index.d.ts.map +1 -1
  99. package/dist/mcp/index.js +154 -15
  100. package/dist/mcp/index.js.map +1 -1
  101. package/dist/mcp/version-selection.d.ts +55 -0
  102. package/dist/mcp/version-selection.d.ts.map +1 -0
  103. package/dist/mcp/version-selection.js +152 -0
  104. package/dist/mcp/version-selection.js.map +1 -0
  105. package/dist/utils/azure-cli.d.ts +44 -0
  106. package/dist/utils/azure-cli.d.ts.map +1 -0
  107. package/dist/utils/azure-cli.js +164 -0
  108. package/dist/utils/azure-cli.js.map +1 -0
  109. package/dist/utils/semver.d.ts +14 -0
  110. package/dist/utils/semver.d.ts.map +1 -0
  111. package/dist/utils/semver.js +61 -0
  112. package/dist/utils/semver.js.map +1 -0
  113. package/package.json +6 -3
  114. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  115. package/src/__tests__/agent-loop-phase34.test.ts +124 -0
  116. package/src/__tests__/azure-cli.test.ts +199 -0
  117. package/src/__tests__/boundaries.test.ts +42 -0
  118. package/src/__tests__/csharp-schema.test.ts +261 -0
  119. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  120. package/src/__tests__/mcp-version.test.ts +262 -0
  121. package/src/__tests__/mcp.test.ts +64 -3
  122. package/src/__tests__/multi-auth.test.ts +115 -4
  123. package/src/__tests__/plan-apply.test.ts +170 -0
  124. package/src/__tests__/schema-analysis.test.ts +329 -0
  125. package/src/cli/commands/add-auth.ts +36 -6
  126. package/src/cli/commands/configure-mcp.ts +54 -0
  127. package/src/cli/commands/init.ts +23 -8
  128. package/src/cli/commands/migrations.ts +22 -0
  129. package/src/cli/commands/provision.ts +53 -17
  130. package/src/cli/commands/scaffold.ts +105 -3
  131. package/src/cli/index.ts +28 -5
  132. package/src/core/operations/create-model.ts +51 -13
  133. package/src/core/operations/model-migration.ts +52 -0
  134. package/src/core/operations/provision-operations.ts +86 -42
  135. package/src/core/operations/scaffold-apply.ts +2 -0
  136. package/src/core/operations/scaffold-machine.ts +1 -0
  137. package/src/core/operations/scaffold-plan.ts +5 -1
  138. package/src/core/project/agent-templates/AGENTS.md +67 -6
  139. package/src/core/project/agent-templates/CLAUDE.md +7 -1
  140. package/src/core/project/agent-templates/copilot-instructions.md +7 -1
  141. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  142. package/src/core/project/boundaries.ts +62 -20
  143. package/src/core/project/capabilities.ts +32 -5
  144. package/src/core/project/mcp-config.ts +84 -0
  145. package/src/core/project/react-project.ts +1 -1
  146. package/src/core/project/workflows.ts +23 -5
  147. package/src/core/scaffold/auth-generator.ts +18 -19
  148. package/src/core/scaffold/model-parser.ts +413 -129
  149. package/src/core/scaffold/native-schema-generator.ts +313 -78
  150. package/src/core/scaffold/openapi-generator.ts +63 -5
  151. package/src/core/scaffold/ui-generator.ts +70 -8
  152. package/src/core/verify/index.ts +118 -6
  153. package/src/machine/contracts.ts +1 -0
  154. package/src/machine/errors.ts +4 -2
  155. package/src/machine/index.ts +45 -20
  156. package/src/mcp/index.ts +173 -18
  157. package/src/mcp/version-selection.ts +163 -0
  158. package/src/utils/azure-cli.ts +154 -0
  159. package/src/utils/semver.ts +59 -0
@@ -0,0 +1,262 @@
1
+ import { spawn } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+ import { compareSemver, isPrerelease, parseSemver } from "../utils/semver";
6
+ import {
7
+ MCP_DELEGATED_FROM_ENV,
8
+ buildPinnedUpdateNotice,
9
+ decideMcpLaunch,
10
+ resolveProjectSwallowKit,
11
+ } from "../mcp/version-selection";
12
+ import { readMcpConfigVersion, resolveMcpPinVersion } from "../core/project/mcp-config";
13
+ import { configureMcp } from "../cli/commands/configure-mcp";
14
+ import { buildSwallowKitMcpProjectConfigSource, selectInitialMcpVersion } from "../cli/commands/init";
15
+ import { getSwallowKitVersion } from "../version";
16
+
17
+ const repositoryRoot = path.resolve(__dirname, "..", "..");
18
+ const builtMcpEntrypoint = path.join(repositoryRoot, "dist", "mcp", "index.js");
19
+ const itWhenBuilt = fs.existsSync(builtMcpEntrypoint) ? it : it.skip;
20
+
21
+ function createProject(version: string | null, options: { mcpScript?: string } = {}): string {
22
+ const project = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-mcp-version-"));
23
+ fs.writeFileSync(path.join(project, "package.json"), JSON.stringify({ name: "sample", devEngines: { packageManager: { name: "pnpm" } } }));
24
+ fs.writeFileSync(path.join(project, ".mcp.json"), buildSwallowKitMcpProjectConfigSource("pnpm"));
25
+ if (version) {
26
+ const root = path.join(project, "node_modules", "swallowkit");
27
+ fs.mkdirSync(path.join(root, "dist", "mcp"), { recursive: true });
28
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({
29
+ name: "swallowkit",
30
+ version,
31
+ bin: { swallowkit: "dist/cli/index.js", "swallowkit-mcp": "dist/mcp/index.js" },
32
+ }));
33
+ fs.writeFileSync(path.join(root, "dist", "mcp", "index.js"), options.mcpScript ?? "");
34
+ }
35
+ return project;
36
+ }
37
+
38
+ describe("semantic version precedence", () => {
39
+ it.each([
40
+ ["2.0.0-alpha.3", "1.1.2", 1],
41
+ ["2.0.0-alpha.3", "2.0.0", -1],
42
+ ["2.0.0", "2.0.0-alpha.3", 1],
43
+ ["2.0.0-alpha.3", "2.0.0-alpha.3", 0],
44
+ ["2.0.0-alpha.3", "2.0.0-alpha.10", -1],
45
+ ["2.0.0-alpha.3", "2.0.0-beta.1", -1],
46
+ ["2.0.0-beta.1", "2.0.0-rc.1", -1],
47
+ ["2.0.0-alpha", "2.0.0-alpha.1", -1],
48
+ ["2.0.0-1", "2.0.0-alpha", -1],
49
+ ["1.10.0", "1.9.9", 1],
50
+ ["1.0.0+build.1", "1.0.0", 0],
51
+ ])("compares %s with %s", (left, right, expected) => {
52
+ expect(compareSemver(left, right)).toBe(expected);
53
+ expect(compareSemver(right, left)).toBe(-expected || 0);
54
+ });
55
+
56
+ it("rejects values that are not semantic versions", () => {
57
+ expect(parseSemver("latest")).toBeNull();
58
+ expect(parseSemver("1.2")).toBeNull();
59
+ expect(parseSemver("01.2.3")).toBeNull();
60
+ expect(compareSemver("latest", "1.0.0")).toBeNull();
61
+ expect(isPrerelease("2.0.0-alpha.3")).toBe(true);
62
+ expect(isPrerelease("2.0.0")).toBe(false);
63
+ });
64
+ });
65
+
66
+ describe("MCP launch version selection", () => {
67
+ const selfPackageRoot = path.join(os.tmpdir(), "published-swallowkit");
68
+ const project = (version: string, entrypoint: string | null = "/project/node_modules/swallowkit/dist/mcp/index.js") => ({
69
+ version,
70
+ packageRoot: "/project/node_modules/swallowkit",
71
+ mcpEntrypoint: entrypoint,
72
+ });
73
+
74
+ it("delegates when the project install is newer than the latest launcher", () => {
75
+ const decision = decideMcpLaunch({
76
+ selfVersion: "1.1.2",
77
+ selfPackageRoot,
78
+ env: { SWALLOWKIT_MCP_VERSION: "latest" },
79
+ project: project("2.0.0-alpha.3"),
80
+ });
81
+ expect(decision).toEqual({
82
+ action: "delegate",
83
+ entrypoint: "/project/node_modules/swallowkit/dist/mcp/index.js",
84
+ fromVersion: "1.1.2",
85
+ toVersion: "2.0.0-alpha.3",
86
+ });
87
+ });
88
+
89
+ it("treats an unset version like latest", () => {
90
+ expect(decideMcpLaunch({ selfVersion: "1.1.2", selfPackageRoot, env: {}, project: project("2.0.0-alpha.3") }).action)
91
+ .toBe("delegate");
92
+ });
93
+
94
+ it.each([
95
+ ["latest caught up with a stable release", "2.0.0", "2.0.0-alpha.3"],
96
+ ["same version", "2.0.0-alpha.3", "2.0.0-alpha.3"],
97
+ ["latest is newer", "2.1.0", "2.0.0"],
98
+ ])("keeps the launcher when %s", (_label, selfVersion, projectVersion) => {
99
+ const decision = decideMcpLaunch({ selfVersion, selfPackageRoot, env: { SWALLOWKIT_MCP_VERSION: "latest" }, project: project(projectVersion) });
100
+ expect(decision.action).toBe("serve");
101
+ if (decision.action !== "serve") return;
102
+ expect(decision.context).toMatchObject({ mcpVersion: selfVersion, projectVersion, selection: "running-version", delegated: false, warnings: [] });
103
+ });
104
+
105
+ it("serves the launcher when SwallowKit is not installed in the project", () => {
106
+ const decision = decideMcpLaunch({ selfVersion: "1.1.2", selfPackageRoot, env: { SWALLOWKIT_MCP_VERSION: "latest" }, project: null });
107
+ expect(decision).toMatchObject({ action: "serve", context: { projectVersion: null, warnings: [], selection: "running-version" } });
108
+ });
109
+
110
+ it("honors an explicit pin and warns, without delegating, when the pin is older than the project", () => {
111
+ const decision = decideMcpLaunch({
112
+ selfVersion: "2.0.0-alpha.3",
113
+ selfPackageRoot,
114
+ env: { SWALLOWKIT_MCP_VERSION: "2.0.0-alpha.3" },
115
+ project: project("2.0.0-alpha.4"),
116
+ execCommand: "pnpm exec",
117
+ });
118
+ expect(decision.action).toBe("serve");
119
+ if (decision.action !== "serve") return;
120
+ expect(decision.context.selection).toBe("pinned");
121
+ expect(decision.context.warnings[0]).toMatch(/^mcp-older-than-project: swallowkit-mcp 2\.0\.0-alpha\.3 is older than the project's swallowkit 2\.0\.0-alpha\.4/);
122
+ expect(decision.context.nextActions.map((action) => action.command)).toEqual([
123
+ "pnpm exec swallowkit machine inspect capabilities",
124
+ "pnpm exec swallowkit configure-mcp",
125
+ ]);
126
+ });
127
+
128
+ it("does not delegate again inside a delegated process", () => {
129
+ const decision = decideMcpLaunch({
130
+ selfVersion: "2.0.0-alpha.3",
131
+ selfPackageRoot,
132
+ env: { SWALLOWKIT_MCP_VERSION: "latest", [MCP_DELEGATED_FROM_ENV]: "1.1.2" },
133
+ project: project("2.0.0-alpha.3"),
134
+ });
135
+ expect(decision).toMatchObject({
136
+ action: "serve",
137
+ context: { selection: "project-version", delegated: true, delegatedFrom: "1.1.2", mcpVersion: "2.0.0-alpha.3" },
138
+ });
139
+ });
140
+
141
+ it("warns instead of delegating when the newer project install has no MCP entrypoint", () => {
142
+ const decision = decideMcpLaunch({ selfVersion: "1.1.2", selfPackageRoot, env: {}, project: project("2.0.0-alpha.3", null) });
143
+ expect(decision.action).toBe("serve");
144
+ if (decision.action === "serve") expect(decision.context.warnings).toHaveLength(1);
145
+ });
146
+
147
+ it("reports a pinned launcher once published latest catches up", () => {
148
+ const context = {
149
+ mcpVersion: "2.0.0-alpha.3", requestedVersion: "2.0.0-alpha.3", projectVersion: "2.0.0-alpha.3",
150
+ selection: "pinned" as const, delegated: false, warnings: [], nextActions: [],
151
+ };
152
+ expect(buildPinnedUpdateNotice(context, "1.1.2")).toBeNull();
153
+ expect(buildPinnedUpdateNotice(context, null)).toBeNull();
154
+ const notice = buildPinnedUpdateNotice(context, "2.0.0", "pnpm exec");
155
+ expect(notice?.warning).toMatch(/pinned to 2\.0\.0-alpha\.3, but published latest is 2\.0\.0/);
156
+ expect(notice?.nextAction.command).toBe("pnpm exec swallowkit configure-mcp");
157
+ expect(buildPinnedUpdateNotice({ ...context, selection: "running-version" }, "2.0.0")).toBeNull();
158
+ });
159
+
160
+ it("finds the project-installed package and its MCP entrypoint", () => {
161
+ const directory = createProject("2.0.0-alpha.3");
162
+ const empty = createProject(null);
163
+ try {
164
+ const install = resolveProjectSwallowKit(directory);
165
+ expect(install?.version).toBe("2.0.0-alpha.3");
166
+ expect(install?.mcpEntrypoint).toBe(path.join(fs.realpathSync(directory), "node_modules", "swallowkit", "dist", "mcp", "index.js"));
167
+ expect(resolveProjectSwallowKit(empty)).toBeNull();
168
+ } finally {
169
+ fs.rmSync(directory, { recursive: true, force: true });
170
+ fs.rmSync(empty, { recursive: true, force: true });
171
+ }
172
+ });
173
+ });
174
+
175
+ describe("MCP launcher version in .mcp.json", () => {
176
+ it.each([
177
+ ["2.0.0-alpha.3", "1.1.2", "2.0.0-alpha.3"],
178
+ ["2.0.0-alpha.3", "2.0.0", "latest"],
179
+ ["2.0.0-alpha.3", "2.0.0-alpha.3", "latest"],
180
+ ["1.1.2", "1.1.2", "latest"],
181
+ ["1.1.1", "1.1.2", "latest"],
182
+ ["2.0.0-alpha.3", null, "2.0.0-alpha.3"],
183
+ ["1.1.2", null, "latest"],
184
+ ])("project %s with published latest %s uses %s", (projectVersion, latest, expected) => {
185
+ expect(resolveMcpPinVersion(projectVersion, latest).version).toBe(expected);
186
+ });
187
+
188
+ it("selects the init value from this CLI version and the registry", () => {
189
+ const current = getSwallowKitVersion();
190
+ expect(selectInitialMcpVersion(() => current).version).toBe("latest");
191
+ expect(selectInitialMcpVersion(() => "0.0.1").version).toBe(current);
192
+ });
193
+
194
+ it("re-selects and preserves the rest of .mcp.json with configure-mcp", () => {
195
+ const directory = createProject("2.0.0-alpha.3");
196
+ try {
197
+ expect(configureMcp({ cwd: directory }, () => "1.1.2")).toMatchObject({ previousVersion: "latest", version: "2.0.0-alpha.3" });
198
+ expect(readMcpConfigVersion(directory)).toBe("2.0.0-alpha.3");
199
+ expect(configureMcp({ cwd: directory }, () => "2.0.0")).toMatchObject({ previousVersion: "2.0.0-alpha.3", version: "latest" });
200
+ expect(configureMcp({ cwd: directory, version: "2.0.0-alpha.3" }).version).toBe("2.0.0-alpha.3");
201
+ expect(configureMcp({ cwd: directory, version: "latest" }).version).toBe("latest");
202
+ expect(() => configureMcp({ cwd: directory, version: "next" })).toThrow(/auto/);
203
+ const config = JSON.parse(fs.readFileSync(path.join(directory, ".mcp.json"), "utf8"));
204
+ expect(config.mcpServers.swallowkit.args).toEqual(["dlx", "--package", "swallowkit@${SWALLOWKIT_MCP_VERSION}", "swallowkit-mcp"]);
205
+ } finally {
206
+ fs.rmSync(directory, { recursive: true, force: true });
207
+ }
208
+ });
209
+ });
210
+
211
+ describe("built MCP entrypoint version delegation", () => {
212
+ const runEntrypoint = (cwd: string, env: NodeJS.ProcessEnv, until = /version: .*selection/) =>
213
+ new Promise<{ stderr: string; code: number | null }>((resolve, reject) => {
214
+ const child = spawn(process.execPath, [builtMcpEntrypoint], {
215
+ cwd,
216
+ env: { ...process.env, SWALLOWKIT_MCP_UPDATE_CHECK: "0", ...env },
217
+ stdio: ["pipe", "pipe", "pipe"],
218
+ });
219
+ let stderr = "";
220
+ const timer = setTimeout(() => child.kill(), 10000);
221
+ child.stderr.on("data", (chunk) => {
222
+ stderr += chunk;
223
+ if (until.test(stderr)) child.kill();
224
+ });
225
+ child.on("error", reject);
226
+ child.on("close", (code) => {
227
+ clearTimeout(timer);
228
+ resolve({ stderr, code });
229
+ });
230
+ });
231
+
232
+ itWhenBuilt("hands the session to a newer project install", async () => {
233
+ const directory = createProject("999.0.0-alpha.1", {
234
+ mcpScript: `process.stderr.write("project-mcp delegatedFrom=" + process.env.${MCP_DELEGATED_FROM_ENV} + "\\n");`,
235
+ });
236
+ try {
237
+ const result = await runEntrypoint(directory, { SWALLOWKIT_MCP_VERSION: "latest" });
238
+ expect(result.stderr).toContain("delegating to the project-installed swallowkit-mcp 999.0.0-alpha.1");
239
+ expect(result.stderr).toContain(`project-mcp delegatedFrom=${getSwallowKitVersion()}`);
240
+ expect(result.code).toBe(0);
241
+ } finally {
242
+ fs.rmSync(directory, { recursive: true, force: true });
243
+ }
244
+ }, 20000);
245
+
246
+ itWhenBuilt("keeps serving when the project install is older or the version is pinned", async () => {
247
+ const older = createProject("0.0.1", { mcpScript: "process.stderr.write('should-not-run');" });
248
+ const newer = createProject("999.0.0", { mcpScript: "process.stderr.write('should-not-run');" });
249
+ try {
250
+ const fromOlder = await runEntrypoint(older, { SWALLOWKIT_MCP_VERSION: "latest" });
251
+ expect(fromOlder.stderr).toContain(`version: ${getSwallowKitVersion()} (selection: running-version)`);
252
+ expect(fromOlder.stderr).not.toContain("should-not-run");
253
+ const pinned = await runEntrypoint(newer, { SWALLOWKIT_MCP_VERSION: getSwallowKitVersion() }, /mcp-older-than-project/);
254
+ expect(pinned.stderr).toContain("(selection: pinned)");
255
+ expect(pinned.stderr).toContain("mcp-older-than-project");
256
+ expect(pinned.stderr).not.toContain("should-not-run");
257
+ } finally {
258
+ fs.rmSync(older, { recursive: true, force: true });
259
+ fs.rmSync(newer, { recursive: true, force: true });
260
+ }
261
+ }, 30000);
262
+ });
@@ -8,6 +8,21 @@ const builtMcpEntrypoint = path.resolve(__dirname, "..", "..", "dist", "mcp", "i
8
8
  const itWhenBuiltEntrypointExists = fs.existsSync(builtMcpEntrypoint) ? it : it.skip;
9
9
 
10
10
  describe("SwallowKit MCP tool definitions", () => {
11
+ it("preserves blocked auth prerequisites and nextActions as structured MCP errors", async () => {
12
+ const nextActions = [{ command: "swallowkit machine plan auth --provider swa", description: "Configure auth first." }];
13
+ const runner = jest.fn(async () => ({
14
+ stdout: JSON.stringify({ ok: false, command: "plan-scaffold", status: "blocked",
15
+ error: { code: "auth-policy-without-auth", message: "Authentication is not configured." }, nextActions }),
16
+ stderr: "", exitCode: 1,
17
+ }));
18
+ const tool = buildSwallowKitToolDefinitions(runner).find(candidate => candidate.name === "swallowkit_plan_scaffold")!;
19
+ const result = await tool.handler({ model: "probe-item" });
20
+ expect(result.isError).toBe(true);
21
+ expect(JSON.parse(result.content[0].text)).toMatchObject({
22
+ status: "blocked", error: { code: "auth-policy-without-auth" }, nextActions,
23
+ metadata: { swallowkitVersion: expect.any(String) },
24
+ });
25
+ });
11
26
  it("delegates inspect_project to the machine CLI", async () => {
12
27
  const runner = jest.fn(async () => ({
13
28
  stdout: JSON.stringify({
@@ -26,10 +41,48 @@ describe("SwallowKit MCP tool definitions", () => {
26
41
  expect(runner).toHaveBeenCalledWith(["inspect", "project"]);
27
42
  expect(JSON.parse(result.content[0].text)).toEqual({
28
43
  manifestSource: "file",
29
- metadata: { swallowkitVersion: expect.stringMatching(/^\d+\.\d+\.\d+/) },
44
+ metadata: {
45
+ swallowkitVersion: expect.stringMatching(/^\d+\.\d+\.\d+/),
46
+ mcp: expect.objectContaining({ selection: "running-version", delegated: false }),
47
+ },
30
48
  });
31
49
  });
32
50
 
51
+ it("reports delegation, the serving version, and version warnings on every tool", async () => {
52
+ const runner = jest.fn(async () => ({
53
+ stdout: JSON.stringify({ ok: true, command: "inspect-entities", data: { entities: [] } }),
54
+ stderr: "",
55
+ exitCode: 0,
56
+ }));
57
+ const context = {
58
+ mcpVersion: "2.0.0-alpha.3",
59
+ requestedVersion: "latest",
60
+ projectVersion: "2.0.0-alpha.4",
61
+ selection: "project-version" as const,
62
+ delegated: true,
63
+ delegatedFrom: "1.1.2",
64
+ warnings: ["mcp-older-than-project: example"],
65
+ nextActions: [{ command: "pnpm exec swallowkit configure-mcp", description: "Re-select." }],
66
+ };
67
+ const tools = buildSwallowKitToolDefinitions(runner as MachineCliRunner, context);
68
+ for (const name of ["swallowkit_inspect_entities", "swallowkit_inspect_capabilities", "swallowkit_inspect_boundaries"]) {
69
+ const parsed = JSON.parse((await tools.find((tool) => tool.name === name)!.handler({})).content[0].text);
70
+ expect(parsed.metadata).toEqual({
71
+ swallowkitVersion: "2.0.0-alpha.3",
72
+ mcp: {
73
+ version: "2.0.0-alpha.3",
74
+ requestedVersion: "latest",
75
+ projectVersion: "2.0.0-alpha.4",
76
+ selection: "project-version",
77
+ delegated: true,
78
+ delegatedFrom: "1.1.2",
79
+ },
80
+ warnings: ["mcp-older-than-project: example"],
81
+ });
82
+ expect(parsed.nextActions).toEqual(context.nextActions);
83
+ }
84
+ });
85
+
33
86
  it("includes the MCP version in validate_project metadata", async () => {
34
87
  const runner = jest.fn(async () => ({
35
88
  stdout: JSON.stringify({ ok: true, command: "validate-project", data: { valid: true } }),
@@ -43,7 +96,7 @@ describe("SwallowKit MCP tool definitions", () => {
43
96
  const result = await tool!.handler({});
44
97
  expect(JSON.parse(result.content[0].text)).toEqual({
45
98
  valid: true,
46
- metadata: { swallowkitVersion: expect.stringMatching(/^\d+\.\d+\.\d+/) },
99
+ metadata: expect.objectContaining({ swallowkitVersion: expect.stringMatching(/^\d+\.\d+\.\d+/) }),
47
100
  });
48
101
  });
49
102
 
@@ -78,7 +131,10 @@ describe("SwallowKit MCP tool definitions", () => {
78
131
  "app/api",
79
132
  "--api-only",
80
133
  ]);
81
- expect(JSON.parse(result.content[0].text)).toEqual({ createdFiles: ["functions/src/todo.ts"] });
134
+ expect(JSON.parse(result.content[0].text)).toEqual({
135
+ createdFiles: ["functions/src/todo.ts"],
136
+ metadata: expect.objectContaining({ swallowkitVersion: expect.any(String) }),
137
+ });
82
138
  });
83
139
 
84
140
  it("exposes the agent-loop tools (plan/apply/artifacts/drift/verify/explain)", () => {
@@ -114,6 +170,11 @@ describe("SwallowKit MCP tool definitions", () => {
114
170
  const result = await tool!.handler({ model: "todo", apiOnly: true });
115
171
 
116
172
  expect(runner).toHaveBeenCalledWith(["plan", "scaffold", "todo", "--api-only"]);
173
+ await tool!.handler({ model: "todo", skipInfra: true });
174
+ expect(runner).toHaveBeenLastCalledWith(["plan", "scaffold", "todo", "--skip-infra"]);
175
+ const applyTool = buildSwallowKitToolDefinitions(runner as MachineCliRunner).find((candidate) => candidate.name === "swallowkit_apply_scaffold");
176
+ await applyTool!.handler({ model: "todo", skipInfra: true });
177
+ expect(runner).toHaveBeenLastCalledWith(["apply", "scaffold", "todo", "--skip-infra"]);
117
178
  const parsed = JSON.parse(result.content[0].text);
118
179
  expect(parsed.status).toBe("requires-human");
119
180
  expect(parsed.planId).toBe("abc");
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as os from "os";
3
3
  import * as path from "path";
4
+ import { spawnSync } from "child_process";
4
5
  import { addAuthCommand } from "../cli/commands/add-auth";
5
6
  import { createUIAuthOptions } from "../cli/commands/scaffold";
6
7
  import {
@@ -9,12 +10,12 @@ import {
9
10
  generateNamedAuthRouterPython,
10
11
  generateNamedAuthRouterTS,
11
12
  } from "../core/scaffold/auth-generator";
12
- import { normalizeAuthConfig } from "../core/config";
13
+ import { normalizeAuthConfig, validateConfig } from "../core/config";
13
14
  import { parseAuthPolicy } from "../core/scaffold/model-parser";
14
15
  import { createBasicModelInfo } from "./fixtures";
15
16
  import { generateCSharpAzureFunctionsCRUD, generateCompactAzureFunctionsCRUD, generatePythonAzureFunctionsCRUD } from "../core/scaffold/functions-generator";
16
17
  import { generateAuthLayout, generateNewPage } from "../core/scaffold/ui-generator";
17
- import { AuthConfig } from "../types";
18
+ import { AuthConfig, SwallowKitConfig } from "../types";
18
19
 
19
20
  const namedAuthConfig: AuthConfig = {
20
21
  schemes: {
@@ -57,10 +58,24 @@ describe("named authentication routers", () => {
57
58
  it("generates valid branch structure for an SWA-only Python router", () => {
58
59
  const swaOnly = normalizeAuthConfig({ schemes: { admin: { provider: "swa", swa: { allowedProviders: ["github"] } } }, authorization: { defaultPolicy: "anonymous", policies: { adminOnly: { schemes: ["admin"] } } } })!;
59
60
  const python = generateNamedAuthRouterPython(swaOnly);
60
- expect(python).toContain(' if PROVIDERS[scheme] == "swa":');
61
- expect(python).not.toContain(' elif PROVIDERS[scheme] == "swa":');
61
+ expect(python).toContain('\n if PROVIDERS[scheme] == "swa":');
62
+ expect(python).not.toContain('\n elif PROVIDERS[scheme] == "swa":');
62
63
  expect(python).toContain("ALLOWED_PROVIDERS");
63
64
  expect(generateNamedAuthRouterCSharp(swaOnly)).toContain("AllowedProviders[scheme]");
65
+ expect(generateNamedAuthRouterCSharp(swaOnly)).not.toContain("using Functions.Auth.Schemes;");
66
+ });
67
+
68
+ const pythonCommand = ["python3", "python"].find(command => spawnSync(command, ["--version"], { encoding: "utf-8" }).status === 0);
69
+ (pythonCommand ? it : it.skip).each([false, true])("compiles named Python auth router syntax (external schemes: %s)", (external) => {
70
+ const config = external ? auth : normalizeAuthConfig({
71
+ schemes: { admin: { provider: "swa" } },
72
+ authorization: { defaultPolicy: "anonymous", policies: { adminOnly: { schemes: ["admin"] } } },
73
+ })!;
74
+ const result = spawnSync(pythonCommand!, ["-c", "import ast,sys; ast.parse(sys.stdin.read())"], {
75
+ encoding: "utf-8", input: generateNamedAuthRouterPython(config),
76
+ });
77
+ expect(result.stderr).toBe("");
78
+ expect(result.status).toBe(0);
64
79
  });
65
80
 
66
81
  it("emits Array.Empty<string>() instead of untyped new[] { } for empty C# arrays (CS0826)", () => {
@@ -157,4 +172,100 @@ describe("add-auth named scheme CLI", () => {
157
172
  new Function("module", content)(configModule);
158
173
  expect((configModule.exports as { auth: { schemes: { admin: { provider: string } } } }).auth.schemes.admin.provider).toBe("swa");
159
174
  });
175
+
176
+ it("writes named schemes to swallowkit.config.json when both config files exist (loadConfig precedence)", async () => {
177
+ const js = "module.exports = { backend: { language: 'typescript' } };\n";
178
+ fs.writeFileSync(path.join(root, "swallowkit.config.js"), js);
179
+ await addAuthCommand({ scheme: "admin", provider: "swa" });
180
+ expect(JSON.parse(fs.readFileSync(path.join(root, "swallowkit.config.json"), "utf-8")).auth.schemes.admin.provider).toBe("swa");
181
+ expect(fs.readFileSync(path.join(root, "swallowkit.config.js"), "utf-8")).toBe(js);
182
+ });
183
+
184
+ it.each(["json", "js"] as const)("disables auth in a %s config with an anonymous default that passes validation", async (format) => {
185
+ fs.rmSync(path.join(root, "swallowkit.config.json"));
186
+ if (format === "json") fs.writeFileSync(path.join(root, "swallowkit.config.json"), JSON.stringify({ backend: { language: "typescript" } }));
187
+ else fs.writeFileSync(path.join(root, "swallowkit.config.js"), "module.exports = {\n backend: { language: 'typescript' },\n}\n");
188
+ await addAuthCommand({ provider: "none" });
189
+ let config: SwallowKitConfig;
190
+ if (format === "json") {
191
+ config = JSON.parse(fs.readFileSync(path.join(root, "swallowkit.config.json"), "utf-8"));
192
+ } else {
193
+ const configModule = { exports: {} as SwallowKitConfig };
194
+ new Function("module", fs.readFileSync(path.join(root, "swallowkit.config.js"), "utf-8"))(configModule);
195
+ config = configModule.exports;
196
+ }
197
+ expect(config.auth).toMatchObject({ provider: "none", authorization: { defaultPolicy: "anonymous" } });
198
+ expect(validateConfig(config)).toEqual({ valid: true, errors: [] });
199
+ });
200
+
201
+ it("records single-provider auth in swallowkit.config.json instead of silently skipping it", async () => {
202
+ fs.writeFileSync(path.join(root, "swallowkit.config.json"), JSON.stringify({ backend: { language: "typescript" } }));
203
+ fs.mkdirSync(path.join(root, "app"), { recursive: true });
204
+ fs.writeFileSync(path.join(root, "app", "layout.tsx"), "export default function L({ children }: any) { return <html><body>{children}</body></html>; }\n");
205
+ await addAuthCommand({ provider: "swa", allowedProviders: ["github"] });
206
+ expect(JSON.parse(fs.readFileSync(path.join(root, "swallowkit.config.json"), "utf-8")).auth).toEqual({
207
+ provider: "swa",
208
+ swa: { allowedProviders: ["github"], roleSource: "swa-roles" },
209
+ authorization: { defaultPolicy: "authenticated" },
210
+ });
211
+ });
212
+
213
+ it.each(["typescript", "csharp", "python"] as const)("declares %s backend dependencies for a named custom-jwt scheme", async (language) => {
214
+ fs.writeFileSync(path.join(root, "swallowkit.config.json"), JSON.stringify({ backend: { language } }));
215
+ fs.mkdirSync(path.join(root, "functions"), { recursive: true });
216
+ if (language === "typescript") fs.writeFileSync(path.join(root, "functions", "package.json"), JSON.stringify({ name: "functions", dependencies: {} }));
217
+ if (language === "csharp") fs.writeFileSync(path.join(root, "functions", "App.Functions.csproj"), "<Project>\n <ItemGroup>\n </ItemGroup>\n</Project>\n");
218
+ if (language === "python") fs.writeFileSync(path.join(root, "functions", "requirements.txt"), "azure-functions\n");
219
+ await addAuthCommand({ scheme: "staff", provider: "custom-jwt" });
220
+ if (language === "typescript") {
221
+ expect(JSON.parse(fs.readFileSync(path.join(root, "functions", "package.json"), "utf-8")).dependencies).toEqual(expect.objectContaining({ jsonwebtoken: expect.any(String), bcryptjs: expect.any(String) }));
222
+ } else if (language === "csharp") {
223
+ const csproj = fs.readFileSync(path.join(root, "functions", "App.Functions.csproj"), "utf-8");
224
+ expect(csproj).toContain('Include="System.IdentityModel.Tokens.Jwt"');
225
+ expect(csproj).toContain('Include="BCrypt.Net-Next"');
226
+ } else {
227
+ expect(fs.readFileSync(path.join(root, "functions", "requirements.txt"), "utf-8")).toMatch(/PyJWT[\s\S]*bcrypt/);
228
+ }
229
+ });
230
+ });
231
+
232
+ describe("Python custom-jwt blueprint registration", () => {
233
+ const originalCwd = process.cwd();
234
+ let root: string;
235
+
236
+ beforeEach(() => {
237
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-py-auth-"));
238
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "fixture" }));
239
+ fs.writeFileSync(path.join(root, "swallowkit.config.json"), JSON.stringify({ backend: { language: "python" } }));
240
+ fs.mkdirSync(path.join(root, "shared"), { recursive: true });
241
+ fs.writeFileSync(path.join(root, "shared", "package.json"), JSON.stringify({ name: "@fixture/shared" }));
242
+ fs.mkdirSync(path.join(root, "functions"), { recursive: true });
243
+ fs.mkdirSync(path.join(root, "app"), { recursive: true });
244
+ fs.writeFileSync(path.join(root, "app", "layout.tsx"), "export default function L({ children }: any) { return <html><body>{children}</body></html>; }\n");
245
+ process.chdir(root);
246
+ });
247
+
248
+ afterEach(() => {
249
+ process.chdir(originalCwd);
250
+ fs.rmSync(root, { recursive: true, force: true });
251
+ });
252
+
253
+ const register = (body: string) => fs.writeFileSync(path.join(root, "functions", "function_app.py"), body);
254
+
255
+ it("imports the blueprint object the generated module actually defines", async () => {
256
+ register("import azure.functions as func\napp = func.FunctionApp()\n# SwallowKit scaffold registrations\n");
257
+ await addAuthCommand({ provider: "custom-jwt" });
258
+ const blueprint = fs.readFileSync(path.join(root, "functions", "blueprints", "auth.py"), "utf-8");
259
+ const app = fs.readFileSync(path.join(root, "functions", "function_app.py"), "utf-8");
260
+ expect(blueprint).toMatch(/^auth_bp = func\.Blueprint\(\)$/m);
261
+ expect(app).toContain("from blueprints.auth import auth_bp\napp.register_blueprint(auth_bp)");
262
+ });
263
+
264
+ it("repairs the broken import written by earlier releases without duplicating the registration", async () => {
265
+ register("import azure.functions as func\napp = func.FunctionApp()\nfrom blueprints.auth import bp as auth_bp\napp.register_blueprint(auth_bp)\n# SwallowKit scaffold registrations\n");
266
+ await addAuthCommand({ provider: "custom-jwt" });
267
+ const app = fs.readFileSync(path.join(root, "functions", "function_app.py"), "utf-8");
268
+ expect(app).not.toContain("import bp as auth_bp");
269
+ expect(app.match(/app\.register_blueprint\(auth_bp\)/g)).toHaveLength(1);
270
+ });
160
271
  });