@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.
Files changed (171) hide show
  1. package/dist/bin.js +11036 -1992
  2. package/dist/client/assets/AgentDetailView-B7QRcHJH.css +1 -0
  3. package/dist/client/assets/AgentDetailView-DwLmRXTY.js +18 -0
  4. package/dist/client/assets/{AgentsView-D6Zi5zfP.js → AgentsView-D-N6aA0P.js} +12 -7
  5. package/dist/client/assets/ChatView-DnCdKu8Z.js +1 -0
  6. package/dist/client/assets/{DevServerView--_WBvIDQ.js → DevServerView-BiA1nYtt.js} +1 -1
  7. package/dist/client/assets/{DirectoryPicker-xedtR-Rd.js → DirectoryPicker-DvBviDG6.js} +1 -1
  8. package/dist/client/assets/{DocumentsView-Bg2oaZks.js → DocumentsView-BWXOxpuq.js} +1 -1
  9. package/dist/client/assets/{EvalsView-B3uOCXfr.js → EvalsView-CJFbtL7i.js} +1 -1
  10. package/dist/client/assets/{ExperimentalAgentOnboardingModal-Bx6yXVS5.js → ExperimentalAgentOnboardingModal-DuGIPd0B.js} +1 -1
  11. package/dist/client/assets/InsightsView-BBpRiolN.js +11 -0
  12. package/dist/client/assets/{MemoryView-xcN_eouf.js → MemoryView-48LuNkKk.js} +2 -2
  13. package/dist/client/assets/NodesView-CGQWSNZM.js +14 -0
  14. package/dist/client/assets/{PiExtensionsManager-Cc8aAZXg.js → PiExtensionsManager-i-7UL2oh.js} +2 -2
  15. package/dist/client/assets/PluginManager-DoSAykD6.js +1 -0
  16. package/dist/client/assets/{ResearchView-CERNf7sJ.js → ResearchView-XZuRtOxE.js} +1 -1
  17. package/dist/client/assets/{SettingsModal-Cis-4Lot.css → SettingsModal-Ci0_sqbU.css} +1 -1
  18. package/dist/client/assets/{SettingsModal-B1r0yASu.js → SettingsModal-CmeF8CN4.js} +1 -1
  19. package/dist/client/assets/SettingsModal-DBcjf9Bu.js +31 -0
  20. package/dist/client/assets/SettingsModal-DWKgRxBA.css +1 -0
  21. package/dist/client/assets/{SetupWizardModal-D1q548_L.js → SetupWizardModal-CgtvpMX9.js} +1 -1
  22. package/dist/client/assets/{SkillsView-ClLM6u6p.js → SkillsView-DErYRumF.js} +1 -1
  23. package/dist/client/assets/{StashRecoveryView-ze0pEZ5U.js → StashRecoveryView-QJrNS4Vg.js} +1 -1
  24. package/dist/client/assets/{TodoView-CTmIfy2M.js → TodoView-BD9NRwq0.js} +2 -2
  25. package/dist/client/assets/{dashboard-view-CyWN-d02.js → dashboard-view-BWGH_fAq.js} +1 -1
  26. package/dist/client/assets/dashboard-view-BoTzlP8b.css +1 -0
  27. package/dist/client/assets/dashboard-view-Ws9_ZnKu.js +21 -0
  28. package/dist/client/assets/{folder-open-BZuKESeq.js → folder-open-CHSlllzf.js} +1 -1
  29. package/dist/client/assets/index-DCovGm5b.css +1 -0
  30. package/dist/client/assets/index-bEwSVl7B.js +692 -0
  31. package/dist/client/assets/{star-D75YKEq-.js → star-BgVwWAPz.js} +1 -1
  32. package/dist/client/assets/{upload-BYYTgWFj.js → upload-CAzycxr9.js} +1 -1
  33. package/dist/client/assets/{users-RS90Aii3.js → users-CZnxCCCJ.js} +1 -1
  34. package/dist/client/index.html +2 -2
  35. package/dist/client/version.json +1 -1
  36. package/dist/droid-cli/package.json +1 -1
  37. package/dist/droid-cli/src/__tests__/index.test.ts +228 -0
  38. package/dist/extension.js +5517 -1193
  39. package/dist/pi-claude-cli/package.json +1 -1
  40. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +36 -22
  41. package/dist/pi-claude-cli/src/provider.ts +7 -1
  42. package/dist/plugins/fusion-plugin-cli-printing-press/manifest.json +19 -1
  43. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +20 -2
  44. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/TestRunnerPanel.test.tsx +99 -0
  45. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/config-flow.test.ts +91 -0
  46. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-view.test.tsx +40 -0
  47. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-views.test.ts +46 -0
  48. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/draft-store.test.ts +50 -0
  49. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/exec-mock.ts +80 -0
  50. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/fixtures.test.ts +40 -0
  51. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/registry.ts +82 -0
  52. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/generator.test.ts +54 -0
  53. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manage-view.test.tsx +98 -0
  54. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manifest.test.ts +21 -5
  55. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/registration.test.ts +29 -0
  56. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/run-routes.test.ts +98 -0
  57. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runner.test.ts +55 -0
  58. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runtime-availability.test.ts +61 -0
  59. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/validation.test.ts +30 -0
  60. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/wizard-routes.test.ts +61 -0
  61. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/workflow-integration.test.ts +19 -0
  62. package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.css +43 -0
  63. package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.tsx +49 -0
  64. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/generator.ts +95 -0
  65. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/redact.ts +9 -0
  66. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/runner.ts +79 -0
  67. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/types.ts +31 -0
  68. package/dist/plugins/fusion-plugin-cli-printing-press/src/index.ts +46 -2
  69. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/EditDraftModal.tsx +75 -0
  70. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/useDrafts.ts +73 -0
  71. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.css +79 -0
  72. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.tsx +122 -0
  73. package/dist/plugins/fusion-plugin-cli-printing-press/src/routes/wizard-routes.ts +272 -0
  74. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.css +70 -0
  75. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.tsx +98 -0
  76. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/useRunGeneratedCli.ts +37 -0
  77. package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/__tests__/executor-runtime-env.test.ts +191 -0
  78. package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/executor-runtime-env.ts +75 -0
  79. package/dist/plugins/fusion-plugin-cli-printing-press/src/storage/draft-store.ts +85 -0
  80. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/cli-press-store.test.ts +128 -0
  81. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/credentials.test.ts +62 -0
  82. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-store.ts +427 -0
  83. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-types.ts +110 -0
  84. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/credentials.ts +95 -0
  85. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/steps.tsx +55 -0
  86. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/types.ts +33 -0
  87. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/validation.ts +63 -0
  88. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  89. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  90. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  91. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  92. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  93. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  94. package/dist/plugins/fusion-plugin-reports/manifest.json +10 -0
  95. package/dist/plugins/fusion-plugin-reports/package.json +18 -2
  96. package/dist/plugins/fusion-plugin-reports/src/__tests__/approval.test.ts +164 -0
  97. package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +14 -0
  98. package/dist/plugins/fusion-plugin-reports/src/__tests__/routes-approval.test.ts +109 -0
  99. package/dist/plugins/fusion-plugin-reports/src/__tests__/scaffold.test.ts +60 -0
  100. package/dist/plugins/fusion-plugin-reports/src/__tests__/share-blocks.test.ts +83 -0
  101. package/dist/plugins/fusion-plugin-reports/src/aggregation.ts +23 -0
  102. package/dist/plugins/fusion-plugin-reports/src/approval.ts +97 -0
  103. package/dist/plugins/fusion-plugin-reports/src/cadence.ts +23 -0
  104. package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.css +82 -0
  105. package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.tsx +24 -0
  106. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportComparisonDrawer.test.tsx +12 -0
  107. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportDetailPanel.test.tsx +12 -0
  108. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportFiltersBar.test.tsx +14 -0
  109. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportsView.test.tsx +27 -0
  110. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/api.test.ts +19 -0
  111. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReportSectionDiff.test.ts +11 -0
  112. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReports.test.ts +13 -0
  113. package/dist/plugins/fusion-plugin-reports/src/dashboard/api.ts +85 -0
  114. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.css +59 -0
  115. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.tsx +58 -0
  116. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportComparisonDrawer.tsx +21 -0
  117. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportDetailPanel.tsx +29 -0
  118. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportEmptyState.tsx +3 -0
  119. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportFiltersBar.tsx +19 -0
  120. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportListItem.tsx +8 -0
  121. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.css +29 -0
  122. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.tsx +43 -0
  123. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ReportApprovalPanel.test.tsx +38 -0
  124. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ShareBlocksPanel.test.tsx +24 -0
  125. package/dist/plugins/fusion-plugin-reports/src/dashboard/test-setup.ts +18 -0
  126. package/dist/plugins/fusion-plugin-reports/src/dashboard/types.ts +22 -0
  127. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportPreview.ts +44 -0
  128. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportSectionDiff.ts +59 -0
  129. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReports.ts +71 -0
  130. package/dist/plugins/fusion-plugin-reports/src/dashboard/useViewportMode.ts +13 -0
  131. package/dist/plugins/fusion-plugin-reports/src/dashboard-view.tsx +6 -0
  132. package/dist/plugins/fusion-plugin-reports/src/index.ts +48 -2
  133. package/dist/plugins/fusion-plugin-reports/src/pipeline.ts +58 -0
  134. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/escape.test.ts +20 -0
  135. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/html-template.test.ts +110 -0
  136. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/standalone-html.test.ts +66 -0
  137. package/dist/plugins/fusion-plugin-reports/src/render/escape.ts +12 -0
  138. package/dist/plugins/fusion-plugin-reports/src/render/html-styles.ts +40 -0
  139. package/dist/plugins/fusion-plugin-reports/src/render/html-template.ts +137 -0
  140. package/dist/plugins/fusion-plugin-reports/src/render/index.ts +4 -0
  141. package/dist/plugins/fusion-plugin-reports/src/render/standalone-html.ts +75 -0
  142. package/dist/plugins/fusion-plugin-reports/src/report-schema.ts +31 -0
  143. package/dist/plugins/fusion-plugin-reports/src/routes/__tests__/report-export-routes.test.ts +104 -0
  144. package/dist/plugins/fusion-plugin-reports/src/routes/report-approval-routes.ts +98 -0
  145. package/dist/plugins/fusion-plugin-reports/src/routes/report-export-routes.ts +77 -0
  146. package/dist/plugins/fusion-plugin-reports/src/routes/report-list-routes.ts +72 -0
  147. package/dist/plugins/fusion-plugin-reports/src/runs-store.ts +69 -0
  148. package/dist/plugins/fusion-plugin-reports/src/share-blocks.ts +82 -0
  149. package/dist/plugins/fusion-plugin-reports/src/store/report-store.ts +51 -2
  150. package/dist/plugins/fusion-plugin-reports/src/store/report-types.ts +6 -1
  151. package/dist/plugins/fusion-plugin-roadmap/bundled.js +1528 -29391
  152. package/dist/plugins/fusion-plugin-roadmap/manifest.json +1 -1
  153. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  154. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  155. package/package.json +1 -1
  156. package/skill/fusion/references/engine-tools.md +1 -1
  157. package/skill/fusion/references/extension-tools.md +3 -3
  158. package/skill/fusion/references/fusion-capabilities.md +1 -1
  159. package/dist/client/assets/AgentDetailView-BwJaLqZh.css +0 -1
  160. package/dist/client/assets/AgentDetailView-Cv-vgOj3.js +0 -18
  161. package/dist/client/assets/ChatView-CAHjY9uO.js +0 -1
  162. package/dist/client/assets/InsightsView-Q1zvtF4F.js +0 -11
  163. package/dist/client/assets/NodesView-RxXg58_Q.js +0 -14
  164. package/dist/client/assets/PluginManager-BEkyBajl.js +0 -1
  165. package/dist/client/assets/SettingsModal-BLsac7CJ.js +0 -31
  166. package/dist/client/assets/SettingsModal-BNSrO1M9.css +0 -1
  167. package/dist/client/assets/dashboard-view-4xAN3yO5.js +0 -21
  168. package/dist/client/assets/dashboard-view-BkTMSZYn.css +0 -1
  169. package/dist/client/assets/index-Bdw6llW6.js +0 -692
  170. package/dist/client/assets/index-CZGlyJuS.css +0 -1
  171. 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
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusion-plugin-examples/cursor-runtime",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusion-plugin-examples/dependency-graph",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusion-plugin-examples/droid-runtime",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusion-plugin-examples/hermes-runtime",
3
- "version": "0.2.34",
3
+ "version": "0.2.35",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusion-plugin-examples/openclaw-runtime",
3
- "version": "0.2.34",
3
+ "version": "0.2.35",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusion-plugin-examples/paperclip-runtime",
3
- "version": "0.2.34",
3
+ "version": "0.2.35",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -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",
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/plugin-sdk": "workspace:*"
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("&lt;script&gt;alert(1)&lt;/script&gt;");
57
+ expect(blocks.emailHtml).toContain("&amp;");
58
+ expect(blocks.emailHtml).toContain("&quot;Status&quot;");
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
+ });