@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,8 @@
|
|
|
1
|
+
import type { ReportListItemVm } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export function ReportListItem({ report, selected, onSelect }: { report: ReportListItemVm; selected: boolean; onSelect: (id: string) => void }) {
|
|
4
|
+
return <button className="card reports-list-item" data-selected={selected ? "true" : "false"} onClick={() => onSelect(report.id)}>
|
|
5
|
+
<div className="card-header"><span className="card-title">{report.title}</span></div>
|
|
6
|
+
<div className="card-meta"><span>{report.cadence}</span><span>{report.status}</span><span>{report.periodStart} → {report.periodEnd}</span></div>
|
|
7
|
+
</button>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
.share-blocks-panel {
|
|
2
|
+
border-top: var(--btn-border-width) solid var(--border);
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: var(--space-md);
|
|
6
|
+
padding-top: var(--space-md);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.share-blocks-panel__tabs {
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-wrap: wrap;
|
|
12
|
+
gap: var(--space-xs);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.share-blocks-panel__content {
|
|
16
|
+
min-height: calc(var(--space-2xl) * 4);
|
|
17
|
+
resize: vertical;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.share-blocks-panel__locked {
|
|
21
|
+
color: var(--text-muted);
|
|
22
|
+
margin: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@media (max-width: 768px) {
|
|
26
|
+
.share-blocks-panel__tabs {
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { getShareBlocks } from "../api.js";
|
|
3
|
+
import type { ReportRecord } from "../types.js";
|
|
4
|
+
import type { ShareBlocks } from "../../share-blocks.js";
|
|
5
|
+
import "./ShareBlocksPanel.css";
|
|
6
|
+
|
|
7
|
+
const TABS: Array<{ key: keyof ShareBlocks; label: string }> = [
|
|
8
|
+
{ key: "plainText", label: "Plain Text" },
|
|
9
|
+
{ key: "markdown", label: "Markdown" },
|
|
10
|
+
{ key: "slack", label: "Slack" },
|
|
11
|
+
{ key: "emailHtml", label: "Email HTML" },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function ShareBlocksPanel({ report }: { report: ReportRecord }) {
|
|
15
|
+
const [active, setActive] = useState<keyof ShareBlocks>("plainText");
|
|
16
|
+
const [data, setData] = useState<ShareBlocks | null>(null);
|
|
17
|
+
const [locked, setLocked] = useState(false);
|
|
18
|
+
const [copied, setCopied] = useState(false);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
setLocked(false);
|
|
22
|
+
setData(null);
|
|
23
|
+
getShareBlocks(report.id).then(setData).catch((error: Error) => {
|
|
24
|
+
if (error.message.includes("409")) setLocked(true);
|
|
25
|
+
});
|
|
26
|
+
}, [report.id]);
|
|
27
|
+
|
|
28
|
+
if (locked) return <section className="share-blocks-panel"><p className="share-blocks-panel__locked">Share blocks unlock after the report is approved.</p></section>;
|
|
29
|
+
if (!data) return <section className="share-blocks-panel"><p>Loading share blocks…</p></section>;
|
|
30
|
+
|
|
31
|
+
const value = data[active];
|
|
32
|
+
return <section className="share-blocks-panel">
|
|
33
|
+
<div className="share-blocks-panel__tabs">
|
|
34
|
+
{TABS.map((tab) => <button key={tab.key} className={`btn btn-sm ${active === tab.key ? "btn-primary" : ""}`} onClick={() => setActive(tab.key)}>{tab.label}</button>)}
|
|
35
|
+
</div>
|
|
36
|
+
<textarea className="input share-blocks-panel__content" readOnly value={value} />
|
|
37
|
+
<button className="btn btn-sm" onClick={async () => {
|
|
38
|
+
await navigator.clipboard.writeText(value);
|
|
39
|
+
setCopied(true);
|
|
40
|
+
setTimeout(() => setCopied(false), 1000);
|
|
41
|
+
}}>{copied ? "Copied" : "Copy"}</button>
|
|
42
|
+
</section>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { ReportApprovalPanel } from "../ReportApprovalPanel.js";
|
|
4
|
+
|
|
5
|
+
vi.mock("../../api.js", () => ({
|
|
6
|
+
approveReport: vi.fn(async () => ({ ...baseReport, approvalState: "approved" })),
|
|
7
|
+
rejectReport: vi.fn(async () => ({ ...baseReport, approvalState: "rejected" })),
|
|
8
|
+
publishReport: vi.fn(async () => ({ ...baseReport, approvalState: "published" })),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const baseReport: any = {
|
|
12
|
+
id: "rep_1",
|
|
13
|
+
approvalState: "awaiting_approval",
|
|
14
|
+
approvalHistory: [],
|
|
15
|
+
status: "review_complete",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
afterEach(() => cleanup());
|
|
19
|
+
|
|
20
|
+
describe("ReportApprovalPanel", () => {
|
|
21
|
+
it("renders actions for awaiting approval and posts approve", async () => {
|
|
22
|
+
const onReportChange = vi.fn();
|
|
23
|
+
render(<ReportApprovalPanel report={baseReport} onReportChange={onReportChange} />);
|
|
24
|
+
fireEvent.click(screen.getByText("Approve"));
|
|
25
|
+
await waitFor(() => expect(onReportChange).toHaveBeenCalled());
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("renders publish action for approved", () => {
|
|
29
|
+
render(<ReportApprovalPanel report={{ ...baseReport, approvalState: "approved" }} onReportChange={vi.fn()} />);
|
|
30
|
+
expect(screen.getByText("Publish")).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("read-only for rejected", () => {
|
|
34
|
+
render(<ReportApprovalPanel report={{ id: "rep_1", status: "review_complete", approvalState: "rejected", approvalHistory: [{ action: "reject", decidedAt: "now", decidedBy: "u" }] } as any} onReportChange={vi.fn()} />);
|
|
35
|
+
expect(screen.queryByText("Publish")).toBeNull();
|
|
36
|
+
expect(screen.getByText(/reject by/i)).toBeInTheDocument();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { ShareBlocksPanel } from "../ShareBlocksPanel.js";
|
|
4
|
+
|
|
5
|
+
const getShareBlocks = vi.fn();
|
|
6
|
+
vi.mock("../../api.js", () => ({ getShareBlocks: (...args: unknown[]) => getShareBlocks(...args) }));
|
|
7
|
+
|
|
8
|
+
describe("ShareBlocksPanel", () => {
|
|
9
|
+
it("renders tabs and copies selected block", async () => {
|
|
10
|
+
getShareBlocks.mockResolvedValue({ plainText: "a", markdown: "b", slack: "c", emailHtml: "d" });
|
|
11
|
+
Object.assign(navigator, { clipboard: { writeText: vi.fn().mockResolvedValue(undefined) } });
|
|
12
|
+
render(<ShareBlocksPanel report={{ id: "rep_1" } as any} />);
|
|
13
|
+
await screen.findByText("Plain Text");
|
|
14
|
+
fireEvent.click(screen.getByText("Markdown"));
|
|
15
|
+
fireEvent.click(screen.getByText("Copy"));
|
|
16
|
+
await waitFor(() => expect(navigator.clipboard.writeText).toHaveBeenCalledWith("b"));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("shows locked message on 409", async () => {
|
|
20
|
+
getShareBlocks.mockRejectedValue(new Error("409 Conflict"));
|
|
21
|
+
render(<ShareBlocksPanel report={{ id: "rep_1" } as any} />);
|
|
22
|
+
await screen.findByText(/unlock after the report is approved/i);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "@testing-library/jest-dom/vitest";
|
|
2
|
+
import { vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
if (typeof window !== "undefined") {
|
|
5
|
+
Object.defineProperty(window, "matchMedia", {
|
|
6
|
+
writable: true,
|
|
7
|
+
value: vi.fn().mockImplementation((query: string) => ({
|
|
8
|
+
matches: false,
|
|
9
|
+
media: query,
|
|
10
|
+
onchange: null,
|
|
11
|
+
addEventListener: vi.fn(),
|
|
12
|
+
removeEventListener: vi.fn(),
|
|
13
|
+
addListener: vi.fn(),
|
|
14
|
+
removeListener: vi.fn(),
|
|
15
|
+
dispatchEvent: vi.fn(),
|
|
16
|
+
})),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Report, ReportCadence, ReportStatus } from "../store/report-types.js";
|
|
2
|
+
|
|
3
|
+
export type ToastType = "success" | "error" | "info" | "warning";
|
|
4
|
+
|
|
5
|
+
export interface ReportFilters {
|
|
6
|
+
cadence: "all" | ReportCadence;
|
|
7
|
+
status: "all" | ReportStatus;
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
q: string;
|
|
11
|
+
agentId: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SectionRef {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
hash: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ReportRecord = Report;
|
|
21
|
+
|
|
22
|
+
export type ReportListItemVm = Pick<ReportRecord, "id" | "title" | "cadence" | "status" | "periodStart" | "periodEnd" | "createdAt" | "metadata">;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { getReportPreviewHtml } from "./api.js";
|
|
3
|
+
|
|
4
|
+
const cache = new Map<string, string>();
|
|
5
|
+
|
|
6
|
+
export function useReportPreview(id?: string, projectId?: string) {
|
|
7
|
+
const [html, setHtml] = useState<string>("");
|
|
8
|
+
const [loading, setLoading] = useState(false);
|
|
9
|
+
const [error, setError] = useState<string | null>(null);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!id) {
|
|
13
|
+
setHtml("");
|
|
14
|
+
setError(null);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const key = `${projectId ?? ""}:${id}`;
|
|
18
|
+
const cached = cache.get(key);
|
|
19
|
+
if (cached) {
|
|
20
|
+
setHtml(cached);
|
|
21
|
+
setError(null);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
setLoading(true);
|
|
26
|
+
setError(null);
|
|
27
|
+
getReportPreviewHtml(id, projectId)
|
|
28
|
+
.then((nextHtml) => {
|
|
29
|
+
if (controller.signal.aborted) return;
|
|
30
|
+
cache.set(key, nextHtml);
|
|
31
|
+
setHtml(nextHtml);
|
|
32
|
+
})
|
|
33
|
+
.catch((err: unknown) => {
|
|
34
|
+
if (controller.signal.aborted) return;
|
|
35
|
+
setError(err instanceof Error ? err.message : "Failed to load preview");
|
|
36
|
+
})
|
|
37
|
+
.finally(() => {
|
|
38
|
+
if (!controller.signal.aborted) setLoading(false);
|
|
39
|
+
});
|
|
40
|
+
return () => controller.abort();
|
|
41
|
+
}, [id, projectId]);
|
|
42
|
+
|
|
43
|
+
return { html, loading, error };
|
|
44
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { ReportRecord, SectionRef } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export interface SectionDiff {
|
|
5
|
+
added: SectionRef[];
|
|
6
|
+
removed: SectionRef[];
|
|
7
|
+
changed: SectionRef[];
|
|
8
|
+
unchanged: SectionRef[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ALL_SECTIONS: Array<{ id: string; label: string }> = [
|
|
12
|
+
{ id: "summary", label: "Summary" },
|
|
13
|
+
{ id: "system-wins", label: "System wins" },
|
|
14
|
+
{ id: "system-highlights", label: "System highlights" },
|
|
15
|
+
{ id: "system-lowlights", label: "System lowlights" },
|
|
16
|
+
{ id: "system-proposals", label: "System proposals" },
|
|
17
|
+
{ id: "system-deep-dives", label: "System deep dives" },
|
|
18
|
+
{ id: "agent-card", label: "Per-agent" },
|
|
19
|
+
{ id: "data-coverage", label: "Data coverage" },
|
|
20
|
+
{ id: "review-panel", label: "Review panel" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function extractValue(report: ReportRecord | undefined, id: string): unknown {
|
|
24
|
+
if (!report) return undefined;
|
|
25
|
+
switch (id) {
|
|
26
|
+
case "summary": return report.metadata?.summary;
|
|
27
|
+
case "system-wins": return report.metadata?.wins;
|
|
28
|
+
case "system-highlights": return report.metadata?.highlights;
|
|
29
|
+
case "system-lowlights": return report.metadata?.lowlights;
|
|
30
|
+
case "system-proposals": return report.metadata?.proposals;
|
|
31
|
+
case "system-deep-dives": return report.metadata?.deepDives;
|
|
32
|
+
case "agent-card": return report.metadata?.perAgent;
|
|
33
|
+
case "data-coverage": return report.metadata?.dataCoverage;
|
|
34
|
+
case "review-panel": return report.combinedReview;
|
|
35
|
+
default: return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function diffReportSections(a?: ReportRecord, b?: ReportRecord): SectionDiff {
|
|
40
|
+
const added: SectionRef[] = [];
|
|
41
|
+
const removed: SectionRef[] = [];
|
|
42
|
+
const changed: SectionRef[] = [];
|
|
43
|
+
const unchanged: SectionRef[] = [];
|
|
44
|
+
|
|
45
|
+
for (const section of ALL_SECTIONS) {
|
|
46
|
+
const left = extractValue(a, section.id);
|
|
47
|
+
const right = extractValue(b, section.id);
|
|
48
|
+
const ref: SectionRef = { id: section.id, label: section.label, hash: section.id };
|
|
49
|
+
if (left == null && right != null) added.push(ref);
|
|
50
|
+
else if (left != null && right == null) removed.push(ref);
|
|
51
|
+
else if (JSON.stringify(left) !== JSON.stringify(right)) changed.push(ref);
|
|
52
|
+
else unchanged.push(ref);
|
|
53
|
+
}
|
|
54
|
+
return { added, removed, changed, unchanged };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useReportSectionDiff(a?: ReportRecord, b?: ReportRecord): SectionDiff {
|
|
58
|
+
return useMemo(() => diffReportSections(a, b), [a, b]);
|
|
59
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { getReport, listReports } from "./api.js";
|
|
3
|
+
import type { ReportFilters, ReportRecord, ToastType } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_FILTERS: ReportFilters = { cadence: "all", status: "all", from: "", to: "", q: "", agentId: "" };
|
|
6
|
+
|
|
7
|
+
export function useReports({ projectId, addToast }: { projectId?: string; addToast: (message: string, type?: ToastType) => void }) {
|
|
8
|
+
const [filters, setFilters] = useState<ReportFilters>(DEFAULT_FILTERS);
|
|
9
|
+
const [reports, setReports] = useState<ReportRecord[]>([]);
|
|
10
|
+
const [loading, setLoading] = useState(false);
|
|
11
|
+
const [selectedId, setSelectedId] = useState<string | undefined>();
|
|
12
|
+
const [selectedReport, setSelectedReport] = useState<ReportRecord | undefined>();
|
|
13
|
+
const [compareMode, setCompareMode] = useState(false);
|
|
14
|
+
const [compareA, setCompareA] = useState<string | undefined>();
|
|
15
|
+
const [compareB, setCompareB] = useState<string | undefined>();
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
setLoading(true);
|
|
20
|
+
listReports({
|
|
21
|
+
cadence: filters.cadence === "all" ? undefined : filters.cadence,
|
|
22
|
+
status: filters.status === "all" ? undefined : filters.status,
|
|
23
|
+
from: filters.from || undefined,
|
|
24
|
+
to: filters.to || undefined,
|
|
25
|
+
q: filters.q || undefined,
|
|
26
|
+
agentId: filters.agentId || undefined,
|
|
27
|
+
projectId,
|
|
28
|
+
}).then((items) => {
|
|
29
|
+
if (controller.signal.aborted) return;
|
|
30
|
+
setReports(items);
|
|
31
|
+
if (!selectedId && items[0]) setSelectedId(items[0].id);
|
|
32
|
+
}).catch((err: unknown) => {
|
|
33
|
+
if (controller.signal.aborted) return;
|
|
34
|
+
addToast(err instanceof Error ? err.message : "Failed to load reports", "error");
|
|
35
|
+
}).finally(() => {
|
|
36
|
+
if (!controller.signal.aborted) setLoading(false);
|
|
37
|
+
});
|
|
38
|
+
return () => controller.abort();
|
|
39
|
+
}, [filters, projectId, addToast, selectedId]);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!selectedId) return;
|
|
43
|
+
getReport(selectedId, projectId).then(setSelectedReport).catch((err: unknown) => {
|
|
44
|
+
addToast(err instanceof Error ? err.message : "Failed to load report", "error");
|
|
45
|
+
});
|
|
46
|
+
}, [selectedId, projectId, addToast]);
|
|
47
|
+
|
|
48
|
+
const selectId = useCallback((id: string) => setSelectedId(id), []);
|
|
49
|
+
const enterCompareMode = useCallback(() => setCompareMode(true), []);
|
|
50
|
+
const closeCompareMode = useCallback(() => setCompareMode(false), []);
|
|
51
|
+
const setCompareSlot = useCallback((slot: "a" | "b", id: string) => {
|
|
52
|
+
if (slot === "a") setCompareA(id);
|
|
53
|
+
else setCompareB(id);
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
return useMemo(() => ({
|
|
57
|
+
filters,
|
|
58
|
+
setFilters,
|
|
59
|
+
reports,
|
|
60
|
+
loading,
|
|
61
|
+
selectedId,
|
|
62
|
+
selectedReport,
|
|
63
|
+
selectId,
|
|
64
|
+
compareMode,
|
|
65
|
+
compareA,
|
|
66
|
+
compareB,
|
|
67
|
+
enterCompareMode,
|
|
68
|
+
closeCompareMode,
|
|
69
|
+
setCompareSlot,
|
|
70
|
+
}), [filters, reports, loading, selectedId, selectedReport, selectId, compareMode, compareA, compareB, enterCompareMode, closeCompareMode, setCompareSlot]);
|
|
71
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function useViewportMode() {
|
|
4
|
+
const [mobile, setMobile] = useState(false);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
const mq = window.matchMedia("(max-width: 768px)");
|
|
7
|
+
const onChange = () => setMobile(mq.matches);
|
|
8
|
+
onChange();
|
|
9
|
+
mq.addEventListener("change", onChange);
|
|
10
|
+
return () => mq.removeEventListener("change", onChange);
|
|
11
|
+
}, []);
|
|
12
|
+
return { mobile };
|
|
13
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PluginDashboardViewContext } from "@fusion/dashboard/app/plugins/types";
|
|
2
|
+
import { ReportsView } from "./dashboard/ReportsView.js";
|
|
3
|
+
|
|
4
|
+
export function ReportsDashboardView({ context }: { context?: PluginDashboardViewContext }) {
|
|
5
|
+
return <ReportsView projectId={context?.projectId} addToast={context?.addToast ?? (() => undefined)} />;
|
|
6
|
+
}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import type { PluginContext } from "@fusion/core";
|
|
2
2
|
import { definePlugin } from "@fusion/plugin-sdk";
|
|
3
|
+
import { initializeApprovalState } from "./approval.js";
|
|
3
4
|
import { runReviewPanel } from "./review-panel.js";
|
|
4
5
|
import { ensureReportSchema } from "./report-schema.js";
|
|
6
|
+
import { createReportApprovalRoutes } from "./routes/report-approval-routes.js";
|
|
7
|
+
import { createReportExportRoutes } from "./routes/report-export-routes.js";
|
|
8
|
+
import { createReportListRoutes } from "./routes/report-list-routes.js";
|
|
5
9
|
import type { CombinedReview, ReviewPanelMember, RunReviewPanelInput } from "./review-types.js";
|
|
10
|
+
import {
|
|
11
|
+
getApprovalRequired,
|
|
12
|
+
getApproverAgentIds,
|
|
13
|
+
getAutoPublishOnApproval,
|
|
14
|
+
getPublishTargets,
|
|
15
|
+
settingsSchema,
|
|
16
|
+
} from "./settings.js";
|
|
6
17
|
import type { ReportCadence, ReportCreateInput } from "./store/report-types.js";
|
|
7
18
|
import { ReportStore } from "./store/report-store.js";
|
|
8
|
-
|
|
19
|
+
export { ReportsDashboardView } from "./dashboard-view.js";
|
|
9
20
|
|
|
10
21
|
const plugin = definePlugin({
|
|
11
22
|
manifest: {
|
|
@@ -21,6 +32,17 @@ const plugin = definePlugin({
|
|
|
21
32
|
hooks: {
|
|
22
33
|
onSchemaInit: ensureReportSchema,
|
|
23
34
|
},
|
|
35
|
+
routes: [...createReportListRoutes(), ...createReportExportRoutes(), ...createReportApprovalRoutes()],
|
|
36
|
+
dashboardViews: [
|
|
37
|
+
{
|
|
38
|
+
viewId: "reports",
|
|
39
|
+
label: "Reports",
|
|
40
|
+
componentPath: "./dashboard-view",
|
|
41
|
+
icon: "FileText",
|
|
42
|
+
placement: "primary",
|
|
43
|
+
order: 35,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
24
46
|
});
|
|
25
47
|
|
|
26
48
|
export interface RunGeneratedReportReviewInput {
|
|
@@ -73,15 +95,39 @@ export async function runGeneratedReportReview(input: RunGeneratedReportReviewIn
|
|
|
73
95
|
cwd: input.cwd,
|
|
74
96
|
}, ctx);
|
|
75
97
|
|
|
76
|
-
store.attachReview(report.id, combinedReview);
|
|
98
|
+
const reviewed = store.attachReview(report.id, combinedReview);
|
|
99
|
+
|
|
100
|
+
const nextApprovalState = initializeApprovalState(reviewed.status, {
|
|
101
|
+
approvalRequired: getApprovalRequired(ctx.settings),
|
|
102
|
+
autoPublishOnApproval: getAutoPublishOnApproval(ctx.settings),
|
|
103
|
+
approverAgentIds: getApproverAgentIds(ctx.settings),
|
|
104
|
+
publishTargets: getPublishTargets(ctx.settings),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (nextApprovalState !== "not_required") {
|
|
108
|
+
const now = new Date().toISOString();
|
|
109
|
+
store.updateReport(report.id, {
|
|
110
|
+
approvalState: nextApprovalState,
|
|
111
|
+
...(nextApprovalState === "approved"
|
|
112
|
+
? { status: "approved", approvedAt: now, approvedBy: "system" }
|
|
113
|
+
: {}),
|
|
114
|
+
...(nextApprovalState === "published" ? { status: "published", publishedAt: now } : {}),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
77
118
|
return combinedReview;
|
|
78
119
|
}
|
|
79
120
|
|
|
80
121
|
export default plugin;
|
|
81
122
|
|
|
82
123
|
export * from "./settings.js";
|
|
124
|
+
export * from "./cadence.js";
|
|
125
|
+
export * from "./aggregation.js";
|
|
126
|
+
export * from "./pipeline.js";
|
|
127
|
+
export * from "./runs-store.js";
|
|
83
128
|
export * from "./review-types.js";
|
|
84
129
|
export * from "./review-panel.js";
|
|
85
130
|
export { ensureReportSchema } from "./report-schema.js";
|
|
86
131
|
export { ReportStore, ReportStoreError, type ReportStoreEvents } from "./store/report-store.js";
|
|
87
132
|
export * from "./store/report-types.js";
|
|
133
|
+
export * from "./render/index.js";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* INTERIM ORCHESTRATOR. FN-3779 replaces this with cadence-registry + cron-sentinel wiring;
|
|
3
|
+
* FN-3780 replaces the aggregate dependency with the real aggregation layer.
|
|
4
|
+
* Keep the `ReportsPipelineDependencies` shape stable so both can plug in without callsite churn.
|
|
5
|
+
*/
|
|
6
|
+
import type { ReportsAggregator } from "./aggregation.js";
|
|
7
|
+
import type { ReportsCadence } from "./cadence.js";
|
|
8
|
+
import type { ReportRunRecord, ReportsRunsStore } from "./runs-store.js";
|
|
9
|
+
|
|
10
|
+
export interface ReportsPipelineDependencies {
|
|
11
|
+
runsStore: ReportsRunsStore;
|
|
12
|
+
aggregate: ReportsAggregator;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface StartPipelineInput {
|
|
16
|
+
runId: string;
|
|
17
|
+
cadence: ReportsCadence;
|
|
18
|
+
settings: Record<string, unknown>;
|
|
19
|
+
now?: Date;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function startReportsPipeline(
|
|
23
|
+
input: StartPipelineInput,
|
|
24
|
+
deps: ReportsPipelineDependencies,
|
|
25
|
+
): Promise<ReportRunRecord> {
|
|
26
|
+
const nowIso = (input.now ?? new Date()).toISOString();
|
|
27
|
+
|
|
28
|
+
const created = await deps.runsStore.create({
|
|
29
|
+
id: input.runId,
|
|
30
|
+
cadence: input.cadence,
|
|
31
|
+
status: "queued",
|
|
32
|
+
createdAt: nowIso,
|
|
33
|
+
updatedAt: nowIso,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await deps.runsStore.update(input.runId, { status: "running", updatedAt: nowIso });
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
await deps.aggregate({
|
|
40
|
+
runId: input.runId,
|
|
41
|
+
cadence: input.cadence,
|
|
42
|
+
settings: input.settings,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const updatedAt = new Date().toISOString();
|
|
46
|
+
const reviewed = await deps.runsStore.update(input.runId, { status: "review", updatedAt });
|
|
47
|
+
return reviewed ?? { ...created, status: "review", updatedAt };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const updatedAt = new Date().toISOString();
|
|
50
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
51
|
+
const failed = await deps.runsStore.update(input.runId, {
|
|
52
|
+
status: "failed",
|
|
53
|
+
error: message,
|
|
54
|
+
updatedAt,
|
|
55
|
+
});
|
|
56
|
+
return failed ?? { ...created, status: "failed", error: message, updatedAt };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { escapeAttr, escapeHtml } from "../escape.js";
|
|
3
|
+
|
|
4
|
+
describe("escape", () => {
|
|
5
|
+
it("escapes html special characters", () => {
|
|
6
|
+
expect(escapeHtml("&<>'\"")).toBe("&<>'"");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("escapes attribute-sensitive characters", () => {
|
|
10
|
+
expect(escapeAttr("a`b&c")).toBe("a`b&c");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("passes unicode through", () => {
|
|
14
|
+
expect(escapeHtml("こんにちは 🌍")).toBe("こんにちは 🌍");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("passes unicode through in attributes", () => {
|
|
18
|
+
expect(escapeAttr("こんにちは 🌍")).toBe("こんにちは 🌍");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { Report } from "../../store/report-types.js";
|
|
3
|
+
import { renderReportHtml } from "../html-template.js";
|
|
4
|
+
|
|
5
|
+
function createRecord(overrides: Partial<Report> = {}, metadata: Record<string, unknown> = {}): Report {
|
|
6
|
+
const base: Report = {
|
|
7
|
+
id: "rep_1",
|
|
8
|
+
cadence: "daily",
|
|
9
|
+
periodStart: "2026-05-01",
|
|
10
|
+
periodEnd: "2026-05-02",
|
|
11
|
+
title: "Weekly report",
|
|
12
|
+
status: "review_complete",
|
|
13
|
+
generationStartedAt: "2026-05-02T00:00:00.000Z",
|
|
14
|
+
generationCompletedAt: "2026-05-02T00:01:00.000Z",
|
|
15
|
+
reviewStartedAt: null,
|
|
16
|
+
reviewCompletedAt: null,
|
|
17
|
+
approvedAt: null,
|
|
18
|
+
approvedBy: null,
|
|
19
|
+
publishedAt: null,
|
|
20
|
+
archivedAt: null,
|
|
21
|
+
failureReason: null,
|
|
22
|
+
approvalState: "not_required",
|
|
23
|
+
approvalHistory: [],
|
|
24
|
+
draftMarkdown: null,
|
|
25
|
+
renderedHtmlPath: null,
|
|
26
|
+
combinedReview: null,
|
|
27
|
+
createdAt: "2026-05-02T00:00:00.000Z",
|
|
28
|
+
updatedAt: "2026-05-02T00:01:00.000Z",
|
|
29
|
+
metadata,
|
|
30
|
+
renderedHtml: null,
|
|
31
|
+
renderedHtmlGeneratedAt: null,
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
...base,
|
|
35
|
+
...overrides,
|
|
36
|
+
renderedHtml: overrides.renderedHtml ?? base.renderedHtml,
|
|
37
|
+
renderedHtmlGeneratedAt: overrides.renderedHtmlGeneratedAt ?? base.renderedHtmlGeneratedAt,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("renderReportHtml", () => {
|
|
42
|
+
it("renders shell for empty record", () => {
|
|
43
|
+
const html = renderReportHtml(createRecord());
|
|
44
|
+
expect(html).toContain("<!doctype html>");
|
|
45
|
+
expect(html).toContain("data-section=\"data-coverage\"");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("renders mixed sections with markers", () => {
|
|
49
|
+
const html = renderReportHtml(createRecord({}, {
|
|
50
|
+
sections: {
|
|
51
|
+
summary: "hello",
|
|
52
|
+
system: { wins: ["w1"], highlights: ["h1"], lowlights: ["l1"], proposals: ["p1"], deepDives: ["d1"] },
|
|
53
|
+
perAgent: [{ agentId: "a1", wins: ["x"] }],
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
expect(html).toContain('data-section="summary"');
|
|
57
|
+
expect(html).toContain('data-section="system-wins"');
|
|
58
|
+
expect(html).toContain('data-section="system-highlights"');
|
|
59
|
+
expect(html).toContain('data-section="system-lowlights"');
|
|
60
|
+
expect(html).toContain('data-section="system-proposals"');
|
|
61
|
+
expect(html).toContain('data-section="system-deep-dives"');
|
|
62
|
+
expect(html).toContain('data-section="agent-card"');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("omits toggled off sections", () => {
|
|
66
|
+
const html = renderReportHtml(createRecord({}, {
|
|
67
|
+
settings: { enabledSections: ["wins"] },
|
|
68
|
+
sections: { system: { wins: ["w1"], highlights: ["h1"] } },
|
|
69
|
+
}));
|
|
70
|
+
expect(html).toContain('data-section="system-wins"');
|
|
71
|
+
expect(html).not.toContain('data-section="system-highlights"');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("respects section order", () => {
|
|
75
|
+
const html = renderReportHtml(createRecord({}, {
|
|
76
|
+
settings: { sectionOrder: ["proposals", "wins"], enabledSections: ["wins", "proposals"] },
|
|
77
|
+
sections: { system: { wins: ["w1"], proposals: ["p1"] } },
|
|
78
|
+
}));
|
|
79
|
+
expect(html.indexOf('data-section="system-proposals"')).toBeLessThan(html.indexOf('data-section="system-wins"'));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("renders per-agent cards with stable ids", () => {
|
|
83
|
+
const html = renderReportHtml(createRecord({}, {
|
|
84
|
+
sections: { perAgent: [{ agentId: "agent-1" }, { agentId: "agent-2" }] },
|
|
85
|
+
}));
|
|
86
|
+
expect(html).toContain('data-agent-id="agent-1"');
|
|
87
|
+
expect(html).toContain('data-agent-id="agent-2"');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("escapes hostile inputs", () => {
|
|
91
|
+
const html = renderReportHtml(createRecord({}, {
|
|
92
|
+
sections: { summary: '<script>alert(1)</script> " onmouseover=' },
|
|
93
|
+
}));
|
|
94
|
+
expect(html).not.toContain("<script>alert(1)</script>");
|
|
95
|
+
expect(html).not.toContain("javascript:");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("applies explicit theme", () => {
|
|
99
|
+
const dark = renderReportHtml(createRecord(), { theme: "dark" });
|
|
100
|
+
const light = renderReportHtml(createRecord(), { theme: "light" });
|
|
101
|
+
expect(dark).toContain('data-theme="dark"');
|
|
102
|
+
expect(light).toContain('data-theme="light"');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("returns body-only when includeChrome false", () => {
|
|
106
|
+
const body = renderReportHtml(createRecord(), { includeChrome: false });
|
|
107
|
+
expect(body.startsWith("<!doctype html>")).toBe(false);
|
|
108
|
+
expect(body.startsWith("<article")).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
});
|