@orboto/mcp 0.178.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +123 -0
- package/dist/event-bridge.js +250 -0
- package/dist/event-bridge.test.js +165 -0
- package/dist/http-transport-binding.test.js +151 -0
- package/dist/http-transport.js +608 -0
- package/dist/http-transport.test.js +408 -0
- package/dist/index.js +128 -0
- package/dist/input-schema.js +293 -0
- package/dist/input-schema.test.js +198 -0
- package/dist/instructions-budget.test.js +34 -0
- package/dist/manifest-size.test.js +192 -0
- package/dist/mcp-instrument.js +24 -0
- package/dist/oauth-bootstrap.js +413 -0
- package/dist/oauth-bootstrap.test.js +212 -0
- package/dist/orboto-client.js +250 -0
- package/dist/orboto-client.test.js +141 -0
- package/dist/prompts.js +219 -0
- package/dist/prompts.test.js +79 -0
- package/dist/protect-text-usage.test.js +38 -0
- package/dist/resources.js +260 -0
- package/dist/resources.test.js +191 -0
- package/dist/response-budget.js +515 -0
- package/dist/response-budget.test.js +343 -0
- package/dist/server.js +547 -0
- package/dist/session-nudge.js +81 -0
- package/dist/session-nudge.test.js +100 -0
- package/dist/session-nudge.transport.test.js +100 -0
- package/dist/tool-annotations.test.js +97 -0
- package/dist/tool-docs.js +64 -0
- package/dist/tool-docs.test.js +90 -0
- package/dist/tool-parameters.test.js +145 -0
- package/dist/tools/absence-writes.js +93 -0
- package/dist/tools/absence-writes.test.js +106 -0
- package/dist/tools/admin-translations.js +83 -0
- package/dist/tools/admin-writes.js +174 -0
- package/dist/tools/admin-writes.test.js +120 -0
- package/dist/tools/agent-coordination.js +160 -0
- package/dist/tools/agent-coordination.test.js +156 -0
- package/dist/tools/agent-drift.js +79 -0
- package/dist/tools/agent-drift.test.js +87 -0
- package/dist/tools/agent-instructions.js +172 -0
- package/dist/tools/agent-instructions.test.js +124 -0
- package/dist/tools/agent-messages.js +75 -0
- package/dist/tools/ai-status.js +55 -0
- package/dist/tools/ai-status.test.js +43 -0
- package/dist/tools/ai-usage.js +68 -0
- package/dist/tools/analytics.js +72 -0
- package/dist/tools/analytics.test.js +71 -0
- package/dist/tools/api-call.js +72 -0
- package/dist/tools/api-call.test.js +104 -0
- package/dist/tools/api-search.js +77 -0
- package/dist/tools/api-search.test.js +90 -0
- package/dist/tools/approvals.js +68 -0
- package/dist/tools/approvals.test.js +48 -0
- package/dist/tools/attach.js +108 -0
- package/dist/tools/attach.test.js +117 -0
- package/dist/tools/backup.js +93 -0
- package/dist/tools/backup.test.js +108 -0
- package/dist/tools/bulk-create.js +213 -0
- package/dist/tools/bulk-create.test.js +135 -0
- package/dist/tools/bulk-writes.js +339 -0
- package/dist/tools/bulk-writes.test.js +239 -0
- package/dist/tools/check-similar.js +84 -0
- package/dist/tools/check-similar.test.js +101 -0
- package/dist/tools/checklist-writes.js +202 -0
- package/dist/tools/checklist-writes.test.js +184 -0
- package/dist/tools/claim.js +236 -0
- package/dist/tools/claim.test.js +207 -0
- package/dist/tools/critical-path.js +83 -0
- package/dist/tools/critical-path.test.js +114 -0
- package/dist/tools/cross-project-links.ee.js +124 -0
- package/dist/tools/cross-project-links.ee.test.js +163 -0
- package/dist/tools/customer-report.js +88 -0
- package/dist/tools/customer-report.test.js +55 -0
- package/dist/tools/doc-attachments.js +184 -0
- package/dist/tools/doc-attachments.test.js +136 -0
- package/dist/tools/doc-comments.js +228 -0
- package/dist/tools/doc-comments.test.js +192 -0
- package/dist/tools/doc-edits.js +229 -0
- package/dist/tools/doc-edits.test.js +218 -0
- package/dist/tools/doc-export.js +87 -0
- package/dist/tools/doc-export.test.js +109 -0
- package/dist/tools/doc-revisions.js +133 -0
- package/dist/tools/doc-revisions.test.js +103 -0
- package/dist/tools/docs-ai.js +171 -0
- package/dist/tools/docs-ai.test.js +160 -0
- package/dist/tools/docs.js +557 -0
- package/dist/tools/docs.test.js +365 -0
- package/dist/tools/embedding-status.js +100 -0
- package/dist/tools/embedding-status.test.js +86 -0
- package/dist/tools/free-busy.js +49 -0
- package/dist/tools/free-busy.test.js +46 -0
- package/dist/tools/get-checklists.js +72 -0
- package/dist/tools/get-project-primer.js +58 -0
- package/dist/tools/get-project.js +77 -0
- package/dist/tools/get-ticket.js +321 -0
- package/dist/tools/get-timer.js +62 -0
- package/dist/tools/help.js +44 -0
- package/dist/tools/identity.js +60 -0
- package/dist/tools/identity.test.js +67 -0
- package/dist/tools/list-git-app-installations.js +45 -0
- package/dist/tools/list-projects.js +72 -0
- package/dist/tools/list-projects.test.js +67 -0
- package/dist/tools/list-tickets.js +86 -0
- package/dist/tools/milestones-crud.test.js +154 -0
- package/dist/tools/milestones.js +279 -0
- package/dist/tools/milestones.test.js +60 -0
- package/dist/tools/my-tickets.js +65 -0
- package/dist/tools/personal-facts.js +72 -0
- package/dist/tools/personal-facts.test.js +45 -0
- package/dist/tools/phase-b-tools.test.js +530 -0
- package/dist/tools/portfolio.js +56 -0
- package/dist/tools/primer-facts.js +346 -0
- package/dist/tools/primer-facts.test.js +353 -0
- package/dist/tools/project-listings.js +116 -0
- package/dist/tools/project-listings.test.js +106 -0
- package/dist/tools/query.js +65 -0
- package/dist/tools/query.test.js +128 -0
- package/dist/tools/raci.js +108 -0
- package/dist/tools/raci.test.js +70 -0
- package/dist/tools/requirements-spec.js +89 -0
- package/dist/tools/requirements-spec.test.js +62 -0
- package/dist/tools/response-expand.js +104 -0
- package/dist/tools/review-policy.js +105 -0
- package/dist/tools/review-policy.test.js +71 -0
- package/dist/tools/search.js +78 -0
- package/dist/tools/session-start.js +435 -0
- package/dist/tools/session-start.test.js +568 -0
- package/dist/tools/set-parent.js +76 -0
- package/dist/tools/set-parent.test.js +115 -0
- package/dist/tools/shared.js +226 -0
- package/dist/tools/shared.test.js +169 -0
- package/dist/tools/similar-projection.js +30 -0
- package/dist/tools/ticket-attachments.js +131 -0
- package/dist/tools/ticket-attachments.test.js +106 -0
- package/dist/tools/ticket-schedules.js +112 -0
- package/dist/tools/ticket-schedules.test.js +102 -0
- package/dist/tools/ticket-writes.js +778 -0
- package/dist/tools/ticket-writes.test.js +643 -0
- package/dist/tools/time-writes.js +238 -0
- package/dist/tools/time-writes.test.js +138 -0
- package/dist/tools/update-project.js +165 -0
- package/dist/tools/update-project.test.js +172 -0
- package/dist/tools/wiki.js +185 -0
- package/dist/tools/wiki.test.js +104 -0
- package/dist/tools/work-sessions-next.test.js +150 -0
- package/dist/tools/work-sessions-start.test.js +174 -0
- package/dist/tools/work-sessions.js +709 -0
- package/dist/tools/work-sessions.test.js +358 -0
- package/dist/toolset.js +149 -0
- package/dist/toolset.test.js +150 -0
- package/dist/version-guard.test.js +51 -0
- package/dist/version.js +28 -0
- package/dist/with-metrics-mail-nudge.test.js +41 -0
- package/dist/with-metrics-output-schema.test.js +91 -0
- package/dist/with-metrics.js +211 -0
- package/dist/with-metrics.test.js +230 -0
- package/package.json +66 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-1455 - orboto_list_ticket_attachments + orboto_get_attachment tests.
|
|
3
|
+
*
|
|
4
|
+
* list: resolves the ticket by key, GETs /tickets/:id/attachments, renders
|
|
5
|
+
* one metadata line per attachment (empty-list case too).
|
|
6
|
+
* get: fetches the base64 route and returns an IMAGE content block for a png,
|
|
7
|
+
* a TEXT block for a pdf, and a size-cap refusal (skill-download pointer)
|
|
8
|
+
* for an over-limit file.
|
|
9
|
+
*/
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { OrbotoClient } from '../orboto-client.js';
|
|
12
|
+
import { makeListTicketAttachmentsHandler, makeGetAttachmentHandler } from './ticket-attachments.js';
|
|
13
|
+
beforeEach(() => { vi.restoreAllMocks(); });
|
|
14
|
+
afterEach(() => { vi.restoreAllMocks(); });
|
|
15
|
+
const client = new OrbotoClient({ baseUrl: 'https://orboto.example.com', apiKey: 'orb_x' });
|
|
16
|
+
const PROJ = { id: 'p1', key: 'ACME', name: 'Acme', description: null, status: 'active' };
|
|
17
|
+
const TICKET = {
|
|
18
|
+
id: 't1', projectId: 'p1', ticketKey: 'ACME-1', ticketNumber: 1,
|
|
19
|
+
title: 'Bug', description: 'body', status: 'TODO', statusName: 'To Do',
|
|
20
|
+
statusCategory: 'todo', type: 'bug', priority: 'normal',
|
|
21
|
+
estimatedTimeMinutes: 0, dueDate: null, isPrivate: false,
|
|
22
|
+
};
|
|
23
|
+
const ATT_ID = 'a0000000-0000-0000-0000-000000000001';
|
|
24
|
+
const PNG_ATTACHMENT = {
|
|
25
|
+
id: ATT_ID, targetType: 'ticket', targetId: 't1',
|
|
26
|
+
filename: 'screenshot.png', contentType: 'image/png', sizeBytes: 4096,
|
|
27
|
+
uploadedBy: 'u1', uploadedAt: '2026-07-09T13:30:00.000Z', downloadUrl: `/attachments/${ATT_ID}`,
|
|
28
|
+
};
|
|
29
|
+
function stubFetch(responses) {
|
|
30
|
+
const calls = [];
|
|
31
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, init) => {
|
|
32
|
+
calls.push({ url: url.toString(), method: init?.method ?? 'GET' });
|
|
33
|
+
const r = responses.shift();
|
|
34
|
+
if (!r)
|
|
35
|
+
throw new Error('unexpected extra fetch');
|
|
36
|
+
return {
|
|
37
|
+
ok: r.ok ?? true,
|
|
38
|
+
status: r.status ?? 200,
|
|
39
|
+
statusText: 'OK',
|
|
40
|
+
json: async () => ('json' in r ? r.json : {}),
|
|
41
|
+
text: async () => '',
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
return calls;
|
|
45
|
+
}
|
|
46
|
+
const TINY_PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAFhAJ/wlseKgAAAABJRU5ErkJggg==';
|
|
47
|
+
describe('orboto_list_ticket_attachments', () => {
|
|
48
|
+
it('resolves the ticket and lists its attachments with KB + id + URL', async () => {
|
|
49
|
+
const calls = stubFetch([
|
|
50
|
+
{ json: PROJ }, // resolveTicketByKey: project
|
|
51
|
+
{ json: TICKET }, // resolveTicketByKey: ticket
|
|
52
|
+
{ json: [PNG_ATTACHMENT] }, // GET /tickets/:id/attachments
|
|
53
|
+
]);
|
|
54
|
+
const res = await makeListTicketAttachmentsHandler(client)({ ticketKey: 'ACME-1' });
|
|
55
|
+
expect(calls[2]).toMatchObject({ method: 'GET', url: 'https://orboto.example.com/tickets/t1/attachments' });
|
|
56
|
+
const text = res.content[0].text;
|
|
57
|
+
expect(text).toContain('screenshot.png');
|
|
58
|
+
expect(text).toContain('4 KB');
|
|
59
|
+
expect(text).toContain(ATT_ID);
|
|
60
|
+
expect(res.structuredContent).toMatchObject({
|
|
61
|
+
ticketKey: 'ACME-1',
|
|
62
|
+
attachments: [{ id: ATT_ID, filename: 'screenshot.png', contentType: 'image/png', sizeBytes: 4096 }],
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
it('reports the empty-list case explicitly', async () => {
|
|
66
|
+
stubFetch([{ json: PROJ }, { json: TICKET }, { json: [] }]);
|
|
67
|
+
const res = await makeListTicketAttachmentsHandler(client)({ ticketKey: 'ACME-1' });
|
|
68
|
+
expect(res.content[0].text).toContain('No attachments');
|
|
69
|
+
expect(res.structuredContent).toMatchObject({ attachments: [] });
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe('orboto_get_attachment', () => {
|
|
73
|
+
it('returns an image content block for a png', async () => {
|
|
74
|
+
const calls = stubFetch([{
|
|
75
|
+
json: { id: ATT_ID, filename: 'screenshot.png', contentType: 'image/png', sizeBytes: 4096, contentBase64: TINY_PNG_BASE64 },
|
|
76
|
+
}]);
|
|
77
|
+
const res = await makeGetAttachmentHandler(client)({ attachmentId: ATT_ID });
|
|
78
|
+
expect(calls[0]).toMatchObject({ method: 'GET', url: `https://orboto.example.com/attachments/${ATT_ID}/base64` });
|
|
79
|
+
const img = res.content.find((c) => c.type === 'image');
|
|
80
|
+
expect(img).toBeTruthy();
|
|
81
|
+
expect(img.data).toBe(TINY_PNG_BASE64);
|
|
82
|
+
expect(img.mimeType).toBe('image/png');
|
|
83
|
+
expect(res.structuredContent).toMatchObject({ id: ATT_ID, inlined: true });
|
|
84
|
+
});
|
|
85
|
+
it('returns a text block (no image) for a pdf', async () => {
|
|
86
|
+
stubFetch([{
|
|
87
|
+
json: { id: ATT_ID, filename: 'spec.pdf', contentType: 'application/pdf', sizeBytes: 2048, contentBase64: TINY_PNG_BASE64 },
|
|
88
|
+
}]);
|
|
89
|
+
const res = await makeGetAttachmentHandler(client)({ attachmentId: ATT_ID });
|
|
90
|
+
expect(res.content.find((c) => c.type === 'image')).toBeUndefined();
|
|
91
|
+
const text = res.content[0].text;
|
|
92
|
+
expect(text).toContain('spec.pdf');
|
|
93
|
+
expect(text).toContain('application/pdf');
|
|
94
|
+
expect(res.structuredContent).toMatchObject({ inlined: true, contentType: 'application/pdf' });
|
|
95
|
+
});
|
|
96
|
+
it('refuses an over-limit file inline and points at the skill download', async () => {
|
|
97
|
+
stubFetch([{
|
|
98
|
+
json: { id: ATT_ID, filename: 'huge.png', contentType: 'image/png', sizeBytes: 8 * 1024 * 1024, contentBase64: TINY_PNG_BASE64 },
|
|
99
|
+
}]);
|
|
100
|
+
const res = await makeGetAttachmentHandler(client)({ attachmentId: ATT_ID });
|
|
101
|
+
expect(res.content.find((c) => c.type === 'image')).toBeUndefined();
|
|
102
|
+
const text = res.content[0].text;
|
|
103
|
+
expect(text).toContain('download-attachment');
|
|
104
|
+
expect(res.structuredContent).toMatchObject({ inlined: false });
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-626 - ticket meeting-schedule tools.
|
|
3
|
+
*
|
|
4
|
+
* Wrap the /tickets/:id/schedules endpoints:
|
|
5
|
+
* - orboto_list_ticket_schedules - list a ticket's working sessions.
|
|
6
|
+
* - orboto_schedule_ticket_session - schedule a working session (drops a
|
|
7
|
+
* plan-block on every orboto attendee + sends an iCal invite).
|
|
8
|
+
* - orboto_cancel_ticket_session - cancel a scheduled session (removes
|
|
9
|
+
* the plan-blocks + sends an iCal cancellation).
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { resolveTicketByKey } from './shared.js';
|
|
13
|
+
function scheduleLine(s) {
|
|
14
|
+
const when = `${s.startsAt} -> ${s.endsAt}`;
|
|
15
|
+
const people = s.attendees.length + s.externalAttendees.length;
|
|
16
|
+
const state = s.cancelledAt ? ' [CANCELLED]' : '';
|
|
17
|
+
return `- ${s.title}${state} - ${when} - ${people} attendee(s) (id ${s.id})`;
|
|
18
|
+
}
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// orboto_list_ticket_schedules
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
export const listTicketSchedulesToolConfig = {
|
|
23
|
+
title: 'List a ticket\'s scheduled working sessions',
|
|
24
|
+
description: 'List the meeting / working sessions scheduled on a ticket (by key). Active sessions only by default; pass includeCancelled=true for history. Returns each session\'s id, title, window and attendee count - get the id here before cancelling one with orboto_cancel_ticket_session.',
|
|
25
|
+
inputSchema: z.object({
|
|
26
|
+
ticketKey: z.string().min(3),
|
|
27
|
+
includeCancelled: z.boolean().optional().describe('Include cancelled sessions (default: active only).'),
|
|
28
|
+
}).shape,
|
|
29
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
30
|
+
};
|
|
31
|
+
export function makeListTicketSchedulesHandler(client) {
|
|
32
|
+
return async ({ ticketKey, includeCancelled }) => {
|
|
33
|
+
const ticket = await resolveTicketByKey(client, ticketKey);
|
|
34
|
+
const qs = includeCancelled ? '?includeCancelled=true' : '';
|
|
35
|
+
const rows = await client.get(`/tickets/${ticket.id}/schedules${qs}`);
|
|
36
|
+
return {
|
|
37
|
+
content: [{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: rows.length
|
|
40
|
+
? `Sessions on [${ticket.ticketKey}]:\n${rows.map(scheduleLine).join('\n')}`
|
|
41
|
+
: `No scheduled sessions on [${ticket.ticketKey}].`,
|
|
42
|
+
}],
|
|
43
|
+
structuredContent: { ticketKey: ticket.ticketKey, schedules: rows },
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// orboto_schedule_ticket_session
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
export const scheduleTicketSessionToolConfig = {
|
|
51
|
+
title: 'Schedule a working session on a ticket',
|
|
52
|
+
description: 'Schedule a working session ("scope review", "design crit") on a ticket. Drops a plan-block on every orboto attendee and emails each attendee an iCal invite. startsAt/endsAt are ISO 8601 datetimes. attendeeUserIds are orboto user UUIDs (from orboto_list_users); externalEmails are non-orboto guests who get the invite but no plan-block. Title defaults to "Working session: <TICKET-KEY>".',
|
|
53
|
+
inputSchema: z.object({
|
|
54
|
+
ticketKey: z.string().min(3),
|
|
55
|
+
startsAt: z.string().datetime().describe('ISO 8601 start datetime.'),
|
|
56
|
+
endsAt: z.string().datetime().describe('ISO 8601 end datetime (must be after startsAt).'),
|
|
57
|
+
title: z.string().optional(),
|
|
58
|
+
location: z.string().optional().describe('Free text - Zoom URL, room name, "remote".'),
|
|
59
|
+
notes: z.string().optional(),
|
|
60
|
+
attendeeUserIds: z.array(z.string().uuid()).optional().describe('orboto attendee user UUIDs.'),
|
|
61
|
+
externalEmails: z.array(z.string().email()).optional().describe('External (non-orboto) guest emails.'),
|
|
62
|
+
}).shape,
|
|
63
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
64
|
+
};
|
|
65
|
+
export function makeScheduleTicketSessionHandler(client) {
|
|
66
|
+
return async (args) => {
|
|
67
|
+
const ticket = await resolveTicketByKey(client, args.ticketKey);
|
|
68
|
+
const body = {
|
|
69
|
+
startsAt: args.startsAt,
|
|
70
|
+
endsAt: args.endsAt,
|
|
71
|
+
attendeeUserIds: args.attendeeUserIds ?? [],
|
|
72
|
+
externalEmails: args.externalEmails ?? [],
|
|
73
|
+
};
|
|
74
|
+
if (args.title !== undefined)
|
|
75
|
+
body.title = args.title;
|
|
76
|
+
if (args.location !== undefined)
|
|
77
|
+
body.location = args.location;
|
|
78
|
+
if (args.notes !== undefined)
|
|
79
|
+
body.notes = args.notes;
|
|
80
|
+
const row = await client.post(`/tickets/${ticket.id}/schedules`, body);
|
|
81
|
+
const people = row.attendees.length + row.externalAttendees.length;
|
|
82
|
+
return {
|
|
83
|
+
content: [{
|
|
84
|
+
type: 'text',
|
|
85
|
+
text: `Scheduled "${row.title}" on [${ticket.ticketKey}] (${row.startsAt} -> ${row.endsAt}) - ${people} attendee(s) invited. (id ${row.id})`,
|
|
86
|
+
}],
|
|
87
|
+
structuredContent: { ticketKey: ticket.ticketKey, scheduleId: row.id, schedule: row },
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// orboto_cancel_ticket_session
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
export const cancelTicketSessionToolConfig = {
|
|
95
|
+
title: 'Cancel a scheduled working session',
|
|
96
|
+
description: 'Cancel a working session on a ticket. Removes every attendee\'s plan-block and emails an iCal cancellation to all attendees. Get the scheduleId from orboto_list_ticket_schedules.',
|
|
97
|
+
inputSchema: z.object({
|
|
98
|
+
ticketKey: z.string().min(3),
|
|
99
|
+
scheduleId: z.string().uuid(),
|
|
100
|
+
}).shape,
|
|
101
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
102
|
+
};
|
|
103
|
+
export function makeCancelTicketSessionHandler(client) {
|
|
104
|
+
return async ({ ticketKey, scheduleId }) => {
|
|
105
|
+
const ticket = await resolveTicketByKey(client, ticketKey);
|
|
106
|
+
const row = await client.delete(`/tickets/${ticket.id}/schedules/${scheduleId}`);
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: 'text', text: `Cancelled session "${row.title}" on [${ticket.ticketKey}]. Attendees notified.` }],
|
|
109
|
+
structuredContent: { ticketKey: ticket.ticketKey, scheduleId, cancelled: true },
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-626 - ticket meeting-schedule tool tests.
|
|
3
|
+
*/
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { OrbotoClient } from '../orboto-client.js';
|
|
6
|
+
import { makeListTicketSchedulesHandler, makeScheduleTicketSessionHandler, makeCancelTicketSessionHandler, } from './ticket-schedules.js';
|
|
7
|
+
beforeEach(() => { vi.restoreAllMocks(); });
|
|
8
|
+
afterEach(() => { vi.restoreAllMocks(); });
|
|
9
|
+
function stub(responses) {
|
|
10
|
+
const calls = [];
|
|
11
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, init) => {
|
|
12
|
+
const u = url.toString();
|
|
13
|
+
const m = init?.method ?? 'GET';
|
|
14
|
+
const b = init?.body ? JSON.parse(init.body) : undefined;
|
|
15
|
+
calls.push({ url: u, method: m, body: b });
|
|
16
|
+
const r = responses.shift();
|
|
17
|
+
if (!r)
|
|
18
|
+
throw new Error(`unexpected extra fetch ${m} ${u}`);
|
|
19
|
+
const payload = 'json' in r ? r.json : {};
|
|
20
|
+
return {
|
|
21
|
+
ok: r.ok ?? true,
|
|
22
|
+
status: r.status ?? 200,
|
|
23
|
+
statusText: 'OK',
|
|
24
|
+
json: async () => payload,
|
|
25
|
+
// delete<T> reads text() then JSON.parses it - serialise the payload.
|
|
26
|
+
text: async () => JSON.stringify(payload),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
return calls;
|
|
30
|
+
}
|
|
31
|
+
const client = new OrbotoClient({ baseUrl: 'https://orboto.example.com', apiKey: 'orb_x' });
|
|
32
|
+
const PROJ = { id: 'p1', key: 'ACME', name: 'Acme', description: null, status: 'active' };
|
|
33
|
+
const TICKET = { id: 't1', projectId: 'p1', ticketKey: 'ACME-1', title: 'Bug' };
|
|
34
|
+
function schedule(overrides = {}) {
|
|
35
|
+
return {
|
|
36
|
+
id: 's1', ticketId: 't1', createdBy: 'u1', title: 'Working session: ACME-1',
|
|
37
|
+
startsAt: '2026-07-20T10:00:00Z', endsAt: '2026-07-20T11:00:00Z',
|
|
38
|
+
location: null, notes: null, attendees: [{ userId: 'u2', email: 'a@x.io' }],
|
|
39
|
+
externalAttendees: [{ email: 'ext@client.com' }], sequence: 0,
|
|
40
|
+
cancelledAt: null, createdAt: 'now', updatedAt: 'now', ...overrides,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
describe('orboto_list_ticket_schedules', () => {
|
|
44
|
+
it('lists active sessions by default', async () => {
|
|
45
|
+
const calls = stub([{ json: PROJ }, { json: TICKET }, { json: [schedule()] }]);
|
|
46
|
+
const res = await makeListTicketSchedulesHandler(client)({ ticketKey: 'ACME-1' });
|
|
47
|
+
expect(calls[2].method).toBe('GET');
|
|
48
|
+
expect(calls[2].url).toContain('/tickets/t1/schedules');
|
|
49
|
+
expect(calls[2].url).not.toContain('includeCancelled');
|
|
50
|
+
expect(res.structuredContent.schedules).toHaveLength(1);
|
|
51
|
+
});
|
|
52
|
+
it('passes includeCancelled=true when requested', async () => {
|
|
53
|
+
const calls = stub([{ json: PROJ }, { json: TICKET }, { json: [] }]);
|
|
54
|
+
await makeListTicketSchedulesHandler(client)({ ticketKey: 'ACME-1', includeCancelled: true });
|
|
55
|
+
expect(calls[2].url).toContain('includeCancelled=true');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('orboto_schedule_ticket_session', () => {
|
|
59
|
+
it('POSTs the session with attendees + external emails', async () => {
|
|
60
|
+
const calls = stub([{ json: PROJ }, { json: TICKET }, { json: schedule() }]);
|
|
61
|
+
const res = await makeScheduleTicketSessionHandler(client)({
|
|
62
|
+
ticketKey: 'ACME-1',
|
|
63
|
+
startsAt: '2026-07-20T10:00:00Z',
|
|
64
|
+
endsAt: '2026-07-20T11:00:00Z',
|
|
65
|
+
attendeeUserIds: ['u2'],
|
|
66
|
+
externalEmails: ['ext@client.com'],
|
|
67
|
+
});
|
|
68
|
+
expect(calls[2].method).toBe('POST');
|
|
69
|
+
expect(calls[2].url).toContain('/tickets/t1/schedules');
|
|
70
|
+
expect(calls[2].body).toEqual({
|
|
71
|
+
startsAt: '2026-07-20T10:00:00Z',
|
|
72
|
+
endsAt: '2026-07-20T11:00:00Z',
|
|
73
|
+
attendeeUserIds: ['u2'],
|
|
74
|
+
externalEmails: ['ext@client.com'],
|
|
75
|
+
});
|
|
76
|
+
expect(res.structuredContent.scheduleId).toBe('s1');
|
|
77
|
+
});
|
|
78
|
+
it('defaults attendee arrays to empty when omitted', async () => {
|
|
79
|
+
const calls = stub([{ json: PROJ }, { json: TICKET }, { json: schedule({ attendees: [], externalAttendees: [] }) }]);
|
|
80
|
+
await makeScheduleTicketSessionHandler(client)({
|
|
81
|
+
ticketKey: 'ACME-1',
|
|
82
|
+
startsAt: '2026-07-20T10:00:00Z',
|
|
83
|
+
endsAt: '2026-07-20T11:00:00Z',
|
|
84
|
+
});
|
|
85
|
+
expect(calls[2].body).toEqual({
|
|
86
|
+
startsAt: '2026-07-20T10:00:00Z',
|
|
87
|
+
endsAt: '2026-07-20T11:00:00Z',
|
|
88
|
+
attendeeUserIds: [],
|
|
89
|
+
externalEmails: [],
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe('orboto_cancel_ticket_session', () => {
|
|
94
|
+
it('DELETEs the session by id and reports the cancelled title', async () => {
|
|
95
|
+
const calls = stub([{ json: PROJ }, { json: TICKET }, { json: schedule({ cancelledAt: 'now' }) }]);
|
|
96
|
+
const res = await makeCancelTicketSessionHandler(client)({ ticketKey: 'ACME-1', scheduleId: 's1' });
|
|
97
|
+
expect(calls[2].method).toBe('DELETE');
|
|
98
|
+
expect(calls[2].url).toContain('/tickets/t1/schedules/s1');
|
|
99
|
+
expect(res.structuredContent.cancelled).toBe(true);
|
|
100
|
+
expect(res.content[0].text).toContain('Working session: ACME-1');
|
|
101
|
+
});
|
|
102
|
+
});
|