mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
fix(briefs): add settings and dismissal controls
This commit is contained in:
1 parent
602c68370c
commit
f9a88c02da
16 files changed
+1060
-147
No files matched your search
@@ -23,6 +23,7 @@ const bridgeActor = {
|
||||
describe("Briefs managed resources", () => {
|
||||
it("declares the Briefing Analyst, skills, routines, and agent tools", () => {
|
||||
expect(manifest.capabilities).toEqual(expect.arrayContaining([
|
||||
"projects.managed",
|
||||
"agents.resume",
|
||||
"agents.managed",
|
||||
"skills.managed",
|
||||
@@ -51,10 +52,25 @@ describe("Briefs managed resources", () => {
|
||||
expect(manifest.agents?.[0]?.instructions?.content).toContain("executive standup updates");
|
||||
expect(manifest.agents?.[0]?.instructions?.content).toContain("Do not put issue identifiers");
|
||||
expect(manifest.agents?.[0]?.instructions?.content).toContain("focus more on what is left to do");
|
||||
expect(manifest.agents?.[0]?.instructions?.content).toContain("Dismissed cards are intentional user feedback");
|
||||
expect(manifest.skills?.find((skill) => skill.skillKey === "briefs-discover-cards")?.markdown).toContain("includeHidden: true");
|
||||
expect(manifest.skills?.find((skill) => skill.skillKey === "briefs-update-cards")?.markdown).toContain("refresh every visible card");
|
||||
expect(manifest.skills?.find((skill) => skill.skillKey === "briefs-update-cards")?.markdown).toContain("Hidden cards were dismissed by the user");
|
||||
expect(manifest.routines?.find((routine) => routine.routineKey === "briefs-discover-cards")?.description).toContain("skip hidden/dismissed roots");
|
||||
expect(manifest.routines?.find((routine) => routine.routineKey === "briefs-update-cards")?.description).toContain("manual/API runs are rewrite passes");
|
||||
expect(manifest.skills?.map((skill) => skill.skillKey)).toEqual([...BRIEFS_MANAGED_SKILL_KEYS]);
|
||||
expect(manifest.projects).toEqual([
|
||||
expect.objectContaining({
|
||||
projectKey: BRIEFS_PROJECT_KEY,
|
||||
displayName: "Briefs",
|
||||
status: "in_progress",
|
||||
}),
|
||||
]);
|
||||
expect(manifest.routines?.map((routine) => routine.routineKey)).toEqual([...BRIEFS_MANAGED_ROUTINE_KEYS]);
|
||||
expect(manifest.routines?.every((routine) =>
|
||||
routine.projectRef?.resourceKind === "project"
|
||||
&& routine.projectRef.resourceKey === BRIEFS_PROJECT_KEY
|
||||
)).toBe(true);
|
||||
expect(manifest.routines?.find((routine) => routine.routineKey === MANUAL_REFRESH_ROUTINE_KEY)).toMatchObject({
|
||||
assigneeRef: { resourceKind: "agent", resourceKey: BRIEFING_ANALYST_AGENT_KEY },
|
||||
projectRef: { resourceKind: "project", resourceKey: BRIEFS_PROJECT_KEY },
|
||||
@@ -131,7 +147,7 @@ describe("Briefs managed resources", () => {
|
||||
managedProject: { status: string; projectId: string | null };
|
||||
managedAgent: { status: string; agentId: string | null };
|
||||
managedSkills: Array<{ status: string; skillId: string | null }>;
|
||||
managedRoutines: Array<{ status: string; routineId: string | null; missingRefs: unknown[] }>;
|
||||
managedRoutines: Array<{ status: string; routineId: string | null; routine: { projectId: string | null } | null; missingRefs: unknown[] }>;
|
||||
}>("reconcile-managed-resources", { companyId });
|
||||
|
||||
expect(result.managedProject).toMatchObject({ status: "created" });
|
||||
@@ -141,10 +157,75 @@ describe("Briefs managed resources", () => {
|
||||
for (const routine of result.managedRoutines) {
|
||||
expect(routine.status).toBe("created");
|
||||
expect(routine.routineId).toBeTruthy();
|
||||
expect(routine.routine?.projectId).toBe(result.managedProject.projectId);
|
||||
expect(routine.missingRefs).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it("reconciles managed routines through the plugin-managed project from settings", async () => {
|
||||
const harness = createTestHarness({ manifest });
|
||||
await plugin.definition.setup?.(harness.ctx);
|
||||
|
||||
const result = await harness.performAction<{
|
||||
managedProject: { status: string; projectId: string | null };
|
||||
managedAgent: { status: string; agentId: string | null };
|
||||
managedRoutines: Array<{ status: string; routineId: string | null; routine: { projectId: string | null } | null; missingRefs: unknown[] }>;
|
||||
}>("reconcile-managed-routines", { companyId });
|
||||
|
||||
expect(result.managedProject).toMatchObject({ status: "created" });
|
||||
expect(result.managedProject.projectId).toBeTruthy();
|
||||
expect(result.managedAgent.agentId).toBeTruthy();
|
||||
expect(result.managedRoutines).toHaveLength(3);
|
||||
for (const routine of result.managedRoutines) {
|
||||
expect(routine.status).toBe("created");
|
||||
expect(routine.routine?.projectId).toBe(result.managedProject.projectId);
|
||||
expect(routine.missingRefs).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores caller-supplied project overrides when repairing Briefs routines", async () => {
|
||||
const harness = createTestHarness({ manifest });
|
||||
await plugin.definition.setup?.(harness.ctx);
|
||||
|
||||
const routine = await harness.performAction<{
|
||||
status: string;
|
||||
routineId: string | null;
|
||||
routine: { projectId: string | null } | null;
|
||||
missingRefs: unknown[];
|
||||
}>("reconcile-managed-routine", {
|
||||
companyId,
|
||||
routineKey: MANUAL_REFRESH_ROUTINE_KEY,
|
||||
projectId: "operator-selected-project",
|
||||
});
|
||||
const managedProject = await harness.ctx.projects.managed.get(BRIEFS_PROJECT_KEY, companyId);
|
||||
|
||||
expect(routine.status).toBe("created");
|
||||
expect(routine.routineId).toBeTruthy();
|
||||
expect(routine.routine?.projectId).toBe(managedProject.projectId);
|
||||
expect(routine.routine?.projectId).not.toBe("operator-selected-project");
|
||||
expect(routine.missingRefs).toEqual([]);
|
||||
});
|
||||
|
||||
it("repairs legacy Briefs routines that are not linked to the plugin-managed project", async () => {
|
||||
const harness = createTestHarness({ manifest });
|
||||
await plugin.definition.setup?.(harness.ctx);
|
||||
const managedProject = await harness.ctx.projects.managed.reconcile(BRIEFS_PROJECT_KEY, companyId);
|
||||
const managedAgent = await harness.ctx.agents.managed.reconcile(BRIEFING_ANALYST_AGENT_KEY, companyId);
|
||||
const legacy = await harness.ctx.routines.managed.reconcile(MANUAL_REFRESH_ROUTINE_KEY, companyId, {
|
||||
assigneeAgentId: managedAgent.agentId,
|
||||
projectId: "legacy-project",
|
||||
});
|
||||
expect(legacy.routine?.projectId).toBe("legacy-project");
|
||||
|
||||
const result = await harness.performAction<{
|
||||
managedRoutines: Array<{ status: string; resourceKey: string; routine: { projectId: string | null } | null }>;
|
||||
}>("reconcile-managed-resources", { companyId });
|
||||
const repaired = result.managedRoutines.find((routine) => routine.resourceKey === MANUAL_REFRESH_ROUTINE_KEY);
|
||||
|
||||
expect(repaired?.status).toBe("reset");
|
||||
expect(repaired?.routine?.projectId).toBe(managedProject.projectId);
|
||||
});
|
||||
|
||||
it("resumes the managed analyst before running user briefing routines", async () => {
|
||||
const harness = createTestHarness({ manifest });
|
||||
await plugin.definition.setup?.(harness.ctx);
|
||||
@@ -153,12 +234,18 @@ describe("Briefs managed resources", () => {
|
||||
const before = await harness.ctx.agents.managed.get(BRIEFING_ANALYST_AGENT_KEY, companyId);
|
||||
expect(before.agent?.status).toBe("paused");
|
||||
|
||||
const managedProject = await harness.ctx.projects.managed.get(BRIEFS_PROJECT_KEY, companyId);
|
||||
const result = await harness.performAction<{
|
||||
runs: Array<{ status: string; triggerPayload: { variables: Record<string, unknown> } | null }>;
|
||||
}>("run-briefing-routines", { companyId }, { actor: bridgeActor });
|
||||
}>("run-briefing-routines", { companyId, projectId: "operator-selected-project" }, { actor: bridgeActor });
|
||||
|
||||
const after = await harness.ctx.agents.managed.get(BRIEFING_ANALYST_AGENT_KEY, companyId);
|
||||
expect(after.agent?.status).toBe("idle");
|
||||
for (const routineKey of BRIEFS_MANAGED_ROUTINE_KEYS.filter((routineKey) => routineKey !== MANUAL_REFRESH_ROUTINE_KEY)) {
|
||||
const routine = await harness.ctx.routines.managed.get(routineKey, companyId);
|
||||
expect(routine.routine?.projectId).toBe(managedProject.projectId);
|
||||
expect(routine.routine?.projectId).not.toBe("operator-selected-project");
|
||||
}
|
||||
expect(result.runs).toHaveLength(2);
|
||||
expect(result.runs.map((run) => run.status)).toEqual(["queued", "queued"]);
|
||||
expect(result.runs.map((run) => run.triggerPayload?.variables?.userId)).toEqual([
|
||||
@@ -180,9 +267,14 @@ describe("Briefs managed resources", () => {
|
||||
cardId: "card-1",
|
||||
pinned: true,
|
||||
}, context)).rejects.toThrow("Briefs user scope mismatch");
|
||||
await expect(harness.performAction("dismiss-card", {
|
||||
...victimParams,
|
||||
cardId: "card-1",
|
||||
}, context)).rejects.toThrow("Briefs user scope mismatch");
|
||||
await expect(harness.performAction("update-preferences", {
|
||||
...victimParams,
|
||||
cadence: "daily",
|
||||
}, context)).rejects.toThrow("Briefs user scope mismatch");
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { PluginDatabaseClient } from "@paperclipai/plugin-sdk";
|
||||
|
||||
import {
|
||||
buildDeterministicBriefCard,
|
||||
type BriefsIssueInput,
|
||||
type BriefsSourceBundle,
|
||||
} from "../src/deterministic-card-service.js";
|
||||
import { createBriefsStore } from "../src/store.js";
|
||||
|
||||
const companyId = "11111111-1111-4111-8111-111111111111";
|
||||
const userId = "user-1";
|
||||
const rootIssueId = "22222222-2222-4222-8222-222222222222";
|
||||
|
||||
function ids() {
|
||||
let counter = 0;
|
||||
return () => `00000000-0000-4000-8000-${String(++counter).padStart(12, "0")}`;
|
||||
}
|
||||
|
||||
function issue(overrides: Partial<BriefsIssueInput> = {}): BriefsIssueInput {
|
||||
return {
|
||||
id: rootIssueId,
|
||||
companyId,
|
||||
parentId: null,
|
||||
title: "Dismissed card source",
|
||||
identifier: "PAP-1",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
assigneeAgentId: null,
|
||||
assigneeUserId: null,
|
||||
createdByUserId: userId,
|
||||
createdAt: "2026-05-22T10:00:00.000Z",
|
||||
updatedAt: "2026-05-22T11:00:00.000Z",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function bundle(): BriefsSourceBundle {
|
||||
return {
|
||||
companyId,
|
||||
userId,
|
||||
rootIssueId,
|
||||
title: "Dismissed card source",
|
||||
groupingDescription: "Issue tree rooted at PAP-1: Dismissed card source",
|
||||
issues: [issue()],
|
||||
relations: {},
|
||||
activeRuns: {},
|
||||
runs: [],
|
||||
comments: [],
|
||||
documents: [],
|
||||
interactions: [],
|
||||
approvals: [],
|
||||
workProducts: [],
|
||||
relevantAgentIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
type CardRow = {
|
||||
id: string;
|
||||
company_id: string;
|
||||
user_id: string;
|
||||
slug: string;
|
||||
pinned: boolean;
|
||||
hidden: boolean;
|
||||
};
|
||||
|
||||
function createFakeDb(): PluginDatabaseClient {
|
||||
const cards = new Map<string, CardRow>();
|
||||
const key = (company: unknown, user: unknown, slug: unknown) => `${company}:${user}:${slug}`;
|
||||
|
||||
return {
|
||||
namespace: "plugin_briefs_test",
|
||||
async query<T>(sql: string, params: unknown[] = []): Promise<T[]> {
|
||||
if (sql.includes("SELECT id, pinned, hidden FROM plugin_briefs_test.briefs_cards")) {
|
||||
const row = cards.get(key(params[0], params[1], params[2]));
|
||||
return (row ? [{ id: row.id, pinned: row.pinned, hidden: row.hidden }] : []) as T[];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
async execute(sql: string, params: unknown[] = []): Promise<{ rowCount: number }> {
|
||||
if (sql.includes("INSERT INTO plugin_briefs_test.briefs_cards")) {
|
||||
const rowKey = key(params[1], params[2], params[3]);
|
||||
const existing = cards.get(rowKey);
|
||||
cards.set(rowKey, {
|
||||
id: existing?.id ?? String(params[0]),
|
||||
company_id: String(params[1]),
|
||||
user_id: String(params[2]),
|
||||
slug: String(params[3]),
|
||||
pinned: Boolean(existing?.pinned || params[9]),
|
||||
hidden: Boolean(existing?.hidden || params[10]),
|
||||
});
|
||||
}
|
||||
if (sql.includes("SET hidden = true, pinned = false")) {
|
||||
for (const [rowKey, row] of cards) {
|
||||
if (row.company_id === params[0] && row.user_id === params[1] && row.id === params[2]) {
|
||||
cards.set(rowKey, { ...row, hidden: true, pinned: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
return { rowCount: 1 };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("Briefs store", () => {
|
||||
it("keeps dismissed cards hidden when the same card is regenerated", async () => {
|
||||
const store = createBriefsStore(createFakeDb());
|
||||
const first = await store.saveCard(buildDeterministicBriefCard(bundle(), {
|
||||
now: "2026-05-22T12:00:00.000Z",
|
||||
idFactory: ids(),
|
||||
summaryStatus: "ok",
|
||||
summaryParagraph: "The source area is being prepared for follow-up. Next action is to finish the implementation pass.",
|
||||
summaryModel: "test",
|
||||
allowGeneratedSummary: true,
|
||||
}));
|
||||
|
||||
await store.dismissCard({ companyId, userId, cardId: first.id });
|
||||
|
||||
const regenerated = await store.saveCard(buildDeterministicBriefCard(bundle(), {
|
||||
now: "2026-05-22T13:00:00.000Z",
|
||||
idFactory: ids(),
|
||||
summaryStatus: "ok",
|
||||
summaryParagraph: "A newer generated summary should not resurrect the dismissed card. Next action is still hidden from the dashboard.",
|
||||
summaryModel: "test",
|
||||
allowGeneratedSummary: true,
|
||||
}));
|
||||
|
||||
expect(regenerated.id).toBe(first.id);
|
||||
expect(regenerated.hidden).toBe(true);
|
||||
expect(regenerated.pinned).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -9,8 +9,8 @@ vi.mock("@paperclipai/plugin-sdk/ui", () => {
|
||||
linkProps: (to: string) => ({ href: to, onClick: () => {} }),
|
||||
}),
|
||||
usePluginAction: () => vi.fn(async () => ({ ok: true })),
|
||||
IssueRow: ({ issue, trailingMeta }: { issue: { identifier?: string | null; title: string }; trailingMeta?: ReactNode }) => (
|
||||
<a data-plugin-issue-row={issue.identifier ?? ""} href={`/issues/${issue.identifier ?? ""}`}>{issue.identifier} {issue.title} {trailingMeta}</a>
|
||||
IssueRow: ({ issue, trailingMeta, className }: { issue: { identifier?: string | null; title: string }; trailingMeta?: ReactNode; className?: string }) => (
|
||||
<a data-plugin-issue-row={issue.identifier ?? ""} className={className} href={`/issues/${issue.identifier ?? ""}`}>{issue.identifier} {issue.title} {trailingMeta}</a>
|
||||
),
|
||||
usePluginData: () => ({ data: null, loading: false, error: null, refresh: () => {} }),
|
||||
usePluginToast: () => vi.fn(),
|
||||
@@ -118,4 +118,13 @@ describe("BriefCardView", () => {
|
||||
const unpinned = makeCard({ pinned: false });
|
||||
expect(renderCard(unpinned)).toMatch(/aria-label="Pin card"/);
|
||||
});
|
||||
|
||||
it("renders a dismiss action and removes issue-row bottom dividers", () => {
|
||||
const card = makeCard();
|
||||
const html = renderCard(card);
|
||||
|
||||
expect(html).toContain('aria-label="Dismiss briefing card"');
|
||||
expect(html).toContain("Dismiss");
|
||||
expect(html).toContain("!border-b-0");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { BriefCard } from "../../src/contracts.js";
|
||||
import { makeCard, makeSnapshot, makeTaskRow } from "./fixtures.js";
|
||||
|
||||
const actionCalls = vi.hoisted(() => ({
|
||||
pin: vi.fn(async () => ({ ok: true })),
|
||||
dismiss: vi.fn(async () => ({ ok: true })),
|
||||
}));
|
||||
|
||||
let mockPageData: { cards: BriefCard[]; fetchedAt: string };
|
||||
|
||||
vi.mock("@paperclipai/plugin-sdk/ui", () => {
|
||||
return {
|
||||
ManagedRoutinesList: () => null,
|
||||
IssueRow: ({ issue, trailingMeta, className }: { issue: { identifier?: string | null; title: string }; trailingMeta?: ReactNode; className?: string }) => (
|
||||
<a data-plugin-issue-row={issue.identifier ?? ""} className={className} href={`/issues/${issue.identifier ?? ""}`}>{issue.identifier} {issue.title} {trailingMeta}</a>
|
||||
),
|
||||
useHostNavigation: () => ({
|
||||
resolveHref: (to: string) => to,
|
||||
navigate: () => {},
|
||||
linkProps: (to: string) => ({ href: to, onClick: () => {} }),
|
||||
}),
|
||||
usePluginAction: (key: string) => {
|
||||
if (key === "pin-card") return actionCalls.pin;
|
||||
if (key === "dismiss-card") return actionCalls.dismiss;
|
||||
return vi.fn(async () => ({ ok: true }));
|
||||
},
|
||||
usePluginData: (key: string) => {
|
||||
if (key === "page") {
|
||||
return { data: mockPageData, loading: false, error: null, refresh: vi.fn() };
|
||||
}
|
||||
return { data: null, loading: false, error: null, refresh: vi.fn() };
|
||||
},
|
||||
usePluginToast: () => vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import { BriefingPage } from "../../src/ui/app.js";
|
||||
|
||||
const hostContext = {
|
||||
companyId: "company-1",
|
||||
companyPrefix: "PAP",
|
||||
projectId: null,
|
||||
entityId: null,
|
||||
entityType: null,
|
||||
userId: "user-1",
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
function makeCards(): BriefCard[] {
|
||||
const newest = makeCard({
|
||||
id: "card-new",
|
||||
title: "Newest unpinned work",
|
||||
pinned: false,
|
||||
lastMeaningfulEventAt: "2026-05-23T11:00:00.000Z",
|
||||
snapshot: makeSnapshot({
|
||||
taskRows: [makeTaskRow({ identifier: "PAP-1", titleLine: "Newest issue" })],
|
||||
}),
|
||||
});
|
||||
const older = makeCard({
|
||||
id: "card-old",
|
||||
title: "Older unpinned work",
|
||||
pinned: false,
|
||||
lastMeaningfulEventAt: "2026-05-22T11:00:00.000Z",
|
||||
snapshot: makeSnapshot({
|
||||
taskRows: [makeTaskRow({ identifier: "PAP-2", titleLine: "Older issue" })],
|
||||
}),
|
||||
});
|
||||
return [newest, older];
|
||||
}
|
||||
|
||||
describe("BriefingPage interactions", () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: ReturnType<typeof createRoot>;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
mockPageData = { cards: makeCards(), fetchedAt: "2026-05-23T12:00:00.000Z" };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
flushSync(() => root.unmount());
|
||||
container.remove();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
function renderPage() {
|
||||
flushSync(() => {
|
||||
root.render(<BriefingPage context={hostContext as never} />);
|
||||
});
|
||||
}
|
||||
|
||||
it("pins optimistically and moves the card to the top without a full refresh", async () => {
|
||||
renderPage();
|
||||
|
||||
const oldCard = [...container.querySelectorAll("article[data-briefs-card]")]
|
||||
.find((node) => node.textContent?.includes("Older unpinned work"));
|
||||
const pinButton = oldCard?.querySelector('button[aria-label="Pin card"]') as HTMLButtonElement | null;
|
||||
expect(pinButton).not.toBeNull();
|
||||
|
||||
flushSync(() => {
|
||||
pinButton?.click();
|
||||
});
|
||||
|
||||
const titles = [...container.querySelectorAll("article[data-briefs-card] h3")].map((node) => node.textContent);
|
||||
expect(titles[0]).toBe("Older unpinned work");
|
||||
expect(actionCalls.pin).toHaveBeenCalledWith(expect.objectContaining({
|
||||
cardId: "card-old",
|
||||
pinned: true,
|
||||
}));
|
||||
});
|
||||
|
||||
it("starts the dismiss fade immediately and persists the hidden card", async () => {
|
||||
renderPage();
|
||||
|
||||
const firstCard = container.querySelector("article[data-briefs-card]") as HTMLElement | null;
|
||||
const dismissButton = firstCard?.querySelector('button[aria-label="Dismiss briefing card"]') as HTMLButtonElement | null;
|
||||
expect(firstCard).not.toBeNull();
|
||||
expect(dismissButton).not.toBeNull();
|
||||
|
||||
flushSync(() => {
|
||||
dismissButton?.click();
|
||||
});
|
||||
|
||||
expect(firstCard?.style.opacity).toBe("0");
|
||||
expect(actionCalls.dismiss).toHaveBeenCalledWith(expect.objectContaining({
|
||||
cardId: "card-new",
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -27,6 +27,14 @@ vi.mock("@paperclipai/plugin-sdk/ui", () => {
|
||||
navigate: () => {},
|
||||
linkProps: (to: string) => ({ href: to, onClick: () => {} }),
|
||||
}),
|
||||
useHostContext: () => ({
|
||||
companyId: "company-1",
|
||||
companyPrefix: "PAP",
|
||||
projectId: null,
|
||||
entityId: null,
|
||||
entityType: null,
|
||||
userId: "user-1",
|
||||
}),
|
||||
usePluginAction: () => vi.fn(async () => ({ ok: true })),
|
||||
usePluginData: (key: string) => {
|
||||
if (key === "page") {
|
||||
@@ -63,6 +71,16 @@ vi.mock("@paperclipai/plugin-sdk/ui", () => {
|
||||
routine: { id: "routine-2", title: "Update Briefing cards for {{userId}}", status: "paused", projectId: "project-1", assigneeAgentId: "agent-1" },
|
||||
},
|
||||
],
|
||||
preferences: {
|
||||
companyId: "company-1",
|
||||
userId: "user-1",
|
||||
cadence: "hourly",
|
||||
retentionDays: 7,
|
||||
doneRetentionHours: 72,
|
||||
staleAfterDays: 7,
|
||||
maxUnpinnedCards: 30,
|
||||
scope: "user",
|
||||
},
|
||||
agentOptions: [{ id: "agent-1", name: "Briefing Analyst", icon: "newspaper" }],
|
||||
projectOptions: [{ id: "project-1", name: "Briefs", color: "#0f766e" }],
|
||||
},
|
||||
@@ -101,6 +119,8 @@ describe("BriefingPage", () => {
|
||||
const html = renderPage(gallery());
|
||||
|
||||
expect(html).toContain("data-briefs-list");
|
||||
expect(html).toContain("data-briefs-card-grid");
|
||||
expect(html).toContain("grid-template-columns:repeat(2, minmax(0, 1fr))");
|
||||
expect(html).toContain("Recent work and next steps");
|
||||
expect(html).not.toContain("data-briefs-mobile-tabs");
|
||||
expect(html).not.toContain("data-briefs-section");
|
||||
@@ -136,6 +156,8 @@ describe("BriefingPage", () => {
|
||||
|
||||
expect(html).toContain("data-briefs-page-header");
|
||||
expect(html).toContain("data-briefs-page-meta");
|
||||
expect(html).toContain('aria-label="Briefing settings"');
|
||||
expect(html).toContain("/PAP/instance/settings/plugins/paperclipai.plugin-briefs");
|
||||
expect(html).not.toContain("Preferences");
|
||||
expect(html).not.toContain("Durable cards for areas of work");
|
||||
});
|
||||
@@ -148,13 +170,14 @@ describe("BriefingPage", () => {
|
||||
});
|
||||
|
||||
describe("SidebarLink", () => {
|
||||
it("renders the Briefing sidebar entry with an icon and company route", () => {
|
||||
it("renders the Briefing sidebar entry with an icon, company route, and no badge count", () => {
|
||||
const html = renderToStaticMarkup(<SidebarLink context={hostContext as never} />);
|
||||
|
||||
expect(html).toContain("Briefing");
|
||||
expect(html).toContain('href="/briefs"');
|
||||
expect(html).toContain("data-briefs-sidebar-icon");
|
||||
expect(html).toContain("color:currentColor");
|
||||
expect(html).not.toContain("briefs need your attention");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -163,6 +186,9 @@ describe("SettingsPage", () => {
|
||||
const html = renderToStaticMarkup(<SettingsPage context={hostContext as never} />);
|
||||
|
||||
expect(html).toContain("Managed resources");
|
||||
expect(html).toContain("Dashboard settings");
|
||||
expect(html).toContain("Maximum unpinned cards");
|
||||
expect(html).toContain("Mark stale after days");
|
||||
expect(html).toContain("Briefing Analyst");
|
||||
expect(html).toContain("data-managed-routines-list");
|
||||
expect(html).toContain("Discover Briefing cards");
|
||||
|
||||
Reference in new issue
Block a user