mirror of
https://github.com/Dvorinka/Productier.git
synced 2026-06-04 04:23:00 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,879 @@
|
||||
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 <alex@example.com>, 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");
|
||||
});
|
||||
Reference in New Issue
Block a user