@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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type CredentialPattern =
|
|
2
|
+
| { kind: "none" }
|
|
3
|
+
| { kind: "apiKey"; header: string; envVar: string }
|
|
4
|
+
| { kind: "bearerToken"; envVar: string }
|
|
5
|
+
| { kind: "basicAuth"; usernameEnvVar: string; passwordEnvVar: string };
|
|
6
|
+
// TODO(FN-3762/FN-3766): OAuth credential variants.
|
|
7
|
+
|
|
8
|
+
export interface ServiceEndpoint {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
12
|
+
path: string;
|
|
13
|
+
summary?: string;
|
|
14
|
+
params?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ServiceDraft {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
slug: string;
|
|
21
|
+
description: string;
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
transport: "http";
|
|
24
|
+
endpoints: ServiceEndpoint[];
|
|
25
|
+
credential: CredentialPattern;
|
|
26
|
+
createdAt: string;
|
|
27
|
+
updatedAt: string;
|
|
28
|
+
regeneratedAt?: string;
|
|
29
|
+
generatedAt?: string;
|
|
30
|
+
artifactPath?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type WizardStep = "basics" | "transport" | "endpoints" | "credentials" | "review";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { CredentialPattern, ServiceDraft, ServiceEndpoint } from "./types.js";
|
|
2
|
+
|
|
3
|
+
type Ok = { ok: true };
|
|
4
|
+
type Fail = { ok: false; errors: Record<string, string> };
|
|
5
|
+
export type ValidationResult = Ok | Fail;
|
|
6
|
+
|
|
7
|
+
const SLUG_PATTERN = /^[a-z0-9-]+$/;
|
|
8
|
+
const METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
9
|
+
|
|
10
|
+
function fail(errors: Record<string, string>): Fail {
|
|
11
|
+
return { ok: false, errors };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function validateBasics(draft: ServiceDraft): ValidationResult {
|
|
15
|
+
const errors: Record<string, string> = {};
|
|
16
|
+
if (!draft.name.trim()) errors.name = "Name is required";
|
|
17
|
+
if (!SLUG_PATTERN.test(draft.slug)) errors.slug = "Slug must use lowercase letters, numbers, and single hyphens";
|
|
18
|
+
if (!draft.baseUrl.trim()) {
|
|
19
|
+
errors.baseUrl = "Base URL is required";
|
|
20
|
+
} else {
|
|
21
|
+
try { new URL(draft.baseUrl); } catch { errors.baseUrl = "Base URL must be a valid URL"; }
|
|
22
|
+
}
|
|
23
|
+
return Object.keys(errors).length ? fail(errors) : { ok: true };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function validateTransport(draft: ServiceDraft): ValidationResult {
|
|
27
|
+
return draft.transport === "http" ? { ok: true } : fail({ transport: "Only HTTP transport is supported in v1" });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validateEndpoint(endpoint: ServiceEndpoint, index: number): Record<string, string> {
|
|
31
|
+
const errors: Record<string, string> = {};
|
|
32
|
+
if (!endpoint.name.trim()) errors[`endpoints.${index}.name`] = "Endpoint name is required";
|
|
33
|
+
if (!METHODS.has(endpoint.method)) errors[`endpoints.${index}.method`] = "HTTP method is invalid";
|
|
34
|
+
if (!endpoint.path.trim()) errors[`endpoints.${index}.path`] = "Endpoint path is required";
|
|
35
|
+
return errors;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateEndpoints(draft: ServiceDraft): ValidationResult {
|
|
39
|
+
const errors: Record<string, string> = {};
|
|
40
|
+
if (draft.endpoints.length < 1) errors.endpoints = "At least one endpoint is required";
|
|
41
|
+
draft.endpoints.forEach((endpoint, index) => Object.assign(errors, validateEndpoint(endpoint, index)));
|
|
42
|
+
return Object.keys(errors).length ? fail(errors) : { ok: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function validateCredentials(credential: CredentialPattern): ValidationResult {
|
|
46
|
+
const errors: Record<string, string> = {};
|
|
47
|
+
if (credential.kind === "apiKey") {
|
|
48
|
+
if (!credential.header.trim()) errors.header = "Header is required";
|
|
49
|
+
if (!credential.envVar.trim()) errors.envVar = "Environment variable is required";
|
|
50
|
+
}
|
|
51
|
+
if (credential.kind === "bearerToken" && !credential.envVar.trim()) errors.envVar = "Environment variable is required";
|
|
52
|
+
if (credential.kind === "basicAuth") {
|
|
53
|
+
if (!credential.usernameEnvVar.trim()) errors.usernameEnvVar = "Username env var is required";
|
|
54
|
+
if (!credential.passwordEnvVar.trim()) errors.passwordEnvVar = "Password env var is required";
|
|
55
|
+
}
|
|
56
|
+
return Object.keys(errors).length ? fail(errors) : { ok: true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function validateDraft(draft: ServiceDraft): ValidationResult {
|
|
60
|
+
const validations = [validateBasics(draft), validateTransport(draft), validateEndpoints(draft), validateCredentials(draft.credential)];
|
|
61
|
+
const errors = validations.filter((r): r is Fail => !r.ok).reduce((acc, item) => ({ ...acc, ...item.errors }), {});
|
|
62
|
+
return Object.keys(errors).length ? fail(errors) : { ok: true };
|
|
63
|
+
}
|
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
"description": "Generates beautiful HTML system-activity reports with multi-agent review.",
|
|
6
6
|
"author": "Fusion Team",
|
|
7
7
|
"fusionVersion": ">=0.1.0",
|
|
8
|
+
"dashboardViews": [
|
|
9
|
+
{
|
|
10
|
+
"viewId": "reports",
|
|
11
|
+
"label": "Reports",
|
|
12
|
+
"componentPath": "./dashboard-view",
|
|
13
|
+
"icon": "FileText",
|
|
14
|
+
"placement": "primary",
|
|
15
|
+
"order": 35
|
|
16
|
+
}
|
|
17
|
+
],
|
|
8
18
|
"settingsSchema": {
|
|
9
19
|
"dailyEnabled": { "type": "boolean", "label": "Enable Daily Reports", "description": "Generate daily reports on schedule.", "group": "Schedules", "defaultValue": true },
|
|
10
20
|
"dailyCron": { "type": "string", "label": "Daily Schedule (cron)", "description": "Cron expression for daily report generation.", "group": "Schedules", "required": true, "defaultValue": "0 8 * * *" },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion-plugin-examples/reports",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Reports plugin 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
|
+
"./render": {
|
|
13
|
+
"types": "./src/render/index.ts",
|
|
14
|
+
"import": "./src/render/index.ts"
|
|
15
|
+
},
|
|
16
|
+
"./dashboard-view": {
|
|
17
|
+
"types": "./src/dashboard-view.tsx",
|
|
18
|
+
"import": "./src/dashboard-view.tsx"
|
|
11
19
|
}
|
|
12
20
|
},
|
|
13
21
|
"scripts": {
|
|
@@ -16,9 +24,17 @@
|
|
|
16
24
|
},
|
|
17
25
|
"dependencies": {
|
|
18
26
|
"@fusion/core": "workspace:*",
|
|
19
|
-
"@fusion/
|
|
27
|
+
"@fusion/dashboard": "workspace:*",
|
|
28
|
+
"@fusion/plugin-sdk": "workspace:*",
|
|
29
|
+
"lucide-react": "^0.542.0",
|
|
30
|
+
"react": "^19.0.0",
|
|
31
|
+
"react-dom": "^19.2.4"
|
|
20
32
|
},
|
|
21
33
|
"devDependencies": {
|
|
34
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
35
|
+
"@testing-library/react": "^16.3.2",
|
|
36
|
+
"@testing-library/user-event": "^14.6.1",
|
|
37
|
+
"@types/react": "^19.0.0",
|
|
22
38
|
"@types/node": "^25.5.2",
|
|
23
39
|
"typescript": "^5.7.0",
|
|
24
40
|
"vitest": "^3.2.4"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { applyDecision, nextApprovalState, type ApprovalAction, type ApprovalActor, type ApprovalDecision, type ApprovalSettings, type ApprovalState } from "../approval.js";
|
|
3
|
+
import type { Report } from "../store/report-types.js";
|
|
4
|
+
|
|
5
|
+
interface MatrixCase {
|
|
6
|
+
approvalRequired: boolean;
|
|
7
|
+
autoPublishOnApproval: boolean;
|
|
8
|
+
actorIsApprover: boolean;
|
|
9
|
+
action: ApprovalAction;
|
|
10
|
+
expected: ApprovalState | "invalid_transition" | "unauthorized";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const matrix: MatrixCase[] = [
|
|
14
|
+
{ approvalRequired: false, autoPublishOnApproval: false, actorIsApprover: false, action: "approve", expected: "approved" },
|
|
15
|
+
{ approvalRequired: false, autoPublishOnApproval: false, actorIsApprover: false, action: "reject", expected: "rejected" },
|
|
16
|
+
{ approvalRequired: false, autoPublishOnApproval: false, actorIsApprover: false, action: "publish", expected: "invalid_transition" },
|
|
17
|
+
{ approvalRequired: false, autoPublishOnApproval: false, actorIsApprover: true, action: "approve", expected: "approved" },
|
|
18
|
+
{ approvalRequired: false, autoPublishOnApproval: false, actorIsApprover: true, action: "reject", expected: "rejected" },
|
|
19
|
+
{ approvalRequired: false, autoPublishOnApproval: false, actorIsApprover: true, action: "publish", expected: "invalid_transition" },
|
|
20
|
+
{ approvalRequired: false, autoPublishOnApproval: true, actorIsApprover: false, action: "approve", expected: "published" },
|
|
21
|
+
{ approvalRequired: false, autoPublishOnApproval: true, actorIsApprover: false, action: "reject", expected: "rejected" },
|
|
22
|
+
{ approvalRequired: false, autoPublishOnApproval: true, actorIsApprover: false, action: "publish", expected: "invalid_transition" },
|
|
23
|
+
{ approvalRequired: false, autoPublishOnApproval: true, actorIsApprover: true, action: "approve", expected: "published" },
|
|
24
|
+
{ approvalRequired: false, autoPublishOnApproval: true, actorIsApprover: true, action: "reject", expected: "rejected" },
|
|
25
|
+
{ approvalRequired: false, autoPublishOnApproval: true, actorIsApprover: true, action: "publish", expected: "invalid_transition" },
|
|
26
|
+
{ approvalRequired: true, autoPublishOnApproval: false, actorIsApprover: false, action: "approve", expected: "unauthorized" },
|
|
27
|
+
{ approvalRequired: true, autoPublishOnApproval: false, actorIsApprover: false, action: "reject", expected: "unauthorized" },
|
|
28
|
+
{ approvalRequired: true, autoPublishOnApproval: false, actorIsApprover: false, action: "publish", expected: "unauthorized" },
|
|
29
|
+
{ approvalRequired: true, autoPublishOnApproval: false, actorIsApprover: true, action: "approve", expected: "approved" },
|
|
30
|
+
{ approvalRequired: true, autoPublishOnApproval: false, actorIsApprover: true, action: "reject", expected: "rejected" },
|
|
31
|
+
{ approvalRequired: true, autoPublishOnApproval: false, actorIsApprover: true, action: "publish", expected: "invalid_transition" },
|
|
32
|
+
{ approvalRequired: true, autoPublishOnApproval: true, actorIsApprover: false, action: "approve", expected: "unauthorized" },
|
|
33
|
+
{ approvalRequired: true, autoPublishOnApproval: true, actorIsApprover: false, action: "reject", expected: "unauthorized" },
|
|
34
|
+
{ approvalRequired: true, autoPublishOnApproval: true, actorIsApprover: false, action: "publish", expected: "unauthorized" },
|
|
35
|
+
{ approvalRequired: true, autoPublishOnApproval: true, actorIsApprover: true, action: "approve", expected: "published" },
|
|
36
|
+
{ approvalRequired: true, autoPublishOnApproval: true, actorIsApprover: true, action: "reject", expected: "rejected" },
|
|
37
|
+
{ approvalRequired: true, autoPublishOnApproval: true, actorIsApprover: true, action: "publish", expected: "invalid_transition" },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function makeSettings(input: Pick<MatrixCase, "approvalRequired" | "autoPublishOnApproval" | "actorIsApprover">): ApprovalSettings {
|
|
41
|
+
return {
|
|
42
|
+
approvalRequired: input.approvalRequired,
|
|
43
|
+
autoPublishOnApproval: input.autoPublishOnApproval,
|
|
44
|
+
approverAgentIds: input.actorIsApprover ? ["approver-1"] : ["approver-2"],
|
|
45
|
+
publishTargets: ["dashboard"],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function makeDecision(action: ApprovalAction): ApprovalDecision {
|
|
50
|
+
return { action, decidedAt: "2026-05-10T00:00:00.000Z", decidedBy: "approver-1", note: "ship" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeReport(state: ApprovalState): Report {
|
|
54
|
+
return {
|
|
55
|
+
id: "rep_1",
|
|
56
|
+
cadence: "daily",
|
|
57
|
+
periodStart: "2026-05-01",
|
|
58
|
+
periodEnd: "2026-05-01",
|
|
59
|
+
title: "Title",
|
|
60
|
+
status: "review_complete",
|
|
61
|
+
generationStartedAt: "2026-05-01T00:00:00.000Z",
|
|
62
|
+
generationCompletedAt: null,
|
|
63
|
+
reviewStartedAt: null,
|
|
64
|
+
reviewCompletedAt: "2026-05-01T01:00:00.000Z",
|
|
65
|
+
approvedAt: null,
|
|
66
|
+
approvedBy: null,
|
|
67
|
+
publishedAt: null,
|
|
68
|
+
archivedAt: null,
|
|
69
|
+
failureReason: null,
|
|
70
|
+
approvalState: state,
|
|
71
|
+
approvalHistory: [],
|
|
72
|
+
draftMarkdown: "# x",
|
|
73
|
+
renderedHtmlPath: null,
|
|
74
|
+
renderedHtml: null,
|
|
75
|
+
renderedHtmlGeneratedAt: null,
|
|
76
|
+
metadata: {},
|
|
77
|
+
combinedReview: null,
|
|
78
|
+
createdAt: "2026-05-01T00:00:00.000Z",
|
|
79
|
+
updatedAt: "2026-05-01T00:00:00.000Z",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe("approval state machine", () => {
|
|
84
|
+
it.each(matrix)("transition matrix %#", (entry) => {
|
|
85
|
+
const settings = makeSettings(entry);
|
|
86
|
+
const actor: ApprovalActor = { id: "approver-1", type: "agent" };
|
|
87
|
+
const result = nextApprovalState("awaiting_approval", entry.action, settings, actor);
|
|
88
|
+
if ("error" in result) {
|
|
89
|
+
expect(result.error).toBe(entry.expected);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
expect(result.next).toBe(entry.expected);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns invalid_transition for not_required actions", () => {
|
|
96
|
+
const settings: ApprovalSettings = {
|
|
97
|
+
approvalRequired: true,
|
|
98
|
+
autoPublishOnApproval: false,
|
|
99
|
+
approverAgentIds: ["approver-1"],
|
|
100
|
+
publishTargets: [],
|
|
101
|
+
};
|
|
102
|
+
const actor: ApprovalActor = { id: "approver-1", type: "agent" };
|
|
103
|
+
expect(nextApprovalState("not_required", "approve", settings, actor)).toEqual({ error: "invalid_transition" });
|
|
104
|
+
expect(nextApprovalState("not_required", "reject", settings, actor)).toEqual({ error: "invalid_transition" });
|
|
105
|
+
expect(nextApprovalState("not_required", "publish", settings, actor)).toEqual({ error: "invalid_transition" });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("allows any human when approverAgentIds is empty", () => {
|
|
109
|
+
const settings: ApprovalSettings = {
|
|
110
|
+
approvalRequired: true,
|
|
111
|
+
autoPublishOnApproval: false,
|
|
112
|
+
approverAgentIds: [],
|
|
113
|
+
publishTargets: [],
|
|
114
|
+
};
|
|
115
|
+
const human: ApprovalActor = { id: "u-1", type: "human" };
|
|
116
|
+
expect(nextApprovalState("awaiting_approval", "approve", settings, human)).toEqual({ next: "approved" });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("keeps agents unauthorized when approverAgentIds is empty", () => {
|
|
120
|
+
const settings: ApprovalSettings = {
|
|
121
|
+
approvalRequired: true,
|
|
122
|
+
autoPublishOnApproval: false,
|
|
123
|
+
approverAgentIds: [],
|
|
124
|
+
publishTargets: [],
|
|
125
|
+
};
|
|
126
|
+
const agent: ApprovalActor = { id: "approver-1", type: "agent" };
|
|
127
|
+
expect(nextApprovalState("awaiting_approval", "approve", settings, agent)).toEqual({ error: "unauthorized" });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("applyDecision auto-publish chain updates report fields", () => {
|
|
131
|
+
const report = makeReport("awaiting_approval");
|
|
132
|
+
const settings: ApprovalSettings = {
|
|
133
|
+
approvalRequired: true,
|
|
134
|
+
autoPublishOnApproval: true,
|
|
135
|
+
approverAgentIds: ["approver-1"],
|
|
136
|
+
publishTargets: ["dashboard", "html-export"],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const result = applyDecision(report, makeDecision("approve"), settings, { id: "approver-1", type: "agent" });
|
|
140
|
+
expect("error" in result).toBe(false);
|
|
141
|
+
if ("error" in result) return;
|
|
142
|
+
expect(result.updatedReport.approvalState).toBe("published");
|
|
143
|
+
expect(result.updatedReport.status).toBe("published");
|
|
144
|
+
expect(result.updatedReport.approvedBy).toBe("approver-1");
|
|
145
|
+
expect(result.sideEffects.publishTargets).toEqual(["dashboard", "html-export"]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("applyDecision publish action from approved sets published state", () => {
|
|
149
|
+
const report = makeReport("approved");
|
|
150
|
+
const settings: ApprovalSettings = {
|
|
151
|
+
approvalRequired: true,
|
|
152
|
+
autoPublishOnApproval: false,
|
|
153
|
+
approverAgentIds: ["approver-1"],
|
|
154
|
+
publishTargets: ["dashboard"],
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const result = applyDecision(report, makeDecision("publish"), settings, { id: "approver-1", type: "agent" });
|
|
158
|
+
expect("error" in result).toBe(false);
|
|
159
|
+
if ("error" in result) return;
|
|
160
|
+
expect(result.updatedReport.approvalState).toBe("published");
|
|
161
|
+
expect(result.updatedReport.status).toBe("published");
|
|
162
|
+
expect(result.updatedReport.publishedAt).toBe("2026-05-10T00:00:00.000Z");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -42,6 +42,20 @@ describe("reports plugin manifest", () => {
|
|
|
42
42
|
expect(plugin.manifest.fusionVersion).toBe(manifest.fusionVersion);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
it("registers dashboard view", () => {
|
|
46
|
+
expect(plugin.dashboardViews).toEqual([
|
|
47
|
+
{
|
|
48
|
+
viewId: "reports",
|
|
49
|
+
label: "Reports",
|
|
50
|
+
componentPath: "./dashboard-view",
|
|
51
|
+
icon: "FileText",
|
|
52
|
+
placement: "primary",
|
|
53
|
+
order: 35,
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
expect(manifest.dashboardViews).toEqual(plugin.dashboardViews);
|
|
57
|
+
});
|
|
58
|
+
|
|
45
59
|
it("includes full settings schema", () => {
|
|
46
60
|
expect(plugin.manifest.settingsSchema).toBeDefined();
|
|
47
61
|
for (const key of expectedKeys) {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { PluginContext } from "@fusion/core";
|
|
3
|
+
import type { Report } from "../store/report-types.js";
|
|
4
|
+
import { createReportApprovalRoutes } from "../routes/report-approval-routes.js";
|
|
5
|
+
|
|
6
|
+
function makeReport(overrides: Partial<Report> = {}): Report {
|
|
7
|
+
return {
|
|
8
|
+
id: "rep_1",
|
|
9
|
+
cadence: "daily",
|
|
10
|
+
periodStart: "2026-05-01",
|
|
11
|
+
periodEnd: "2026-05-01",
|
|
12
|
+
title: "Report",
|
|
13
|
+
status: "review_complete",
|
|
14
|
+
generationStartedAt: "2026-05-01T00:00:00.000Z",
|
|
15
|
+
generationCompletedAt: null,
|
|
16
|
+
reviewStartedAt: null,
|
|
17
|
+
reviewCompletedAt: null,
|
|
18
|
+
approvedAt: null,
|
|
19
|
+
approvedBy: null,
|
|
20
|
+
publishedAt: null,
|
|
21
|
+
archivedAt: null,
|
|
22
|
+
failureReason: null,
|
|
23
|
+
approvalState: "awaiting_approval",
|
|
24
|
+
approvalHistory: [],
|
|
25
|
+
draftMarkdown: null,
|
|
26
|
+
renderedHtmlPath: null,
|
|
27
|
+
renderedHtml: null,
|
|
28
|
+
renderedHtmlGeneratedAt: null,
|
|
29
|
+
metadata: {},
|
|
30
|
+
combinedReview: null,
|
|
31
|
+
createdAt: "2026-05-01T00:00:00.000Z",
|
|
32
|
+
updatedAt: "2026-05-01T00:00:00.000Z",
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ctxWithStore(store: { getReport: (id: string) => Report | null; updateReport: (id: string, patch: Partial<Report>) => Report }, settings: Record<string, unknown> = {}): PluginContext {
|
|
38
|
+
return {
|
|
39
|
+
pluginId: "fusion-plugin-reports",
|
|
40
|
+
taskStore: { getDatabase: () => ({}), getReportStore: () => store } as any,
|
|
41
|
+
settings,
|
|
42
|
+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
43
|
+
emitEvent: vi.fn(),
|
|
44
|
+
} as PluginContext;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function route(path: string, method: string) {
|
|
48
|
+
return createReportApprovalRoutes().find((entry) => entry.path === path && entry.method === method)!;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("report approval routes", () => {
|
|
52
|
+
it("approve then publish happy path", async () => {
|
|
53
|
+
let current = makeReport({ approvalState: "awaiting_approval" });
|
|
54
|
+
const store = {
|
|
55
|
+
getReport: vi.fn(() => current),
|
|
56
|
+
updateReport: vi.fn((_id: string, patch: Partial<Report>) => {
|
|
57
|
+
current = { ...current, ...patch };
|
|
58
|
+
return current;
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
const ctx = ctxWithStore(store, { approvalRequired: true, autoPublishOnApproval: false, approverAgentIds: ["agent-1"] });
|
|
62
|
+
|
|
63
|
+
const approve = await route("/reports/:id/approve", "POST").handler({ params: { id: "rep_1" }, headers: { "x-fusion-actor-type": "agent", "x-fusion-user": "agent-1" } }, ctx as any) as any;
|
|
64
|
+
expect(approve.status).toBe(200);
|
|
65
|
+
expect(approve.body.report.approvalState).toBe("approved");
|
|
66
|
+
|
|
67
|
+
const publish = await route("/reports/:id/publish", "POST").handler({ params: { id: "rep_1" }, headers: { "x-fusion-actor-type": "agent", "x-fusion-user": "agent-1" } }, ctx as any) as any;
|
|
68
|
+
expect(publish.status).toBe(200);
|
|
69
|
+
expect(publish.body.report.approvalState).toBe("published");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("supports reject path", async () => {
|
|
73
|
+
const store = {
|
|
74
|
+
getReport: vi.fn(() => makeReport()),
|
|
75
|
+
updateReport: vi.fn((_id: string, patch: Partial<Report>) => ({ ...makeReport(), ...patch })),
|
|
76
|
+
};
|
|
77
|
+
const ctx = ctxWithStore(store, { approvalRequired: true, autoPublishOnApproval: false, approverAgentIds: ["agent-1"] });
|
|
78
|
+
const reject = await route("/reports/:id/reject", "POST").handler({ params: { id: "rep_1" }, headers: { "x-fusion-actor-type": "agent", "x-fusion-user": "agent-1" } }, ctx as any) as any;
|
|
79
|
+
expect(reject.status).toBe(200);
|
|
80
|
+
expect(reject.body.report.approvalState).toBe("rejected");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns 403 for unauthorized approver", async () => {
|
|
84
|
+
const store = {
|
|
85
|
+
getReport: vi.fn(() => makeReport()),
|
|
86
|
+
updateReport: vi.fn(),
|
|
87
|
+
};
|
|
88
|
+
const ctx = ctxWithStore(store, { approvalRequired: true, autoPublishOnApproval: false, approverAgentIds: ["agent-1"] });
|
|
89
|
+
const res = await route("/reports/:id/approve", "POST").handler({ params: { id: "rep_1" }, headers: { "x-fusion-actor-type": "agent", "x-fusion-user": "agent-2" } }, ctx as any) as any;
|
|
90
|
+
expect(res.status).toBe(403);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("share-blocks returns 409 before approval and 200 after", async () => {
|
|
94
|
+
const current = makeReport({ approvalState: "awaiting_approval", combinedReview: { overallVerdict: "approve", consensusSummary: "ok", mergedHighlights: ["a"], mergedLowlights: [], mergedSuggestions: [], individual: [], failures: [] } });
|
|
95
|
+
const store = {
|
|
96
|
+
getReport: vi.fn(() => current),
|
|
97
|
+
updateReport: vi.fn(),
|
|
98
|
+
};
|
|
99
|
+
const ctx = ctxWithStore(store);
|
|
100
|
+
const locked = await route("/reports/:id/share-blocks", "GET").handler({ params: { id: "rep_1" } }, ctx as any) as any;
|
|
101
|
+
expect(locked.status).toBe(409);
|
|
102
|
+
|
|
103
|
+
const openStore = { ...store, getReport: vi.fn(() => ({ ...current, approvalState: "approved" as const })) };
|
|
104
|
+
const openCtx = ctxWithStore(openStore);
|
|
105
|
+
const open = await route("/reports/:id/share-blocks", "GET").handler({ params: { id: "rep_1" } }, openCtx as any) as any;
|
|
106
|
+
expect(open.status).toBe(200);
|
|
107
|
+
expect(open.body).toHaveProperty("plainText");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { aggregateReportData } from "../aggregation.js";
|
|
3
|
+
import { resolveEnabledCadences } from "../cadence.js";
|
|
4
|
+
import { startReportsPipeline } from "../pipeline.js";
|
|
5
|
+
import { createInMemoryReportsRunsStore } from "../runs-store.js";
|
|
6
|
+
|
|
7
|
+
describe("reports scaffold seams", () => {
|
|
8
|
+
it("resolves daily and weekly cadence by default in UTC", () => {
|
|
9
|
+
expect(resolveEnabledCadences({})).toEqual([
|
|
10
|
+
{ cadence: "daily", timezone: "UTC" },
|
|
11
|
+
{ cadence: "weekly", timezone: "UTC" },
|
|
12
|
+
]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("resolves weekly-only cadence with configured timezone", () => {
|
|
16
|
+
expect(
|
|
17
|
+
resolveEnabledCadences({ dailyEnabled: false, weeklyEnabled: true, timezone: "America/Los_Angeles" }),
|
|
18
|
+
).toEqual([{ cadence: "weekly", timezone: "America/Los_Angeles" }]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns empty sections from scaffold aggregator", async () => {
|
|
22
|
+
const output = await aggregateReportData({ runId: "run-1", cadence: "daily", settings: {} });
|
|
23
|
+
expect(output.sections).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("runs pipeline to review status on happy path", async () => {
|
|
27
|
+
const runsStore = createInMemoryReportsRunsStore();
|
|
28
|
+
const result = await startReportsPipeline(
|
|
29
|
+
{ runId: "run-1", cadence: "daily", settings: {} },
|
|
30
|
+
{ runsStore, aggregate: aggregateReportData },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(result.status).toBe("review");
|
|
34
|
+
const stored = await runsStore.get("run-1");
|
|
35
|
+
expect(stored).toEqual(result);
|
|
36
|
+
expect(stored?.cadence).toBe("daily");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("marks pipeline run failed when aggregation throws and does not rethrow", async () => {
|
|
40
|
+
const runsStore = createInMemoryReportsRunsStore();
|
|
41
|
+
const result = await startReportsPipeline(
|
|
42
|
+
{ runId: "run-2", cadence: "weekly", settings: {} },
|
|
43
|
+
{
|
|
44
|
+
runsStore,
|
|
45
|
+
aggregate: async () => {
|
|
46
|
+
throw new Error("boom");
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(result.status).toBe("failed");
|
|
52
|
+
expect(result.error).toBe("boom");
|
|
53
|
+
await expect(runsStore.get("run-2")).resolves.toEqual(result);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns undefined when updating unknown run", async () => {
|
|
57
|
+
const runsStore = createInMemoryReportsRunsStore();
|
|
58
|
+
await expect(runsStore.update("missing", { status: "failed" })).resolves.toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildShareBlocks } from "../share-blocks.js";
|
|
3
|
+
import type { Report } from "../store/report-types.js";
|
|
4
|
+
|
|
5
|
+
function makeReport(): Report {
|
|
6
|
+
return {
|
|
7
|
+
id: "rep_1",
|
|
8
|
+
cadence: "weekly",
|
|
9
|
+
periodStart: "2026-05-01",
|
|
10
|
+
periodEnd: "2026-05-07",
|
|
11
|
+
title: "Weekly <Report> & \"Status\"",
|
|
12
|
+
status: "published",
|
|
13
|
+
generationStartedAt: "2026-05-01T00:00:00.000Z",
|
|
14
|
+
generationCompletedAt: null,
|
|
15
|
+
reviewStartedAt: null,
|
|
16
|
+
reviewCompletedAt: null,
|
|
17
|
+
approvedAt: null,
|
|
18
|
+
approvedBy: null,
|
|
19
|
+
publishedAt: null,
|
|
20
|
+
archivedAt: null,
|
|
21
|
+
failureReason: null,
|
|
22
|
+
approvalState: "published",
|
|
23
|
+
approvalHistory: [],
|
|
24
|
+
draftMarkdown: null,
|
|
25
|
+
renderedHtmlPath: null,
|
|
26
|
+
renderedHtml: null,
|
|
27
|
+
renderedHtmlGeneratedAt: null,
|
|
28
|
+
metadata: {},
|
|
29
|
+
combinedReview: {
|
|
30
|
+
overallVerdict: "approve",
|
|
31
|
+
consensusSummary: "ok",
|
|
32
|
+
mergedHighlights: ["Win <script>alert(1)</script>", "Win 2"],
|
|
33
|
+
mergedLowlights: ["Low & bad"],
|
|
34
|
+
mergedSuggestions: ["Do \"x\""],
|
|
35
|
+
individual: [],
|
|
36
|
+
failures: [],
|
|
37
|
+
},
|
|
38
|
+
createdAt: "2026-05-01T00:00:00.000Z",
|
|
39
|
+
updatedAt: "2026-05-01T00:00:00.000Z",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("buildShareBlocks", () => {
|
|
44
|
+
it("builds deterministic outputs", () => {
|
|
45
|
+
const report = makeReport();
|
|
46
|
+
const first = buildShareBlocks(report);
|
|
47
|
+
const second = buildShareBlocks(report);
|
|
48
|
+
expect(first).toEqual(second);
|
|
49
|
+
expect(first.markdown).toContain("## Weekly \\\<Report\\\>");
|
|
50
|
+
expect(first.slack).toContain("*Weekly <Report> & \"Status\"*");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("escapes markdown and html", () => {
|
|
54
|
+
const blocks = buildShareBlocks(makeReport());
|
|
55
|
+
expect(blocks.markdown).toContain("\\<Report\\>");
|
|
56
|
+
expect(blocks.emailHtml).toContain("<script>alert(1)</script>");
|
|
57
|
+
expect(blocks.emailHtml).toContain("&");
|
|
58
|
+
expect(blocks.emailHtml).toContain(""Status"");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("omits empty sections", () => {
|
|
62
|
+
const report = makeReport();
|
|
63
|
+
report.combinedReview = {
|
|
64
|
+
overallVerdict: "approve",
|
|
65
|
+
consensusSummary: "ok",
|
|
66
|
+
mergedHighlights: [],
|
|
67
|
+
mergedLowlights: [],
|
|
68
|
+
mergedSuggestions: [],
|
|
69
|
+
individual: [],
|
|
70
|
+
failures: [],
|
|
71
|
+
};
|
|
72
|
+
const blocks = buildShareBlocks(report);
|
|
73
|
+
expect(blocks.plainText).not.toContain("Wins:");
|
|
74
|
+
expect(blocks.markdown).not.toContain("### Wins");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("slack block avoids markdown-only constructs", () => {
|
|
78
|
+
const blocks = buildShareBlocks(makeReport());
|
|
79
|
+
expect(blocks.slack).not.toContain("#");
|
|
80
|
+
expect(blocks.slack).not.toContain("**");
|
|
81
|
+
expect(blocks.slack).not.toContain("- ");
|
|
82
|
+
});
|
|
83
|
+
});
|