mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 09:59:44 +02:00
2279 lines
91 KiB
TypeScript
2279 lines
91 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { createElement } from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { createTestHarness } from "@paperclipai/plugin-sdk/testing";
|
|
import type { Agent, Issue, PluginManagedRoutineResolution, Project } from "@paperclipai/plugin-sdk";
|
|
import manifest, {
|
|
CURSOR_WINDOW_ROUTINE_KEY,
|
|
INDEX_REFRESH_ROUTINE_KEY,
|
|
NIGHTLY_LINT_ROUTINE_KEY,
|
|
WIKI_MAINTAINER_AGENT_KEY,
|
|
WIKI_MAINTENANCE_ROUTINE_KEYS,
|
|
WIKI_PROJECT_KEY,
|
|
} from "../src/manifest.js";
|
|
import {
|
|
DEFAULT_AGENT_INSTRUCTIONS,
|
|
DEFAULT_IDEA,
|
|
DEFAULT_INDEX,
|
|
DEFAULT_LOG,
|
|
DEFAULT_WIKI_SCHEMA,
|
|
INGEST_PROMPT,
|
|
KARPATHY_LLM_WIKI_GIST_URL,
|
|
LINT_PROMPT,
|
|
BACKFILL_PROMPT,
|
|
DECISION_DISTILL_PROMPT,
|
|
PAPERCLIP_SOURCE_BUNDLE_PROMPT,
|
|
PROJECT_PAGE_DISTILL_PROMPT,
|
|
QUERY_PROMPT,
|
|
STATUS_REFRESH_PROMPT,
|
|
} from "../src/templates.js";
|
|
import { SettingsPage, SidebarLink, WikiPage, WikiRouteSidebar } from "../src/ui/index.js";
|
|
import plugin from "../src/worker.js";
|
|
import { OPERATION_ORIGIN_KIND } from "../src/wiki.js";
|
|
|
|
const COMPANY_ID = "11111111-1111-4111-8111-111111111111";
|
|
const OTHER_COMPANY_ID = "99999999-9999-4999-8999-999999999999";
|
|
const ORIGINAL_DEPLOYMENT_MODE = process.env.PAPERCLIP_DEPLOYMENT_MODE;
|
|
const ORIGINAL_DEPLOYMENT_EXPOSURE = process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE;
|
|
type TestBridgeGlobal = typeof globalThis & {
|
|
__paperclipPluginBridge__?: {
|
|
sdkUi?: Record<string, unknown>;
|
|
};
|
|
};
|
|
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as {
|
|
dependencies?: Record<string, string>;
|
|
devDependencies?: Record<string, string>;
|
|
peerDependencies?: Record<string, string>;
|
|
};
|
|
|
|
let mockPathname = "/PAP/wiki";
|
|
let mockSearch = "";
|
|
let mockAutoSelectFile: string | null = null;
|
|
let mockNavigatedTo: string | null = null;
|
|
let mockOverviewFolder: Record<string, unknown> | null = null;
|
|
let mockSettingsManagedAgent: Record<string, unknown> | null = null;
|
|
let mockSettingsManagedRoutines: Array<Record<string, unknown>> = [];
|
|
let mockPageContentsByPath: Record<string, string> = {};
|
|
|
|
beforeEach(() => {
|
|
if (ORIGINAL_DEPLOYMENT_MODE == null) {
|
|
delete process.env.PAPERCLIP_DEPLOYMENT_MODE;
|
|
} else {
|
|
process.env.PAPERCLIP_DEPLOYMENT_MODE = ORIGINAL_DEPLOYMENT_MODE;
|
|
}
|
|
if (ORIGINAL_DEPLOYMENT_EXPOSURE == null) {
|
|
delete process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE;
|
|
} else {
|
|
process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE = ORIGINAL_DEPLOYMENT_EXPOSURE;
|
|
}
|
|
mockPathname = "/PAP/wiki";
|
|
mockSearch = "";
|
|
mockAutoSelectFile = null;
|
|
mockNavigatedTo = null;
|
|
mockOverviewFolder = null;
|
|
mockSettingsManagedAgent = null;
|
|
mockSettingsManagedRoutines = [];
|
|
mockPageContentsByPath = {};
|
|
(globalThis as TestBridgeGlobal).__paperclipPluginBridge__ = {
|
|
sdkUi: {
|
|
usePluginData: (key: string, params?: Record<string, unknown>) => {
|
|
if (key === "overview") {
|
|
return {
|
|
data: {
|
|
status: "ok",
|
|
checkedAt: new Date().toISOString(),
|
|
wikiId: "default",
|
|
folder: mockOverviewFolder ?? {
|
|
configured: true,
|
|
path: "/tmp/company-wiki",
|
|
realPath: "/tmp/company-wiki",
|
|
access: "readWrite",
|
|
readable: true,
|
|
writable: true,
|
|
requiredDirectories: [],
|
|
requiredFiles: [],
|
|
missingDirectories: [],
|
|
missingFiles: [],
|
|
healthy: true,
|
|
problems: [],
|
|
checkedAt: new Date().toISOString(),
|
|
},
|
|
managedAgent: { status: "resolved", details: { name: "Wiki Maintainer", status: "idle", adapterType: "claude_local", icon: "book-open", urlKey: "wiki-maintainer" } },
|
|
managedProject: { status: "resolved", details: { name: "LLM Wiki", status: "in_progress" } },
|
|
operationCount: 0,
|
|
eventIngestion: {
|
|
enabled: false,
|
|
sources: { issues: false, comments: false, documents: false },
|
|
wikiId: "default",
|
|
maxCharacters: 12000,
|
|
},
|
|
capabilities: [],
|
|
prompts: { query: QUERY_PROMPT, lint: LINT_PROMPT },
|
|
},
|
|
loading: false,
|
|
error: null,
|
|
refresh: () => undefined,
|
|
};
|
|
}
|
|
if (key === "pages") {
|
|
return {
|
|
data: {
|
|
pages: [
|
|
{
|
|
path: "wiki/concepts/sidebar-navigation.md",
|
|
title: "Sidebar navigation",
|
|
pageType: "concepts",
|
|
backlinkCount: 0,
|
|
sourceCount: 0,
|
|
contentHash: "abc123",
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
],
|
|
sources: [
|
|
{
|
|
rawPath: "raw/sidebar-notes.md",
|
|
title: "Sidebar notes",
|
|
sourceType: "text",
|
|
url: null,
|
|
status: "captured",
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
],
|
|
},
|
|
loading: false,
|
|
error: null,
|
|
refresh: () => undefined,
|
|
};
|
|
}
|
|
if (key === "settings") {
|
|
return {
|
|
data: {
|
|
status: "ok",
|
|
checkedAt: new Date().toISOString(),
|
|
wikiId: "default",
|
|
folder: {
|
|
configured: true,
|
|
path: "/tmp/company-wiki",
|
|
realPath: "/tmp/company-wiki",
|
|
access: "readWrite",
|
|
readable: true,
|
|
writable: true,
|
|
requiredDirectories: [],
|
|
requiredFiles: [],
|
|
missingDirectories: [],
|
|
missingFiles: [],
|
|
healthy: true,
|
|
problems: [],
|
|
checkedAt: new Date().toISOString(),
|
|
},
|
|
managedAgent: mockSettingsManagedAgent ?? { status: "resolved", source: "managed", agentId: "agent-1", resourceKey: "wiki-maintainer", details: { name: "Wiki Maintainer", status: "idle", adapterType: "claude_local", icon: "book-open", urlKey: "wiki-maintainer" } },
|
|
managedProject: { status: "resolved", source: "managed", projectId: "project-1", resourceKey: "llm-wiki", details: { name: "LLM Wiki", status: "in_progress" } },
|
|
managedRoutines: mockSettingsManagedRoutines,
|
|
managedRoutine: mockSettingsManagedRoutines[0] ?? null,
|
|
eventIngestion: {
|
|
enabled: false,
|
|
sources: { issues: false, comments: false, documents: false },
|
|
wikiId: "default",
|
|
maxCharacters: 12000,
|
|
},
|
|
agentOptions: [{ id: "agent-1", name: "Wiki Maintainer", status: "idle", icon: "book-open", urlKey: "wiki-maintainer" }],
|
|
projectOptions: [{ id: "project-1", name: "LLM Wiki", status: "in_progress", color: "#2563eb" }],
|
|
capabilities: [],
|
|
},
|
|
loading: false,
|
|
error: null,
|
|
refresh: () => undefined,
|
|
};
|
|
}
|
|
if (key === "page-content") {
|
|
const path = typeof params?.path === "string" ? params.path : "wiki/index.md";
|
|
const contents = mockPageContentsByPath[path] ?? (path === "AGENTS.md"
|
|
? DEFAULT_AGENT_INSTRUCTIONS
|
|
: path === "IDEA.md"
|
|
? DEFAULT_IDEA
|
|
: `# ${path}\n`);
|
|
return {
|
|
data: {
|
|
wikiId: "default",
|
|
path,
|
|
contents,
|
|
title: path === "AGENTS.md" ? "LLM Wiki Maintainer" : path.replace(/\.md$/, ""),
|
|
pageType: path === "AGENTS.md" ? null : "index",
|
|
backlinks: [],
|
|
sourceRefs: [],
|
|
updatedAt: null,
|
|
hash: "abc123",
|
|
},
|
|
loading: false,
|
|
error: null,
|
|
refresh: () => undefined,
|
|
};
|
|
}
|
|
return { data: null, loading: false, error: null, refresh: () => undefined };
|
|
},
|
|
usePluginAction: () => async () => ({}),
|
|
usePluginToast: () => () => null,
|
|
useHostNavigation: () => ({
|
|
resolveHref: (to: string) => `/PAP${to.startsWith("/") ? to : `/${to}`}`,
|
|
navigate: (to: string) => { mockNavigatedTo = to; },
|
|
linkProps: (to: string) => ({
|
|
href: `/PAP${to.startsWith("/") ? to : `/${to}`}`,
|
|
onClick: () => undefined,
|
|
}),
|
|
}),
|
|
useHostLocation: () => ({
|
|
pathname: mockPathname,
|
|
search: mockSearch,
|
|
hash: "",
|
|
}),
|
|
FileTree: (props: {
|
|
nodes: Array<{ name: string; path: string; kind: string; children?: Array<{ name: string; path: string; kind: string }> }>;
|
|
selectedFile?: string | null;
|
|
ariaLabel?: string;
|
|
wrapLabels?: boolean;
|
|
fileBadges?: Record<string, unknown>;
|
|
onSelectFile?: (path: string) => void;
|
|
}) => {
|
|
if (mockAutoSelectFile) props.onSelectFile?.(mockAutoSelectFile);
|
|
return createElement(
|
|
"div",
|
|
{
|
|
role: "tree",
|
|
"aria-label": props.ariaLabel,
|
|
"data-selected-file": props.selectedFile ?? "",
|
|
"data-wrap-labels": String(props.wrapLabels),
|
|
"data-has-file-badges": String(Boolean(props.fileBadges && Object.keys(props.fileBadges).length > 0)),
|
|
},
|
|
props.nodes.map((node) => createElement("div", { key: node.path }, node.name)),
|
|
);
|
|
},
|
|
MarkdownBlock: ({ content }: { content: string }) => createElement("div", { "data-testid": "markdown-block" }, content),
|
|
MarkdownEditor: ({ value, onChange }: { value: string; onChange: (value: string) => void }) => createElement("textarea", {
|
|
"data-testid": "markdown-editor",
|
|
value,
|
|
onChange: (event: { currentTarget: { value: string } }) => onChange(event.currentTarget.value),
|
|
}),
|
|
AssigneePicker: (props: {
|
|
value: string;
|
|
placeholder?: string;
|
|
onChange: (value: string, selection: { assigneeAgentId: string | null; assigneeUserId: string | null }) => void;
|
|
}) => createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
"data-testid": "assignee-picker",
|
|
onClick: () => props.onChange("agent:agent-1", { assigneeAgentId: "agent-1", assigneeUserId: null }),
|
|
},
|
|
props.value === "agent:agent-1" ? "Wiki Maintainer" : (props.placeholder ?? "Select assignee"),
|
|
),
|
|
ProjectPicker: (props: {
|
|
value: string;
|
|
placeholder?: string;
|
|
onChange: (projectId: string) => void;
|
|
}) => createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
"data-testid": "project-picker",
|
|
onClick: () => props.onChange("project-1"),
|
|
},
|
|
props.value === "project-1" ? "LLM Wiki" : (props.placeholder ?? "Project"),
|
|
),
|
|
ManagedRoutinesList: (props: {
|
|
routines: Array<{
|
|
key: string;
|
|
title: string;
|
|
href?: string | null;
|
|
projectId?: string | null;
|
|
assigneeAgentId?: string | null;
|
|
}>;
|
|
agents?: Array<{ id: string; name: string }>;
|
|
projects?: Array<{ id: string; name: string }>;
|
|
}) => createElement(
|
|
"div",
|
|
{ "data-testid": "managed-routines-list" },
|
|
props.routines.map((routine) => {
|
|
const agent = props.agents?.find((item) => item.id === routine.assigneeAgentId);
|
|
const project = props.projects?.find((item) => item.id === routine.projectId);
|
|
return createElement(
|
|
"div",
|
|
{ key: routine.key },
|
|
createElement("span", null, routine.title),
|
|
createElement("button", null, "Run now"),
|
|
createElement("button", { role: "switch" }, "On"),
|
|
routine.href ? createElement("a", { href: `/PAP${routine.href}` }, "Configure") : null,
|
|
createElement("span", null, `${project?.name ?? "No project"} · ${agent?.name ?? "No default agent"}`),
|
|
);
|
|
}),
|
|
),
|
|
},
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (ORIGINAL_DEPLOYMENT_MODE == null) {
|
|
delete process.env.PAPERCLIP_DEPLOYMENT_MODE;
|
|
} else {
|
|
process.env.PAPERCLIP_DEPLOYMENT_MODE = ORIGINAL_DEPLOYMENT_MODE;
|
|
}
|
|
if (ORIGINAL_DEPLOYMENT_EXPOSURE == null) {
|
|
delete process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE;
|
|
} else {
|
|
process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE = ORIGINAL_DEPLOYMENT_EXPOSURE;
|
|
}
|
|
delete (globalThis as TestBridgeGlobal).__paperclipPluginBridge__;
|
|
});
|
|
|
|
function wikiMaintainerAgent(): Agent {
|
|
const now = new Date();
|
|
return {
|
|
id: "22222222-2222-4222-8222-222222222222",
|
|
companyId: COMPANY_ID,
|
|
name: "Wiki Maintainer",
|
|
urlKey: "wiki-maintainer",
|
|
role: "general",
|
|
title: "LLM Wiki Maintainer",
|
|
icon: "book-open",
|
|
status: "idle",
|
|
reportsTo: null,
|
|
capabilities: "Maintains the wiki",
|
|
adapterType: "claude_local",
|
|
adapterConfig: {},
|
|
runtimeConfig: {},
|
|
budgetMonthlyCents: 0,
|
|
spentMonthlyCents: 0,
|
|
pauseReason: null,
|
|
pausedAt: null,
|
|
permissions: { canCreateAgents: false },
|
|
lastHeartbeatAt: null,
|
|
metadata: {
|
|
paperclipManagedResource: {
|
|
pluginKey: manifest.id,
|
|
resourceKind: "agent",
|
|
resourceKey: "wiki-maintainer",
|
|
},
|
|
},
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
}
|
|
|
|
function existingAgent(): Agent {
|
|
return {
|
|
...wikiMaintainerAgent(),
|
|
id: "44444444-4444-4444-8444-444444444444",
|
|
name: "Existing Knowledge Agent",
|
|
urlKey: "existing-knowledge-agent",
|
|
title: "Knowledge Agent",
|
|
metadata: {},
|
|
};
|
|
}
|
|
|
|
function existingProject(): Project {
|
|
const now = new Date();
|
|
return {
|
|
id: "55555555-5555-4555-8555-555555555555",
|
|
companyId: COMPANY_ID,
|
|
urlKey: "existing-wiki-project",
|
|
goalId: null,
|
|
goalIds: [],
|
|
goals: [],
|
|
name: "Existing Wiki Project",
|
|
description: "Existing project selected for wiki operations.",
|
|
status: "in_progress",
|
|
leadAgentId: null,
|
|
targetDate: null,
|
|
color: "#0f766e",
|
|
env: null,
|
|
pauseReason: null,
|
|
pausedAt: null,
|
|
executionWorkspacePolicy: null,
|
|
codebase: {
|
|
workspaceId: null,
|
|
repoUrl: null,
|
|
repoRef: null,
|
|
defaultRef: null,
|
|
repoName: null,
|
|
localFolder: null,
|
|
managedFolder: "/tmp/existing-wiki-project",
|
|
effectiveLocalFolder: "/tmp/existing-wiki-project",
|
|
origin: "managed_checkout",
|
|
},
|
|
workspaces: [],
|
|
primaryWorkspace: null,
|
|
managedByPlugin: null,
|
|
archivedAt: null,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
}
|
|
|
|
function paperclipIssue(overrides: Partial<Issue> = {}): Issue {
|
|
const now = new Date();
|
|
return {
|
|
id: "66666666-6666-4666-8666-666666666666",
|
|
companyId: COMPANY_ID,
|
|
projectId: null,
|
|
projectWorkspaceId: null,
|
|
goalId: null,
|
|
parentId: null,
|
|
title: "Design event ingestion controls",
|
|
description: "Decide which Paperclip issues, comments, and documents can be ingested into the wiki.",
|
|
status: "todo",
|
|
priority: "medium",
|
|
assigneeAgentId: null,
|
|
assigneeUserId: null,
|
|
checkoutRunId: null,
|
|
executionRunId: null,
|
|
executionAgentNameKey: null,
|
|
executionLockedAt: null,
|
|
createdByAgentId: null,
|
|
createdByUserId: null,
|
|
issueNumber: 3204,
|
|
identifier: "PAP-3204",
|
|
originId: null,
|
|
originRunId: null,
|
|
originFingerprint: null,
|
|
requestDepth: 0,
|
|
billingCode: null,
|
|
assigneeAdapterOverrides: null,
|
|
executionPolicy: null,
|
|
executionState: null,
|
|
executionWorkspaceId: null,
|
|
executionWorkspacePreference: null,
|
|
executionWorkspaceSettings: null,
|
|
startedAt: null,
|
|
completedAt: null,
|
|
cancelledAt: null,
|
|
hiddenAt: null,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("LLM Wiki plugin scaffold", () => {
|
|
it("declares standalone plugin surfaces without core wiki coupling", () => {
|
|
expect(manifest.id).toBe("paperclipai.plugin-llm-wiki");
|
|
expect(manifest.entrypoints.worker).toBe("./dist/worker.js");
|
|
expect(manifest.entrypoints.ui).toBe("./dist/ui");
|
|
expect(manifest.database?.namespaceSlug).toBe("llm_wiki");
|
|
expect(manifest.localFolders?.[0]?.requiredFiles).toEqual([
|
|
"AGENTS.md",
|
|
"IDEA.md",
|
|
"wiki/index.md",
|
|
"wiki/log.md",
|
|
]);
|
|
expect(manifest.agents?.[0]?.agentKey).toBe("wiki-maintainer");
|
|
expect(manifest.agents?.[0]?.adapterType).toBe("claude_local");
|
|
expect(manifest.agents?.[0]?.instructions?.content).toContain("keep the company wiki useful, cited, navigable, and current");
|
|
expect(manifest.projects?.[0]?.projectKey).toBe("llm-wiki");
|
|
expect(manifest.routines?.map((routine) => routine.routineKey)).toEqual([
|
|
CURSOR_WINDOW_ROUTINE_KEY,
|
|
NIGHTLY_LINT_ROUTINE_KEY,
|
|
INDEX_REFRESH_ROUTINE_KEY,
|
|
]);
|
|
expect(manifest.routines).toEqual(
|
|
WIKI_MAINTENANCE_ROUTINE_KEYS.map((routineKey) => expect.objectContaining({
|
|
routineKey,
|
|
assigneeRef: { resourceKind: "agent", resourceKey: WIKI_MAINTAINER_AGENT_KEY },
|
|
projectRef: { resourceKind: "project", resourceKey: WIKI_PROJECT_KEY },
|
|
concurrencyPolicy: "skip_if_active",
|
|
catchUpPolicy: "skip_missed",
|
|
issueTemplate: expect.objectContaining({
|
|
surfaceVisibility: "plugin_operation",
|
|
billingCode: expect.stringMatching(/^plugin-llm-wiki:/),
|
|
}),
|
|
})),
|
|
);
|
|
expect(manifest.tools?.map((tool) => tool.name)).toEqual([
|
|
"wiki_search",
|
|
"wiki_read_page",
|
|
"wiki_write_page",
|
|
"wiki_propose_patch",
|
|
"wiki_list_sources",
|
|
"wiki_read_source",
|
|
"wiki_append_log",
|
|
"wiki_update_index",
|
|
"wiki_list_backlinks",
|
|
"wiki_list_pages",
|
|
]);
|
|
expect(manifest.ui?.slots?.map((slot) => slot.type)).toEqual([
|
|
"sidebar",
|
|
"page",
|
|
"routeSidebar",
|
|
"settingsPage",
|
|
]);
|
|
const routeSidebarSlot = manifest.ui?.slots?.find((slot) => slot.type === "routeSidebar");
|
|
expect(routeSidebarSlot).toMatchObject({
|
|
id: "wiki-route-sidebar",
|
|
exportName: "WikiRouteSidebar",
|
|
routePath: "wiki",
|
|
});
|
|
expect(packageJson.dependencies).toBeUndefined();
|
|
expect(packageJson.devDependencies?.react).toBeUndefined();
|
|
expect(packageJson.devDependencies?.["react-dom"]).toBeDefined();
|
|
expect(packageJson.devDependencies?.["@types/react-dom"]).toBeDefined();
|
|
expect(packageJson.peerDependencies?.react).toBe(">=18");
|
|
});
|
|
|
|
it("renders a host-aligned sidebar link with an open-book icon", () => {
|
|
const markup = renderToStaticMarkup(createElement(SidebarLink, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain('href="/PAP/wiki"');
|
|
expect(markup).toContain("gap-2.5 px-3 py-2 text-[13px] font-medium");
|
|
expect(markup).toContain("hover:bg-accent/50 hover:text-foreground");
|
|
expect(markup).toContain("<svg");
|
|
expect(markup).toContain("M12 7v14");
|
|
expect(markup).not.toContain("Wiki plugin");
|
|
expect(markup).not.toContain("border-radius:999");
|
|
expect(markup).not.toContain("📖");
|
|
});
|
|
|
|
it("ships Karpathy-pattern schema and workflow prompts by default", () => {
|
|
expect(DEFAULT_WIKI_SCHEMA).toContain("You are the maintainer of this personal wiki");
|
|
expect(DEFAULT_WIKI_SCHEMA).toContain("raw/");
|
|
expect(DEFAULT_WIKI_SCHEMA).toContain("wiki/");
|
|
expect(DEFAULT_WIKI_SCHEMA).toContain("AGENTS.md");
|
|
expect(DEFAULT_IDEA).toContain("persistent, compounding artifact");
|
|
expect(DEFAULT_AGENT_INSTRUCTIONS).toContain("keep the company wiki useful, cited, navigable, and current");
|
|
expect(DEFAULT_AGENT_INSTRUCTIONS).toContain("ingest, query, lint, index, or maintenance work");
|
|
expect(INGEST_PROMPT).toContain("Follow AGENTS.md");
|
|
expect(QUERY_PROMPT).toContain("filed back into wiki/");
|
|
expect(LINT_PROMPT).toContain("severity");
|
|
expect(PAPERCLIP_SOURCE_BUNDLE_PROMPT).toContain("structured JSON");
|
|
expect(PROJECT_PAGE_DISTILL_PROMPT).toContain("Source Provenance");
|
|
expect(DECISION_DISTILL_PROMPT).toContain("accepted");
|
|
expect(STATUS_REFRESH_PROMPT).toContain("Refuse stale hashes");
|
|
expect(BACKFILL_PROMPT).toContain("proposed patches first");
|
|
});
|
|
|
|
it("renders the route-scoped Wiki sidebar with tool actions, page navigation, and a back link", () => {
|
|
const markup = renderToStaticMarkup(createElement(WikiRouteSidebar, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain('href="/PAP/dashboard"');
|
|
expect(markup).toContain("PAP");
|
|
expect(markup).toContain('href="/PAP/wiki/query"');
|
|
expect(markup).not.toContain('href="/PAP/wiki/lint"');
|
|
expect(markup).toContain('href="/PAP/wiki/history"');
|
|
expect(markup).toContain('href="/PAP/wiki/settings"');
|
|
expect(markup).not.toContain('href="/PAP/wiki/operations"');
|
|
expect(markup).not.toContain('href="/PAP/wiki/ingest"');
|
|
expect(markup).toContain('aria-label="Wiki primary"');
|
|
expect(markup).toContain('aria-label="Wiki secondary"');
|
|
for (const label of ["Ask", "History", "Settings", "Pages", "raw", "wiki", "AGENTS.md", "IDEA.md"]) {
|
|
expect(markup).toContain(label);
|
|
}
|
|
expect(markup).not.toContain(">Lint</span>");
|
|
expect(markup).not.toContain(">Ingest</span>");
|
|
expect(markup).not.toContain(">Operations</span>");
|
|
expect(markup).not.toContain('text-sm font-bold text-foreground">Wiki');
|
|
expect(markup).not.toContain("Browse");
|
|
expect(markup).not.toContain(">Query<");
|
|
expect(markup).toContain('role="tree"');
|
|
expect(markup).toContain('data-selected-file=""');
|
|
expect(markup).toContain('data-wrap-labels="false"');
|
|
expect(markup).toContain('data-has-file-badges="false"');
|
|
});
|
|
|
|
it("routes legacy Wiki operations URLs to the History run view", () => {
|
|
mockPathname = "/PAP/wiki/operations";
|
|
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain(">History</h2>");
|
|
expect(markup).toContain("No distillation history yet.");
|
|
expect(markup).not.toContain(">Operations</h2>");
|
|
});
|
|
|
|
it("loads AGENTS.md from the Wiki page and exposes an edit affordance", () => {
|
|
mockPathname = "/PAP/wiki/page/templates/AGENTS.md";
|
|
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain("AGENTS");
|
|
expect(markup).toContain(">AGENTS.md</h1>");
|
|
expect(markup).not.toContain(">LLM Wiki Maintainer</h1>");
|
|
expect(markup).toContain("keep the company wiki useful");
|
|
expect(markup).toContain("wiki-root `AGENTS.md`");
|
|
expect(markup).toContain("Edit page");
|
|
expect(markup).toContain("Updated —");
|
|
expect(markup).not.toContain("0 backlinks");
|
|
expect(markup).not.toContain("0 sources");
|
|
expect(markup).not.toContain("abc123");
|
|
expect(markup).not.toContain("+ Ingest");
|
|
expect(markup).not.toContain("Folder healthy");
|
|
});
|
|
|
|
it("renders YAML frontmatter as foldable properties without duplicating title", () => {
|
|
mockPathname = "/PAP/wiki/page/wiki/concepts/sidebar-navigation.md";
|
|
mockPageContentsByPath["wiki/concepts/sidebar-navigation.md"] = `---
|
|
title: Sidebar navigation
|
|
type: concept
|
|
tags: [paperclip, wiki]
|
|
sources:
|
|
- raw/sidebar-notes.md
|
|
created: 2026-05-04
|
|
updated: 2026-05-04
|
|
---
|
|
# Sidebar navigation
|
|
|
|
Route sidebar state stays attached to the selected wiki page.
|
|
`;
|
|
|
|
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain("<summary");
|
|
expect(markup).toContain("Properties");
|
|
expect(markup).toContain(">type</dt>");
|
|
expect(markup).toContain("concept");
|
|
expect(markup).toContain(">tags</dt>");
|
|
expect(markup).toContain("paperclip");
|
|
expect(markup).toContain("wiki");
|
|
expect(markup).toContain(">sources</dt>");
|
|
expect(markup).toContain("raw/sidebar-notes.md");
|
|
expect(markup).toContain("Route sidebar state");
|
|
expect(markup).not.toContain("title: Sidebar navigation");
|
|
expect(markup).not.toContain("title</dt>");
|
|
expect(markup).not.toContain("---");
|
|
});
|
|
|
|
it("shows folder repair instead of reading pages when the wiki root is stale", () => {
|
|
mockPathname = "/PAP/wiki/page/AGENTS.md";
|
|
mockOverviewFolder = {
|
|
configured: true,
|
|
path: "/tmp/deleted-wiki-root",
|
|
realPath: null,
|
|
access: "readWrite",
|
|
readable: false,
|
|
writable: false,
|
|
requiredDirectories: ["raw", "wiki"],
|
|
requiredFiles: ["AGENTS.md"],
|
|
missingDirectories: ["raw", "wiki"],
|
|
missingFiles: ["AGENTS.md"],
|
|
healthy: false,
|
|
problems: [
|
|
{ code: "missing", message: "Configured local folder cannot be inspected.", path: "/tmp/deleted-wiki-root" },
|
|
],
|
|
checkedAt: new Date().toISOString(),
|
|
};
|
|
|
|
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain("Repair wiki root folder");
|
|
expect(markup).toContain("/tmp/deleted-wiki-root");
|
|
expect(markup).toContain("Configured local folder cannot be inspected.");
|
|
expect(markup).toContain("Repair & bootstrap");
|
|
expect(markup).not.toContain("Failed to read AGENTS.md");
|
|
expect(markup).not.toContain(">AGENTS.md</h1>");
|
|
});
|
|
|
|
it("serializes root template pages as regular path segments", () => {
|
|
mockAutoSelectFile = "AGENTS.md";
|
|
renderToStaticMarkup(createElement(WikiRouteSidebar, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(mockNavigatedTo).toBe("/wiki/page/AGENTS.md");
|
|
});
|
|
|
|
it("highlights Settings for legacy lint links after lint moved under Settings", () => {
|
|
mockPathname = "/PAP/wiki/lint";
|
|
const markup = renderToStaticMarkup(createElement(WikiRouteSidebar, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).not.toContain('href="/PAP/wiki/lint"');
|
|
const settingsAnchor = markup.match(/<a[^>]*href="\/PAP\/wiki\/settings"[^>]*>/);
|
|
expect(settingsAnchor?.[0]).toContain('aria-current="page"');
|
|
expect(settingsAnchor?.[0]).toContain("text-foreground");
|
|
expect(settingsAnchor?.[0]).not.toContain("bg-accent");
|
|
const askAnchor = markup.match(/<a[^>]*href="\/PAP\/wiki\/query"[^>]*>/);
|
|
expect(askAnchor?.[0]).not.toContain('aria-current="page"');
|
|
});
|
|
|
|
it("renders the maintainer settings without plugin metadata or inline AGENTS.md editing", () => {
|
|
mockPathname = "/PAP/wiki/settings/maintainer";
|
|
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain("Wiki Maintainer");
|
|
expect(markup).toContain("Adapter: claude local");
|
|
expect(markup).toContain("Maintainer");
|
|
expect(markup).toContain("Repair");
|
|
expect(markup).toContain("Reset to defaults");
|
|
expect(markup).not.toContain("Provided maintainer");
|
|
expect(markup).not.toContain("Managed by LLM Wiki");
|
|
expect(markup).not.toContain("Suggested default");
|
|
expect(markup).not.toContain("AGENT INSTRUCTIONS");
|
|
expect(markup).not.toContain("Stable key");
|
|
expect(markup).not.toContain("Plugin managed default");
|
|
});
|
|
|
|
it("recommends approval when the wiki maintainer is pending board approval", () => {
|
|
mockPathname = "/PAP/wiki/settings/maintainer";
|
|
mockSettingsManagedAgent = {
|
|
status: "created",
|
|
source: "managed",
|
|
agentId: "agent-1",
|
|
resourceKey: "wiki-maintainer",
|
|
details: { name: "Wiki Maintainer", status: "pending_approval", adapterType: "claude_local", icon: "book-open", urlKey: "wiki-maintainer" },
|
|
};
|
|
|
|
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain("pending approval");
|
|
expect(markup).toContain("Approve the agent");
|
|
expect(markup).toContain("Adapter: claude local");
|
|
});
|
|
|
|
it("renders root settings as a compact health checklist with the shared path picker", () => {
|
|
mockPathname = "/PAP/wiki/settings";
|
|
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain(">Setup</");
|
|
expect(markup).toContain("Base Folder");
|
|
expect(markup).toContain("Managed Agents");
|
|
expect(markup).toContain("Managed Projects");
|
|
expect(markup).toContain("Adapter: claude local");
|
|
expect(markup).toContain("Status: in progress");
|
|
expect(markup).toContain("Wiki root health checklist");
|
|
expect(markup).toContain("Health check");
|
|
for (const headline of ["Path configured", "Readable", "Writable", "Baseline files", "Wiki folders"]) {
|
|
expect(markup).toContain(headline);
|
|
}
|
|
expect(markup).toContain("Local wiki folder");
|
|
expect(markup).toContain("Choose");
|
|
expect(markup).toContain("Apply path");
|
|
expect(markup).not.toContain("AGENTS.md, IDEA.md");
|
|
expect(markup).not.toContain("Ready</span>");
|
|
expect(markup).not.toContain("Needs attention</span>");
|
|
expect(markup).not.toContain("wiki/sources/");
|
|
expect(markup).not.toContain("wiki/entities/");
|
|
expect(markup).not.toContain("Wiki root folder");
|
|
expect(markup).not.toContain("Provided maintainer");
|
|
expect(markup).not.toContain("Wiki Maintainer</span>");
|
|
expect(markup).not.toContain(">Project</span>");
|
|
expect(markup).toContain("Ingestion Settings");
|
|
});
|
|
|
|
it("renders host settings directly as the Setup page without an extra plugin heading", () => {
|
|
const markup = renderToStaticMarkup(createElement(SettingsPage, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain(">Setup</h1>");
|
|
expect(markup).toContain("Base Folder");
|
|
expect(markup).not.toContain("LLM Wiki Settings");
|
|
expect(markup).not.toContain("These settings live inside the plugin");
|
|
});
|
|
|
|
it("renders project settings as a project picker without managed-resource metadata", () => {
|
|
mockPathname = "/PAP/wiki/settings/project";
|
|
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain("Use existing project");
|
|
expect(markup).toContain("LLM Wiki");
|
|
expect(markup).toContain("Save project");
|
|
expect(markup).toContain("Open project");
|
|
expect(markup).toContain("Repair / reconcile");
|
|
expect(markup).toContain("Reset to plugin defaults");
|
|
expect(markup).not.toContain("Managed by LLM Wiki");
|
|
expect(markup).not.toContain("Operations project binding");
|
|
expect(markup).not.toContain("Stable key");
|
|
expect(markup).not.toContain("Resolved project");
|
|
expect(markup).toContain("Status: in progress");
|
|
expect(markup).not.toContain("Plugin managed default");
|
|
});
|
|
|
|
it("renders managed routines as normal routine rows with run, toggle, and configure controls", () => {
|
|
mockPathname = "/PAP/wiki/settings/routines";
|
|
mockSettingsManagedRoutines = [{
|
|
status: "resolved",
|
|
routineId: "routine-1",
|
|
resourceKey: "nightly-wiki-lint",
|
|
routine: {
|
|
id: "routine-1",
|
|
title: "Run LLM Wiki lint",
|
|
status: "active",
|
|
assigneeAgentId: "agent-1",
|
|
projectId: "project-1",
|
|
lastTriggeredAt: "2026-05-03T12:00:00Z",
|
|
managedByPlugin: { pluginDisplayName: "LLM Wiki", resourceKey: "nightly-wiki-lint" },
|
|
},
|
|
details: { cronExpression: "0 3 * * *" },
|
|
}];
|
|
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain("Run LLM Wiki lint");
|
|
expect(markup).toContain("Run now");
|
|
expect(markup).toContain("Configure");
|
|
expect(markup).toContain("role=\"switch\"");
|
|
expect(markup).toContain("LLM Wiki · Wiki Maintainer");
|
|
expect(markup).toContain("href=\"/PAP/routines/routine-1\"");
|
|
});
|
|
|
|
it("shows one routine repair warning instead of per-routine reconcile controls", () => {
|
|
mockPathname = "/PAP/wiki/settings/routines";
|
|
mockSettingsManagedRoutines = [{
|
|
status: "resolved",
|
|
routineId: "routine-1",
|
|
resourceKey: "nightly-wiki-lint",
|
|
routine: {
|
|
id: "routine-1",
|
|
title: "Run LLM Wiki lint",
|
|
status: "active",
|
|
assigneeAgentId: "other-agent",
|
|
projectId: "project-1",
|
|
managedByPlugin: { pluginDisplayName: "LLM Wiki", resourceKey: "nightly-wiki-lint" },
|
|
},
|
|
details: { cronExpression: "0 3 * * *" },
|
|
}];
|
|
|
|
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain("Routine setup needs repair");
|
|
expect(markup).toContain("Fix routines");
|
|
expect(markup).toContain("not assigned to the Wiki Maintainer");
|
|
expect(markup).not.toContain("Plugin-managed routine defaults can be reconciled from here");
|
|
expect(markup).not.toContain(">Reconcile</button>");
|
|
expect(markup).not.toContain(">Reset</button>");
|
|
});
|
|
|
|
it("renders legacy lint routes inside the Wiki settings section", () => {
|
|
mockPathname = "/PAP/wiki/lint";
|
|
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain("Run lint now");
|
|
expect(markup).toContain("Recent lint runs");
|
|
expect(markup).toContain("LLM Wiki settings sections");
|
|
});
|
|
|
|
it("does not expose IDEA.md pattern editing as a settings section", () => {
|
|
mockPathname = "/PAP/wiki/settings";
|
|
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain("LLM Wiki settings sections");
|
|
expect(markup).not.toContain(">Pattern</span>");
|
|
expect(markup).not.toContain("Pattern · IDEA.md");
|
|
expect(markup).not.toContain("IDEA.md skeleton reference");
|
|
});
|
|
|
|
it("does not expose plugin capabilities as a settings section", () => {
|
|
mockPathname = "/PAP/wiki/settings";
|
|
const markup = renderToStaticMarkup(createElement(WikiPage, {
|
|
context: { companyId: COMPANY_ID, companyPrefix: "PAP" },
|
|
} as never));
|
|
|
|
expect(markup).toContain("LLM Wiki settings sections");
|
|
expect(markup).not.toContain("Plugin capabilities");
|
|
expect(markup).not.toContain("api.routes.register");
|
|
});
|
|
|
|
it("reconciles managed maintenance routines through stable agent and project refs", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const writes: Array<{ path: string; contents: string }> = [];
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
writes.push({ path: relativePath, contents });
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
|
|
const missing = await harness.performAction<PluginManagedRoutineResolution>("reconcile-managed-routine", {
|
|
companyId: COMPANY_ID,
|
|
routineKey: NIGHTLY_LINT_ROUTINE_KEY,
|
|
});
|
|
expect(missing.status).toBe("missing_refs");
|
|
expect(missing.missingRefs).toEqual([
|
|
expect.objectContaining({ resourceKind: "agent", resourceKey: WIKI_MAINTAINER_AGENT_KEY }),
|
|
expect.objectContaining({ resourceKind: "project", resourceKey: WIKI_PROJECT_KEY }),
|
|
]);
|
|
|
|
await harness.performAction("bootstrap-root", { companyId: COMPANY_ID, path: "/tmp/company-wiki" });
|
|
const reconciled = await Promise.all(
|
|
WIKI_MAINTENANCE_ROUTINE_KEYS.map((routineKey) =>
|
|
harness.performAction<PluginManagedRoutineResolution>("reconcile-managed-routine", {
|
|
companyId: COMPANY_ID,
|
|
routineKey,
|
|
})),
|
|
);
|
|
|
|
expect(reconciled.map((routine) => routine.resourceKey)).toEqual([...WIKI_MAINTENANCE_ROUTINE_KEYS]);
|
|
expect(reconciled).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
resourceKey: NIGHTLY_LINT_ROUTINE_KEY,
|
|
routine: expect.objectContaining({
|
|
projectId: expect.any(String),
|
|
assigneeAgentId: expect.any(String),
|
|
managedByPlugin: expect.objectContaining({
|
|
defaultsJson: expect.objectContaining({
|
|
issueTemplate: expect.objectContaining({ surfaceVisibility: "plugin_operation" }),
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
expect.objectContaining({ resourceKey: INDEX_REFRESH_ROUTINE_KEY, routineId: expect.any(String) }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("repairs all managed maintenance routines through one action", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
await plugin.definition.setup(harness.ctx);
|
|
await harness.performAction("bootstrap-root", { companyId: COMPANY_ID, path: "/tmp/company-wiki" });
|
|
|
|
const repaired = await harness.performAction<{
|
|
managedRoutines: PluginManagedRoutineResolution[];
|
|
}>("reconcile-managed-routines", { companyId: COMPANY_ID });
|
|
|
|
expect(repaired.managedRoutines).toHaveLength(WIKI_MAINTENANCE_ROUTINE_KEYS.length);
|
|
expect(repaired.managedRoutines.map((routine) => routine.resourceKey)).toEqual([...WIKI_MAINTENANCE_ROUTINE_KEYS]);
|
|
for (const routine of repaired.managedRoutines) {
|
|
expect(routine.routine).toEqual(expect.objectContaining({
|
|
assigneeAgentId: expect.any(String),
|
|
projectId: expect.any(String),
|
|
}));
|
|
expect(routine.missingRefs).toEqual([]);
|
|
}
|
|
});
|
|
|
|
it("registers worker data, actions, jobs, and tools", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
await plugin.definition.setup(harness.ctx);
|
|
|
|
const overview = await harness.getData<{ status: string; operationCount: number; eventIngestion: { enabled: boolean } }>("overview", {
|
|
companyId: COMPANY_ID,
|
|
});
|
|
expect(overview.status).toBe("ok");
|
|
expect(overview.operationCount).toBe(0);
|
|
expect(overview.eventIngestion.enabled).toBe(false);
|
|
|
|
await harness.runJob("folder-health-check");
|
|
const pages = await harness.executeTool<{ content?: string }>("wiki_list_pages", {
|
|
companyId: COMPANY_ID,
|
|
wikiId: "default",
|
|
});
|
|
expect(pages.content).toBe("No pages indexed yet.");
|
|
});
|
|
|
|
it("filters stale page and raw source rows out of browse data", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const files = new Map<string, string>([
|
|
["wiki/concepts/live.md", "# Live Page\n"],
|
|
["raw/live-source.md", "# Live Source\n"],
|
|
]);
|
|
const now = new Date().toISOString();
|
|
harness.ctx.localFolders.readText = async (_companyId, _folderKey, relativePath) => {
|
|
const contents = files.get(relativePath);
|
|
if (contents == null) throw new Error(`missing ${relativePath}`);
|
|
return contents;
|
|
};
|
|
harness.ctx.db.query = async <T = Record<string, unknown>>(sql: string, params?: unknown[]) => {
|
|
harness.dbQueries.push({ sql, params });
|
|
if (sql.includes("wiki_pages")) {
|
|
return [
|
|
{
|
|
path: "wiki/concepts/live.md",
|
|
title: "Live Page",
|
|
page_type: "concepts",
|
|
backlinks: [],
|
|
source_refs: [],
|
|
content_hash: "live",
|
|
updated_at: now,
|
|
},
|
|
{
|
|
path: "wiki/concepts/stale.md",
|
|
title: "Stale Page",
|
|
page_type: "concepts",
|
|
backlinks: [],
|
|
source_refs: [],
|
|
content_hash: "stale",
|
|
updated_at: now,
|
|
},
|
|
] as T[];
|
|
}
|
|
if (sql.includes("wiki_sources")) {
|
|
return [
|
|
{
|
|
raw_path: "raw/missing-source.md",
|
|
title: "Missing Source",
|
|
source_type: "text",
|
|
url: null,
|
|
status: "captured",
|
|
created_at: now,
|
|
},
|
|
{
|
|
raw_path: "raw/live-source.md",
|
|
title: "Live Source",
|
|
source_type: "text",
|
|
url: null,
|
|
status: "captured",
|
|
created_at: now,
|
|
},
|
|
] as T[];
|
|
}
|
|
return [];
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
const result = await harness.getData<{
|
|
pages: Array<{ path: string }>;
|
|
sources: Array<{ rawPath: string }>;
|
|
}>("pages", {
|
|
companyId: COMPANY_ID,
|
|
wikiId: "default",
|
|
includeRaw: true,
|
|
});
|
|
|
|
expect(result.pages.map((page) => page.path)).toEqual(["wiki/concepts/live.md"]);
|
|
expect(result.sources.map((source) => source.rawPath)).toEqual(["raw/live-source.md"]);
|
|
});
|
|
|
|
it("includes local wiki files in browse data before metadata is indexed", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const modifiedAt = new Date().toISOString();
|
|
harness.ctx.localFolders.list = async (_companyId, folderKey, options) => ({
|
|
folderKey,
|
|
relativePath: options?.relativePath ?? null,
|
|
truncated: false,
|
|
entries: options?.relativePath === "wiki"
|
|
? [
|
|
{ path: "wiki/concepts/agent-memory-layer.md", name: "agent-memory-layer.md", kind: "file", size: 12, modifiedAt },
|
|
{ path: "wiki/entities/paperclip.md", name: "paperclip.md", kind: "file", size: 10, modifiedAt },
|
|
]
|
|
: [
|
|
{ path: "raw/2026-04-09-thomas-gieselmann-fundraising-call.md", name: "2026-04-09-thomas-gieselmann-fundraising-call.md", kind: "file", size: 14, modifiedAt },
|
|
],
|
|
});
|
|
harness.ctx.db.query = async <T = Record<string, unknown>>(sql: string, params?: unknown[]) => {
|
|
harness.dbQueries.push({ sql, params });
|
|
return [] as T[];
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
const result = await harness.getData<{
|
|
pages: Array<{ path: string; title: string | null }>;
|
|
sources: Array<{ rawPath: string; title: string | null }>;
|
|
}>("pages", {
|
|
companyId: COMPANY_ID,
|
|
wikiId: "default",
|
|
includeRaw: true,
|
|
});
|
|
|
|
expect(result.pages).toEqual([
|
|
expect.objectContaining({ path: "wiki/concepts/agent-memory-layer.md", title: null }),
|
|
expect.objectContaining({ path: "wiki/entities/paperclip.md", title: null }),
|
|
]);
|
|
expect(result.sources).toEqual([
|
|
expect.objectContaining({ rawPath: "raw/2026-04-09-thomas-gieselmann-fundraising-call.md", title: null }),
|
|
]);
|
|
});
|
|
|
|
it("does not ingest Paperclip events until operator controls enable them", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const issue = paperclipIssue();
|
|
harness.seed({ issues: [issue] });
|
|
const writes: Array<{ path: string; contents: string }> = [];
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
writes.push({ path: relativePath, contents });
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
await harness.emit("issue.created", { identifier: issue.identifier }, {
|
|
companyId: COMPANY_ID,
|
|
entityId: issue.id,
|
|
entityType: "issue",
|
|
eventId: "event-disabled-issue-created",
|
|
});
|
|
|
|
expect(writes).toHaveLength(0);
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("wiki_sources"))).toBe(false);
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("wiki_operations"))).toBe(false);
|
|
});
|
|
|
|
it("records enabled Paperclip issue events as cursor observations without creating ingest operations", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const issue = paperclipIssue();
|
|
harness.seed({ issues: [issue] });
|
|
const writes: Array<{ path: string; contents: string }> = [];
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
writes.push({ path: relativePath, contents });
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
const policy = await harness.performAction<{ enabled: boolean; sources: { issues: boolean; comments: boolean; documents: boolean } }>(
|
|
"update-event-ingestion-settings",
|
|
{
|
|
companyId: COMPANY_ID,
|
|
enabled: true,
|
|
sources: { issues: true, comments: false, documents: false },
|
|
},
|
|
);
|
|
expect(policy).toMatchObject({ enabled: true, sources: { issues: true, comments: false, documents: false } });
|
|
|
|
await harness.emit("issue.created", { identifier: issue.identifier }, {
|
|
companyId: COMPANY_ID,
|
|
entityId: issue.id,
|
|
entityType: "issue",
|
|
eventId: "event-enabled-issue-created",
|
|
});
|
|
|
|
expect(writes).toHaveLength(0);
|
|
const operations = await harness.ctx.issues.list({
|
|
companyId: COMPANY_ID,
|
|
originKindPrefix: String(OPERATION_ORIGIN_KIND),
|
|
});
|
|
expect(operations).toHaveLength(0);
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("wiki_sources"))).toBe(false);
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("wiki_operations"))).toBe(false);
|
|
const cursorUpsert = harness.dbExecutes.find((execute) => execute.sql.includes("paperclip_distillation_cursors"));
|
|
expect(cursorUpsert?.params).toEqual(expect.arrayContaining([
|
|
COMPANY_ID,
|
|
"default",
|
|
"company",
|
|
null,
|
|
null,
|
|
]));
|
|
});
|
|
|
|
it("keeps Paperclip event cursor observations company scoped and ignores plugin-operation issues", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const visibleIssue = paperclipIssue({ projectId: "77777777-7777-4777-8777-777777777777" });
|
|
const otherCompanyIssue = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-777777777778",
|
|
companyId: OTHER_COMPANY_ID,
|
|
projectId: "77777777-7777-4777-8777-777777777779",
|
|
identifier: "PAP-9999",
|
|
});
|
|
const operationIssue = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-777777777780",
|
|
originKind: `${OPERATION_ORIGIN_KIND}:ingest`,
|
|
});
|
|
harness.seed({ issues: [visibleIssue, otherCompanyIssue, operationIssue] });
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
await harness.performAction("update-event-ingestion-settings", {
|
|
companyId: COMPANY_ID,
|
|
enabled: true,
|
|
sources: { issues: true, comments: true, documents: true },
|
|
});
|
|
|
|
await harness.emit("issue.updated", {}, {
|
|
companyId: COMPANY_ID,
|
|
entityId: visibleIssue.id,
|
|
entityType: "issue",
|
|
eventId: "event-visible",
|
|
});
|
|
await harness.emit("issue.updated", {}, {
|
|
companyId: OTHER_COMPANY_ID,
|
|
entityId: otherCompanyIssue.id,
|
|
entityType: "issue",
|
|
eventId: "event-other-company",
|
|
});
|
|
await harness.emit("issue.updated", {}, {
|
|
companyId: COMPANY_ID,
|
|
entityId: operationIssue.id,
|
|
entityType: "issue",
|
|
eventId: "event-plugin-operation",
|
|
});
|
|
|
|
const cursorWrites = harness.dbExecutes.filter((execute) => execute.sql.includes("paperclip_distillation_cursors"));
|
|
expect(cursorWrites).toHaveLength(1);
|
|
expect(cursorWrites[0].params).toEqual(expect.arrayContaining([
|
|
COMPANY_ID,
|
|
"default",
|
|
"project",
|
|
visibleIssue.projectId,
|
|
]));
|
|
});
|
|
|
|
it("assembles deterministic Paperclip source bundles with issue, document, and comment provenance", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const root = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-777777777781",
|
|
identifier: "PAP-4000",
|
|
title: "Root distillation issue",
|
|
projectId: "77777777-7777-4777-8777-777777777777",
|
|
updatedAt: new Date("2026-05-01T10:00:00Z"),
|
|
});
|
|
const child = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-777777777782",
|
|
identifier: "PAP-4001",
|
|
title: "Child source issue",
|
|
parentId: root.id,
|
|
projectId: root.projectId,
|
|
description: "Child issue has a decision and implementation notes.",
|
|
updatedAt: new Date("2026-05-02T10:00:00Z"),
|
|
});
|
|
harness.seed({
|
|
issues: [root, child],
|
|
issueComments: [{
|
|
id: "77777777-7777-4777-8777-777777777783",
|
|
companyId: COMPANY_ID,
|
|
issueId: child.id,
|
|
authorAgentId: null,
|
|
authorUserId: null,
|
|
body: "Comment evidence for the source bundle.",
|
|
createdAt: new Date("2026-05-03T10:00:00Z"),
|
|
updatedAt: new Date("2026-05-03T10:00:00Z"),
|
|
}],
|
|
});
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
await harness.ctx.issues.documents.upsert({
|
|
companyId: COMPANY_ID,
|
|
issueId: child.id,
|
|
key: "plan",
|
|
title: "Plan",
|
|
body: "Document evidence for the source bundle.",
|
|
});
|
|
|
|
const first = await harness.performAction<{
|
|
markdown: string;
|
|
sourceRefs: Array<{ kind: string; issueIdentifier: string | null; documentKey?: string; commentId?: string }>;
|
|
sourceHash: string;
|
|
sourceWindowEnd: string | null;
|
|
}>("assemble-paperclip-source-bundle", {
|
|
companyId: COMPANY_ID,
|
|
rootIssueId: root.id,
|
|
maxCharacters: 20000,
|
|
});
|
|
const second = await harness.performAction<typeof first>("assemble-paperclip-source-bundle", {
|
|
companyId: COMPANY_ID,
|
|
rootIssueId: root.id,
|
|
maxCharacters: 20000,
|
|
});
|
|
|
|
expect(second.sourceHash).toBe(first.sourceHash);
|
|
expect(first.markdown).toContain("Root distillation issue");
|
|
expect(first.markdown).toContain("Child issue has a decision");
|
|
expect(first.markdown).toContain("Document evidence for the source bundle.");
|
|
expect(first.markdown).toContain("Comment evidence for the source bundle.");
|
|
expect(first.sourceRefs).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "issue", issueIdentifier: "PAP-4001" }),
|
|
expect.objectContaining({ kind: "document", documentKey: "plan" }),
|
|
expect.objectContaining({ kind: "comment", commentId: "77777777-7777-4777-8777-777777777783" }),
|
|
]));
|
|
expect(first.sourceWindowEnd).toEqual(expect.any(String));
|
|
});
|
|
|
|
it("suppresses secret-like comment and document bodies before storing distillation snapshots", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const issue = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-777777777784",
|
|
identifier: "PAP-4002",
|
|
title: "Sensitive source issue",
|
|
projectId: "77777777-7777-4777-8777-777777777777",
|
|
description: "Keep the project page current without copying credential material into the wiki.",
|
|
updatedAt: new Date("2026-05-04T10:00:00Z"),
|
|
});
|
|
harness.seed({
|
|
issues: [issue],
|
|
issueComments: [{
|
|
id: "77777777-7777-4777-8777-777777777785",
|
|
companyId: COMPANY_ID,
|
|
issueId: issue.id,
|
|
authorAgentId: null,
|
|
authorUserId: null,
|
|
body: "Authorization: Bearer ghp_supersecretcommenttoken1234567890",
|
|
createdAt: new Date("2026-05-04T11:00:00Z"),
|
|
updatedAt: new Date("2026-05-04T11:00:00Z"),
|
|
}],
|
|
});
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
await harness.ctx.issues.documents.upsert({
|
|
companyId: COMPANY_ID,
|
|
issueId: issue.id,
|
|
key: "plan",
|
|
title: "Plan",
|
|
body: "OPENAI_API_KEY=sk-supersecretdocumentvalue1234567890",
|
|
});
|
|
|
|
const run = await harness.performAction<{
|
|
bundle: {
|
|
markdown: string;
|
|
warnings: string[];
|
|
sourceRefs: Array<Record<string, unknown>>;
|
|
};
|
|
}>("create-paperclip-distillation-run", {
|
|
companyId: COMPANY_ID,
|
|
projectId: issue.projectId,
|
|
maxCharacters: 20000,
|
|
});
|
|
|
|
expect(run.bundle.markdown).toContain("Suppressed by LLM Wiki distillation security policy");
|
|
expect(run.bundle.markdown).not.toContain("ghp_supersecretcommenttoken1234567890");
|
|
expect(run.bundle.markdown).not.toContain("sk-supersecretdocumentvalue1234567890");
|
|
expect(run.bundle.warnings).toEqual(expect.arrayContaining([
|
|
expect.stringContaining("Suppressed comment content"),
|
|
expect.stringContaining("Suppressed document content"),
|
|
]));
|
|
expect(run.bundle.sourceRefs).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "comment", redactionStatus: "suppressed_sensitive_content" }),
|
|
expect.objectContaining({ kind: "document", redactionStatus: "suppressed_sensitive_content" }),
|
|
]));
|
|
|
|
const snapshotInsert = harness.dbExecutes.find((execute) => execute.sql.includes("paperclip_source_snapshots"));
|
|
const storedSourceRefs = String(snapshotInsert?.params?.[9] ?? "");
|
|
const storedMarkdown = String(snapshotInsert?.params?.[10] ?? "");
|
|
expect(storedSourceRefs).toContain("suppressed_sensitive_content");
|
|
expect(storedMarkdown).toContain("Suppressed by LLM Wiki distillation security policy");
|
|
expect(storedMarkdown).not.toContain("ghp_supersecretcommenttoken1234567890");
|
|
expect(storedMarkdown).not.toContain("sk-supersecretdocumentvalue1234567890");
|
|
});
|
|
|
|
it("creates source snapshots and only advances cursors after successful distillation outcomes", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const issue = paperclipIssue({
|
|
projectId: "77777777-7777-4777-8777-777777777777",
|
|
updatedAt: new Date("2026-05-02T10:00:00Z"),
|
|
});
|
|
harness.seed({ issues: [issue] });
|
|
await plugin.definition.setup(harness.ctx);
|
|
|
|
const run = await harness.performAction<{
|
|
runId: string;
|
|
cursorId: string;
|
|
snapshotId: string;
|
|
bundle: { sourceHash: string; sourceWindowEnd: string };
|
|
}>("create-paperclip-distillation-run", {
|
|
companyId: COMPANY_ID,
|
|
projectId: issue.projectId,
|
|
});
|
|
expect(run.snapshotId).toEqual(expect.any(String));
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("paperclip_source_snapshots"))).toBe(true);
|
|
|
|
const failed = await harness.performAction<{ cursorAdvanced: boolean }>("record-paperclip-distillation-outcome", {
|
|
companyId: COMPANY_ID,
|
|
runId: run.runId,
|
|
cursorId: run.cursorId,
|
|
status: "failed",
|
|
sourceHash: run.bundle.sourceHash,
|
|
sourceWindowEnd: run.bundle.sourceWindowEnd,
|
|
warning: "writer failed",
|
|
});
|
|
expect(failed.cursorAdvanced).toBe(false);
|
|
const cursorUpdatesAfterFailure = harness.dbExecutes.filter((execute) =>
|
|
execute.sql.trim().startsWith("UPDATE") && execute.sql.includes("paperclip_distillation_cursors"));
|
|
expect(cursorUpdatesAfterFailure).toHaveLength(0);
|
|
|
|
const succeeded = await harness.performAction<{ cursorAdvanced: boolean }>("record-paperclip-distillation-outcome", {
|
|
companyId: COMPANY_ID,
|
|
runId: run.runId,
|
|
cursorId: run.cursorId,
|
|
status: "succeeded",
|
|
sourceHash: run.bundle.sourceHash,
|
|
sourceWindowEnd: run.bundle.sourceWindowEnd,
|
|
});
|
|
expect(succeeded.cursorAdvanced).toBe(true);
|
|
const cursorSuccessUpdate = harness.dbExecutes.find((execute) =>
|
|
execute.sql.trim().startsWith("UPDATE") && execute.sql.includes("paperclip_distillation_cursors"));
|
|
expect(cursorSuccessUpdate?.params).toEqual(expect.arrayContaining([
|
|
COMPANY_ID,
|
|
"default",
|
|
run.runId,
|
|
run.bundle.sourceWindowEnd,
|
|
run.bundle.sourceHash,
|
|
run.cursorId,
|
|
]));
|
|
});
|
|
|
|
it("creates explicit distillation work items for manual, retry, and backfill lanes", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
await plugin.definition.setup(harness.ctx);
|
|
|
|
await harness.performAction("create-paperclip-distillation-work-item", {
|
|
companyId: COMPANY_ID,
|
|
kind: "manual",
|
|
projectId: "77777777-7777-4777-8777-777777777777",
|
|
idempotencyKey: "manual:project:77777777-7777-4777-8777-777777777777",
|
|
});
|
|
await harness.performAction("create-paperclip-distillation-work-item", {
|
|
companyId: COMPANY_ID,
|
|
kind: "retry",
|
|
rootIssueId: "77777777-7777-4777-8777-777777777781",
|
|
priority: "high",
|
|
idempotencyKey: "retry:run:1",
|
|
});
|
|
await harness.performAction("create-paperclip-distillation-work-item", {
|
|
companyId: COMPANY_ID,
|
|
kind: "backfill",
|
|
projectId: "77777777-7777-4777-8777-777777777777",
|
|
priority: "low",
|
|
metadata: { window: "last-30-days" },
|
|
idempotencyKey: "backfill:last-30-days",
|
|
});
|
|
|
|
const workItemWrites = harness.dbExecutes.filter((execute) => execute.sql.includes("paperclip_distillation_work_items"));
|
|
expect(workItemWrites).toHaveLength(3);
|
|
expect(workItemWrites.map((write) => write.params?.[3])).toEqual(["manual", "retry", "backfill"]);
|
|
expect(String(workItemWrites[0].params?.[9])).toContain('"sourceScope":"project"');
|
|
expect(String(workItemWrites[1].params?.[9])).toContain('"sourceScope":"root_issue"');
|
|
expect(String(workItemWrites[2].params?.[9])).toContain('"sourceScope":"project"');
|
|
|
|
await expect(harness.performAction("create-paperclip-distillation-work-item", {
|
|
companyId: COMPANY_ID,
|
|
kind: "backfill",
|
|
idempotencyKey: "backfill:company",
|
|
})).rejects.toThrow("whole-company backfill is not allowed");
|
|
});
|
|
|
|
it("refuses Paperclip distillation before wiki generation when estimated source cost exceeds the cap", async () => {
|
|
const harness = createTestHarness({
|
|
manifest,
|
|
config: {
|
|
maxPaperclipRoutineRunCostCents: 1,
|
|
paperclipCostCentsPerThousandSourceCharacters: 100,
|
|
},
|
|
});
|
|
const project = existingProject();
|
|
const issue = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-777777777795",
|
|
identifier: "PAP-4104",
|
|
title: "Large source bundle",
|
|
description: `Accepted plan. ${"Detailed implementation note. ".repeat(160)}`,
|
|
projectId: project.id,
|
|
status: "in_progress",
|
|
});
|
|
const writes: string[] = [];
|
|
harness.seed({ projects: [project], issues: [issue] });
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
writes.push(relativePath);
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
const result = await harness.performAction<{
|
|
status: string;
|
|
estimatedCostCents: number;
|
|
maxCostCents: number;
|
|
patches: unknown[];
|
|
warnings: string[];
|
|
}>("distill-paperclip-project-page", {
|
|
companyId: COMPANY_ID,
|
|
projectId: project.id,
|
|
routineRun: true,
|
|
includeSupportingPages: false,
|
|
});
|
|
|
|
expect(result.status).toBe("refused_cost_cap");
|
|
expect(result.estimatedCostCents).toBeGreaterThan(result.maxCostCents);
|
|
expect(result.patches).toEqual([]);
|
|
expect(result.warnings.join("\n")).toContain("estimated cost");
|
|
expect(writes).toHaveLength(0);
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("paperclip_source_snapshots"))).toBe(false);
|
|
const refusedRun = harness.dbExecutes.find((execute) =>
|
|
execute.sql.includes("paperclip_distillation_runs") && execute.sql.includes("refused_cost_cap"));
|
|
expect(refusedRun?.params).toEqual(expect.arrayContaining([
|
|
COMPANY_ID,
|
|
"default",
|
|
expect.any(String),
|
|
expect.any(String),
|
|
null,
|
|
project.id,
|
|
]));
|
|
});
|
|
|
|
it("creates visible manual distill operation issues outside the recurring cadence", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const project = existingProject();
|
|
const issue = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-777777777796",
|
|
identifier: "PAP-4105",
|
|
title: "Manual distillation target",
|
|
description: "Implemented enough evidence to manually distill into the wiki.",
|
|
status: "done",
|
|
projectId: project.id,
|
|
});
|
|
harness.seed({ agents: [wikiMaintainerAgent()], projects: [project], issues: [issue] });
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
const result = await harness.performAction<{
|
|
status: string;
|
|
operation: { issue: { originKind: string; billingCode: string | null; assigneeAgentId: string | null } };
|
|
workItem: { kind: string; workItemId: string };
|
|
}>("distill-paperclip-now", {
|
|
companyId: COMPANY_ID,
|
|
projectId: project.id,
|
|
includeSupportingPages: false,
|
|
});
|
|
|
|
expect(result.status).toBe("review_required");
|
|
expect(result.workItem.kind).toBe("manual");
|
|
expect(result.operation.issue.originKind).toBe(`${OPERATION_ORIGIN_KIND}:distill`);
|
|
expect(result.operation.issue.billingCode).toBe("plugin-llm-wiki:default");
|
|
expect(result.operation.issue.assigneeAgentId).toBe(wikiMaintainerAgent().id);
|
|
const runInsert = harness.dbExecutes.find((execute) =>
|
|
execute.sql.includes("paperclip_distillation_runs") && execute.sql.includes("operation_issue_id"));
|
|
expect(runInsert?.params).toEqual(expect.arrayContaining([
|
|
COMPANY_ID,
|
|
"default",
|
|
expect.any(String),
|
|
result.workItem.workItemId,
|
|
project.id,
|
|
]));
|
|
});
|
|
|
|
it("backfills only the selected Paperclip project and date window", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const project = existingProject();
|
|
const inWindow = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-777777777797",
|
|
identifier: "PAP-4106",
|
|
title: "Backfill in-window decision",
|
|
description: "Accepted historical decision that should appear in the backfill page.",
|
|
status: "done",
|
|
projectId: project.id,
|
|
updatedAt: new Date("2026-04-15T12:00:00Z"),
|
|
});
|
|
const outOfWindow = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-777777777798",
|
|
identifier: "PAP-4107",
|
|
title: "Backfill out-of-window decision",
|
|
description: "This old decision must not be included in the selected date window.",
|
|
status: "done",
|
|
projectId: project.id,
|
|
updatedAt: new Date("2026-03-01T12:00:00Z"),
|
|
});
|
|
const otherProject = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-777777777799",
|
|
identifier: "PAP-4108",
|
|
title: "Other project decision",
|
|
description: "This issue belongs to a different project and must not be included.",
|
|
status: "done",
|
|
projectId: "88888888-8888-4888-8888-888888888888",
|
|
updatedAt: new Date("2026-04-16T12:00:00Z"),
|
|
});
|
|
harness.seed({ agents: [wikiMaintainerAgent()], projects: [project], issues: [inWindow, outOfWindow, otherProject] });
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
const result = await harness.performAction<{
|
|
status: string;
|
|
patches: Array<{ operationType: string; proposedContents: string }>;
|
|
workItem: { kind: string };
|
|
operation: { issue: { originKind: string } };
|
|
}>("backfill-paperclip-distillation", {
|
|
companyId: COMPANY_ID,
|
|
projectId: project.id,
|
|
backfillStartAt: "2026-04-01T00:00:00Z",
|
|
backfillEndAt: "2026-04-30T23:59:59Z",
|
|
includeSupportingPages: false,
|
|
});
|
|
|
|
expect(result.status).toBe("review_required");
|
|
expect(result.workItem.kind).toBe("backfill");
|
|
expect(result.operation.issue.originKind).toBe(`${OPERATION_ORIGIN_KIND}:backfill`);
|
|
const projectPatch = result.patches.find((patch) => patch.operationType === "project_page_distill");
|
|
expect(projectPatch?.proposedContents).toContain("PAP-4106");
|
|
expect(projectPatch?.proposedContents).not.toContain("PAP-4107");
|
|
expect(projectPatch?.proposedContents).not.toContain("PAP-4108");
|
|
const workItemInsert = harness.dbExecutes.find((execute) =>
|
|
execute.sql.includes("paperclip_distillation_work_items") && execute.params?.[3] === "backfill");
|
|
expect(String(workItemInsert?.params?.[9])).toContain('"backfillStartAt":"2026-04-01T00:00:00Z"');
|
|
});
|
|
|
|
it("generates review-required Paperclip project page patches with provenance, index, and log updates", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const project = existingProject();
|
|
const issue = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-777777777791",
|
|
identifier: "PAP-4100",
|
|
title: "Approved project page distillation plan",
|
|
description: "Accepted plan: write stable project overview sections with source provenance.",
|
|
status: "in_progress",
|
|
projectId: project.id,
|
|
updatedAt: new Date("2026-05-03T10:00:00Z"),
|
|
});
|
|
const files = new Map<string, string>([
|
|
["wiki/index.md", DEFAULT_INDEX],
|
|
["wiki/log.md", DEFAULT_LOG],
|
|
]);
|
|
const writes: Array<{ path: string; contents: string }> = [];
|
|
harness.seed({ projects: [project], issues: [issue] });
|
|
harness.ctx.localFolders.readText = async (_companyId, _folderKey, relativePath) => {
|
|
const contents = files.get(relativePath);
|
|
if (contents == null) throw new Error(`missing ${relativePath}`);
|
|
return contents;
|
|
};
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
writes.push({ path: relativePath, contents });
|
|
files.set(relativePath, contents);
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
const result = await harness.performAction<{
|
|
status: string;
|
|
patches: Array<{ pagePath: string; operationType: string; currentHash: string | null; proposedContents: string; sourceRefs: Array<{ issueIdentifier: string | null }> }>;
|
|
warnings: string[];
|
|
}>("distill-paperclip-project-page", {
|
|
companyId: COMPANY_ID,
|
|
projectId: project.id,
|
|
maxCharacters: 20000,
|
|
includeSupportingPages: false,
|
|
});
|
|
|
|
expect(result.status).toBe("review_required");
|
|
expect(writes).toHaveLength(0);
|
|
expect(result.patches.map((patch) => patch.operationType)).toEqual([
|
|
"project_page_distill",
|
|
"index_refresh",
|
|
"log_append",
|
|
]);
|
|
const projectPatch = result.patches[0];
|
|
expect(projectPatch.currentHash).toBeNull();
|
|
expect(projectPatch.pagePath).toBe("wiki/projects/existing-wiki-project.md");
|
|
expect(projectPatch.proposedContents).toContain("## Source Provenance");
|
|
expect(projectPatch.proposedContents).toContain("PAP-4100");
|
|
expect(projectPatch.sourceRefs).toEqual([expect.objectContaining({ issueIdentifier: "PAP-4100" })]);
|
|
expect(result.patches[1].proposedContents).toContain("[Existing Wiki Project](wiki/projects/existing-wiki-project.md)");
|
|
expect(result.patches[2].proposedContents).toContain("paperclip-distill | proposed");
|
|
expect(result.warnings).toContain("Auto-apply policy disabled; proposed patches require review.");
|
|
});
|
|
|
|
it("keeps suppressed secret-like source content out of generated wiki patches", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const project = existingProject();
|
|
const issue = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-777777777796",
|
|
identifier: "PAP-4104",
|
|
title: "Distill sanitized provenance",
|
|
description: "Publish enough project state for a reviewable project page without leaking credentials.",
|
|
status: "in_progress",
|
|
projectId: project.id,
|
|
updatedAt: new Date("2026-05-04T10:00:00Z"),
|
|
});
|
|
const files = new Map<string, string>([
|
|
["wiki/index.md", DEFAULT_INDEX],
|
|
["wiki/log.md", DEFAULT_LOG],
|
|
]);
|
|
harness.seed({
|
|
projects: [project],
|
|
issues: [issue],
|
|
issueComments: [{
|
|
id: "77777777-7777-4777-8777-777777777797",
|
|
companyId: COMPANY_ID,
|
|
issueId: issue.id,
|
|
authorAgentId: null,
|
|
authorUserId: null,
|
|
body: "Authorization: Bearer ghp_patchsecretcommenttoken1234567890",
|
|
createdAt: new Date("2026-05-04T11:00:00Z"),
|
|
updatedAt: new Date("2026-05-04T11:00:00Z"),
|
|
}],
|
|
});
|
|
harness.ctx.localFolders.readText = async (_companyId, _folderKey, relativePath) => {
|
|
const contents = files.get(relativePath);
|
|
if (contents == null) throw new Error(`missing ${relativePath}`);
|
|
return contents;
|
|
};
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
files.set(relativePath, contents);
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
await harness.ctx.issues.documents.upsert({
|
|
companyId: COMPANY_ID,
|
|
issueId: issue.id,
|
|
key: "plan",
|
|
title: "Plan",
|
|
body: "OPENAI_API_KEY=sk-patchsecretdocumentvalue1234567890",
|
|
});
|
|
|
|
const result = await harness.performAction<{
|
|
status: string;
|
|
patches: Array<{ operationType: string; proposedContents: string }>;
|
|
}>("distill-paperclip-project-page", {
|
|
companyId: COMPANY_ID,
|
|
projectId: project.id,
|
|
maxCharacters: 20000,
|
|
includeSupportingPages: false,
|
|
});
|
|
|
|
expect(result.status).toBe("review_required");
|
|
const combinedPatchContents = result.patches.map((patch) => patch.proposedContents).join("\n");
|
|
expect(combinedPatchContents).toContain("redaction=suppressed_sensitive_content");
|
|
expect(combinedPatchContents).toContain("redaction_reasons=secret_like_token");
|
|
expect(combinedPatchContents).not.toContain("ghp_patchsecretcommenttoken1234567890");
|
|
expect(combinedPatchContents).not.toContain("sk-patchsecretdocumentvalue1234567890");
|
|
});
|
|
|
|
it("auto-applies Paperclip project page patches only when policy allows and records page bindings", async () => {
|
|
const harness = createTestHarness({ manifest, config: { autoApplyIngestPatches: true } });
|
|
const project = existingProject();
|
|
const issue = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-777777777792",
|
|
identifier: "PAP-4101",
|
|
title: "Implement project page writer",
|
|
description: "Implementation completed enough to publish the generated project page.",
|
|
status: "done",
|
|
projectId: project.id,
|
|
updatedAt: new Date("2026-05-04T10:00:00Z"),
|
|
});
|
|
const files = new Map<string, string>([
|
|
["wiki/index.md", DEFAULT_INDEX],
|
|
["wiki/log.md", DEFAULT_LOG],
|
|
]);
|
|
harness.seed({ projects: [project], issues: [issue] });
|
|
harness.ctx.localFolders.readText = async (_companyId, _folderKey, relativePath) => {
|
|
const contents = files.get(relativePath);
|
|
if (contents == null) throw new Error(`missing ${relativePath}`);
|
|
return contents;
|
|
};
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
files.set(relativePath, contents);
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
const result = await harness.performAction<{
|
|
status: string;
|
|
appliedPages: string[];
|
|
patches: Array<{ pagePath: string; sourceHash: string }>;
|
|
}>("distill-paperclip-project-page", {
|
|
companyId: COMPANY_ID,
|
|
projectId: project.id,
|
|
autoApply: true,
|
|
maxCharacters: 20000,
|
|
includeSupportingPages: false,
|
|
});
|
|
|
|
expect(result.status).toBe("applied");
|
|
expect(result.appliedPages).toEqual([
|
|
"wiki/projects/existing-wiki-project.md",
|
|
"wiki/index.md",
|
|
"wiki/log.md",
|
|
]);
|
|
expect(files.get("wiki/projects/existing-wiki-project.md")).toContain("## Current Status");
|
|
expect(files.get("wiki/projects/existing-wiki-project.md")).toContain("Source bundle hash");
|
|
expect(files.get("wiki/index.md")).toContain("wiki/projects/existing-wiki-project.md");
|
|
expect(files.get("wiki/log.md")).toContain("paperclip-distill | proposed");
|
|
const bindingWrites = harness.dbExecutes.filter((execute) => execute.sql.includes("paperclip_page_bindings"));
|
|
expect(bindingWrites).toHaveLength(3);
|
|
expect(bindingWrites[0].params).toEqual(expect.arrayContaining([
|
|
COMPANY_ID,
|
|
"default",
|
|
project.id,
|
|
null,
|
|
"wiki/projects/existing-wiki-project.md",
|
|
result.patches[0].sourceHash,
|
|
]));
|
|
});
|
|
|
|
it("refuses auto-apply Paperclip project page patches in authenticated/public deployments", async () => {
|
|
process.env.PAPERCLIP_DEPLOYMENT_MODE = "authenticated";
|
|
process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE = "public";
|
|
const harness = createTestHarness({ manifest, config: { autoApplyIngestPatches: true } });
|
|
const project = existingProject();
|
|
const issue = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-77777777779a",
|
|
identifier: "PAP-4101",
|
|
title: "Implement project page writer",
|
|
description: "Implementation completed enough to publish the generated project page.",
|
|
status: "done",
|
|
projectId: project.id,
|
|
updatedAt: new Date("2026-05-04T10:00:00Z"),
|
|
});
|
|
const files = new Map<string, string>([
|
|
["wiki/index.md", DEFAULT_INDEX],
|
|
["wiki/log.md", DEFAULT_LOG],
|
|
]);
|
|
const writes: string[] = [];
|
|
harness.seed({ projects: [project], issues: [issue] });
|
|
harness.ctx.localFolders.readText = async (_companyId, _folderKey, relativePath) => {
|
|
const contents = files.get(relativePath);
|
|
if (contents == null) throw new Error(`missing ${relativePath}`);
|
|
return contents;
|
|
};
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
writes.push(relativePath);
|
|
files.set(relativePath, contents);
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
const result = await harness.performAction<{
|
|
status: string;
|
|
appliedPages: string[];
|
|
warnings: string[];
|
|
}>("distill-paperclip-project-page", {
|
|
companyId: COMPANY_ID,
|
|
projectId: project.id,
|
|
autoApply: true,
|
|
maxCharacters: 20000,
|
|
includeSupportingPages: false,
|
|
});
|
|
|
|
expect(result.status).toBe("review_required");
|
|
expect(result.appliedPages).toEqual([]);
|
|
expect(writes).toHaveLength(0);
|
|
expect(result.warnings).toContain(
|
|
"Authenticated/public deployments always require manual review before wiki writes.",
|
|
);
|
|
});
|
|
|
|
it("refuses stale project page hashes before writing generated Paperclip pages", async () => {
|
|
const harness = createTestHarness({ manifest, config: { autoApplyIngestPatches: true } });
|
|
const project = existingProject();
|
|
const issue = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-777777777793",
|
|
identifier: "PAP-4102",
|
|
title: "Publish project page",
|
|
description: "Ready to publish.",
|
|
status: "done",
|
|
projectId: project.id,
|
|
});
|
|
const files = new Map<string, string>([
|
|
["wiki/projects/existing-wiki-project.md", "# Existing\n"],
|
|
["wiki/index.md", DEFAULT_INDEX],
|
|
["wiki/log.md", DEFAULT_LOG],
|
|
]);
|
|
const writes: string[] = [];
|
|
harness.seed({ projects: [project], issues: [issue] });
|
|
harness.ctx.localFolders.readText = async (_companyId, _folderKey, relativePath) => {
|
|
const contents = files.get(relativePath);
|
|
if (contents == null) throw new Error(`missing ${relativePath}`);
|
|
return contents;
|
|
};
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
writes.push(relativePath);
|
|
files.set(relativePath, contents);
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
await expect(harness.performAction("distill-paperclip-project-page", {
|
|
companyId: COMPANY_ID,
|
|
projectId: project.id,
|
|
autoApply: true,
|
|
expectedProjectPageHash: "stale",
|
|
includeSupportingPages: false,
|
|
})).rejects.toThrow("Refusing to overwrite");
|
|
expect(writes).toHaveLength(0);
|
|
});
|
|
|
|
it("skips low-signal Paperclip source windows without proposing wiki writes", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const project = existingProject();
|
|
const issue = paperclipIssue({
|
|
id: "77777777-7777-4777-8777-777777777794",
|
|
identifier: "PAP-4103",
|
|
title: "Routine heartbeat",
|
|
description: "",
|
|
status: "todo",
|
|
projectId: project.id,
|
|
});
|
|
const writes: string[] = [];
|
|
harness.seed({ projects: [project], issues: [issue] });
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
writes.push(relativePath);
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
const result = await harness.performAction<{ status: string; reason: string; patches: unknown[] }>("distill-paperclip-project-page", {
|
|
companyId: COMPANY_ID,
|
|
projectId: project.id,
|
|
maxCharacters: 20000,
|
|
});
|
|
|
|
expect(result.status).toBe("skipped");
|
|
expect(result.reason).toBe("low_signal");
|
|
expect(result.patches).toEqual([]);
|
|
expect(writes).toHaveLength(0);
|
|
});
|
|
|
|
it("bootstraps required local wiki files through the local folder API", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const writes: Array<{ path: string; contents: string }> = [];
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
writes.push({ path: relativePath, contents });
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
const result = await harness.performAction<{ writtenFiles: string[] }>("bootstrap-root", {
|
|
companyId: COMPANY_ID,
|
|
path: "/tmp/company-wiki",
|
|
});
|
|
|
|
expect(result.writtenFiles).toContain("AGENTS.md");
|
|
expect(result.writtenFiles).toContain("IDEA.md");
|
|
expect(writes.map((write) => write.path)).toEqual([
|
|
".gitignore",
|
|
"AGENTS.md",
|
|
"IDEA.md",
|
|
"wiki/index.md",
|
|
"wiki/log.md",
|
|
"raw/.gitkeep",
|
|
"wiki/sources/.gitkeep",
|
|
"wiki/projects/.gitkeep",
|
|
"wiki/entities/.gitkeep",
|
|
"wiki/concepts/.gitkeep",
|
|
"wiki/synthesis/.gitkeep",
|
|
]);
|
|
expect(writes.find((write) => write.path === "AGENTS.md")?.contents).toContain("LLM Wiki Schema");
|
|
expect(writes.find((write) => write.path === "AGENTS.md")?.contents).toContain("wiki/projects/<project-slug>.md");
|
|
});
|
|
|
|
it("captures raw sources into local files and plugin metadata", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const writes: Array<{ path: string; contents: string }> = [];
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
writes.push({ path: relativePath, contents });
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
const result = await harness.performAction<{ rawPath: string; hash: string }>("capture-source", {
|
|
companyId: COMPANY_ID,
|
|
wikiId: "default",
|
|
title: "Plugin Boundaries",
|
|
contents: "# Plugin Boundaries\n\nKeep wiki logic in the plugin.",
|
|
});
|
|
|
|
expect(result.rawPath).toMatch(/^raw\/\d{4}-\d{2}-\d{2}-plugin-boundaries-/);
|
|
expect(result.hash).toHaveLength(64);
|
|
expect(writes[0]).toMatchObject({ path: result.rawPath });
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("wiki_sources"))).toBe(true);
|
|
});
|
|
|
|
it("ingests source metadata and creates a hidden plugin-operation issue", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
harness.seed({ agents: [wikiMaintainerAgent()] });
|
|
const writes: Array<{ path: string; contents: string }> = [];
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
writes.push({ path: relativePath, contents });
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
const result = await harness.performAction<{
|
|
source: { rawPath: string; title: string; hash: string };
|
|
operation: { operationId: string; issue: { originKind: string; originId: string | null; billingCode: string | null; assigneeAgentId: string | null } };
|
|
}>("ingest-source", {
|
|
companyId: COMPANY_ID,
|
|
wikiId: "engineering",
|
|
sourceType: "url",
|
|
title: "Standalone Plugin Notes",
|
|
url: "https://example.test/wiki",
|
|
contents: "# Standalone Plugin Notes\n\nKeep wiki behavior in the plugin package.",
|
|
rawPath: "raw/standalone-plugin-notes.md",
|
|
metadata: { importedBy: "alpha-verification" },
|
|
});
|
|
|
|
expect(result.source.rawPath).toBe("raw/standalone-plugin-notes.md");
|
|
expect(result.source.title).toBe("Standalone Plugin Notes");
|
|
expect(result.source.hash).toHaveLength(64);
|
|
expect(writes).toEqual([
|
|
expect.objectContaining({
|
|
path: "raw/standalone-plugin-notes.md",
|
|
contents: expect.stringContaining("Keep wiki behavior in the plugin package."),
|
|
}),
|
|
]);
|
|
expect(result.operation.issue.originKind).toBe(`${OPERATION_ORIGIN_KIND}:ingest`);
|
|
expect(result.operation.issue.originId).toBe(`wiki:engineering:operation:${result.operation.operationId}`);
|
|
expect(result.operation.issue.billingCode).toBe("plugin-llm-wiki:engineering");
|
|
expect(result.operation.issue.assigneeAgentId).toBe(wikiMaintainerAgent().id);
|
|
|
|
const sourceInsert = harness.dbExecutes.find((execute) => execute.sql.includes("wiki_sources"));
|
|
expect(sourceInsert?.params).toEqual(expect.arrayContaining([
|
|
COMPANY_ID,
|
|
"engineering",
|
|
"url",
|
|
"Standalone Plugin Notes",
|
|
"https://example.test/wiki",
|
|
"raw/standalone-plugin-notes.md",
|
|
JSON.stringify({ importedBy: "alpha-verification" }),
|
|
]));
|
|
const operationInsert = harness.dbExecutes.find((execute) => execute.sql.includes("wiki_operations"));
|
|
expect(operationInsert?.params).toEqual(expect.arrayContaining([
|
|
result.operation.operationId,
|
|
COMPANY_ID,
|
|
"engineering",
|
|
"ingest",
|
|
"queued",
|
|
]));
|
|
});
|
|
|
|
it("rejects oversized source capture before raw writes or operation creation", async () => {
|
|
const harness = createTestHarness({ manifest, config: { maxSourceBytes: 16 } });
|
|
harness.seed({ agents: [wikiMaintainerAgent()] });
|
|
const writes: Array<{ path: string; contents: string }> = [];
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
writes.push({ path: relativePath, contents });
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
await expect(harness.performAction("ingest-source", {
|
|
companyId: COMPANY_ID,
|
|
wikiId: "default",
|
|
sourceType: "text",
|
|
title: "Oversized source",
|
|
contents: "x".repeat(17),
|
|
})).rejects.toThrow("exceeds the configured LLM Wiki source limit");
|
|
|
|
expect(writes).toHaveLength(0);
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("wiki_sources"))).toBe(false);
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("wiki_operations"))).toBe(false);
|
|
const operations = await harness.ctx.issues.list({
|
|
companyId: COMPANY_ID,
|
|
originKindPrefix: String(OPERATION_ORIGIN_KIND),
|
|
});
|
|
expect(operations).toHaveLength(0);
|
|
});
|
|
|
|
it("writes pages atomically, records metadata, and rejects stale hashes", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const files = new Map<string, string>([
|
|
["wiki/concepts/plugin-boundaries.md", "# Old Title\n"],
|
|
]);
|
|
harness.ctx.localFolders.readText = async (_companyId, _folderKey, relativePath) => {
|
|
const value = files.get(relativePath);
|
|
if (value == null) throw new Error("missing");
|
|
return value;
|
|
};
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
files.set(relativePath, contents);
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
const staleWrite = harness.executeTool("wiki_write_page", {
|
|
companyId: COMPANY_ID,
|
|
wikiId: "default",
|
|
path: "wiki/concepts/plugin-boundaries.md",
|
|
contents: "# New Title\n",
|
|
expectedHash: "stale",
|
|
});
|
|
await expect(staleWrite).rejects.toThrow("Refusing to overwrite");
|
|
|
|
const result = await harness.executeTool<{ data?: { hash: string } }>("wiki_write_page", {
|
|
companyId: COMPANY_ID,
|
|
wikiId: "default",
|
|
path: "wiki/concepts/plugin-boundaries.md",
|
|
contents: "# Plugin Boundaries\n\nSee [Knowledge](wiki/areas/knowledge.md).",
|
|
});
|
|
|
|
expect(result.data?.hash).toHaveLength(64);
|
|
expect(files.get("wiki/concepts/plugin-boundaries.md")).toContain("Plugin Boundaries");
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("wiki_pages"))).toBe(true);
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("wiki_page_revisions"))).toBe(true);
|
|
});
|
|
|
|
it("blocks agent-tool writes to AGENTS.md but allows explicit board edits", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const files = new Map<string, string>([
|
|
["AGENTS.md", "# LLM Wiki Maintainer\n\nOriginal instructions.\n"],
|
|
]);
|
|
harness.ctx.localFolders.readText = async (_companyId, _folderKey, relativePath) => {
|
|
const value = files.get(relativePath);
|
|
if (value == null) throw new Error("missing");
|
|
return value;
|
|
};
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
files.set(relativePath, contents);
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
|
|
await plugin.definition.setup(harness.ctx);
|
|
|
|
await expect(harness.executeTool("wiki_write_page", {
|
|
companyId: COMPANY_ID,
|
|
wikiId: "default",
|
|
path: "AGENTS.md",
|
|
contents: "# LLM Wiki Maintainer\n\nCompromised instructions.\n",
|
|
})).rejects.toThrow("Refusing to overwrite protected wiki control file AGENTS.md");
|
|
|
|
const result = await harness.performAction<{ hash: string }>("write-page", {
|
|
companyId: COMPANY_ID,
|
|
wikiId: "default",
|
|
path: "AGENTS.md",
|
|
contents: "# LLM Wiki Maintainer\n\nBoard-updated instructions.\n",
|
|
});
|
|
|
|
expect(result.hash).toHaveLength(64);
|
|
expect(files.get("AGENTS.md")).toContain("Board-updated instructions.");
|
|
});
|
|
|
|
it("creates plugin-operation issues for LLM workflows", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
harness.seed({ agents: [wikiMaintainerAgent()] });
|
|
await plugin.definition.setup(harness.ctx);
|
|
|
|
const result = await harness.performAction<{ issue: { originKind: string; billingCode: string | null } }>(
|
|
"create-operation",
|
|
{
|
|
companyId: COMPANY_ID,
|
|
operationType: "query",
|
|
title: "Ask the wiki about plugin boundaries",
|
|
prompt: "Which files own wiki behavior?",
|
|
},
|
|
);
|
|
|
|
expect(result.issue.originKind).toBe(`${OPERATION_ORIGIN_KIND}:query`);
|
|
expect(result.issue.billingCode).toBe("plugin-llm-wiki:default");
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("wiki_resource_bindings"))).toBe(true);
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("wiki_operations"))).toBe(true);
|
|
});
|
|
|
|
it("uses selected existing agent and project bindings for new operations", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
const agent = existingAgent();
|
|
const project = existingProject();
|
|
harness.seed({ agents: [agent], projects: [project] });
|
|
await plugin.definition.setup(harness.ctx);
|
|
|
|
const selectedAgent = await harness.performAction<{ source: string; agentId: string }>("select-managed-agent", {
|
|
companyId: COMPANY_ID,
|
|
agentId: agent.id,
|
|
});
|
|
const selectedProject = await harness.performAction<{ source: string; projectId: string }>("select-managed-project", {
|
|
companyId: COMPANY_ID,
|
|
projectId: project.id,
|
|
});
|
|
|
|
expect(selectedAgent).toMatchObject({ source: "selected", agentId: agent.id });
|
|
expect(selectedProject).toMatchObject({ source: "selected", projectId: project.id });
|
|
expect(harness.dbExecutes.filter((execute) => execute.sql.includes("wiki_resource_bindings"))).toHaveLength(2);
|
|
|
|
harness.ctx.db.query = async <T = Record<string, unknown>>(sql: string, params?: unknown[]) => {
|
|
harness.dbQueries.push({ sql, params });
|
|
if (sql.includes("wiki_resource_bindings") && params?.[2] === "agent") {
|
|
return [{ resolved_id: agent.id, metadata: { source: "selected-existing" } }] as T[];
|
|
}
|
|
if (sql.includes("wiki_resource_bindings") && params?.[2] === "project") {
|
|
return [{ resolved_id: project.id, metadata: { source: "selected-existing" } }] as T[];
|
|
}
|
|
if (sql.includes("wiki_operations")) return [{ count: "0" }] as T[];
|
|
return [];
|
|
};
|
|
|
|
const result = await harness.performAction<{
|
|
issue: { assigneeAgentId: string | null; projectId: string | null };
|
|
}>("create-operation", {
|
|
companyId: COMPANY_ID,
|
|
operationType: "lint",
|
|
title: "Lint selected wiki",
|
|
});
|
|
|
|
expect(result.issue.assigneeAgentId).toBe(agent.id);
|
|
expect(result.issue.projectId).toBe(project.id);
|
|
});
|
|
|
|
it("starts query sessions, records run ids, and forwards session events to plugin streams", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
harness.seed({ agents: [wikiMaintainerAgent()] });
|
|
const streamEvents: unknown[] = [];
|
|
harness.ctx.streams.emit = (_channel, event) => {
|
|
streamEvents.push(event);
|
|
};
|
|
await plugin.definition.setup(harness.ctx);
|
|
|
|
const result = await harness.performAction<{
|
|
operationId: string;
|
|
querySessionId: string;
|
|
sessionId: string;
|
|
runId: string;
|
|
channel: string;
|
|
}>("start-query", {
|
|
companyId: COMPANY_ID,
|
|
question: "Which files own wiki behavior?",
|
|
});
|
|
|
|
expect(result.querySessionId).toBe(result.operationId);
|
|
expect(result.channel).toBe(`llm-wiki:query:${result.operationId}`);
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("wiki_query_sessions"))).toBe(true);
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("run_ids"))).toBe(true);
|
|
|
|
harness.simulateSessionEvent(result.sessionId, {
|
|
runId: result.runId,
|
|
seq: 1,
|
|
eventType: "chunk",
|
|
stream: "stdout",
|
|
message: "Keep wiki behavior in the plugin.",
|
|
payload: null,
|
|
});
|
|
harness.simulateSessionEvent(result.sessionId, {
|
|
runId: result.runId,
|
|
seq: 2,
|
|
eventType: "done",
|
|
stream: "system",
|
|
message: "Run completed",
|
|
payload: null,
|
|
});
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
|
|
expect(streamEvents).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ type: "query.started", operationId: result.operationId }),
|
|
expect.objectContaining({ type: "agent.event", message: "Keep wiki behavior in the plugin." }),
|
|
expect.objectContaining({ type: "query.done", answer: "Keep wiki behavior in the plugin." }),
|
|
]));
|
|
expect(harness.dbExecutes.some((execute) =>
|
|
execute.sql.includes("wiki_query_sessions") && execute.params?.includes("completed"),
|
|
)).toBe(true);
|
|
});
|
|
|
|
it("files a streamed query answer as a page through a hidden file-as-page operation", async () => {
|
|
const harness = createTestHarness({ manifest });
|
|
harness.seed({ agents: [wikiMaintainerAgent()] });
|
|
const writes: Array<{ path: string; contents: string }> = [];
|
|
harness.ctx.localFolders.writeTextAtomic = async (_companyId, _folderKey, relativePath, contents) => {
|
|
writes.push({ path: relativePath, contents });
|
|
return harness.ctx.localFolders.status(COMPANY_ID, "wiki-root");
|
|
};
|
|
await plugin.definition.setup(harness.ctx);
|
|
|
|
const result = await harness.performAction<{ path: string; operationId: string; page: { revisionId: string } }>(
|
|
"file-as-page",
|
|
{
|
|
companyId: COMPANY_ID,
|
|
querySessionId: "33333333-3333-4333-8333-333333333333",
|
|
question: "Where should wiki code live?",
|
|
answer: "Wiki-specific code lives in the standalone plugin package.",
|
|
path: "wiki/concepts/plugin-boundaries.md",
|
|
title: "Plugin Boundaries",
|
|
},
|
|
);
|
|
|
|
expect(result.path).toBe("wiki/concepts/plugin-boundaries.md");
|
|
expect(writes[0]).toMatchObject({ path: "wiki/concepts/plugin-boundaries.md" });
|
|
expect(writes[0]?.contents).toContain("Wiki-specific code lives in the standalone plugin package.");
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("wiki_operations"))).toBe(true);
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("wiki_page_revisions"))).toBe(true);
|
|
expect(harness.dbExecutes.some((execute) => execute.sql.includes("filed_outputs"))).toBe(true);
|
|
});
|
|
});
|