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.
- package/README.ja.md +14 -0
- package/README.md +14 -0
- package/dist/cli/commands/add-auth.js +37 -6
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +2 -2
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +39 -27
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +8 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +67 -132
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts.map +1 -1
- package/dist/cli/commands/migrations.js +25 -0
- package/dist/cli/commands/migrations.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +5 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +278 -55
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +4 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +85 -3
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +43 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/create-model.d.ts +2 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +60 -8
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/operations/migration-generator.d.ts.map +1 -1
- package/dist/core/operations/migration-generator.js +12 -1
- package/dist/core/operations/migration-generator.js.map +1 -1
- package/dist/core/operations/model-migration.d.ts +22 -0
- package/dist/core/operations/model-migration.d.ts.map +1 -0
- package/dist/core/operations/model-migration.js +36 -0
- package/dist/core/operations/model-migration.js.map +1 -0
- package/dist/core/operations/provision-deployment.d.ts +52 -0
- package/dist/core/operations/provision-deployment.d.ts.map +1 -0
- package/dist/core/operations/provision-deployment.js +164 -0
- package/dist/core/operations/provision-deployment.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +62 -6
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +653 -115
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +4 -1
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +51 -20
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +10 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +38 -6
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/home-page.d.ts +4 -0
- package/dist/core/project/home-page.d.ts.map +1 -0
- package/dist/core/project/home-page.js +113 -0
- package/dist/core/project/home-page.js.map +1 -0
- package/dist/core/project/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/react-project.d.ts +1 -1
- package/dist/core/project/react-project.d.ts.map +1 -1
- package/dist/core/project/react-project.js +46 -25
- package/dist/core/project/react-project.js.map +1 -1
- package/dist/core/project/state.d.ts +7 -0
- package/dist/core/project/state.d.ts.map +1 -1
- package/dist/core/project/state.js +86 -0
- package/dist/core/project/state.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +53 -7
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +26 -27
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +23 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +407 -137
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +294 -91
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +4 -3
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +122 -5
- package/dist/core/verify/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +1 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +3 -2
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +2 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +59 -28
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +6 -4
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +210 -25
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/utils/azure-cli.d.ts +56 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +326 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/azure-diagnostics.d.ts +16 -0
- package/dist/utils/azure-diagnostics.d.ts.map +1 -0
- package/dist/utils/azure-diagnostics.js +68 -0
- package/dist/utils/azure-diagnostics.js.map +1 -0
- package/dist/utils/package-manager.js +2 -2
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +5 -7
- package/dist/utils/python-uv.d.ts.map +1 -1
- package/dist/utils/python-uv.js +50 -24
- package/dist/utils/python-uv.js.map +1 -1
- package/dist/utils/react-dev.d.ts +4 -0
- package/dist/utils/react-dev.d.ts.map +1 -0
- package/dist/utils/react-dev.js +35 -0
- package/dist/utils/react-dev.js.map +1 -0
- package/dist/utils/semver.d.ts +14 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +61 -0
- package/dist/utils/semver.js.map +1 -0
- package/package.json +6 -3
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-instructions.test.ts +5 -0
- package/src/__tests__/agent-loop-phase34.test.ts +143 -8
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/azure-cli.test.ts +202 -0
- package/src/__tests__/boundaries.test.ts +42 -0
- package/src/__tests__/csharp-schema.test.ts +261 -0
- package/src/__tests__/dev.test.ts +13 -6
- package/src/__tests__/fixtures/az wrapper.cmd +2 -0
- package/src/__tests__/fixtures/azure-cli.cjs +153 -0
- package/src/__tests__/fixtures/provision-runner.cjs +28 -0
- package/src/__tests__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/home-page.test.ts +50 -0
- package/src/__tests__/init.test.ts +116 -0
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +120 -3
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/package-manager.test.ts +17 -2
- package/src/__tests__/plan-apply.test.ts +179 -0
- package/src/__tests__/provision-runtime.test.ts +722 -0
- package/src/__tests__/python-uv.test.ts +50 -14
- package/src/__tests__/python-venv.test.ts +48 -0
- package/src/__tests__/react-dev.test.ts +72 -0
- package/src/__tests__/react.test.ts +19 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/add-auth.ts +36 -6
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +40 -31
- package/src/cli/commands/init.ts +73 -138
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +294 -56
- package/src/cli/commands/scaffold.ts +105 -3
- package/src/cli/index.ts +40 -7
- package/src/core/operations/create-model.ts +51 -13
- package/src/core/operations/migration-generator.ts +14 -1
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-deployment.ts +183 -0
- package/src/core/operations/provision-operations.ts +662 -154
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-instructions.ts +4 -1
- package/src/core/project/agent-templates/AGENTS.md +89 -6
- package/src/core/project/agent-templates/CLAUDE.md +14 -1
- package/src/core/project/agent-templates/copilot-instructions.md +15 -1
- package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
- package/src/core/project/boundaries.ts +62 -20
- package/src/core/project/capabilities.ts +49 -6
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +50 -28
- package/src/core/project/state.ts +70 -0
- package/src/core/project/workflows.ts +53 -7
- package/src/core/scaffold/auth-generator.ts +26 -27
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +319 -103
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +118 -6
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +65 -34
- package/src/mcp/index.ts +221 -31
- package/src/mcp/version-selection.ts +163 -0
- package/src/utils/azure-cli.ts +315 -0
- package/src/utils/azure-diagnostics.ts +75 -0
- package/src/utils/package-manager.ts +2 -2
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- package/src/utils/semver.ts +59 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { buildHomePage } from "../core/project/home-page";
|
|
2
|
+
import { adaptReactPage } from "../core/scaffold/react-ui-generator";
|
|
3
|
+
|
|
4
|
+
describe("shared home page presentation", () => {
|
|
5
|
+
it.each(["npm", "pnpm"] as const)(
|
|
6
|
+
"differs only in framework label and routing with %s",
|
|
7
|
+
(pm) => {
|
|
8
|
+
const next = buildHomePage("nextjs", pm);
|
|
9
|
+
const react = buildHomePage("react", pm);
|
|
10
|
+
const adaptedNext = adaptReactPage(next)
|
|
11
|
+
.replace("export const dynamic = 'force-dynamic';\n\n", "")
|
|
12
|
+
.replace("Next.js on Azure", "React on Azure");
|
|
13
|
+
|
|
14
|
+
expect(react).toBe(adaptedNext);
|
|
15
|
+
expect(react).toContain("Welcome to SwallowKit");
|
|
16
|
+
expect(react).toContain("Your Models");
|
|
17
|
+
expect(react).toContain("Get Started");
|
|
18
|
+
expect(react).toContain("Built with SwallowKit");
|
|
19
|
+
expect(react).toContain(
|
|
20
|
+
`${pm === "npm" ? "npx" : "pnpm exec"} swallowkit scaffold shared/models/your-model.ts`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
it("keeps Next.js-only directives out of the SPA", () => {
|
|
26
|
+
const next = buildHomePage("nextjs", "pnpm");
|
|
27
|
+
const react = buildHomePage("react", "pnpm");
|
|
28
|
+
|
|
29
|
+
expect(next).toContain("'use client'");
|
|
30
|
+
expect(next).toContain("export const dynamic = 'force-dynamic'");
|
|
31
|
+
expect(next).toContain("href={model.path}");
|
|
32
|
+
expect(react).not.toMatch(/next\/|use client|force-dynamic/);
|
|
33
|
+
expect(react).toContain("import { Link } from 'react-router-dom'");
|
|
34
|
+
expect(react).toContain("to={model.path}");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it.each(["nextjs", "react"] as const)(
|
|
38
|
+
"preserves connection states and same-origin requests for %s",
|
|
39
|
+
(frontend) => {
|
|
40
|
+
const source = buildHomePage(frontend, "pnpm");
|
|
41
|
+
expect(source).toContain("fetch('/api/greet?name=SwallowKit')");
|
|
42
|
+
expect(source).toContain("disabled={greetingStatus === 'loading'}");
|
|
43
|
+
expect(source).toContain("setGreetingStatus('success')");
|
|
44
|
+
expect(source).toContain("setGreetingStatus('error')");
|
|
45
|
+
expect(source).toContain('role="status"');
|
|
46
|
+
expect(source).toContain('role="alert"');
|
|
47
|
+
expect(source).not.toMatch(/BACKEND_FUNCTIONS_|VITE_/);
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getAzureFunctionsPipeline,
|
|
17
17
|
getGitHubFunctionsWorkflow,
|
|
18
18
|
injectSwallowKitNextConfig,
|
|
19
|
+
ensureNextTailwindPostcssConfig,
|
|
19
20
|
parseIgnoredBuilds,
|
|
20
21
|
buildCosmosDbFreeTierBicepSource,
|
|
21
22
|
buildFunctionsHostKeyBicepExpression,
|
|
@@ -25,6 +26,8 @@ import {
|
|
|
25
26
|
getStaticWebAppSku,
|
|
26
27
|
withNpmLockfileSafeManifests,
|
|
27
28
|
} from "../cli/commands/init";
|
|
29
|
+
import { generateModelContainerMigration, initializeLegacyMigrations } from "../core/operations/migration-generator";
|
|
30
|
+
import { FileOperationSession } from "../core/operations/file-session";
|
|
28
31
|
|
|
29
32
|
describe("injectSwallowKitNextConfig", () => {
|
|
30
33
|
it("adds standalone settings without deprecated experimental options", () => {
|
|
@@ -109,6 +112,13 @@ export default nextConfig;
|
|
|
109
112
|
expect(source).toContain('<None Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />');
|
|
110
113
|
});
|
|
111
114
|
|
|
115
|
+
it("uses a current Cosmos SDK with binary query response fixes", () => {
|
|
116
|
+
const source = buildCSharpFunctionsProjectSource();
|
|
117
|
+
|
|
118
|
+
expect(source).toContain('<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.63.2" />');
|
|
119
|
+
expect(source).not.toContain('Version="3.47.0"');
|
|
120
|
+
});
|
|
121
|
+
|
|
112
122
|
it("builds swallowkit.config.js without a local swallowkit package type import", () => {
|
|
113
123
|
const source = buildSwallowKitConfigSource("typescript");
|
|
114
124
|
|
|
@@ -155,9 +165,43 @@ export default nextConfig;
|
|
|
155
165
|
|
|
156
166
|
expect(devDependencies).toEqual({
|
|
157
167
|
swallowkit: expect.stringMatching(/.+/),
|
|
168
|
+
"@tailwindcss/postcss": "^4.0.0",
|
|
158
169
|
});
|
|
159
170
|
});
|
|
160
171
|
|
|
172
|
+
it("does not add the Next.js PostCSS plugin to React projects", () => {
|
|
173
|
+
expect(buildGeneratedProjectDevDependencies("react")).not.toHaveProperty("@tailwindcss/postcss");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("configures Tailwind for webpack even when create-next-app only configured Turbopack", () => {
|
|
177
|
+
const project = fs.mkdtempSync(path.join(process.cwd(), ".tmp-postcss-"));
|
|
178
|
+
try {
|
|
179
|
+
ensureNextTailwindPostcssConfig(project);
|
|
180
|
+
const config = path.join(project, "postcss.config.mjs");
|
|
181
|
+
expect(fs.readFileSync(config, "utf8")).toContain('"@tailwindcss/postcss": {}');
|
|
182
|
+
fs.writeFileSync(config, "export default { plugins: { custom: {} } };");
|
|
183
|
+
ensureNextTailwindPostcssConfig(project);
|
|
184
|
+
expect(fs.readFileSync(config, "utf8")).toBe("export default { plugins: { custom: {} } };");
|
|
185
|
+
} finally {
|
|
186
|
+
fs.rmSync(project, { recursive: true, force: true });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it.each(["postcss.config.js", "postcss.config.cjs", "postcss.config.json"])(
|
|
191
|
+
"preserves an existing %s",
|
|
192
|
+
file => {
|
|
193
|
+
const project = fs.mkdtempSync(path.join(process.cwd(), ".tmp-postcss-"));
|
|
194
|
+
try {
|
|
195
|
+
fs.writeFileSync(path.join(project, file), "{}");
|
|
196
|
+
ensureNextTailwindPostcssConfig(project);
|
|
197
|
+
expect(fs.existsSync(path.join(project, "postcss.config.mjs"))).toBe(false);
|
|
198
|
+
expect(fs.readFileSync(path.join(project, file), "utf8")).toBe("{}");
|
|
199
|
+
} finally {
|
|
200
|
+
fs.rmSync(project, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
|
|
161
205
|
it("builds a project-scoped MCP config that resolves the configured swallowkit-mcp version", () => {
|
|
162
206
|
const source = buildSwallowKitMcpProjectConfigSource();
|
|
163
207
|
const parsed = JSON.parse(source) as {
|
|
@@ -332,9 +376,81 @@ describe("Infrastructure generation", () => {
|
|
|
332
376
|
});
|
|
333
377
|
},
|
|
334
378
|
);
|
|
379
|
+
|
|
380
|
+
it("derives globally unique Cosmos DB and Function App names from the resource group", async () => {
|
|
381
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-infra-names-"));
|
|
382
|
+
try {
|
|
383
|
+
await createInfrastructure(projectDir, "sample-app", {
|
|
384
|
+
cosmosDbMode: "freetier", vnetOption: "none", swaPlan: "free",
|
|
385
|
+
}, "typescript");
|
|
386
|
+
const mainBicep = fs.readFileSync(path.join(projectDir, "infra", "main.bicep"), "utf-8");
|
|
387
|
+
|
|
388
|
+
expect(mainBicep).toContain("var resourceSuffix = take(uniqueString(resourceGroup().id, projectName), 6)");
|
|
389
|
+
expect(mainBicep).toContain("var cosmosAccountName = 'cosmos-${take(toLower(projectName), 30)}-${resourceSuffix}'");
|
|
390
|
+
expect(mainBicep).toContain("var functionsAppName = 'func-${take(projectName, 40)}-${resourceSuffix}'");
|
|
391
|
+
expect(mainBicep.match(/accountName: cosmosAccountName/g)).toHaveLength(2);
|
|
392
|
+
expect(mainBicep).toContain("name: functionsAppName");
|
|
393
|
+
expect(mainBicep).not.toContain("'cosmos-${projectName}'");
|
|
394
|
+
expect(mainBicep).not.toContain("'func-${projectName}'");
|
|
395
|
+
// Azure limits: Cosmos DB account <= 44 chars, Function App <= 60 chars.
|
|
396
|
+
expect("cosmos-".length + 30 + 1 + 6).toBeLessThanOrEqual(44);
|
|
397
|
+
expect("func-".length + 40 + 1 + 6).toBeLessThanOrEqual(60);
|
|
398
|
+
|
|
399
|
+
const migration = generateModelContainerMigration(
|
|
400
|
+
{ name: "Todo", partitionKey: "/id" },
|
|
401
|
+
new FileOperationSession("commit", projectDir)
|
|
402
|
+
);
|
|
403
|
+
const migrationBicep = fs.readFileSync(path.join(projectDir, migration!.template), "utf-8");
|
|
404
|
+
expect(migrationBicep).toContain(
|
|
405
|
+
"name: 'cosmos-${take(toLower(projectName), 30)}-${take(uniqueString(resourceGroup().id, projectName), 6)}'"
|
|
406
|
+
);
|
|
407
|
+
} finally {
|
|
408
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("keeps the original Cosmos DB account name in model migrations for projects without suffixed names", async () => {
|
|
413
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-infra-legacy-names-"));
|
|
414
|
+
try {
|
|
415
|
+
await createInfrastructure(projectDir, "sample-app", {
|
|
416
|
+
cosmosDbMode: "freetier", vnetOption: "none", swaPlan: "free",
|
|
417
|
+
}, "typescript");
|
|
418
|
+
const mainPath = path.join(projectDir, "infra", "main.bicep");
|
|
419
|
+
const legacyMain = fs.readFileSync(mainPath, "utf-8")
|
|
420
|
+
.replace(/^var (resourceSuffix|cosmosAccountName|functionsAppName) = .*\r?\n/gm, "")
|
|
421
|
+
.replace(/accountName: cosmosAccountName/g, "accountName: 'cosmos-${projectName}'")
|
|
422
|
+
.replace("name: functionsAppName", "name: 'func-${projectName}'");
|
|
423
|
+
fs.writeFileSync(mainPath, legacyMain);
|
|
424
|
+
initializeLegacyMigrations(projectDir);
|
|
425
|
+
|
|
426
|
+
const migration = generateModelContainerMigration(
|
|
427
|
+
{ name: "Todo", partitionKey: "/id" },
|
|
428
|
+
new FileOperationSession("commit", projectDir)
|
|
429
|
+
);
|
|
430
|
+
const migrationBicep = fs.readFileSync(path.join(projectDir, migration!.template), "utf-8");
|
|
431
|
+
expect(migrationBicep).toContain("name: 'cosmos-${projectName}'");
|
|
432
|
+
} finally {
|
|
433
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
434
|
+
}
|
|
435
|
+
});
|
|
335
436
|
});
|
|
336
437
|
|
|
337
438
|
describe("GitHub Actions workflow generation", () => {
|
|
439
|
+
it("keeps Python deployment and both CI providers on the same GA runtime", async () => {
|
|
440
|
+
expect(getGitHubFunctionsWorkflow("npm", "python")).toContain("python-version: '3.14'");
|
|
441
|
+
expect(getAzureFunctionsPipeline("npm", "python")).toContain("versionSpec: '3.14'");
|
|
442
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-python-infra-"));
|
|
443
|
+
try {
|
|
444
|
+
await createInfrastructure(projectDir, "sample-app", {
|
|
445
|
+
cosmosDbMode: "serverless", vnetOption: "none", swaPlan: "free",
|
|
446
|
+
}, "python");
|
|
447
|
+
const source = fs.readFileSync(path.join(projectDir, "infra", "main.bicep"), "utf8");
|
|
448
|
+
expect(source).toContain("functionsRuntimeVersion: '3.14'");
|
|
449
|
+
} finally {
|
|
450
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
338
454
|
it("only enables setup-node caching when no package-manager conversion is needed", () => {
|
|
339
455
|
expect(buildGitHubSwaWorkflow("npm")).toContain("Select Node.js 22 for SWA");
|
|
340
456
|
expect(buildGitHubSwaWorkflow("npm")).toContain("app_build_command: 'npm run build'");
|
|
@@ -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,77 @@ 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.each([
|
|
12
|
+
{ status: "in-progress", outcome: "in-progress", state: "Running" },
|
|
13
|
+
{ status: "blocked", outcome: "partial", state: "Succeeded" },
|
|
14
|
+
])("does not promote $outcome provisioning to complete", async ({ status, outcome, state }) => {
|
|
15
|
+
const nextActions = [{ command: "swallowkit machine inspect provision --plan 123456789abc", description: "Read original state." }];
|
|
16
|
+
const runner = jest.fn(async () => ({
|
|
17
|
+
stdout: JSON.stringify({ ok: true, command: "apply-provision", status, nextActions, data: { outcome, deployment: { state } } }),
|
|
18
|
+
stderr: "", exitCode: 0,
|
|
19
|
+
}));
|
|
20
|
+
const tool = buildSwallowKitToolDefinitions(runner).find(candidate => candidate.name === "swallowkit_apply_provision")!;
|
|
21
|
+
const result = await tool.handler({ planId: "123456789abc", approve: true, waitSeconds: 0 });
|
|
22
|
+
expect(runner).toHaveBeenCalledWith(["apply", "provision", "--plan", "123456789abc", "--approve", "--wait-seconds", "0"]);
|
|
23
|
+
expect(JSON.parse(result.content[0].text)).toMatchObject({ status, outcome, deployment: { state }, nextActions });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it.each([
|
|
27
|
+
{ status: "requires-human", code: "approval-required" },
|
|
28
|
+
{ status: "failed", code: "azure-deployment-failed" },
|
|
29
|
+
])("preserves $code and recovery actions", async ({ status, code }) => {
|
|
30
|
+
const nextActions = [{ command: "swallowkit machine apply provision --plan 123456789abc --approve", description: "Obtain human approval." }];
|
|
31
|
+
const runner = jest.fn(async () => ({
|
|
32
|
+
stdout: JSON.stringify({ ok: false, command: "apply-provision", status, error: { code, message: "Fixture error." }, nextActions }),
|
|
33
|
+
stderr: "", exitCode: 1,
|
|
34
|
+
}));
|
|
35
|
+
const tool = buildSwallowKitToolDefinitions(runner).find(candidate => candidate.name === "swallowkit_apply_provision")!;
|
|
36
|
+
const result = await tool.handler({ planId: "123456789abc" });
|
|
37
|
+
expect(runner).toHaveBeenCalledWith(["apply", "provision", "--plan", "123456789abc"]);
|
|
38
|
+
expect(result.isError).toBe(true);
|
|
39
|
+
expect(JSON.parse(result.content[0].text)).toMatchObject({ status, error: { code }, nextActions });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it.each(["swallowkit_apply_provision", "swallowkit_inspect_provision"])("forwards cancellation and preserves an unknown outcome for %s", async name => {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const runner = jest.fn(async (_args: string[], signal?: AbortSignal) => {
|
|
45
|
+
expect(signal).toBe(controller.signal);
|
|
46
|
+
controller.abort();
|
|
47
|
+
return { stdout: "", stderr: "", exitCode: 143 };
|
|
48
|
+
});
|
|
49
|
+
const tool = buildSwallowKitToolDefinitions(runner).find(candidate => candidate.name === name)!;
|
|
50
|
+
const result = await tool.handler({ planId: "123456789abc", approve: true }, controller.signal);
|
|
51
|
+
expect(result.isError).toBe(true);
|
|
52
|
+
expect(JSON.parse(result.content[0].text)).toMatchObject({
|
|
53
|
+
status: "blocked", error: { code: "provision-interrupted", details: { outcome: "unknown", localFailure: "cancelled" } },
|
|
54
|
+
nextActions: expect.arrayContaining([expect.objectContaining({ command: "swallowkit machine inspect provision --plan 123456789abc" })]),
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("does not launch provision after the MCP request was already cancelled", async () => {
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
controller.abort();
|
|
61
|
+
const runner = jest.fn(async () => ({ stdout: "", stderr: "", exitCode: 1 }));
|
|
62
|
+
const tool = buildSwallowKitToolDefinitions(runner).find(candidate => candidate.name === "swallowkit_apply_provision")!;
|
|
63
|
+
expect((await tool.handler({ planId: "123456789abc", approve: true }, controller.signal)).isError).toBe(true);
|
|
64
|
+
expect(runner).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("preserves blocked auth prerequisites and nextActions as structured MCP errors", async () => {
|
|
68
|
+
const nextActions = [{ command: "swallowkit machine plan auth --provider swa", description: "Configure auth first." }];
|
|
69
|
+
const runner = jest.fn(async () => ({
|
|
70
|
+
stdout: JSON.stringify({ ok: false, command: "plan-scaffold", status: "blocked",
|
|
71
|
+
error: { code: "auth-policy-without-auth", message: "Authentication is not configured." }, nextActions }),
|
|
72
|
+
stderr: "", exitCode: 1,
|
|
73
|
+
}));
|
|
74
|
+
const tool = buildSwallowKitToolDefinitions(runner).find(candidate => candidate.name === "swallowkit_plan_scaffold")!;
|
|
75
|
+
const result = await tool.handler({ model: "probe-item" });
|
|
76
|
+
expect(result.isError).toBe(true);
|
|
77
|
+
expect(JSON.parse(result.content[0].text)).toMatchObject({
|
|
78
|
+
status: "blocked", error: { code: "auth-policy-without-auth" }, nextActions,
|
|
79
|
+
metadata: { swallowkitVersion: expect.any(String) },
|
|
80
|
+
});
|
|
81
|
+
});
|
|
11
82
|
it("delegates inspect_project to the machine CLI", async () => {
|
|
12
83
|
const runner = jest.fn(async () => ({
|
|
13
84
|
stdout: JSON.stringify({
|
|
@@ -26,10 +97,48 @@ describe("SwallowKit MCP tool definitions", () => {
|
|
|
26
97
|
expect(runner).toHaveBeenCalledWith(["inspect", "project"]);
|
|
27
98
|
expect(JSON.parse(result.content[0].text)).toEqual({
|
|
28
99
|
manifestSource: "file",
|
|
29
|
-
metadata: {
|
|
100
|
+
metadata: {
|
|
101
|
+
swallowkitVersion: expect.stringMatching(/^\d+\.\d+\.\d+/),
|
|
102
|
+
mcp: expect.objectContaining({ selection: "running-version", delegated: false }),
|
|
103
|
+
},
|
|
30
104
|
});
|
|
31
105
|
});
|
|
32
106
|
|
|
107
|
+
it("reports delegation, the serving version, and version warnings on every tool", async () => {
|
|
108
|
+
const runner = jest.fn(async () => ({
|
|
109
|
+
stdout: JSON.stringify({ ok: true, command: "inspect-entities", data: { entities: [] } }),
|
|
110
|
+
stderr: "",
|
|
111
|
+
exitCode: 0,
|
|
112
|
+
}));
|
|
113
|
+
const context = {
|
|
114
|
+
mcpVersion: "2.0.0-alpha.3",
|
|
115
|
+
requestedVersion: "latest",
|
|
116
|
+
projectVersion: "2.0.0-alpha.4",
|
|
117
|
+
selection: "project-version" as const,
|
|
118
|
+
delegated: true,
|
|
119
|
+
delegatedFrom: "1.1.2",
|
|
120
|
+
warnings: ["mcp-older-than-project: example"],
|
|
121
|
+
nextActions: [{ command: "pnpm exec swallowkit configure-mcp", description: "Re-select." }],
|
|
122
|
+
};
|
|
123
|
+
const tools = buildSwallowKitToolDefinitions(runner as MachineCliRunner, context);
|
|
124
|
+
for (const name of ["swallowkit_inspect_entities", "swallowkit_inspect_capabilities", "swallowkit_inspect_boundaries"]) {
|
|
125
|
+
const parsed = JSON.parse((await tools.find((tool) => tool.name === name)!.handler({})).content[0].text);
|
|
126
|
+
expect(parsed.metadata).toEqual({
|
|
127
|
+
swallowkitVersion: "2.0.0-alpha.3",
|
|
128
|
+
mcp: {
|
|
129
|
+
version: "2.0.0-alpha.3",
|
|
130
|
+
requestedVersion: "latest",
|
|
131
|
+
projectVersion: "2.0.0-alpha.4",
|
|
132
|
+
selection: "project-version",
|
|
133
|
+
delegated: true,
|
|
134
|
+
delegatedFrom: "1.1.2",
|
|
135
|
+
},
|
|
136
|
+
warnings: ["mcp-older-than-project: example"],
|
|
137
|
+
});
|
|
138
|
+
expect(parsed.nextActions).toEqual(context.nextActions);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
33
142
|
it("includes the MCP version in validate_project metadata", async () => {
|
|
34
143
|
const runner = jest.fn(async () => ({
|
|
35
144
|
stdout: JSON.stringify({ ok: true, command: "validate-project", data: { valid: true } }),
|
|
@@ -43,7 +152,7 @@ describe("SwallowKit MCP tool definitions", () => {
|
|
|
43
152
|
const result = await tool!.handler({});
|
|
44
153
|
expect(JSON.parse(result.content[0].text)).toEqual({
|
|
45
154
|
valid: true,
|
|
46
|
-
metadata: { swallowkitVersion: expect.stringMatching(/^\d+\.\d+\.\d+/) },
|
|
155
|
+
metadata: expect.objectContaining({ swallowkitVersion: expect.stringMatching(/^\d+\.\d+\.\d+/) }),
|
|
47
156
|
});
|
|
48
157
|
});
|
|
49
158
|
|
|
@@ -78,7 +187,10 @@ describe("SwallowKit MCP tool definitions", () => {
|
|
|
78
187
|
"app/api",
|
|
79
188
|
"--api-only",
|
|
80
189
|
]);
|
|
81
|
-
expect(JSON.parse(result.content[0].text)).toEqual({
|
|
190
|
+
expect(JSON.parse(result.content[0].text)).toEqual({
|
|
191
|
+
createdFiles: ["functions/src/todo.ts"],
|
|
192
|
+
metadata: expect.objectContaining({ swallowkitVersion: expect.any(String) }),
|
|
193
|
+
});
|
|
82
194
|
});
|
|
83
195
|
|
|
84
196
|
it("exposes the agent-loop tools (plan/apply/artifacts/drift/verify/explain)", () => {
|
|
@@ -114,6 +226,11 @@ describe("SwallowKit MCP tool definitions", () => {
|
|
|
114
226
|
const result = await tool!.handler({ model: "todo", apiOnly: true });
|
|
115
227
|
|
|
116
228
|
expect(runner).toHaveBeenCalledWith(["plan", "scaffold", "todo", "--api-only"]);
|
|
229
|
+
await tool!.handler({ model: "todo", skipInfra: true });
|
|
230
|
+
expect(runner).toHaveBeenLastCalledWith(["plan", "scaffold", "todo", "--skip-infra"]);
|
|
231
|
+
const applyTool = buildSwallowKitToolDefinitions(runner as MachineCliRunner).find((candidate) => candidate.name === "swallowkit_apply_scaffold");
|
|
232
|
+
await applyTool!.handler({ model: "todo", skipInfra: true });
|
|
233
|
+
expect(runner).toHaveBeenLastCalledWith(["apply", "scaffold", "todo", "--skip-infra"]);
|
|
117
234
|
const parsed = JSON.parse(result.content[0].text);
|
|
118
235
|
expect(parsed.status).toBe("requires-human");
|
|
119
236
|
expect(parsed.planId).toBe("abc");
|