Files
TREK/server/tests/unit/mcp/tools-assignments.test.ts
T
2026-10-06 21:50:59 +02:00

743 lines
29 KiB
TypeScript

/**
* Unit tests for MCP assignment tools: assign_place_to_day, unassign_place,
* reorder_day_assignments, update_assignment_time, update_assignment_notes.
*/
import { db as testDb } from '../../../src/db/database';
import {
createUser,
createTrip,
createDay,
createPlace,
createDayAssignment,
createJourney,
} from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
import { resetTestDb } from '../../helpers/test-db';
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
vi.mock('../../../src/db/database', async () => {
const { createSnapshotTestDb, buildDbMock } = await import('../../helpers/db-mock');
return buildDbMock(createSnapshotTestDb());
});
vi.mock('../../../src/config', () => ({
JWT_SECRET: 'test-jwt-secret-for-trek-testing-only',
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
updateJwtSecret: () => {},
}));
const { broadcastMock } = vi.hoisted(() => ({ broadcastMock: vi.fn() }));
vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
/** Link a journey to a trip so reconcileTripSkeletons has a target. */
function linkJourney(journeyId: number, tripId: number) {
testDb
.prepare('INSERT INTO journey_trips (journey_id, trip_id, added_at) VALUES (?, ?, ?)')
.run(journeyId, tripId, Date.now());
}
function skeletonFor(journeyId: number, placeId: number) {
return testDb
.prepare('SELECT * FROM journey_entries WHERE journey_id = ? AND source_place_id = ?')
.get(journeyId, placeId) as any;
}
beforeEach(() => {
resetTestDb(testDb);
broadcastMock.mockClear();
delete process.env.DEMO_MODE;
});
afterAll(() => {
testDb.close();
});
async function withHarness(userId: number, fn: (h: McpHarness) => Promise<void>) {
const h = await createMcpHarness({ userId, withResources: false });
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
// assign_place_to_day
// ---------------------------------------------------------------------------
describe('Tool: assign_place_to_day', () => {
it('sets and clears a visit day end through the shared service', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const visit = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
for (const end_day of [true, false]) {
const changed = await h.client.callTool({
name: 'set_assignment_end_day',
arguments: { tripId: trip.id, assignmentId: visit.id, end_day },
});
expect(parseToolResult(changed)).toMatchObject({ assignment: { end_day } });
}
const other = createTrip(testDb, user.id);
const refused = await h.client.callTool({
name: 'set_assignment_end_day',
arguments: { tripId: other.id, assignmentId: visit.id, end_day: true },
});
expect(refused.isError).toBe(true);
});
});
it('assigns a place to a day', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'assign_place_to_day',
arguments: { tripId: trip.id, dayId: day.id, placeId: place.id },
});
const data = parseToolResult(result) as any;
expect(data.assignment).toBeTruthy();
expect(data.assignment.day_id).toBe(day.id);
expect(data.assignment.place_id).toBe(place.id);
expect(data.assignment.order_index).toBe(0);
});
});
it('creates a skeleton suggestion in a linked journey', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id, { name: 'Linked Place' });
const journey = createJourney(testDb, user.id);
linkJourney(journey.id, trip.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({
name: 'assign_place_to_day',
arguments: { tripId: trip.id, dayId: day.id, placeId: place.id },
});
const skeleton = skeletonFor(journey.id, place.id);
expect(skeleton).toBeDefined();
expect(skeleton.type).toBe('skeleton');
expect(skeleton.title).toBe('Linked Place');
});
});
it('auto-increments order_index for subsequent assignments', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place1 = createPlace(testDb, trip.id, { name: 'P1' });
const place2 = createPlace(testDb, trip.id, { name: 'P2' });
createDayAssignment(testDb, day.id, place1.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'assign_place_to_day',
arguments: { tripId: trip.id, dayId: day.id, placeId: place2.id },
});
const data = parseToolResult(result) as any;
expect(data.assignment.order_index).toBe(1);
});
});
it('broadcasts assignment:created event', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({
name: 'assign_place_to_day',
arguments: { tripId: trip.id, dayId: day.id, placeId: place.id },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'assignment:created', expect.any(Object));
});
});
it('returns error when day does not belong to trip', async () => {
const { user } = createUser(testDb);
const trip1 = createTrip(testDb, user.id);
const trip2 = createTrip(testDb, user.id);
const dayFromTrip2 = createDay(testDb, trip2.id);
const place = createPlace(testDb, trip1.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'assign_place_to_day',
arguments: { tripId: trip1.id, dayId: dayFromTrip2.id, placeId: place.id },
});
expect(result.isError).toBe(true);
});
});
it('returns error when place does not belong to trip', async () => {
const { user } = createUser(testDb);
const trip1 = createTrip(testDb, user.id);
const trip2 = createTrip(testDb, user.id);
const day = createDay(testDb, trip1.id);
const placeFromTrip2 = createPlace(testDb, trip2.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'assign_place_to_day',
arguments: { tripId: trip1.id, dayId: day.id, placeId: placeFromTrip2.id },
});
expect(result.isError).toBe(true);
});
});
it('returns access denied for non-member', async () => {
const { user } = createUser(testDb);
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'assign_place_to_day',
arguments: { tripId: trip.id, dayId: day.id, placeId: place.id },
});
expect(result.isError).toBe(true);
});
});
});
// ---------------------------------------------------------------------------
// unassign_place
// ---------------------------------------------------------------------------
describe('Tool: unassign_place', () => {
it('removes a place assignment from a day', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'unassign_place',
arguments: { tripId: trip.id, dayId: day.id, assignmentId: assignment.id },
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
expect(testDb.prepare('SELECT id FROM day_assignments WHERE id = ?').get(assignment.id)).toBeUndefined();
});
});
it('broadcasts assignment:deleted event', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({
name: 'unassign_place',
arguments: { tripId: trip.id, dayId: day.id, assignmentId: assignment.id },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'assignment:deleted', expect.any(Object));
});
});
it('removes the linked journey skeleton when the place is unassigned', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const journey = createJourney(testDb, user.id);
linkJourney(journey.id, trip.id);
await withHarness(user.id, async (h) => {
// Assign via MCP (materialises the skeleton), then unassign that same assignment.
const assigned = parseToolResult(
await h.client.callTool({
name: 'assign_place_to_day',
arguments: { tripId: trip.id, dayId: day.id, placeId: place.id },
}),
) as any;
expect(skeletonFor(journey.id, place.id)).toBeDefined();
await h.client.callTool({
name: 'unassign_place',
arguments: { tripId: trip.id, dayId: day.id, assignmentId: assigned.assignment.id },
});
expect(skeletonFor(journey.id, place.id)).toBeUndefined();
});
});
it('returns error when assignment is not found', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'unassign_place',
arguments: { tripId: trip.id, dayId: day.id, assignmentId: 99999 },
});
expect(result.isError).toBe(true);
});
});
it('returns access denied for non-member', async () => {
const { user } = createUser(testDb);
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'unassign_place',
arguments: { tripId: trip.id, dayId: day.id, assignmentId: assignment.id },
});
expect(result.isError).toBe(true);
});
});
});
// ---------------------------------------------------------------------------
// reorder_day_assignments
// ---------------------------------------------------------------------------
describe('Tool: clear_day_assignments (#2470)', () => {
it('removes every place from the day and broadcasts each removal', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const a1 = createDayAssignment(testDb, day.id, place.id);
const a2 = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
const data = parseToolResult(
await h.client.callTool({ name: 'clear_day_assignments', arguments: { tripId: trip.id, dayId: day.id } }),
) as any;
expect(data.success).toBe(true);
expect([...data.removedIds].sort()).toEqual([a1.id, a2.id].sort());
expect(testDb.prepare('SELECT COUNT(*) AS n FROM day_assignments WHERE day_id = ?').get(day.id)).toEqual({
n: 0,
});
expect(broadcastMock).toHaveBeenCalledWith(
trip.id,
'assignment:deleted',
expect.objectContaining({ assignmentId: a1.id, dayId: day.id }),
);
});
});
it('returns an error for a day on another trip', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const otherTrip = createTrip(testDb, user.id);
const foreignDay = createDay(testDb, otherTrip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'clear_day_assignments',
arguments: { tripId: trip.id, dayId: foreignDay.id },
});
expect(result.isError).toBe(true);
});
});
it('returns access denied for non-member', async () => {
const { user } = createUser(testDb);
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
const day = createDay(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'clear_day_assignments',
arguments: { tripId: trip.id, dayId: day.id },
});
expect(result.isError).toBe(true);
});
});
});
describe('Tool: reorder_day_assignments', () => {
it('reorders assignments by updating order_index', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place1 = createPlace(testDb, trip.id, { name: 'First' });
const place2 = createPlace(testDb, trip.id, { name: 'Second' });
const a1 = createDayAssignment(testDb, day.id, place1.id, { order_index: 0 });
const a2 = createDayAssignment(testDb, day.id, place2.id, { order_index: 1 });
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'reorder_day_assignments',
arguments: { tripId: trip.id, dayId: day.id, assignmentIds: [a2.id, a1.id] },
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
const a1Updated = testDb.prepare('SELECT order_index FROM day_assignments WHERE id = ?').get(a1.id) as {
order_index: number;
};
const a2Updated = testDb.prepare('SELECT order_index FROM day_assignments WHERE id = ?').get(a2.id) as {
order_index: number;
};
expect(a2Updated.order_index).toBe(0);
expect(a1Updated.order_index).toBe(1);
});
});
it('broadcasts assignment:reordered event', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const a = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({
name: 'reorder_day_assignments',
arguments: { tripId: trip.id, dayId: day.id, assignmentIds: [a.id] },
});
expect(broadcastMock).toHaveBeenCalledWith(
trip.id,
'assignment:reordered',
expect.objectContaining({ dayId: day.id, orderedIds: [a.id] }),
);
});
});
it('returns error when day does not belong to trip', async () => {
const { user } = createUser(testDb);
const trip1 = createTrip(testDb, user.id);
const trip2 = createTrip(testDb, user.id);
const day = createDay(testDb, trip2.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'reorder_day_assignments',
arguments: { tripId: trip1.id, dayId: day.id, assignmentIds: [1] },
});
expect(result.isError).toBe(true);
});
});
it('returns access denied for non-member', async () => {
const { user } = createUser(testDb);
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
const day = createDay(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'reorder_day_assignments',
arguments: { tripId: trip.id, dayId: day.id, assignmentIds: [1] },
});
expect(result.isError).toBe(true);
});
});
});
// ---------------------------------------------------------------------------
// update_assignment_time
// ---------------------------------------------------------------------------
describe('Tool: update_assignment_time', () => {
it('sets start and end times for an assignment', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_assignment_time',
arguments: { tripId: trip.id, assignmentId: assignment.id, place_time: '09:00', end_time: '11:30' },
});
const data = parseToolResult(result) as any;
expect(data.assignment.assignment_time).toBe('09:00');
expect(data.assignment.assignment_end_time).toBe('11:30');
});
});
it('clears times with null', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
testDb
.prepare('UPDATE day_assignments SET assignment_time = ?, assignment_end_time = ? WHERE id = ?')
.run('09:00', '11:00', assignment.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_assignment_time',
arguments: { tripId: trip.id, assignmentId: assignment.id, place_time: null, end_time: null },
});
const data = parseToolResult(result) as any;
expect(data.assignment.assignment_time).toBeNull();
expect(data.assignment.assignment_end_time).toBeNull();
});
});
it('broadcasts assignment:updated event', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({
name: 'update_assignment_time',
arguments: { tripId: trip.id, assignmentId: assignment.id, place_time: '10:00' },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'assignment:updated', expect.any(Object));
});
});
// Same service method as PUT /assignments/:id/time, so the same order and the same
// three events. A: untimed, B: 15:00, C: untimed, D gets 10:00.
it('keeps untimed stops in place, sorts the timed ones and sends the day and its vias like REST', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const [a, b, c, d] = [0, 1, 2, 3].map((i) => createDayAssignment(testDb, day.id, place.id, { order_index: i }).id);
testDb.prepare('UPDATE day_assignments SET assignment_time = ? WHERE id = ?').run('15:00', b);
const via = Number(
testDb
.prepare(
'INSERT INTO roadtrip_vias (day_id, after_order_index, sequence, lat, lng) VALUES (?, 1, 0, 48.1, 11.5)',
)
.run(day.id).lastInsertRowid,
);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_assignment_time',
arguments: { tripId: trip.id, assignmentId: d, place_time: '10:00' },
});
expect((parseToolResult(result) as { assignment: { assignment_time: string } }).assignment.assignment_time).toBe(
'10:00',
);
});
const order = (
testDb.prepare('SELECT id FROM day_assignments WHERE day_id = ? ORDER BY order_index').all(day.id) as {
id: number;
}[]
).map((r) => r.id);
expect(order).toEqual([a, d, b, c]);
expect(testDb.prepare('SELECT after_order_index FROM roadtrip_vias WHERE id = ?').get(via)).toEqual({
after_order_index: 2,
});
expect(broadcastMock).toHaveBeenCalledWith(
trip.id,
'assignment:reordered',
expect.objectContaining({ dayId: day.id, orderedIds: [a, d, b, c] }),
);
expect(broadcastMock).toHaveBeenCalledWith(
trip.id,
'roadtripVia:changed',
expect.objectContaining({
dayId: day.id,
vias: [expect.objectContaining({ id: via, after_order_index: 2 })],
}),
);
});
it('sends only the row when the start leaves the day as it was', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const [a, b, c] = [0, 1, 2].map((i) => createDayAssignment(testDb, day.id, place.id, { order_index: i }).id);
await withHarness(user.id, async (h) => {
await h.client.callTool({
name: 'update_assignment_time',
arguments: { tripId: trip.id, assignmentId: c, place_time: '14:00' },
});
});
const order = (
testDb.prepare('SELECT id FROM day_assignments WHERE day_id = ? ORDER BY order_index').all(day.id) as {
id: number;
}[]
).map((r) => r.id);
expect(order).toEqual([a, b, c]);
expect(broadcastMock.mock.calls.map((call) => call[1])).toEqual(['assignment:updated']);
});
it('leaves a day out of time order as it is when the call names only the end', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
// B starts before A and was put behind it on purpose.
const [a, b] = [0, 1].map((i) => createDayAssignment(testDb, day.id, place.id, { order_index: i }).id);
testDb.prepare("UPDATE day_assignments SET assignment_time = '14:00' WHERE id = ?").run(a);
testDb.prepare("UPDATE day_assignments SET assignment_time = '10:00' WHERE id = ?").run(b);
const via = Number(
testDb
.prepare(
'INSERT INTO roadtrip_vias (day_id, after_order_index, sequence, lat, lng) VALUES (?, 0, 0, 48.1, 11.5)',
)
.run(day.id).lastInsertRowid,
);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_assignment_time',
arguments: { tripId: trip.id, assignmentId: b, end_time: '11:30' },
});
expect(
(parseToolResult(result) as { assignment: { assignment_time: string; assignment_end_time: string } })
.assignment,
).toMatchObject({ assignment_time: '10:00', assignment_end_time: '11:30' });
});
const order = (
testDb.prepare('SELECT id FROM day_assignments WHERE day_id = ? ORDER BY order_index').all(day.id) as {
id: number;
}[]
).map((r) => r.id);
expect(order).toEqual([a, b]);
expect(testDb.prepare('SELECT after_order_index FROM roadtrip_vias WHERE id = ?').get(via)).toEqual({
after_order_index: 0,
});
expect(broadcastMock.mock.calls.map((call) => call[1])).toEqual(['assignment:updated']);
});
it('returns error when assignment not found', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_assignment_time',
arguments: { tripId: trip.id, assignmentId: 99999, place_time: '09:00' },
});
expect(result.isError).toBe(true);
});
});
it('returns access denied for non-member', async () => {
const { user } = createUser(testDb);
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_assignment_time',
arguments: { tripId: trip.id, assignmentId: assignment.id, place_time: '09:00' },
});
expect(result.isError).toBe(true);
});
});
});
// ---------------------------------------------------------------------------
// update_assignment_notes (#2163)
// ---------------------------------------------------------------------------
describe('Tool: update_assignment_notes', () => {
it('sets the day-specific note on an assignment', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_assignment_notes',
arguments: {
tripId: trip.id,
assignmentId: assignment.id,
notes: 'Book the 10:00 timed entry and arrive 15 minutes early',
},
});
const data = parseToolResult(result) as any;
expect(data.assignment.notes).toBe('Book the 10:00 timed entry and arrive 15 minutes early');
expect(testDb.prepare('SELECT notes FROM day_assignments WHERE id = ?').get(assignment.id)).toEqual({
notes: 'Book the 10:00 timed entry and arrive 15 minutes early',
});
});
});
it('clears the note with null and with an empty string', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
testDb.prepare('UPDATE day_assignments SET notes = ? WHERE id = ?').run('old', assignment.id);
await withHarness(user.id, async (h) => {
const cleared = parseToolResult(
await h.client.callTool({
name: 'update_assignment_notes',
arguments: { tripId: trip.id, assignmentId: assignment.id, notes: null },
}),
) as any;
expect(cleared.assignment.notes).toBeNull();
testDb.prepare('UPDATE day_assignments SET notes = ? WHERE id = ?').run('old', assignment.id);
const emptied = parseToolResult(
await h.client.callTool({
name: 'update_assignment_notes',
arguments: { tripId: trip.id, assignmentId: assignment.id, notes: '' },
}),
) as any;
expect(emptied.assignment.notes).toBeNull();
});
});
it('broadcasts assignment:updated event', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({
name: 'update_assignment_notes',
arguments: { tripId: trip.id, assignmentId: assignment.id, notes: 'n' },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'assignment:updated', expect.any(Object));
});
});
it('returns error when assignment not found', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_assignment_notes',
arguments: { tripId: trip.id, assignmentId: 99999, notes: 'n' },
});
expect(result.isError).toBe(true);
});
});
it('returns access denied for non-member', async () => {
const { user } = createUser(testDb);
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
const day = createDay(testDb, trip.id);
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_assignment_notes',
arguments: { tripId: trip.id, assignmentId: assignment.id, notes: 'n' },
});
expect(result.isError).toBe(true);
expect(testDb.prepare('SELECT notes FROM day_assignments WHERE id = ?').get(assignment.id)).toEqual({
notes: null,
});
});
});
});