@runfusion/fusion 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +11036 -1992
- package/dist/client/assets/AgentDetailView-B7QRcHJH.css +1 -0
- package/dist/client/assets/AgentDetailView-DwLmRXTY.js +18 -0
- package/dist/client/assets/{AgentsView-D6Zi5zfP.js → AgentsView-D-N6aA0P.js} +12 -7
- package/dist/client/assets/ChatView-DnCdKu8Z.js +1 -0
- package/dist/client/assets/{DevServerView--_WBvIDQ.js → DevServerView-BiA1nYtt.js} +1 -1
- package/dist/client/assets/{DirectoryPicker-xedtR-Rd.js → DirectoryPicker-DvBviDG6.js} +1 -1
- package/dist/client/assets/{DocumentsView-Bg2oaZks.js → DocumentsView-BWXOxpuq.js} +1 -1
- package/dist/client/assets/{EvalsView-B3uOCXfr.js → EvalsView-CJFbtL7i.js} +1 -1
- package/dist/client/assets/{ExperimentalAgentOnboardingModal-Bx6yXVS5.js → ExperimentalAgentOnboardingModal-DuGIPd0B.js} +1 -1
- package/dist/client/assets/InsightsView-BBpRiolN.js +11 -0
- package/dist/client/assets/{MemoryView-xcN_eouf.js → MemoryView-48LuNkKk.js} +2 -2
- package/dist/client/assets/NodesView-CGQWSNZM.js +14 -0
- package/dist/client/assets/{PiExtensionsManager-Cc8aAZXg.js → PiExtensionsManager-i-7UL2oh.js} +2 -2
- package/dist/client/assets/PluginManager-DoSAykD6.js +1 -0
- package/dist/client/assets/{ResearchView-CERNf7sJ.js → ResearchView-XZuRtOxE.js} +1 -1
- package/dist/client/assets/{SettingsModal-Cis-4Lot.css → SettingsModal-Ci0_sqbU.css} +1 -1
- package/dist/client/assets/{SettingsModal-B1r0yASu.js → SettingsModal-CmeF8CN4.js} +1 -1
- package/dist/client/assets/SettingsModal-DBcjf9Bu.js +31 -0
- package/dist/client/assets/SettingsModal-DWKgRxBA.css +1 -0
- package/dist/client/assets/{SetupWizardModal-D1q548_L.js → SetupWizardModal-CgtvpMX9.js} +1 -1
- package/dist/client/assets/{SkillsView-ClLM6u6p.js → SkillsView-DErYRumF.js} +1 -1
- package/dist/client/assets/{StashRecoveryView-ze0pEZ5U.js → StashRecoveryView-QJrNS4Vg.js} +1 -1
- package/dist/client/assets/{TodoView-CTmIfy2M.js → TodoView-BD9NRwq0.js} +2 -2
- package/dist/client/assets/{dashboard-view-CyWN-d02.js → dashboard-view-BWGH_fAq.js} +1 -1
- package/dist/client/assets/dashboard-view-BoTzlP8b.css +1 -0
- package/dist/client/assets/dashboard-view-Ws9_ZnKu.js +21 -0
- package/dist/client/assets/{folder-open-BZuKESeq.js → folder-open-CHSlllzf.js} +1 -1
- package/dist/client/assets/index-DCovGm5b.css +1 -0
- package/dist/client/assets/index-bEwSVl7B.js +692 -0
- package/dist/client/assets/{star-D75YKEq-.js → star-BgVwWAPz.js} +1 -1
- package/dist/client/assets/{upload-BYYTgWFj.js → upload-CAzycxr9.js} +1 -1
- package/dist/client/assets/{users-RS90Aii3.js → users-CZnxCCCJ.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/version.json +1 -1
- package/dist/droid-cli/package.json +1 -1
- package/dist/droid-cli/src/__tests__/index.test.ts +228 -0
- package/dist/extension.js +5517 -1193
- package/dist/pi-claude-cli/package.json +1 -1
- package/dist/pi-claude-cli/src/__tests__/provider.test.ts +36 -22
- package/dist/pi-claude-cli/src/provider.ts +7 -1
- package/dist/plugins/fusion-plugin-cli-printing-press/manifest.json +19 -1
- package/dist/plugins/fusion-plugin-cli-printing-press/package.json +20 -2
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/TestRunnerPanel.test.tsx +99 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/config-flow.test.ts +91 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-view.test.tsx +40 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-views.test.ts +46 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/draft-store.test.ts +50 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/exec-mock.ts +80 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/fixtures.test.ts +40 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/registry.ts +82 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/generator.test.ts +54 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manage-view.test.tsx +98 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manifest.test.ts +21 -5
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/registration.test.ts +29 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/run-routes.test.ts +98 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runner.test.ts +55 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runtime-availability.test.ts +61 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/validation.test.ts +30 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/wizard-routes.test.ts +61 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/workflow-integration.test.ts +19 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.css +43 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.tsx +49 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/generator.ts +95 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/redact.ts +9 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/runner.ts +79 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/types.ts +31 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/index.ts +46 -2
- package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/EditDraftModal.tsx +75 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/useDrafts.ts +73 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.css +79 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.tsx +122 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/routes/wizard-routes.ts +272 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.css +70 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.tsx +98 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/run/useRunGeneratedCli.ts +37 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/__tests__/executor-runtime-env.test.ts +191 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/executor-runtime-env.ts +75 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/storage/draft-store.ts +85 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/cli-press-store.test.ts +128 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/credentials.test.ts +62 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-store.ts +427 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-types.ts +110 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/store/credentials.ts +95 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/steps.tsx +55 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/types.ts +33 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/validation.ts +63 -0
- package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
- package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-reports/manifest.json +10 -0
- package/dist/plugins/fusion-plugin-reports/package.json +18 -2
- package/dist/plugins/fusion-plugin-reports/src/__tests__/approval.test.ts +164 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +14 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/routes-approval.test.ts +109 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/scaffold.test.ts +60 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/share-blocks.test.ts +83 -0
- package/dist/plugins/fusion-plugin-reports/src/aggregation.ts +23 -0
- package/dist/plugins/fusion-plugin-reports/src/approval.ts +97 -0
- package/dist/plugins/fusion-plugin-reports/src/cadence.ts +23 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.css +82 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.tsx +24 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportComparisonDrawer.test.tsx +12 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportDetailPanel.test.tsx +12 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportFiltersBar.test.tsx +14 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportsView.test.tsx +27 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/api.test.ts +19 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReportSectionDiff.test.ts +11 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReports.test.ts +13 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/api.ts +85 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.css +59 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.tsx +58 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportComparisonDrawer.tsx +21 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportDetailPanel.tsx +29 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportEmptyState.tsx +3 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportFiltersBar.tsx +19 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportListItem.tsx +8 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.css +29 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.tsx +43 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ReportApprovalPanel.test.tsx +38 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ShareBlocksPanel.test.tsx +24 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/test-setup.ts +18 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/types.ts +22 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportPreview.ts +44 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportSectionDiff.ts +59 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/useReports.ts +71 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/useViewportMode.ts +13 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard-view.tsx +6 -0
- package/dist/plugins/fusion-plugin-reports/src/index.ts +48 -2
- package/dist/plugins/fusion-plugin-reports/src/pipeline.ts +58 -0
- package/dist/plugins/fusion-plugin-reports/src/render/__tests__/escape.test.ts +20 -0
- package/dist/plugins/fusion-plugin-reports/src/render/__tests__/html-template.test.ts +110 -0
- package/dist/plugins/fusion-plugin-reports/src/render/__tests__/standalone-html.test.ts +66 -0
- package/dist/plugins/fusion-plugin-reports/src/render/escape.ts +12 -0
- package/dist/plugins/fusion-plugin-reports/src/render/html-styles.ts +40 -0
- package/dist/plugins/fusion-plugin-reports/src/render/html-template.ts +137 -0
- package/dist/plugins/fusion-plugin-reports/src/render/index.ts +4 -0
- package/dist/plugins/fusion-plugin-reports/src/render/standalone-html.ts +75 -0
- package/dist/plugins/fusion-plugin-reports/src/report-schema.ts +31 -0
- package/dist/plugins/fusion-plugin-reports/src/routes/__tests__/report-export-routes.test.ts +104 -0
- package/dist/plugins/fusion-plugin-reports/src/routes/report-approval-routes.ts +98 -0
- package/dist/plugins/fusion-plugin-reports/src/routes/report-export-routes.ts +77 -0
- package/dist/plugins/fusion-plugin-reports/src/routes/report-list-routes.ts +72 -0
- package/dist/plugins/fusion-plugin-reports/src/runs-store.ts +69 -0
- package/dist/plugins/fusion-plugin-reports/src/share-blocks.ts +82 -0
- package/dist/plugins/fusion-plugin-reports/src/store/report-store.ts +51 -2
- package/dist/plugins/fusion-plugin-reports/src/store/report-types.ts +6 -1
- package/dist/plugins/fusion-plugin-roadmap/bundled.js +1528 -29391
- package/dist/plugins/fusion-plugin-roadmap/manifest.json +1 -1
- package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
- package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
- package/package.json +1 -1
- package/skill/fusion/references/engine-tools.md +1 -1
- package/skill/fusion/references/extension-tools.md +3 -3
- package/skill/fusion/references/fusion-capabilities.md +1 -1
- package/dist/client/assets/AgentDetailView-BwJaLqZh.css +0 -1
- package/dist/client/assets/AgentDetailView-Cv-vgOj3.js +0 -18
- package/dist/client/assets/ChatView-CAHjY9uO.js +0 -1
- package/dist/client/assets/InsightsView-Q1zvtF4F.js +0 -11
- package/dist/client/assets/NodesView-RxXg58_Q.js +0 -14
- package/dist/client/assets/PluginManager-BEkyBajl.js +0 -1
- package/dist/client/assets/SettingsModal-BLsac7CJ.js +0 -31
- package/dist/client/assets/SettingsModal-BNSrO1M9.css +0 -1
- package/dist/client/assets/dashboard-view-4xAN3yO5.js +0 -21
- package/dist/client/assets/dashboard-view-BkTMSZYn.css +0 -1
- package/dist/client/assets/index-Bdw6llW6.js +0 -692
- package/dist/client/assets/index-CZGlyJuS.css +0 -1
- package/dist/plugins/fusion-plugin-roadmap/bundled.css +0 -1093
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion/pi-claude-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Fusion vendored fork: pi coding-agent extension that routes LLM calls through the Claude Code CLI. Forked from rchern/pi-claude-cli (MIT). See UPSTREAM.md.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": true,
|
|
@@ -1188,28 +1188,42 @@ describe("streamViaCli", { timeout: 90_000 }, () => {
|
|
|
1188
1188
|
expect(doneEvent.message.content).toBeDefined();
|
|
1189
1189
|
});
|
|
1190
1190
|
|
|
1191
|
-
it(
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1191
|
+
it.each([
|
|
1192
|
+
{ code: 0, shouldWarn: false },
|
|
1193
|
+
{ code: 42, shouldWarn: true },
|
|
1194
|
+
])(
|
|
1195
|
+
"FN-3815: routes close stderr logging by exit code=$code",
|
|
1196
|
+
async ({ code, shouldWarn }) => {
|
|
1197
|
+
const model = mockModels[0] as any;
|
|
1198
|
+
const context = {
|
|
1199
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
1200
|
+
};
|
|
1201
|
+
|
|
1202
|
+
const warnSpy = vi.spyOn(console, "warn");
|
|
1203
|
+
|
|
1204
|
+
streamViaCli(model, context);
|
|
1205
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
1206
|
+
|
|
1207
|
+
const proc = (spawn as any).mock.results[0].value;
|
|
1208
|
+
|
|
1209
|
+
proc.stderr.emit("data", Buffer.from("minor warning from cli"));
|
|
1210
|
+
proc.emit("close", code, null);
|
|
1211
|
+
proc.stdout.end();
|
|
1212
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
1213
|
+
|
|
1214
|
+
const stderrWarnCalls = warnSpy.mock.calls.filter(([message]) =>
|
|
1215
|
+
typeof message === "string" &&
|
|
1216
|
+
message.includes("[pi-claude-cli] Claude CLI stderr on close:"),
|
|
1217
|
+
);
|
|
1218
|
+
|
|
1219
|
+
if (shouldWarn) {
|
|
1220
|
+
expect(stderrWarnCalls).toHaveLength(1);
|
|
1221
|
+
expect(stderrWarnCalls[0]?.[0]).toContain("minor warning from cli");
|
|
1222
|
+
} else {
|
|
1223
|
+
expect(stderrWarnCalls).toHaveLength(0);
|
|
1224
|
+
}
|
|
1225
|
+
},
|
|
1226
|
+
);
|
|
1213
1227
|
|
|
1214
1228
|
it("warns when subprocess closes successfully with no content events", async () => {
|
|
1215
1229
|
const model = mockModels[0] as any;
|
|
@@ -252,7 +252,13 @@ export function streamViaCli(
|
|
|
252
252
|
if (broken) return; // Break-early kill, expected
|
|
253
253
|
const stderr = getStderr().trim();
|
|
254
254
|
if (stderr) {
|
|
255
|
-
|
|
255
|
+
if (code === 0 || code === null) {
|
|
256
|
+
// FN-3815: Claude CLI writes benign MCP bring-up diagnostics to stderr
|
|
257
|
+
// on clean/abort shutdown; keep these debug-only to avoid false TUI warnings.
|
|
258
|
+
debugLog(`Claude CLI stderr on close (clean exit): ${stderr}`);
|
|
259
|
+
} else {
|
|
260
|
+
console.warn(`[pi-claude-cli] Claude CLI stderr on close: ${stderr}`);
|
|
261
|
+
}
|
|
256
262
|
}
|
|
257
263
|
if (code !== 0 && code !== null) {
|
|
258
264
|
const message = stderr
|
|
@@ -2,5 +2,23 @@
|
|
|
2
2
|
"id": "fusion-plugin-cli-printing-press",
|
|
3
3
|
"name": "CLI Printing Press",
|
|
4
4
|
"version": "0.1.0",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Guided wizard for drafting external service CLI definitions",
|
|
6
|
+
"dashboardViews": [
|
|
7
|
+
{
|
|
8
|
+
"viewId": "wizard",
|
|
9
|
+
"label": "Create Service CLI",
|
|
10
|
+
"componentPath": "./dashboard-view",
|
|
11
|
+
"icon": "Wand2",
|
|
12
|
+
"placement": "primary",
|
|
13
|
+
"order": 60
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"viewId": "manage",
|
|
17
|
+
"label": "Manage Service CLIs",
|
|
18
|
+
"componentPath": "./manage-view",
|
|
19
|
+
"icon": "List",
|
|
20
|
+
"placement": "primary",
|
|
21
|
+
"order": 61
|
|
22
|
+
}
|
|
23
|
+
]
|
|
6
24
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion-plugin-examples/cli-printing-press",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI Printing Press plugin package for Fusion",
|
|
6
6
|
"private": true,
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
".": {
|
|
9
9
|
"types": "./src/index.ts",
|
|
10
10
|
"import": "./src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"./dashboard-view": {
|
|
13
|
+
"types": "./src/dashboard-view.tsx",
|
|
14
|
+
"import": "./src/dashboard-view.tsx"
|
|
15
|
+
},
|
|
16
|
+
"./manage-view": {
|
|
17
|
+
"types": "./src/manage-view.tsx",
|
|
18
|
+
"import": "./src/manage-view.tsx"
|
|
11
19
|
}
|
|
12
20
|
},
|
|
13
21
|
"scripts": {
|
|
@@ -16,10 +24,20 @@
|
|
|
16
24
|
},
|
|
17
25
|
"dependencies": {
|
|
18
26
|
"@fusion/core": "workspace:*",
|
|
19
|
-
"@fusion/
|
|
27
|
+
"@fusion/dashboard": "workspace:*",
|
|
28
|
+
"@fusion/plugin-sdk": "workspace:*",
|
|
29
|
+
"express": "^5.1.0",
|
|
30
|
+
"lucide-react": "^0.542.0",
|
|
31
|
+
"react": "^19.0.0",
|
|
32
|
+
"react-dom": "^19.2.4"
|
|
20
33
|
},
|
|
21
34
|
"devDependencies": {
|
|
35
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
36
|
+
"@testing-library/react": "^16.3.2",
|
|
37
|
+
"@testing-library/user-event": "^14.6.1",
|
|
38
|
+
"@types/express": "^5.0.5",
|
|
22
39
|
"@types/node": "^25.5.2",
|
|
40
|
+
"@types/react": "^19.0.0",
|
|
23
41
|
"typescript": "^5.7.0",
|
|
24
42
|
"vitest": "^3.2.4"
|
|
25
43
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { CliPrintingPressTestRunner } from "../run/TestRunnerPanel";
|
|
6
|
+
import type { ServiceDraft } from "../wizard/types";
|
|
7
|
+
|
|
8
|
+
vi.mock("lucide-react", () => ({
|
|
9
|
+
Play: () => null,
|
|
10
|
+
RefreshCw: () => null,
|
|
11
|
+
CheckCircle2: () => null,
|
|
12
|
+
AlertTriangle: () => null,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
function makeDraft(): ServiceDraft {
|
|
16
|
+
const now = new Date().toISOString();
|
|
17
|
+
return {
|
|
18
|
+
id: "draft-1",
|
|
19
|
+
name: "Demo",
|
|
20
|
+
slug: "demo",
|
|
21
|
+
description: "",
|
|
22
|
+
baseUrl: "https://example.com",
|
|
23
|
+
transport: "http",
|
|
24
|
+
endpoints: [{ id: "e1", name: "Ping", method: "GET", path: "/ping", params: "q" }],
|
|
25
|
+
credential: { kind: "none" },
|
|
26
|
+
createdAt: now,
|
|
27
|
+
updatedAt: now,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("CliPrintingPressTestRunner", () => {
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
cleanup();
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("submits run request and renders output states", async () => {
|
|
38
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
39
|
+
const url = String(input);
|
|
40
|
+
if (url.endsWith("/run")) {
|
|
41
|
+
return {
|
|
42
|
+
ok: true,
|
|
43
|
+
json: async () => ({ stdout: "ok", stderr: "", exitCode: 0, durationMs: 12, timedOut: false, argv: ["demo.mjs", "--endpoint", "e1", "--q", "***"] }),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (url.endsWith("/regenerate")) {
|
|
47
|
+
return { ok: true, json: async () => ({ draft: makeDraft(), artifact: { draftId: "draft-1", slug: "demo", binPath: "/tmp/demo.mjs", entrypoint: "node", generatedAt: new Date().toISOString() } }) };
|
|
48
|
+
}
|
|
49
|
+
return { ok: false, status: 404, json: async () => ({ error: "missing" }) };
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
53
|
+
|
|
54
|
+
const user = userEvent.setup();
|
|
55
|
+
render(<CliPrintingPressTestRunner draftId="draft-1" draft={makeDraft()} />);
|
|
56
|
+
|
|
57
|
+
expect(screen.getByRole("combobox")).toBeTruthy();
|
|
58
|
+
await user.type(screen.getByLabelText("q"), "hello");
|
|
59
|
+
await user.type(screen.getByPlaceholderText("api_key"), "secret-value");
|
|
60
|
+
await user.click(screen.getByRole("button", { name: /^Run$/i }));
|
|
61
|
+
|
|
62
|
+
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining("/run"), expect.objectContaining({ method: "POST" })));
|
|
63
|
+
const runCall = fetchMock.mock.calls.find((call) => String(call[0]).endsWith("/run"));
|
|
64
|
+
expect(runCall).toBeTruthy();
|
|
65
|
+
expect(JSON.parse(String((runCall?.[1] as RequestInit).body))).toMatchObject({ endpointId: "e1", params: { q: "hello" } });
|
|
66
|
+
expect(await screen.findByText("ok")).toBeTruthy();
|
|
67
|
+
expect(screen.queryByText("secret-value")).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("renders error and timeout output states", async () => {
|
|
71
|
+
let calls = 0;
|
|
72
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
|
73
|
+
const url = String(input);
|
|
74
|
+
if (url.endsWith("/run")) {
|
|
75
|
+
calls += 1;
|
|
76
|
+
if (calls === 1) {
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
json: async () => ({ stdout: "", stderr: "boom", exitCode: 1, durationMs: 22, timedOut: false, argv: ["demo.mjs"] }),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
ok: true,
|
|
84
|
+
json: async () => ({ stdout: "", stderr: "", exitCode: null, durationMs: 33, timedOut: true, argv: ["demo.mjs"] }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return { ok: false, status: 404, json: async () => ({ error: "missing" }) };
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
91
|
+
const user = userEvent.setup();
|
|
92
|
+
render(<CliPrintingPressTestRunner draftId="draft-1" draft={makeDraft()} />);
|
|
93
|
+
|
|
94
|
+
await user.click(screen.getByRole("button", { name: /^Run$/i }));
|
|
95
|
+
expect(await screen.findByText("boom")).toBeTruthy();
|
|
96
|
+
await user.click(screen.getByRole("button", { name: /^Run$/i }));
|
|
97
|
+
expect(await screen.findByText("Failed")).toBeTruthy();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { makeFakeRegistry } from "./fixtures/registry.js";
|
|
3
|
+
import { InvalidCredentialPlacementError, OAuthNotSupportedError } from "../store/cli-press-types.js";
|
|
4
|
+
|
|
5
|
+
describe("cli press store config flow", () => {
|
|
6
|
+
it("round-trips service/spec/settings CRUD", () => {
|
|
7
|
+
const h = makeFakeRegistry();
|
|
8
|
+
try {
|
|
9
|
+
const created = h.store.createService({
|
|
10
|
+
slug: "gamma",
|
|
11
|
+
displayName: "Gamma Service",
|
|
12
|
+
description: "gamma",
|
|
13
|
+
baseUrl: "https://gamma.example.com",
|
|
14
|
+
sourceKind: "manual",
|
|
15
|
+
});
|
|
16
|
+
expect(h.store.getService(created.id)?.slug).toBe("gamma");
|
|
17
|
+
|
|
18
|
+
const spec = h.store.createSpec({
|
|
19
|
+
serviceId: created.id,
|
|
20
|
+
name: "gamma-cli",
|
|
21
|
+
version: "1.0.0",
|
|
22
|
+
generatorVersion: "cli-printing-press",
|
|
23
|
+
specJson: JSON.stringify({ id: created.id, regeneratedAt: "before" }),
|
|
24
|
+
status: "draft",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const updated = h.store.updateSpec(spec.id, {
|
|
28
|
+
status: "generated",
|
|
29
|
+
generatedAt: new Date().toISOString(),
|
|
30
|
+
specJson: JSON.stringify({ id: created.id, regeneratedAt: "after", artifactPath: "/tmp/gamma" }),
|
|
31
|
+
});
|
|
32
|
+
expect(updated.status).toBe("generated");
|
|
33
|
+
expect(JSON.parse(updated.specJson)).toMatchObject({ regeneratedAt: "after" });
|
|
34
|
+
|
|
35
|
+
const setting = h.store.setSetting({
|
|
36
|
+
serviceId: created.id,
|
|
37
|
+
key: "runner.timeoutMs",
|
|
38
|
+
value: "120000",
|
|
39
|
+
scope: "wizard",
|
|
40
|
+
});
|
|
41
|
+
expect(h.store.listSettings(created.id).find((entry) => entry.id === setting.id)?.value).toBe("120000");
|
|
42
|
+
|
|
43
|
+
h.store.deleteSpec(spec.id);
|
|
44
|
+
expect(h.store.getSpec(spec.id)).toBeUndefined();
|
|
45
|
+
h.store.deleteService(created.id);
|
|
46
|
+
expect(h.store.getService(created.id)).toBeUndefined();
|
|
47
|
+
} finally {
|
|
48
|
+
h.cleanup();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("stores credential values encoded and does not expose raw secrets", () => {
|
|
53
|
+
const h = makeFakeRegistry();
|
|
54
|
+
try {
|
|
55
|
+
const acme = h.services.acme;
|
|
56
|
+
const credential = h.store.listCredentials(acme.id)[0];
|
|
57
|
+
expect(credential.value).toMatchObject({ encoding: "base64" });
|
|
58
|
+
expect(JSON.stringify(credential.value)).not.toContain("acme-secret");
|
|
59
|
+
} finally {
|
|
60
|
+
h.cleanup();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("rejects oauth credentials and mismatched placement", () => {
|
|
65
|
+
const h = makeFakeRegistry();
|
|
66
|
+
try {
|
|
67
|
+
const serviceId = h.services.acme.id;
|
|
68
|
+
expect(() =>
|
|
69
|
+
h.store.createCredential({
|
|
70
|
+
serviceId,
|
|
71
|
+
name: "oauth",
|
|
72
|
+
kind: "oauth",
|
|
73
|
+
placement: { kind: "oauth", provider: "acme" } as never,
|
|
74
|
+
value: { encoding: "base64", value: "abc" },
|
|
75
|
+
} as never),
|
|
76
|
+
).toThrow(OAuthNotSupportedError);
|
|
77
|
+
|
|
78
|
+
expect(() =>
|
|
79
|
+
h.store.createCredential({
|
|
80
|
+
serviceId,
|
|
81
|
+
name: "bad-placement",
|
|
82
|
+
kind: "header",
|
|
83
|
+
placement: { kind: "query_param", queryParam: "token" } as never,
|
|
84
|
+
value: { encoding: "base64", value: "abc" },
|
|
85
|
+
} as never),
|
|
86
|
+
).toThrow(InvalidCredentialPlacementError);
|
|
87
|
+
} finally {
|
|
88
|
+
h.cleanup();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { CliPrintingPressWizardView } from "../dashboard-view";
|
|
6
|
+
|
|
7
|
+
vi.mock("lucide-react", () => ({ Wand2: () => null }));
|
|
8
|
+
|
|
9
|
+
describe("CliPrintingPressWizardView", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, json: async () => ({ id: "draft-1" }) })));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("walks steps, gates validation, and posts draft", async () => {
|
|
15
|
+
const user = userEvent.setup();
|
|
16
|
+
render(<CliPrintingPressWizardView />);
|
|
17
|
+
|
|
18
|
+
const next = screen.getByRole("button", { name: "Next" });
|
|
19
|
+
expect((next as HTMLButtonElement).disabled).toBe(true);
|
|
20
|
+
|
|
21
|
+
await user.type(screen.getByLabelText("Name"), "GitHub");
|
|
22
|
+
await user.type(screen.getByLabelText("Slug"), "github");
|
|
23
|
+
await user.type(screen.getByLabelText("Base URL"), "https://api.github.com");
|
|
24
|
+
expect((next as HTMLButtonElement).disabled).toBe(false);
|
|
25
|
+
|
|
26
|
+
await user.click(next);
|
|
27
|
+
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
28
|
+
|
|
29
|
+
await user.type(screen.getByPlaceholderText("Name"), "List Repos");
|
|
30
|
+
await user.type(screen.getByPlaceholderText("/path"), "/user/repos");
|
|
31
|
+
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
32
|
+
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
33
|
+
|
|
34
|
+
await user.click(screen.getByRole("button", { name: "Save draft" }));
|
|
35
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
36
|
+
const [, options] = (fetch as any).mock.calls[0];
|
|
37
|
+
expect(JSON.parse(options.body)).toMatchObject({ name: "GitHub", slug: "github", baseUrl: "https://api.github.com", transport: "http" });
|
|
38
|
+
expect(await screen.findByText(/Saved — draft id draft-1/)).toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createCliPrintingPressRoutes } from "../routes/wizard-routes.js";
|
|
3
|
+
import { makeFakeRegistry } from "./fixtures/registry.js";
|
|
4
|
+
|
|
5
|
+
function route(method: string, path: string) {
|
|
6
|
+
const found = createCliPrintingPressRoutes().find((entry) => entry.method === method && entry.path === path);
|
|
7
|
+
if (!found) throw new Error(`missing route ${method} ${path}`);
|
|
8
|
+
return found;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("dashboard API route contracts", () => {
|
|
12
|
+
it("defines expected wizard/list/detail/run endpoints", () => {
|
|
13
|
+
const routes = createCliPrintingPressRoutes();
|
|
14
|
+
expect(routes.map((entry) => `${entry.method} ${entry.path}`)).toEqual(
|
|
15
|
+
expect.arrayContaining([
|
|
16
|
+
"POST /drafts",
|
|
17
|
+
"GET /drafts",
|
|
18
|
+
"GET /drafts/:id",
|
|
19
|
+
"PUT /drafts/:id",
|
|
20
|
+
"POST /drafts/:id/regenerate",
|
|
21
|
+
"POST /drafts/:id/run",
|
|
22
|
+
]),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("supports happy and error API paths", async () => {
|
|
27
|
+
const h = makeFakeRegistry();
|
|
28
|
+
try {
|
|
29
|
+
const ctx = { taskStore: { getRootDir: () => h.rootDir, getDatabase: () => h.db } } as any;
|
|
30
|
+
const listRes = await route("GET", "/drafts").handler({ params: {} }, ctx);
|
|
31
|
+
expect(listRes.status).toBe(200);
|
|
32
|
+
|
|
33
|
+
const missRes = await route("GET", "/drafts/:id").handler({ params: { id: "missing" } }, ctx);
|
|
34
|
+
expect(missRes.status).toBe(404);
|
|
35
|
+
|
|
36
|
+
const badRun = await route("POST", "/drafts/:id/run").handler({ params: { id: h.services.acme.id }, body: { endpointId: "", params: {} } }, ctx);
|
|
37
|
+
expect(badRun.status).toBe(400);
|
|
38
|
+
} finally {
|
|
39
|
+
h.cleanup();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("documents plugin-prefixed mount contract", () => {
|
|
44
|
+
expect("/api/plugins/cli-printing-press/drafts").toContain("/api/plugins/cli-printing-press/");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mkdtemp, readdir } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { createDraftStore, NotFoundError } from "../storage/draft-store";
|
|
6
|
+
import type { ServiceDraft } from "../wizard/types";
|
|
7
|
+
|
|
8
|
+
function makeDraft(): ServiceDraft {
|
|
9
|
+
const now = new Date().toISOString();
|
|
10
|
+
return { id: "", name: "Demo", slug: "demo", description: "", baseUrl: "https://example.com", transport: "http", endpoints: [{ id: "e1", name: "Ping", method: "GET", path: "/ping" }], credential: { kind: "none" }, createdAt: now, updatedAt: now };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("draft store", () => {
|
|
14
|
+
it("creates, lists, gets, and deletes drafts", async () => {
|
|
15
|
+
const rootDir = await mkdtemp(join(tmpdir(), "cli-printing-press-"));
|
|
16
|
+
const store = createDraftStore({ rootDir });
|
|
17
|
+
const created = await store.create(makeDraft());
|
|
18
|
+
expect(created.id).toBeTruthy();
|
|
19
|
+
const list = await store.list();
|
|
20
|
+
expect(list).toHaveLength(1);
|
|
21
|
+
expect(await store.get(created.id)).toMatchObject({ id: created.id, slug: "demo" });
|
|
22
|
+
await store.delete(created.id);
|
|
23
|
+
expect(await store.get(created.id)).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("updates an existing draft and replaces endpoints", async () => {
|
|
27
|
+
const rootDir = await mkdtemp(join(tmpdir(), "cli-printing-press-"));
|
|
28
|
+
const store = createDraftStore({ rootDir });
|
|
29
|
+
const created = await store.create(makeDraft());
|
|
30
|
+
|
|
31
|
+
const updated = await store.update(created.id, {
|
|
32
|
+
name: "Renamed",
|
|
33
|
+
endpoints: [{ id: "e2", name: "Health", method: "GET", path: "/health" }],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(updated.name).toBe("Renamed");
|
|
37
|
+
expect(updated.endpoints).toHaveLength(1);
|
|
38
|
+
expect(updated.endpoints[0]?.id).toBe("e2");
|
|
39
|
+
expect(updated.updatedAt).not.toBe(created.updatedAt);
|
|
40
|
+
|
|
41
|
+
const draftFiles = await readdir(join(rootDir, ".fusion", "plugins", "cli-printing-press", "drafts"));
|
|
42
|
+
expect(draftFiles.some((entry) => entry.includes(".tmp-"))).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("throws NotFoundError on update for unknown ids", async () => {
|
|
46
|
+
const rootDir = await mkdtemp(join(tmpdir(), "cli-printing-press-"));
|
|
47
|
+
const store = createDraftStore({ rootDir });
|
|
48
|
+
await expect(store.update("missing", { name: "Nope" })).rejects.toBeInstanceOf(NotFoundError);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { expect, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
type ExecResult = {
|
|
5
|
+
stdout?: string;
|
|
6
|
+
stderr?: string;
|
|
7
|
+
code?: number;
|
|
8
|
+
timeoutAfterMs?: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type ExecCall = {
|
|
12
|
+
command: string;
|
|
13
|
+
options: Record<string, unknown>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const calls: ExecCall[] = [];
|
|
17
|
+
const queue: ExecResult[] = [];
|
|
18
|
+
|
|
19
|
+
vi.mock("node:child_process", () => {
|
|
20
|
+
const execMock = vi.fn((command: string, options: Record<string, unknown>, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
|
|
21
|
+
calls.push({ command, options });
|
|
22
|
+
const next = queue.shift() ?? { stdout: "", stderr: "", code: 0 };
|
|
23
|
+
if (next.timeoutAfterMs) {
|
|
24
|
+
const err = new Error(`Command timed out after ${next.timeoutAfterMs}ms`) as Error & { killed?: boolean; signal?: string };
|
|
25
|
+
err.killed = true;
|
|
26
|
+
err.signal = "SIGTERM";
|
|
27
|
+
callback(err, next.stdout ?? "", next.stderr ?? "");
|
|
28
|
+
return { kill: () => true };
|
|
29
|
+
}
|
|
30
|
+
if ((next.code ?? 0) !== 0) {
|
|
31
|
+
const err = new Error(next.stderr || `Command failed with code ${next.code}`) as Error & {
|
|
32
|
+
code?: number;
|
|
33
|
+
stdout?: string;
|
|
34
|
+
stderr?: string;
|
|
35
|
+
};
|
|
36
|
+
err.code = next.code;
|
|
37
|
+
err.stdout = next.stdout ?? "";
|
|
38
|
+
err.stderr = next.stderr ?? "";
|
|
39
|
+
callback(err, next.stdout ?? "", next.stderr ?? "");
|
|
40
|
+
return { kill: () => true };
|
|
41
|
+
}
|
|
42
|
+
callback(null, next.stdout ?? "", next.stderr ?? "");
|
|
43
|
+
return { kill: () => true };
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
execMock[Symbol.for("nodejs.util.promisify.custom")] = (command: string, options: Record<string, unknown>) =>
|
|
47
|
+
new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
|
48
|
+
execMock(command, options, (error: Error | null, stdout: string, stderr: string) => {
|
|
49
|
+
if (error) {
|
|
50
|
+
reject(error);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
resolve({ stdout, stderr });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
exec: execMock,
|
|
59
|
+
execSync: vi.fn(() => {
|
|
60
|
+
throw new Error("execSync should never be called");
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export function installExecMock() {
|
|
66
|
+
calls.length = 0;
|
|
67
|
+
queue.length = 0;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
setNextResult(result: ExecResult) {
|
|
71
|
+
queue.push(result);
|
|
72
|
+
},
|
|
73
|
+
getCalls() {
|
|
74
|
+
return [...calls];
|
|
75
|
+
},
|
|
76
|
+
assertExecSyncUnused() {
|
|
77
|
+
expect(execSync).not.toHaveBeenCalled();
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { promisify } from "node:util";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { makeFakeRegistry } from "./registry.js";
|
|
4
|
+
import { installExecMock } from "./exec-mock.js";
|
|
5
|
+
|
|
6
|
+
describe("test fixtures", () => {
|
|
7
|
+
it("creates seeded fake registry", () => {
|
|
8
|
+
const registry = makeFakeRegistry();
|
|
9
|
+
try {
|
|
10
|
+
expect(registry.store.listServices().map((s) => s.slug).sort()).toEqual(["acme", "beta"]);
|
|
11
|
+
expect(registry.store.listArtifacts(registry.specs.acme.id)).toHaveLength(1);
|
|
12
|
+
expect(registry.store.listArtifacts(registry.specs.beta.id)).toHaveLength(0);
|
|
13
|
+
} finally {
|
|
14
|
+
registry.cleanup();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("records mocked exec calls", async () => {
|
|
19
|
+
const execMock = installExecMock();
|
|
20
|
+
execMock.setNextResult({ stdout: "ok" });
|
|
21
|
+
|
|
22
|
+
const { exec } = await import("node:child_process");
|
|
23
|
+
const execAsync = promisify(exec);
|
|
24
|
+
const result = await execAsync("node --version", { cwd: "/tmp" });
|
|
25
|
+
|
|
26
|
+
expect(result.stdout).toBe("ok");
|
|
27
|
+
expect(execMock.getCalls()).toEqual([{ command: "node --version", options: { cwd: "/tmp" } }]);
|
|
28
|
+
execMock.assertExecSyncUnused();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("simulates timeout and blocks execSync", async () => {
|
|
32
|
+
const execMock = installExecMock();
|
|
33
|
+
execMock.setNextResult({ timeoutAfterMs: 25, stderr: "timed out" });
|
|
34
|
+
const { exec, execSync } = await import("node:child_process");
|
|
35
|
+
const execAsync = promisify(exec);
|
|
36
|
+
|
|
37
|
+
await expect(execAsync("echo slow", { timeout: 25 })).rejects.toThrow(/timed out/i);
|
|
38
|
+
expect(() => execSync("echo no")).toThrow("execSync should never be called");
|
|
39
|
+
});
|
|
40
|
+
});
|