import assert from "node:assert/strict"; import test from "node:test"; import { createPlugin } from "../src/index.mjs"; function createJSONResponse(payload, status = 200) { return new Response(JSON.stringify(payload), { status, headers: { "Content-Type": "application/json" } }); } test("lists readonly tool metadata", () => { const plugin = createPlugin({ fetchImpl: async () => createJSONResponse({ data: [] }) }); const tools = plugin.listTools(); assert.equal(tools.length, 2); assert.deepEqual( tools.map(tool => tool.name), ["productier_list_workspaces", "productier_list_tasks"], ); }); test("lists standard profile tool metadata", () => { const plugin = createPlugin({ profile: "standard", fetchImpl: async () => createJSONResponse({ data: [] }) }); assert.deepEqual( plugin.listTools().map(tool => tool.name), [ "productier_list_workspaces", "productier_list_board_groups", "productier_list_tasks", "productier_list_calendar_events", "productier_list_notes", "productier_list_mailboxes", "productier_list_mail_messages", "productier_list_outgoing_mails", "productier_connect_mailbox", "productier_sync_mailbox", "productier_create_board_group", "productier_create_task", "productier_create_calendar_event", "productier_create_note", "productier_create_outgoing_mail", "productier_create_task_from_mail", "productier_update_board_group", "productier_update_task", "productier_update_calendar_event", "productier_update_note" ], ); }); test("returns tool_not_found for unknown tool", async () => { const plugin = createPlugin({ fetchImpl: async () => createJSONResponse({ data: [] }) }); const result = await plugin.runTool("unknown_tool", {}); assert.equal(result.ok, false); assert.equal(result.error.code, "tool_not_found"); }); test("runs productier_list_workspaces", async () => { const plugin = createPlugin({ fetchImpl: async () => createJSONResponse({ data: [{ id: "ws-1", slug: "personal", name: "Personal HQ", role: "owner", createdAt: "2026-01-01T00:00:00Z" }] }) }); const result = await plugin.runTool("productier_list_workspaces", {}); assert.equal(result.ok, true); assert.equal(result.data.count, 1); assert.equal(result.data.workspaces[0].slug, "personal"); }); test("runs productier_list_board_groups with query/color filter", async () => { const plugin = createPlugin({ profile: "standard", defaultWorkspaceSlug: "personal", fetchImpl: async () => createJSONResponse({ data: [ { id: "group-1", name: "Inbox", color: "slate", order: 0 }, { id: "group-2", name: "In progress", color: "sky", order: 1 }, { id: "group-3", name: "Done", color: "slate", order: 2 } ] }) }); const result = await plugin.runTool("productier_list_board_groups", { query: "in", color: "slate", limit: 1 }); assert.equal(result.ok, true); assert.equal(result.data.workspaceSlug, "personal"); assert.equal(result.data.count, 1); assert.equal(result.data.truncated, true); assert.equal(result.data.boardGroups[0].id, "group-1"); }); test("runs productier_list_tasks with local filtering", async () => { const plugin = createPlugin({ fetchImpl: async () => createJSONResponse({ data: [ { id: "task-1", title: "Ship docs", description: "Write release notes", status: "todo" }, { id: "task-2", title: "Review API", description: "Read endpoints", status: "in_progress" }, { id: "task-3", title: "Docs QA", description: "Review docs", status: "todo" } ] }), defaultWorkspaceSlug: "personal" }); const result = await plugin.runTool("productier_list_tasks", { status: "todo", query: "docs", limit: 1 }); assert.equal(result.ok, true); assert.equal(result.data.workspaceSlug, "personal"); assert.equal(result.data.count, 1); assert.equal(result.data.truncated, true); assert.equal(result.data.tasks[0].id, "task-1"); }); test("runs productier_list_calendar_events with date/query filters", async () => { const plugin = createPlugin({ profile: "standard", defaultWorkspaceSlug: "personal", fetchImpl: async () => createJSONResponse({ data: [ { id: "ev-1", title: "Planning", description: "Q2 goals", startsAt: "2026-04-01T10:00:00Z" }, { id: "ev-2", title: "Review", description: "Retro", startsAt: "2026-04-10T10:00:00Z" }, { id: "ev-3", title: "Planning follow-up", description: "", startsAt: "2026-05-01T10:00:00Z" } ] }) }); const result = await plugin.runTool("productier_list_calendar_events", { query: "planning", from: "2026-04-01T00:00:00Z", to: "2026-04-30T23:59:59Z" }); assert.equal(result.ok, true); assert.equal(result.data.count, 1); assert.equal(result.data.events[0].id, "ev-1"); }); test("runs productier_list_notes with query filter", async () => { const plugin = createPlugin({ profile: "standard", defaultWorkspaceSlug: "personal", fetchImpl: async () => createJSONResponse({ data: [ { id: "note-1", title: "Sprint plan", content: "Ship board polish" }, { id: "note-2", title: "Random", content: "groceries" } ] }) }); const result = await plugin.runTool("productier_list_notes", { query: "sprint" }); assert.equal(result.ok, true); assert.equal(result.data.count, 1); assert.equal(result.data.notes[0].id, "note-1"); }); test("runs productier_list_mail_messages with filters", async () => { const plugin = createPlugin({ profile: "standard", defaultWorkspaceSlug: "personal", fetchImpl: async () => createJSONResponse({ data: [ { id: "mail-1", subject: "Sprint planning", snippet: "please review", textBody: "board updates", from: { name: "Alex", email: "alex@example.com" }, isRead: false, linkedTaskId: undefined }, { id: "mail-2", subject: "Random", snippet: "hello", textBody: "", from: { name: "Jamie", email: "jamie@example.com" }, isRead: true, linkedTaskId: "task-9" } ] }) }); const result = await plugin.runTool("productier_list_mail_messages", { unreadOnly: true, linked: "unlinked", query: "planning" }); assert.equal(result.ok, true); assert.equal(result.data.count, 1); assert.equal(result.data.messages[0].id, "mail-1"); }); test("runs productier_list_mailboxes with filters", async () => { const plugin = createPlugin({ profile: "standard", defaultWorkspaceSlug: "personal", fetchImpl: async () => createJSONResponse({ data: [ { id: "mb-1", label: "Team", email: "team@example.com", syncStatus: "ready", syncError: "" }, { id: "mb-2", label: "Alerts", email: "alerts@example.com", syncStatus: "error", syncError: "auth failed" } ] }) }); const result = await plugin.runTool("productier_list_mailboxes", { query: "auth", syncStatus: "error" }); assert.equal(result.ok, true); assert.equal(result.data.count, 1); assert.equal(result.data.mailboxes[0].id, "mb-2"); }); test("runs productier_list_outgoing_mails with filters", async () => { const plugin = createPlugin({ profile: "standard", defaultWorkspaceSlug: "personal", fetchImpl: async () => createJSONResponse({ data: [ { id: "out-1", mailboxId: "mb-1", status: "queued", subject: "Release draft", textBody: "please review", htmlBody: "", to: [{ email: "alex@example.com" }], cc: [], bcc: [] }, { id: "out-2", mailboxId: "mb-1", status: "sent", subject: "Invoice", textBody: "", htmlBody: "", to: [{ email: "billing@example.com" }], cc: [], bcc: [] } ] }) }); const result = await plugin.runTool("productier_list_outgoing_mails", { status: "queued", query: "release" }); assert.equal(result.ok, true); assert.equal(result.data.count, 1); assert.equal(result.data.outgoing[0].id, "out-1"); }); test("runs productier_create_board_group", async () => { const calls = []; const plugin = createPlugin({ profile: "standard", defaultWorkspaceSlug: "personal", fetchImpl: async (url, init) => { calls.push({ url: String(url), init }); return createJSONResponse({ data: { id: "group-new", workspaceSlug: "personal", name: "Blocked", color: "slate", order: 3 } }, 201); } }); const result = await plugin.runTool("productier_create_board_group", { name: "Blocked" }); assert.equal(result.ok, true); assert.equal(result.data.boardGroup.id, "group-new"); assert.equal(calls[0].init.method, "POST"); assert.match(calls[0].url, /\/v1\/board-groups$/); }); test("runs productier_create_task and forwards payload", async () => { const calls = []; const plugin = createPlugin({ profile: "standard", defaultWorkspaceSlug: "personal", defaultBoardGroupId: "group-inbox", fetchImpl: async (url, init) => { calls.push({ url: String(url), init }); return createJSONResponse({ data: { id: "task-10", workspaceSlug: "personal", boardGroupId: "group-inbox", title: "Write docs", description: "", status: "todo", color: "slate", labelIds: [], attachments: [], comments: [], createdAt: "2026-03-01T08:00:00Z", updatedAt: "2026-03-01T08:00:00Z" } }, 201); } }); const result = await plugin.runTool("productier_create_task", { title: "Write docs" }); assert.equal(result.ok, true); assert.equal(result.data.task.id, "task-10"); assert.equal(calls.length, 1); assert.equal(calls[0].init.method, "POST"); assert.match(calls[0].url, /\/v1\/tasks$/); assert.deepEqual(JSON.parse(calls[0].init.body), { workspaceSlug: "personal", boardGroupId: "group-inbox", title: "Write docs", description: "", color: "slate" }); }); test("runs productier_create_calendar_event and validates date ordering", async () => { const calls = []; const plugin = createPlugin({ profile: "standard", defaultWorkspaceSlug: "personal", fetchImpl: async (url, init) => { calls.push({ url: String(url), init }); return createJSONResponse({ data: { id: "ev-new", workspaceSlug: "personal", title: "Sync", startsAt: "2026-04-03T10:00:00Z", endsAt: "2026-04-03T11:00:00Z" } }, 201); } }); const result = await plugin.runTool("productier_create_calendar_event", { title: "Sync", startsAt: "2026-04-03T10:00:00Z", endsAt: "2026-04-03T11:00:00Z" }); assert.equal(result.ok, true); assert.equal(calls[0].init.method, "POST"); assert.match(calls[0].url, /\/v1\/calendar\/events$/); const invalid = await plugin.runTool("productier_create_calendar_event", { title: "Bad", startsAt: "2026-04-03T12:00:00Z", endsAt: "2026-04-03T11:00:00Z" }); assert.equal(invalid.ok, false); assert.match(invalid.error.message, /endsAt must be greater than or equal to startsAt/i); }); test("runs productier_create_note", async () => { const calls = []; const plugin = createPlugin({ profile: "standard", defaultWorkspaceSlug: "personal", fetchImpl: async (url, init) => { calls.push({ url: String(url), init }); return createJSONResponse({ data: { id: "note-new", workspaceSlug: "personal", title: "Changelog", content: "Draft" } }, 201); } }); const result = await plugin.runTool("productier_create_note", { title: "Changelog", content: "Draft" }); assert.equal(result.ok, true); assert.equal(calls[0].init.method, "POST"); assert.match(calls[0].url, /\/v1\/notes$/); }); test("runs productier_connect_mailbox with defaults", async () => { const calls = []; const plugin = createPlugin({ profile: "standard", defaultWorkspaceSlug: "personal", fetchImpl: async (url, init) => { calls.push({ url: String(url), init }); return createJSONResponse({ data: { id: "mb-1", workspaceSlug: "personal", label: "team@example.com", email: "team@example.com" } }, 201); } }); const result = await plugin.runTool("productier_connect_mailbox", { email: "team@example.com", imapHost: "imap.example.com", imapPassword: "imap-secret", smtpHost: "smtp.example.com" }); assert.equal(result.ok, true); assert.equal(result.data.connected, true); assert.equal(calls[0].init.method, "POST"); assert.match(calls[0].url, /\/v1\/mailboxes$/); assert.deepEqual(JSON.parse(calls[0].init.body), { workspaceSlug: "personal", email: "team@example.com", imapHost: "imap.example.com", imapPort: 993, imapPassword: "imap-secret", imapUseTls: true, smtpHost: "smtp.example.com", smtpPort: 587, smtpUseTls: true }); }); test("runs productier_sync_mailbox", async () => { const calls = []; const plugin = createPlugin({ profile: "standard", fetchImpl: async (url, init) => { calls.push({ url: String(url), init }); return createJSONResponse({ data: { id: "mb-1", syncStatus: "ready" } }); } }); const result = await plugin.runTool("productier_sync_mailbox", { mailboxId: "mb-1" }); assert.equal(result.ok, true); assert.equal(result.data.synced, true); assert.equal(calls[0].init.method, "POST"); assert.match(calls[0].url, /\/v1\/mailboxes\/mb-1\/sync$/); }); test("runs productier_create_outgoing_mail and normalizes recipients", async () => { const calls = []; const plugin = createPlugin({ profile: "standard", defaultWorkspaceSlug: "personal", fetchImpl: async (url, init) => { calls.push({ url: String(url), init }); return createJSONResponse({ data: { id: "out-3", workspaceSlug: "personal", mailboxId: "mb-1", status: "scheduled" } }, 201); } }); const result = await plugin.runTool("productier_create_outgoing_mail", { mailboxId: "mb-1", to: "Alex , jamie@example.com", cc: [{ email: "ops@example.com", name: "Ops" }], subject: "Plan", textBody: "Draft", scheduledFor: "2026-04-10T09:00:00Z" }); assert.equal(result.ok, true); assert.equal(result.data.queued, true); assert.equal(calls[0].init.method, "POST"); assert.match(calls[0].url, /\/v1\/mail\/outgoing$/); assert.deepEqual(JSON.parse(calls[0].init.body), { workspaceSlug: "personal", mailboxId: "mb-1", to: [ { name: "Alex", email: "alex@example.com" }, { email: "jamie@example.com" } ], cc: [{ name: "Ops", email: "ops@example.com" }], subject: "Plan", textBody: "Draft", scheduledFor: "2026-04-10T09:00:00Z" }); }); test("runs productier_create_task_from_mail with defaults", async () => { const calls = []; const plugin = createPlugin({ profile: "standard", defaultBoardGroupId: "group-inbox", defaultWorkspaceSlug: "personal", fetchImpl: async (url, init) => { calls.push({ url: String(url), init }); return createJSONResponse({ data: { id: "task-from-mail", workspaceSlug: "personal", boardGroupId: "group-inbox", title: "From mail" } }, 201); } }); const result = await plugin.runTool("productier_create_task_from_mail", { messageId: "mail-1" }); assert.equal(result.ok, true); assert.equal(result.data.sourceMessageId, "mail-1"); assert.equal(result.data.workspaceSlug, "personal"); assert.equal(calls[0].init.method, "POST"); assert.match(calls[0].url, /\/v1\/mail\/messages\/mail-1\/create-task$/); assert.deepEqual(JSON.parse(calls[0].init.body), { boardGroupId: "group-inbox" }); }); test("runs productier_update_board_group with partial fields", async () => { const calls = []; const plugin = createPlugin({ profile: "standard", fetchImpl: async (url, init) => { calls.push({ url: String(url), init }); return createJSONResponse({ data: { id: "group-2", name: "Doing", color: "sky", order: 1 } }); } }); const result = await plugin.runTool("productier_update_board_group", { groupId: "group-2", name: "Doing" }); assert.equal(result.ok, true); assert.equal(calls[0].init.method, "PATCH"); assert.match(calls[0].url, /\/v1\/board-groups\/group-2$/); }); test("runs productier_update_task with partial fields", async () => { const calls = []; const plugin = createPlugin({ profile: "standard", fetchImpl: async (url, init) => { calls.push({ url: String(url), init }); return createJSONResponse({ data: { id: "task-11", workspaceSlug: "personal", boardGroupId: "group-inbox", title: "Renamed", description: "Body", status: "done", color: "sky", labelIds: [], attachments: [], comments: [], createdAt: "2026-03-01T08:00:00Z", updatedAt: "2026-03-01T08:00:00Z" } }); } }); const result = await plugin.runTool("productier_update_task", { taskId: "task-11", status: "done", title: "Renamed" }); assert.equal(result.ok, true); assert.equal(calls.length, 1); assert.equal(calls[0].init.method, "PATCH"); assert.match(calls[0].url, /\/v1\/tasks\/task-11$/); assert.deepEqual(JSON.parse(calls[0].init.body), { title: "Renamed", status: "done" }); }); test("runs productier_update_calendar_event and validates empty patch", async () => { const calls = []; const plugin = createPlugin({ profile: "standard", fetchImpl: async (url, init) => { calls.push({ url: String(url), init }); return createJSONResponse({ data: { id: "ev-1", title: "Updated", startsAt: "2026-04-03T10:00:00Z", endsAt: "2026-04-03T11:00:00Z" } }); } }); const result = await plugin.runTool("productier_update_calendar_event", { eventId: "ev-1", title: "Updated" }); assert.equal(result.ok, true); assert.equal(calls[0].init.method, "PATCH"); assert.match(calls[0].url, /\/v1\/calendar\/events\/ev-1$/); const invalid = await plugin.runTool("productier_update_calendar_event", { eventId: "ev-1" }); assert.equal(invalid.ok, false); assert.match(invalid.error.message, /at least one field/i); }); test("runs productier_update_note and validates empty patch", async () => { const calls = []; const plugin = createPlugin({ profile: "standard", fetchImpl: async (url, init) => { calls.push({ url: String(url), init }); return createJSONResponse({ data: { id: "note-1", title: "Renamed", content: "Body" } }); } }); const result = await plugin.runTool("productier_update_note", { noteId: "note-1", title: "Renamed" }); assert.equal(result.ok, true); assert.equal(calls[0].init.method, "PATCH"); assert.match(calls[0].url, /\/v1\/notes\/note-1$/); const invalid = await plugin.runTool("productier_update_note", { noteId: "note-1" }); assert.equal(invalid.ok, false); assert.match(invalid.error.message, /at least one field/i); }); test("maps backend structured errors", async () => { const plugin = createPlugin({ fetchImpl: async () => createJSONResponse({ error: { code: "unauthorized", message: "authentication required", requestId: "req-123" } }, 401) }); const result = await plugin.runTool("productier_list_tasks", { workspaceSlug: "personal" }); assert.equal(result.ok, false); assert.equal(result.error.code, "unauthorized"); assert.equal(result.error.status, 401); assert.equal(result.error.requestId, "req-123"); }); test("returns validation error when workspace slug is missing", async () => { const plugin = createPlugin({ fetchImpl: async () => createJSONResponse({ data: [] }) }); const result = await plugin.runTool("productier_list_tasks", {}); assert.equal(result.ok, false); assert.equal(result.error.code, "tool_execution_error"); assert.match(result.error.message, /workspaceSlug is required/i); }); test("returns validation error for invalid update task status", async () => { const plugin = createPlugin({ profile: "standard", fetchImpl: async () => createJSONResponse({ data: [] }) }); const result = await plugin.runTool("productier_update_task", { taskId: "task-1", status: "bad_status" }); assert.equal(result.ok, false); assert.equal(result.error.code, "tool_execution_error"); assert.match(result.error.message, /status must be one of/i); }); test("returns validation error for invalid mail linked filter", async () => { const plugin = createPlugin({ profile: "standard", fetchImpl: async () => createJSONResponse({ data: [] }) }); const result = await plugin.runTool("productier_list_mail_messages", { workspaceSlug: "personal", linked: "bad-value" }); assert.equal(result.ok, false); assert.equal(result.error.code, "tool_execution_error"); assert.match(result.error.message, /linked must be one of/i); }); test("returns validation error for invalid mailbox sync filter", async () => { const plugin = createPlugin({ profile: "standard", fetchImpl: async () => createJSONResponse({ data: [] }) }); const result = await plugin.runTool("productier_list_mailboxes", { workspaceSlug: "personal", syncStatus: "bad-value" }); assert.equal(result.ok, false); assert.equal(result.error.code, "tool_execution_error"); assert.match(result.error.message, /syncStatus must be one of/i); }); test("returns validation error for invalid outgoing status filter", async () => { const plugin = createPlugin({ profile: "standard", fetchImpl: async () => createJSONResponse({ data: [] }) }); const result = await plugin.runTool("productier_list_outgoing_mails", { workspaceSlug: "personal", status: "bad-value" }); assert.equal(result.ok, false); assert.equal(result.error.code, "tool_execution_error"); assert.match(result.error.message, /status must be one of/i); }); test("retries transient backend errors and succeeds", async () => { let calls = 0; const plugin = createPlugin({ retryMaxAttempts: 3, retryBaseDelayMs: 0, retryMaxDelayMs: 0, retryJitterMs: 0, fetchImpl: async () => { calls += 1; if (calls === 1) { return createJSONResponse( { error: { code: "service_unavailable", message: "temporary outage" } }, 503, ); } return createJSONResponse({ data: [{ id: "ws-1", slug: "personal", name: "Personal HQ", role: "owner", createdAt: "2026-01-01T00:00:00Z" }] }); } }); const result = await plugin.runTool("productier_list_workspaces", {}); assert.equal(result.ok, true); assert.equal(calls, 2); assert.equal(result.meta.attempts, 2); }); test("returns retry metadata when retries are exhausted", async () => { const plugin = createPlugin({ retryMaxAttempts: 2, retryBaseDelayMs: 0, retryMaxDelayMs: 0, retryJitterMs: 0, fetchImpl: async () => createJSONResponse( { error: { code: "service_unavailable", message: "temporary outage" } }, 503, ) }); const result = await plugin.runTool("productier_list_tasks", { workspaceSlug: "personal" }); assert.equal(result.ok, false); assert.equal(result.error.code, "service_unavailable"); assert.equal(result.error.status, 503); assert.equal(result.error.retryable, true); assert.equal(result.error.attempts, 2); }); test("enforces per-tool rate limits", async () => { let now = 1_000; let fetchCalls = 0; const plugin = createPlugin({ rateLimitMaxCalls: 1, rateLimitWindowMs: 60_000, nowFn: () => now, fetchImpl: async () => { fetchCalls += 1; return createJSONResponse({ data: [] }); } }); const first = await plugin.runTool("productier_list_tasks", { workspaceSlug: "personal" }); assert.equal(first.ok, true); const second = await plugin.runTool("productier_list_tasks", { workspaceSlug: "personal" }); assert.equal(second.ok, false); assert.equal(second.error.code, "tool_rate_limited"); assert.equal(second.error.status, 429); assert.ok(second.error.retryAfterMs >= 1); now += 60_001; const third = await plugin.runTool("productier_list_tasks", { workspaceSlug: "personal" }); assert.equal(third.ok, true); assert.equal(fetchCalls, 2); }); test("writes structured audit entries for success and failure", async () => { const auditEntries = []; const plugin = createPlugin({ profile: "standard", auditLogFn: async entry => { auditEntries.push(entry); }, fetchImpl: async () => createJSONResponse({ data: [] }) }); const success = await plugin.runTool("productier_list_workspaces", {}); assert.equal(success.ok, true); const failure = await plugin.runTool("productier_create_task", {}); assert.equal(failure.ok, false); assert.equal(failure.error.code, "tool_execution_error"); assert.equal(auditEntries.length, 2); assert.equal(auditEntries[0].tool, "productier_list_workspaces"); assert.equal(auditEntries[0].ok, true); assert.equal(auditEntries[1].tool, "productier_create_task"); assert.equal(auditEntries[1].ok, false); assert.equal(auditEntries[1].errorCode, "tool_execution_error"); });