chore: apply prettier on the entire project

This commit is contained in:
jubnl
2026-05-25 21:59:42 +02:00
parent c130ed41be
commit 6bcdfbc34b
488 changed files with 82986 additions and 45830 deletions
+43 -12
View File
@@ -2,6 +2,27 @@
* Unit tests for MCP resources (resources.ts).
* Tests all 14 resources via InMemoryTransport + Client.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import {
createUser,
createTrip,
createDay,
createPlace,
addTripMember,
createBudgetItem,
createPackingItem,
createReservation,
createDayNote,
createCollabNote,
createBucketListItem,
createVisitedCountry,
createDayAssignment,
createDayAccommodation,
} from '../../helpers/factories';
import { createMcpHarness, parseResourceResult, type McpHarness } from '../../helpers/mcp-harness';
import { resetTestDb } from '../../helpers/test-db';
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll, afterEach } from 'vitest';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -15,13 +36,29 @@ const { testDb, dbMock } = vi.hoisted(() => {
closeDb: () => {},
reinitialize: () => {},
getPlaceWithTags: (placeId: number) => {
const place: any = db.prepare(`SELECT p.*, c.name as category_name, c.color as category_color, c.icon as category_icon FROM places p LEFT JOIN categories c ON p.category_id = c.id WHERE p.id = ?`).get(placeId);
const place: any = db
.prepare(
`SELECT p.*, c.name as category_name, c.color as category_color, c.icon as category_icon FROM places p LEFT JOIN categories c ON p.category_id = c.id WHERE p.id = ?`,
)
.get(placeId);
if (!place) return null;
const tags = db.prepare(`SELECT t.* FROM tags t JOIN place_tags pt ON t.id = pt.tag_id WHERE pt.place_id = ?`).all(placeId);
return { ...place, category: place.category_id ? { id: place.category_id, name: place.category_name, color: place.category_color, icon: place.category_icon } : null, tags };
const tags = db
.prepare(`SELECT t.* FROM tags t JOIN place_tags pt ON t.id = pt.tag_id WHERE pt.place_id = ?`)
.all(placeId);
return {
...place,
category: place.category_id
? { id: place.category_id, name: place.category_name, color: place.category_color, icon: place.category_icon }
: null,
tags,
};
},
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -36,12 +73,6 @@ vi.mock('../../../src/config', () => ({
}));
vi.mock('../../../src/websocket', () => ({ broadcast: vi.fn() }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createDay, createPlace, addTripMember, createBudgetItem, createPackingItem, createReservation, createDayNote, createCollabNote, createBucketListItem, createVisitedCountry, createDayAssignment, createDayAccommodation } from '../../helpers/factories';
import { createMcpHarness, parseResourceResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -433,7 +464,7 @@ describe('Resource: trek://categories', () => {
});
describe('Resource: trek://bucket-list', () => {
it('returns only the current user\'s bucket list items', async () => {
it("returns only the current user's bucket list items", async () => {
const { user } = createUser(testDb);
const { user: other } = createUser(testDb);
createBucketListItem(testDb, user.id, { name: 'Tokyo' });
@@ -459,7 +490,7 @@ describe('Resource: trek://bucket-list', () => {
});
describe('Resource: trek://visited-countries', () => {
it('returns only the current user\'s visited countries', async () => {
it("returns only the current user's visited countries", async () => {
const { user } = createUser(testDb);
const { user: other } = createUser(testDb);
createVisitedCountry(testDb, user.id, 'FR');
+2 -1
View File
@@ -2,7 +2,6 @@
* Unit tests for MCP scope helper functions in server/src/mcp/scopes.ts.
* No DB or mocks needed — pure functions only.
*/
import { describe, it, expect } from 'vitest';
import {
validateScopes,
canReadTrips,
@@ -14,6 +13,8 @@ import {
SCOPE_INFO,
} from '../../../src/mcp/scopes';
import { describe, it, expect } from 'vitest';
// ---------------------------------------------------------------------------
// ALL_SCOPES
// ---------------------------------------------------------------------------
+11 -4
View File
@@ -2,9 +2,10 @@
* Unit tests for MCP sessionManager — SESS-001 to SESS-010.
* Covers revokeUserSessions and revokeUserSessionsForClient.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { sessions, revokeUserSessions, revokeUserSessionsForClient, McpSession } from '../../../src/mcp/sessionManager';
import { describe, it, expect, vi, beforeEach } from 'vitest';
function makeSession(overrides: Partial<McpSession> = {}): McpSession {
return {
server: { close: vi.fn() } as any,
@@ -60,7 +61,9 @@ describe('revokeUserSessions', () => {
it('SESS-005: tolerates server.close() throwing (swallows error)', () => {
const s = makeSession({ userId: 1 });
(s.server.close as ReturnType<typeof vi.fn>).mockImplementation(() => { throw new Error('close failed'); });
(s.server.close as ReturnType<typeof vi.fn>).mockImplementation(() => {
throw new Error('close failed');
});
sessions.set('sid-1', s);
expect(() => revokeUserSessions(1)).not.toThrow();
@@ -69,7 +72,9 @@ describe('revokeUserSessions', () => {
it('SESS-006: tolerates transport.close() throwing (swallows error)', () => {
const s = makeSession({ userId: 1 });
(s.transport.close as ReturnType<typeof vi.fn>).mockImplementation(() => { throw new Error('transport error'); });
(s.transport.close as ReturnType<typeof vi.fn>).mockImplementation(() => {
throw new Error('transport error');
});
sessions.set('sid-1', s);
expect(() => revokeUserSessions(1)).not.toThrow();
@@ -112,7 +117,9 @@ describe('revokeUserSessionsForClient', () => {
it('SESS-010: tolerates close() throwing for matched sessions', () => {
const s = makeSession({ userId: 1, clientId: 'c' });
(s.server.close as ReturnType<typeof vi.fn>).mockImplementation(() => { throw new Error('x'); });
(s.server.close as ReturnType<typeof vi.fn>).mockImplementation(() => {
throw new Error('x');
});
sessions.set('sid-1', s);
expect(() => revokeUserSessionsForClient(1, 'c')).not.toThrow();
@@ -1,6 +1,12 @@
/**
* Unit tests for MCP addon gating and scope enforcement in tools.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser, createTrip } 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -15,7 +21,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -41,12 +51,6 @@ vi.mock('../../../src/services/adminService', () => ({
getCollabFeatures: vi.fn().mockReturnValue({ chat: true, notes: true, polls: true, whatsnext: true }),
}));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -62,13 +66,13 @@ afterAll(() => {
testDb.close();
});
async function withHarness(
userId: number,
fn: (h: McpHarness) => Promise<void>,
scopes?: string[] | null
) {
async function withHarness(userId: number, fn: (h: McpHarness) => Promise<void>, scopes?: string[] | null) {
const h = await createMcpHarness({ userId, withResources: false, scopes: scopes ?? null });
try { await fn(h); } finally { await h.cleanup(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -153,7 +157,10 @@ describe('Budget tools — addon gating', () => {
isAddonEnabledMock.mockImplementation((id: string) => id !== 'budget');
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_budget_item', arguments: { tripId: 1, name: 'Test', total_price: 100 } });
const result = await h.client.callTool({
name: 'create_budget_item',
arguments: { tripId: 1, name: 'Test', total_price: 100 },
});
expect(result.isError).toBe(true);
});
});
@@ -170,7 +177,10 @@ describe('Packing tools — addon gating', () => {
isAddonEnabledMock.mockImplementation((id: string) => id !== 'packing');
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_packing_item', arguments: { tripId: 1, name: 'Sunscreen' } });
const result = await h.client.callTool({
name: 'create_packing_item',
arguments: { tripId: 1, name: 'Sunscreen' },
});
expect(result.isError).toBe(true);
});
});
@@ -187,7 +197,10 @@ describe('Collab tools — addon gating', () => {
isAddonEnabledMock.mockImplementation((id: string) => id !== 'collab');
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_collab_note', arguments: { tripId: 1, title: 'Test Note' } });
const result = await h.client.callTool({
name: 'create_collab_note',
arguments: { tripId: 1, title: 'Test Note' },
});
expect(result.isError).toBe(true);
});
});
@@ -229,51 +242,74 @@ describe('Scope enforcement in tools', () => {
it('with scopes trips:read, create_trip is not registered (write not in scopes)', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_trip', arguments: { title: 'Should Fail' } });
expect(result.isError).toBe(true);
}, ['trips:read']);
await withHarness(
user.id,
async (h) => {
const result = await h.client.callTool({ name: 'create_trip', arguments: { title: 'Should Fail' } });
expect(result.isError).toBe(true);
},
['trips:read'],
);
});
it('with scopes trips:write, create_trip is registered and works', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_trip', arguments: { title: 'My Trip' } });
expect(result.isError).toBeFalsy();
const data = parseToolResult(result) as any;
expect(data.trip.title).toBe('My Trip');
}, ['trips:write']);
await withHarness(
user.id,
async (h) => {
const result = await h.client.callTool({ name: 'create_trip', arguments: { title: 'My Trip' } });
expect(result.isError).toBeFalsy();
const data = parseToolResult(result) as any;
expect(data.trip.title).toBe('My Trip');
},
['trips:write'],
);
});
it('with scopes null (full access), create_trip is registered', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_trip', arguments: { title: 'Full Access Trip' } });
expect(result.isError).toBeFalsy();
}, null);
await withHarness(
user.id,
async (h) => {
const result = await h.client.callTool({ name: 'create_trip', arguments: { title: 'Full Access Trip' } });
expect(result.isError).toBeFalsy();
},
null,
);
});
it('with scopes trips:read, create_budget_item is not registered (budget:write not in scopes)', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_budget_item', arguments: { tripId: 1, name: 'Hotel', total_price: 200 } });
expect(result.isError).toBe(true);
}, ['trips:read']);
await withHarness(
user.id,
async (h) => {
const result = await h.client.callTool({
name: 'create_budget_item',
arguments: { tripId: 1, name: 'Hotel', total_price: 200 },
});
expect(result.isError).toBe(true);
},
['trips:read'],
);
});
it('with scopes budget:write and trips:read, create_budget_item is registered (budget addon enabled)', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id, { title: 'Budget Trip' });
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'create_budget_item',
arguments: { tripId: trip.id, name: 'Hotel', total_price: 200 },
});
expect(result.isError).toBeFalsy();
}, ['budget:write', 'trips:read']);
await withHarness(
user.id,
async (h) => {
const result = await h.client.callTool({
name: 'create_budget_item',
arguments: { tripId: trip.id, name: 'Hotel', total_price: 200 },
});
expect(result.isError).toBeFalsy();
},
['budget:write', 'trips:read'],
);
});
});
@@ -2,6 +2,19 @@
* Unit tests for MCP extra assignment/reservation tools:
* move_assignment, get_assignment_participants, set_assignment_participants, reorder_reservations.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import {
createUser,
createTrip,
createDay,
createPlace,
createDayAssignment,
createReservation,
} 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -16,7 +29,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -33,12 +50,6 @@ vi.mock('../../../src/config', () => ({
const { broadcastMock } = vi.hoisted(() => ({ broadcastMock: vi.fn() }));
vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createDay, createPlace, createDayAssignment, createReservation } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -56,7 +67,11 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -75,7 +90,13 @@ describe('Tool: move_assignment', () => {
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'move_assignment',
arguments: { tripId: trip.id, assignmentId: assignment.id, newDayId: day2.id, oldDayId: day1.id, orderIndex: 0 },
arguments: {
tripId: trip.id,
assignmentId: assignment.id,
newDayId: day2.id,
oldDayId: day1.id,
orderIndex: 0,
},
});
const data = parseToolResult(result) as any;
expect(data.assignment).toBeDefined();
@@ -142,7 +163,10 @@ describe('Tool: get_assignment_participants', () => {
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'get_assignment_participants', arguments: { tripId: trip.id, assignmentId: 1 } });
const result = await h.client.callTool({
name: 'get_assignment_participants',
arguments: { tripId: trip.id, assignmentId: 1 },
});
expect(result.isError).toBe(true);
});
});
@@ -177,7 +201,9 @@ describe('Tool: set_assignment_participants', () => {
const place = createPlace(testDb, trip.id);
const assignment = createDayAssignment(testDb, day.id, place.id);
// First set
testDb.prepare('INSERT INTO assignment_participants (assignment_id, user_id) VALUES (?, ?)').run(assignment.id, user.id);
testDb
.prepare('INSERT INTO assignment_participants (assignment_id, user_id) VALUES (?, ?)')
.run(assignment.id, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_assignment_participants',
+69 -22
View File
@@ -2,6 +2,12 @@
* Unit tests for MCP assignment tools: assign_place_to_day, unassign_place,
* reorder_day_assignments, update_assignment_time.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser, createTrip, createDay, createPlace, createDayAssignment } 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -16,7 +22,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -33,12 +43,6 @@ vi.mock('../../../src/config', () => ({
const { broadcastMock } = vi.hoisted(() => ({ broadcastMock: vi.fn() }));
vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createDay, createPlace, createDayAssignment } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -56,7 +60,11 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -107,7 +115,10 @@ describe('Tool: assign_place_to_day', () => {
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 } });
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));
});
});
@@ -151,7 +162,10 @@ describe('Tool: assign_place_to_day', () => {
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 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);
});
});
@@ -187,7 +201,10 @@ describe('Tool: unassign_place', () => {
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 } });
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));
});
});
@@ -197,7 +214,10 @@ describe('Tool: unassign_place', () => {
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 } });
const result = await h.client.callTool({
name: 'unassign_place',
arguments: { tripId: trip.id, dayId: day.id, assignmentId: 99999 },
});
expect(result.isError).toBe(true);
});
});
@@ -210,7 +230,10 @@ describe('Tool: unassign_place', () => {
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 result = await h.client.callTool({
name: 'unassign_place',
arguments: { tripId: trip.id, dayId: day.id, assignmentId: assignment.id },
});
expect(result.isError).toBe(true);
});
});
@@ -238,8 +261,12 @@ describe('Tool: reorder_day_assignments', () => {
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 };
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);
});
@@ -252,7 +279,10 @@ describe('Tool: reorder_day_assignments', () => {
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] } });
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.any(Object));
});
});
@@ -263,7 +293,10 @@ describe('Tool: reorder_day_assignments', () => {
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] } });
const result = await h.client.callTool({
name: 'reorder_day_assignments',
arguments: { tripId: trip1.id, dayId: day.id, assignmentIds: [1] },
});
expect(result.isError).toBe(true);
});
});
@@ -274,7 +307,10 @@ describe('Tool: reorder_day_assignments', () => {
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] } });
const result = await h.client.callTool({
name: 'reorder_day_assignments',
arguments: { tripId: trip.id, dayId: day.id, assignmentIds: [1] },
});
expect(result.isError).toBe(true);
});
});
@@ -309,7 +345,9 @@ describe('Tool: update_assignment_time', () => {
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);
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({
@@ -329,7 +367,10 @@ describe('Tool: update_assignment_time', () => {
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' } });
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));
});
});
@@ -338,7 +379,10 @@ describe('Tool: update_assignment_time', () => {
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' } });
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);
});
});
@@ -351,7 +395,10 @@ describe('Tool: update_assignment_time', () => {
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' } });
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);
});
});
@@ -4,6 +4,12 @@
* get_country_atlas_places, update_bucket_list_item.
* Also covers resources trek://atlas/stats and trek://atlas/regions.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, parseResourceResult, type McpHarness } from '../../helpers/mcp-harness';
import { resetTestDb } from '../../helpers/test-db';
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -18,7 +24,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -40,12 +50,6 @@ vi.mock('../../../src/services/adminService', () => ({
getCollabFeatures: vi.fn().mockReturnValue({ chat: true, notes: true, polls: true, whatsnext: true }),
}));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, parseResourceResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -63,12 +67,20 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
async function withResourceHarness(userId: number, fn: (h: McpHarness) => Promise<void>) {
const h = await createMcpHarness({ userId, withTools: false, withResources: true });
try { await fn(h); } finally { await h.cleanup(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -103,9 +115,9 @@ describe('Tool: list_visited_regions', () => {
it('returns regions after they have been inserted', async () => {
const { user } = createUser(testDb);
testDb.prepare(
'INSERT INTO visited_regions (user_id, region_code, region_name, country_code) VALUES (?, ?, ?, ?)'
).run(user.id, 'FR-75', 'Paris', 'FR');
testDb
.prepare('INSERT INTO visited_regions (user_id, region_code, region_name, country_code) VALUES (?, ?, ?, ?)')
.run(user.id, 'FR-75', 'Paris', 'FR');
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'list_visited_regions', arguments: {} });
const data = parseToolResult(result) as any;
@@ -132,7 +144,9 @@ describe('Tool: mark_region_visited', () => {
expect(data.region.region_code).toBe('US-CA');
expect(data.region.region_name).toBe('California');
expect(data.region.country_code).toBe('US');
const row = testDb.prepare('SELECT * FROM visited_regions WHERE user_id = ? AND region_code = ?').get(user.id, 'US-CA');
const row = testDb
.prepare('SELECT * FROM visited_regions WHERE user_id = ? AND region_code = ?')
.get(user.id, 'US-CA');
expect(row).toBeTruthy();
});
});
@@ -157,9 +171,9 @@ describe('Tool: mark_region_visited', () => {
describe('Tool: unmark_region_visited', () => {
it('removes region and returns success', async () => {
const { user } = createUser(testDb);
testDb.prepare(
'INSERT INTO visited_regions (user_id, region_code, region_name, country_code) VALUES (?, ?, ?, ?)'
).run(user.id, 'IT-LO', 'Lombardy', 'IT');
testDb
.prepare('INSERT INTO visited_regions (user_id, region_code, region_name, country_code) VALUES (?, ?, ?, ?)')
.run(user.id, 'IT-LO', 'Lombardy', 'IT');
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'unmark_region_visited',
@@ -167,7 +181,9 @@ describe('Tool: unmark_region_visited', () => {
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
const row = testDb.prepare('SELECT * FROM visited_regions WHERE user_id = ? AND region_code = ?').get(user.id, 'IT-LO');
const row = testDb
.prepare('SELECT * FROM visited_regions WHERE user_id = ? AND region_code = ?')
.get(user.id, 'IT-LO');
expect(row).toBeUndefined();
});
});
@@ -211,9 +227,9 @@ describe('Tool: get_country_atlas_places', () => {
describe('Tool: update_bucket_list_item', () => {
it('updates notes and returns item', async () => {
const { user } = createUser(testDb);
const r = testDb.prepare(
'INSERT INTO bucket_list (user_id, name, lat, lng) VALUES (?, ?, NULL, NULL)'
).run(user.id, 'Visit Tokyo');
const r = testDb
.prepare('INSERT INTO bucket_list (user_id, name, lat, lng) VALUES (?, ?, NULL, NULL)')
.run(user.id, 'Visit Tokyo');
const itemId = r.lastInsertRowid as number;
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
@@ -228,9 +244,9 @@ describe('Tool: update_bucket_list_item', () => {
it('updates name of existing item', async () => {
const { user } = createUser(testDb);
const r = testDb.prepare(
'INSERT INTO bucket_list (user_id, name, lat, lng) VALUES (?, ?, NULL, NULL)'
).run(user.id, 'Old Name');
const r = testDb
.prepare('INSERT INTO bucket_list (user_id, name, lat, lng) VALUES (?, ?, NULL, NULL)')
.run(user.id, 'Old Name');
const itemId = r.lastInsertRowid as number;
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
@@ -256,9 +272,9 @@ describe('Tool: update_bucket_list_item', () => {
it('blocks demo user', async () => {
process.env.DEMO_MODE = 'true';
const { user } = createUser(testDb, { email: 'demo@nomad.app' });
const r = testDb.prepare(
'INSERT INTO bucket_list (user_id, name, lat, lng) VALUES (?, ?, NULL, NULL)'
).run(user.id, 'Bucket Item');
const r = testDb
.prepare('INSERT INTO bucket_list (user_id, name, lat, lng) VALUES (?, ?, NULL, NULL)')
.run(user.id, 'Bucket Item');
const itemId = r.lastInsertRowid as number;
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
@@ -301,9 +317,9 @@ describe('Resource: trek://atlas/regions', () => {
it('returns inserted regions', async () => {
const { user } = createUser(testDb);
testDb.prepare(
'INSERT INTO visited_regions (user_id, region_code, region_name, country_code) VALUES (?, ?, ?, ?)'
).run(user.id, 'ES-CT', 'Catalonia', 'ES');
testDb
.prepare('INSERT INTO visited_regions (user_id, region_code, region_name, country_code) VALUES (?, ?, ?, ?)')
.run(user.id, 'ES-CT', 'Catalonia', 'ES');
await withResourceHarness(user.id, async (h) => {
const result = await h.client.readResource({ uri: 'trek://atlas/regions' });
const data = parseResourceResult(result) as any;
+27 -11
View File
@@ -2,6 +2,12 @@
* Unit tests for MCP atlas and bucket list tools:
* mark_country_visited, unmark_country_visited, create_bucket_list_item, delete_bucket_list_item.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser, createBucketListItem, createVisitedCountry } 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -16,7 +22,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -32,12 +42,6 @@ vi.mock('../../../src/config', () => ({
vi.mock('../../../src/websocket', () => ({ broadcast: vi.fn() }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createBucketListItem, createVisitedCountry } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -54,7 +58,11 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -69,7 +77,9 @@ describe('Tool: mark_country_visited', () => {
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
expect(data.country_code).toBe('FR');
const row = testDb.prepare('SELECT country_code FROM visited_countries WHERE user_id = ? AND country_code = ?').get(user.id, 'FR');
const row = testDb
.prepare('SELECT country_code FROM visited_countries WHERE user_id = ? AND country_code = ?')
.get(user.id, 'FR');
expect(row).toBeTruthy();
});
});
@@ -81,7 +91,11 @@ describe('Tool: mark_country_visited', () => {
const result = await h.client.callTool({ name: 'mark_country_visited', arguments: { country_code: 'JP' } });
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
const count = (testDb.prepare('SELECT COUNT(*) as c FROM visited_countries WHERE user_id = ? AND country_code = ?').get(user.id, 'JP') as { c: number }).c;
const count = (
testDb
.prepare('SELECT COUNT(*) as c FROM visited_countries WHERE user_id = ? AND country_code = ?')
.get(user.id, 'JP') as { c: number }
).c;
expect(count).toBe(1);
});
});
@@ -108,7 +122,9 @@ describe('Tool: unmark_country_visited', () => {
const result = await h.client.callTool({ name: 'unmark_country_visited', arguments: { country_code: 'ES' } });
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
const row = testDb.prepare('SELECT country_code FROM visited_countries WHERE user_id = ? AND country_code = ?').get(user.id, 'ES');
const row = testDb
.prepare('SELECT country_code FROM visited_countries WHERE user_id = ? AND country_code = ?')
.get(user.id, 'ES');
expect(row).toBeUndefined();
});
});
@@ -3,6 +3,12 @@
* set_budget_item_members, toggle_budget_member_paid.
* Resources: trek://trips/{tripId}/budget/per-person, trek://trips/{tripId}/budget/settlement.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser, createTrip, createBudgetItem } 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -17,7 +23,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -34,12 +44,6 @@ vi.mock('../../../src/config', () => ({
const { broadcastMock } = vi.hoisted(() => ({ broadcastMock: vi.fn() }));
vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createBudgetItem } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -57,12 +61,20 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
async function withResourceHarness(userId: number, fn: (h: McpHarness) => Promise<void>) {
const h = await createMcpHarness({ userId, withResources: true });
try { await fn(h); } finally { await h.cleanup(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -97,7 +109,9 @@ describe('Tool: set_budget_item_members', () => {
});
const data = parseToolResult(result) as any;
expect(data.item).toBeDefined();
const remaining = testDb.prepare('SELECT count(*) as cnt FROM budget_item_members WHERE budget_item_id = ?').get(item.id) as any;
const remaining = testDb
.prepare('SELECT count(*) as cnt FROM budget_item_members WHERE budget_item_id = ?')
.get(item.id) as any;
expect(remaining.cnt).toBe(0);
});
});
@@ -141,7 +155,9 @@ describe('Tool: toggle_budget_member_paid', () => {
const trip = createTrip(testDb, user.id);
const item = createBudgetItem(testDb, trip.id, { total_price: 200 });
// Add member first
testDb.prepare('INSERT INTO budget_item_members (budget_item_id, user_id, paid) VALUES (?, ?, 0)').run(item.id, user.id);
testDb
.prepare('INSERT INTO budget_item_members (budget_item_id, user_id, paid) VALUES (?, ?, 0)')
.run(item.id, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'toggle_budget_member_paid',
+59 -18
View File
@@ -1,6 +1,12 @@
/**
* Unit tests for MCP budget tools: create_budget_item, update_budget_item, delete_budget_item.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser, createTrip, createBudgetItem } 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -15,7 +21,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -32,12 +42,6 @@ vi.mock('../../../src/config', () => ({
const { broadcastMock } = vi.hoisted(() => ({ broadcastMock: vi.fn() }));
vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createBudgetItem } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -55,7 +59,11 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -69,7 +77,13 @@ describe('Tool: create_budget_item', () => {
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'create_budget_item',
arguments: { tripId: trip.id, name: 'Hotel Paris', category: 'Accommodation', total_price: 500, note: 'Prepaid' },
arguments: {
tripId: trip.id,
name: 'Hotel Paris',
category: 'Accommodation',
total_price: 500,
note: 'Prepaid',
},
});
const data = parseToolResult(result) as any;
expect(data.item.name).toBe('Hotel Paris');
@@ -96,7 +110,10 @@ describe('Tool: create_budget_item', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({ name: 'create_budget_item', arguments: { tripId: trip.id, name: 'Taxi', total_price: 25 } });
await h.client.callTool({
name: 'create_budget_item',
arguments: { tripId: trip.id, name: 'Taxi', total_price: 25 },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'budget:created', expect.any(Object));
});
});
@@ -106,7 +123,10 @@ describe('Tool: create_budget_item', () => {
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_budget_item', arguments: { tripId: trip.id, name: 'Hack', total_price: 0 } });
const result = await h.client.callTool({
name: 'create_budget_item',
arguments: { tripId: trip.id, name: 'Hack', total_price: 0 },
});
expect(result.isError).toBe(true);
});
});
@@ -116,7 +136,10 @@ describe('Tool: create_budget_item', () => {
const { user } = createUser(testDb, { email: 'demo@nomad.app' });
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_budget_item', arguments: { tripId: trip.id, name: 'X', total_price: 0 } });
const result = await h.client.callTool({
name: 'create_budget_item',
arguments: { tripId: trip.id, name: 'X', total_price: 0 },
});
expect(result.isError).toBe(true);
});
});
@@ -148,7 +171,10 @@ describe('Tool: update_budget_item', () => {
const trip = createTrip(testDb, user.id);
const item = createBudgetItem(testDb, trip.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({ name: 'update_budget_item', arguments: { tripId: trip.id, itemId: item.id, name: 'Updated' } });
await h.client.callTool({
name: 'update_budget_item',
arguments: { tripId: trip.id, itemId: item.id, name: 'Updated' },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'budget:updated', expect.any(Object));
});
});
@@ -157,7 +183,10 @@ describe('Tool: update_budget_item', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_budget_item', arguments: { tripId: trip.id, itemId: 99999, name: 'X' } });
const result = await h.client.callTool({
name: 'update_budget_item',
arguments: { tripId: trip.id, itemId: 99999, name: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -168,7 +197,10 @@ describe('Tool: update_budget_item', () => {
const trip = createTrip(testDb, other.id);
const item = createBudgetItem(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_budget_item', arguments: { tripId: trip.id, itemId: item.id, name: 'X' } });
const result = await h.client.callTool({
name: 'update_budget_item',
arguments: { tripId: trip.id, itemId: item.id, name: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -184,7 +216,10 @@ describe('Tool: delete_budget_item', () => {
const trip = createTrip(testDb, user.id);
const item = createBudgetItem(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_budget_item', arguments: { tripId: trip.id, itemId: item.id } });
const result = await h.client.callTool({
name: 'delete_budget_item',
arguments: { tripId: trip.id, itemId: item.id },
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
expect(testDb.prepare('SELECT id FROM budget_items WHERE id = ?').get(item.id)).toBeUndefined();
@@ -205,7 +240,10 @@ describe('Tool: delete_budget_item', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_budget_item', arguments: { tripId: trip.id, itemId: 99999 } });
const result = await h.client.callTool({
name: 'delete_budget_item',
arguments: { tripId: trip.id, itemId: 99999 },
});
expect(result.isError).toBe(true);
});
});
@@ -216,7 +254,10 @@ describe('Tool: delete_budget_item', () => {
const trip = createTrip(testDb, other.id);
const item = createBudgetItem(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_budget_item', arguments: { tripId: trip.id, itemId: item.id } });
const result = await h.client.callTool({
name: 'delete_budget_item',
arguments: { tripId: trip.id, itemId: item.id },
});
expect(result.isError).toBe(true);
});
});
@@ -5,6 +5,12 @@
* delete_collab_message, react_collab_message.
* Resources: trek://trips/{tripId}/collab/polls, trek://trips/{tripId}/collab/messages.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser, createTrip } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, parseResourceResult, type McpHarness } from '../../helpers/mcp-harness';
import { resetTestDb } from '../../helpers/test-db';
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -19,7 +25,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -41,12 +51,6 @@ vi.mock('../../../src/services/adminService', () => ({
getCollabFeatures: vi.fn().mockReturnValue({ chat: true, notes: true, polls: true, whatsnext: true }),
}));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, parseResourceResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -64,12 +68,20 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
async function withResourceHarness(userId: number, fn: (h: McpHarness) => Promise<void>) {
const h = await createMcpHarness({ userId, withResources: true });
try { await fn(h); } finally { await h.cleanup(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -162,9 +174,13 @@ describe('Tool: vote_collab_poll', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
// Create a poll directly in the DB
const pollId = (testDb.prepare(
`INSERT INTO collab_polls (trip_id, user_id, question, options, created_at) VALUES (?, ?, ?, ?, datetime('now'))`
).run(trip.id, user.id, 'Best city?', JSON.stringify(['Paris', 'Rome'])) as any).lastInsertRowid;
const pollId = (
testDb
.prepare(
`INSERT INTO collab_polls (trip_id, user_id, question, options, created_at) VALUES (?, ?, ?, ?, datetime('now'))`,
)
.run(trip.id, user.id, 'Best city?', JSON.stringify(['Paris', 'Rome'])) as any
).lastInsertRowid;
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
@@ -199,9 +215,13 @@ describe('Tool: close_collab_poll', () => {
it('sets closed flag and broadcasts collab:poll:closed', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const pollId = (testDb.prepare(
`INSERT INTO collab_polls (trip_id, user_id, question, options, created_at) VALUES (?, ?, ?, ?, datetime('now'))`
).run(trip.id, user.id, 'Vote now?', JSON.stringify(['Yes', 'No'])) as any).lastInsertRowid;
const pollId = (
testDb
.prepare(
`INSERT INTO collab_polls (trip_id, user_id, question, options, created_at) VALUES (?, ?, ?, ?, datetime('now'))`,
)
.run(trip.id, user.id, 'Vote now?', JSON.stringify(['Yes', 'No'])) as any
).lastInsertRowid;
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
@@ -245,9 +265,13 @@ describe('Tool: delete_collab_poll', () => {
it('removes poll and broadcasts collab:poll:deleted', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const pollId = (testDb.prepare(
`INSERT INTO collab_polls (trip_id, user_id, question, options, created_at) VALUES (?, ?, ?, ?, datetime('now'))`
).run(trip.id, user.id, 'Delete me?', JSON.stringify(['Yes', 'No'])) as any).lastInsertRowid;
const pollId = (
testDb
.prepare(
`INSERT INTO collab_polls (trip_id, user_id, question, options, created_at) VALUES (?, ?, ?, ?, datetime('now'))`,
)
.run(trip.id, user.id, 'Delete me?', JSON.stringify(['Yes', 'No'])) as any
).lastInsertRowid;
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
@@ -256,7 +280,11 @@ describe('Tool: delete_collab_poll', () => {
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'collab:poll:deleted', expect.objectContaining({ pollId: Number(pollId) }));
expect(broadcastMock).toHaveBeenCalledWith(
trip.id,
'collab:poll:deleted',
expect.objectContaining({ pollId: Number(pollId) }),
);
expect(testDb.prepare('SELECT id FROM collab_polls WHERE id = ?').get(Number(pollId))).toBeUndefined();
});
});
@@ -324,9 +352,11 @@ describe('Tool: send_collab_message', () => {
it('sends message with replyTo when parent exists', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const msgId = (testDb.prepare(
`INSERT INTO collab_messages (trip_id, user_id, text, created_at) VALUES (?, ?, ?, datetime('now'))`
).run(trip.id, user.id, 'Original message') as any).lastInsertRowid;
const msgId = (
testDb
.prepare(`INSERT INTO collab_messages (trip_id, user_id, text, created_at) VALUES (?, ?, ?, datetime('now'))`)
.run(trip.id, user.id, 'Original message') as any
).lastInsertRowid;
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
@@ -356,7 +386,10 @@ describe('Tool: send_collab_message', () => {
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'send_collab_message', arguments: { tripId: trip.id, text: 'Hi' } });
const result = await h.client.callTool({
name: 'send_collab_message',
arguments: { tripId: trip.id, text: 'Hi' },
});
expect(result.isError).toBe(true);
});
});
@@ -370,9 +403,11 @@ describe('Tool: delete_collab_message', () => {
it('soft-deletes message and broadcasts collab:message:deleted', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const msgId = (testDb.prepare(
`INSERT INTO collab_messages (trip_id, user_id, text, created_at) VALUES (?, ?, ?, datetime('now'))`
).run(trip.id, user.id, 'To be deleted') as any).lastInsertRowid;
const msgId = (
testDb
.prepare(`INSERT INTO collab_messages (trip_id, user_id, text, created_at) VALUES (?, ?, ?, datetime('now'))`)
.run(trip.id, user.id, 'To be deleted') as any
).lastInsertRowid;
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
@@ -391,9 +426,11 @@ describe('Tool: delete_collab_message', () => {
const trip = createTrip(testDb, user.id);
// Add other as trip member
testDb.prepare('INSERT INTO trip_members (trip_id, user_id) VALUES (?, ?)').run(trip.id, other.id);
const msgId = (testDb.prepare(
`INSERT INTO collab_messages (trip_id, user_id, text, created_at) VALUES (?, ?, ?, datetime('now'))`
).run(trip.id, user.id, 'Owner message') as any).lastInsertRowid;
const msgId = (
testDb
.prepare(`INSERT INTO collab_messages (trip_id, user_id, text, created_at) VALUES (?, ?, ?, datetime('now'))`)
.run(trip.id, user.id, 'Owner message') as any
).lastInsertRowid;
await withHarness(other.id, async (h) => {
const result = await h.client.callTool({
@@ -409,7 +446,10 @@ describe('Tool: delete_collab_message', () => {
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_collab_message', arguments: { tripId: trip.id, messageId: 1 } });
const result = await h.client.callTool({
name: 'delete_collab_message',
arguments: { tripId: trip.id, messageId: 1 },
});
expect(result.isError).toBe(true);
});
});
@@ -423,9 +463,11 @@ describe('Tool: react_collab_message', () => {
it('toggles reaction and broadcasts collab:message:reacted', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const msgId = (testDb.prepare(
`INSERT INTO collab_messages (trip_id, user_id, text, created_at) VALUES (?, ?, ?, datetime('now'))`
).run(trip.id, user.id, 'React to me') as any).lastInsertRowid;
const msgId = (
testDb
.prepare(`INSERT INTO collab_messages (trip_id, user_id, text, created_at) VALUES (?, ?, ?, datetime('now'))`)
.run(trip.id, user.id, 'React to me') as any
).lastInsertRowid;
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
@@ -455,7 +497,10 @@ describe('Tool: react_collab_message', () => {
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'react_collab_message', arguments: { tripId: trip.id, messageId: 1, emoji: '👍' } });
const result = await h.client.callTool({
name: 'react_collab_message',
arguments: { tripId: trip.id, messageId: 1, emoji: '👍' },
});
expect(result.isError).toBe(true);
});
});
@@ -3,6 +3,12 @@
* create_day, delete_day,
* create_accommodation, update_accommodation, delete_accommodation.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser, createTrip, createDay, createPlace, createDayAccommodation } 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -17,7 +23,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -34,12 +44,6 @@ vi.mock('../../../src/config', () => ({
const { broadcastMock } = vi.hoisted(() => ({ broadcastMock: vi.fn() }));
vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createDay, createPlace, createDayAccommodation } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -57,7 +61,11 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -277,7 +285,11 @@ describe('Tool: delete_accommodation', () => {
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'accommodation:deleted', expect.objectContaining({ id: acc.id }));
expect(broadcastMock).toHaveBeenCalledWith(
trip.id,
'accommodation:deleted',
expect.objectContaining({ id: acc.id }),
);
expect(testDb.prepare('SELECT id FROM day_accommodations WHERE id = ?').get(acc.id)).toBeUndefined();
});
});
@@ -287,7 +299,10 @@ describe('Tool: delete_accommodation', () => {
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_accommodation', arguments: { tripId: trip.id, accommodationId: 1 } });
const result = await h.client.callTool({
name: 'delete_accommodation',
arguments: { tripId: trip.id, accommodationId: 1 },
});
expect(result.isError).toBe(true);
});
});
+24 -10
View File
@@ -1,6 +1,12 @@
/**
* Unit tests for MCP day tools: update_day.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser, createTrip, createDay } 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -15,7 +21,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -32,12 +42,6 @@ vi.mock('../../../src/config', () => ({
const { broadcastMock } = vi.hoisted(() => ({ broadcastMock: vi.fn() }));
vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createDay } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -55,7 +59,11 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -124,7 +132,10 @@ describe('Tool: update_day', () => {
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: 'update_day', arguments: { tripId: trip.id, dayId: day.id, title: 'X' } });
const result = await h.client.callTool({
name: 'update_day',
arguments: { tripId: trip.id, dayId: day.id, title: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -135,7 +146,10 @@ describe('Tool: update_day', () => {
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: 'update_day', arguments: { tripId: trip.id, dayId: day.id, title: 'X' } });
const result = await h.client.callTool({
name: 'update_day',
arguments: { tripId: trip.id, dayId: day.id, title: 'X' },
});
expect(result.isError).toBe(true);
});
});
+93 -29
View File
@@ -2,6 +2,12 @@
* Unit tests for MCP note tools: create_day_note, update_day_note, delete_day_note,
* create_collab_note, update_collab_note, delete_collab_note.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser, createTrip, createDay, createDayNote, createCollabNote } 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -16,7 +22,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -40,12 +50,6 @@ vi.mock('fs', async (importOriginal) => {
return { ...actual, unlinkSync: unlinkSyncMock };
});
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createDay, createDayNote, createCollabNote } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -64,7 +68,11 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -118,7 +126,10 @@ describe('Tool: create_day_note', () => {
const trip2 = createTrip(testDb, user.id);
const dayFromTrip2 = createDay(testDb, trip2.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_day_note', arguments: { tripId: trip1.id, dayId: dayFromTrip2.id, text: 'Note' } });
const result = await h.client.callTool({
name: 'create_day_note',
arguments: { tripId: trip1.id, dayId: dayFromTrip2.id, text: 'Note' },
});
expect(result.isError).toBe(true);
});
});
@@ -129,7 +140,10 @@ describe('Tool: create_day_note', () => {
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: 'create_day_note', arguments: { tripId: trip.id, dayId: day.id, text: 'X' } });
const result = await h.client.callTool({
name: 'create_day_note',
arguments: { tripId: trip.id, dayId: day.id, text: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -178,7 +192,10 @@ describe('Tool: update_day_note', () => {
const day = createDay(testDb, trip.id);
const note = createDayNote(testDb, day.id, trip.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({ name: 'update_day_note', arguments: { tripId: trip.id, dayId: day.id, noteId: note.id, text: 'Updated' } });
await h.client.callTool({
name: 'update_day_note',
arguments: { tripId: trip.id, dayId: day.id, noteId: note.id, text: 'Updated' },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'dayNote:updated', expect.any(Object));
});
});
@@ -188,7 +205,10 @@ describe('Tool: update_day_note', () => {
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: 'update_day_note', arguments: { tripId: trip.id, dayId: day.id, noteId: 99999, text: 'X' } });
const result = await h.client.callTool({
name: 'update_day_note',
arguments: { tripId: trip.id, dayId: day.id, noteId: 99999, text: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -200,7 +220,10 @@ describe('Tool: update_day_note', () => {
const day = createDay(testDb, trip.id);
const note = createDayNote(testDb, day.id, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_day_note', arguments: { tripId: trip.id, dayId: day.id, noteId: note.id, text: 'X' } });
const result = await h.client.callTool({
name: 'update_day_note',
arguments: { tripId: trip.id, dayId: day.id, noteId: note.id, text: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -217,7 +240,10 @@ describe('Tool: delete_day_note', () => {
const day = createDay(testDb, trip.id);
const note = createDayNote(testDb, day.id, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_day_note', arguments: { tripId: trip.id, dayId: day.id, noteId: note.id } });
const result = await h.client.callTool({
name: 'delete_day_note',
arguments: { tripId: trip.id, dayId: day.id, noteId: note.id },
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
expect(testDb.prepare('SELECT id FROM day_notes WHERE id = ?').get(note.id)).toBeUndefined();
@@ -230,7 +256,10 @@ describe('Tool: delete_day_note', () => {
const day = createDay(testDb, trip.id);
const note = createDayNote(testDb, day.id, trip.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({ name: 'delete_day_note', arguments: { tripId: trip.id, dayId: day.id, noteId: note.id } });
await h.client.callTool({
name: 'delete_day_note',
arguments: { tripId: trip.id, dayId: day.id, noteId: note.id },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'dayNote:deleted', expect.any(Object));
});
});
@@ -240,7 +269,10 @@ describe('Tool: delete_day_note', () => {
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: 'delete_day_note', arguments: { tripId: trip.id, dayId: day.id, noteId: 99999 } });
const result = await h.client.callTool({
name: 'delete_day_note',
arguments: { tripId: trip.id, dayId: day.id, noteId: 99999 },
});
expect(result.isError).toBe(true);
});
});
@@ -252,7 +284,10 @@ describe('Tool: delete_day_note', () => {
const day = createDay(testDb, trip.id);
const note = createDayNote(testDb, day.id, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_day_note', arguments: { tripId: trip.id, dayId: day.id, noteId: note.id } });
const result = await h.client.callTool({
name: 'delete_day_note',
arguments: { tripId: trip.id, dayId: day.id, noteId: note.id },
});
expect(result.isError).toBe(true);
});
});
@@ -283,7 +318,10 @@ describe('Tool: create_collab_note', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_collab_note', arguments: { tripId: trip.id, title: 'Quick note' } });
const result = await h.client.callTool({
name: 'create_collab_note',
arguments: { tripId: trip.id, title: 'Quick note' },
});
const data = parseToolResult(result) as any;
expect(data.note.category).toBe('General');
expect(data.note.color).toBe('#6366f1');
@@ -304,7 +342,10 @@ describe('Tool: create_collab_note', () => {
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_collab_note', arguments: { tripId: trip.id, title: 'X' } });
const result = await h.client.callTool({
name: 'create_collab_note',
arguments: { tripId: trip.id, title: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -336,7 +377,10 @@ describe('Tool: update_collab_note', () => {
const trip = createTrip(testDb, user.id);
const note = createCollabNote(testDb, trip.id, user.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({ name: 'update_collab_note', arguments: { tripId: trip.id, noteId: note.id, title: 'Updated' } });
await h.client.callTool({
name: 'update_collab_note',
arguments: { tripId: trip.id, noteId: note.id, title: 'Updated' },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'collab:note:updated', expect.any(Object));
});
});
@@ -345,7 +389,10 @@ describe('Tool: update_collab_note', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_collab_note', arguments: { tripId: trip.id, noteId: 99999, title: 'X' } });
const result = await h.client.callTool({
name: 'update_collab_note',
arguments: { tripId: trip.id, noteId: 99999, title: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -356,7 +403,10 @@ describe('Tool: update_collab_note', () => {
const trip = createTrip(testDb, other.id);
const note = createCollabNote(testDb, trip.id, other.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_collab_note', arguments: { tripId: trip.id, noteId: note.id, title: 'X' } });
const result = await h.client.callTool({
name: 'update_collab_note',
arguments: { tripId: trip.id, noteId: note.id, title: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -372,7 +422,10 @@ describe('Tool: delete_collab_note', () => {
const trip = createTrip(testDb, user.id);
const note = createCollabNote(testDb, trip.id, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_collab_note', arguments: { tripId: trip.id, noteId: note.id } });
const result = await h.client.callTool({
name: 'delete_collab_note',
arguments: { tripId: trip.id, noteId: note.id },
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
expect(testDb.prepare('SELECT id FROM collab_notes WHERE id = ?').get(note.id)).toBeUndefined();
@@ -384,12 +437,17 @@ describe('Tool: delete_collab_note', () => {
const trip = createTrip(testDb, user.id);
const note = createCollabNote(testDb, trip.id, user.id);
// Insert a trip_file linked to this note
testDb.prepare(
`INSERT INTO trip_files (trip_id, note_id, filename, original_name, mime_type, file_size) VALUES (?, ?, ?, ?, ?, ?)`
).run(trip.id, note.id, 'test-file.pdf', 'document.pdf', 'application/pdf', 1024);
testDb
.prepare(
`INSERT INTO trip_files (trip_id, note_id, filename, original_name, mime_type, file_size) VALUES (?, ?, ?, ?, ?, ?)`,
)
.run(trip.id, note.id, 'test-file.pdf', 'document.pdf', 'application/pdf', 1024);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_collab_note', arguments: { tripId: trip.id, noteId: note.id } });
const result = await h.client.callTool({
name: 'delete_collab_note',
arguments: { tripId: trip.id, noteId: note.id },
});
expect((parseToolResult(result) as any).success).toBe(true);
});
@@ -413,7 +471,10 @@ describe('Tool: delete_collab_note', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_collab_note', arguments: { tripId: trip.id, noteId: 99999 } });
const result = await h.client.callTool({
name: 'delete_collab_note',
arguments: { tripId: trip.id, noteId: 99999 },
});
expect(result.isError).toBe(true);
});
});
@@ -424,7 +485,10 @@ describe('Tool: delete_collab_note', () => {
const trip = createTrip(testDb, other.id);
const note = createCollabNote(testDb, trip.id, other.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_collab_note', arguments: { tripId: trip.id, noteId: note.id } });
const result = await h.client.callTool({
name: 'delete_collab_note',
arguments: { tripId: trip.id, noteId: note.id },
});
expect(result.isError).toBe(true);
});
});
@@ -5,6 +5,12 @@
* delete_all_notifications.
* Also covers the resource trek://notifications/in-app.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, parseResourceResult, type McpHarness } from '../../helpers/mcp-harness';
import { resetTestDb } from '../../helpers/test-db';
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -19,7 +25,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -35,12 +45,6 @@ vi.mock('../../../src/config', () => ({
vi.mock('../../../src/websocket', () => ({ broadcast: vi.fn() }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, parseResourceResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -60,28 +64,38 @@ afterAll(() => {
// ---------------------------------------------------------------------------
function createNotification(db: any, userId: number, overrides: any = {}) {
const r = db.prepare(
`INSERT INTO notifications (type, scope, target, recipient_id, title_key, text_key, is_read)
VALUES (?, ?, ?, ?, ?, ?, 0)`
).run(
overrides.type ?? 'simple',
overrides.scope ?? 'user',
overrides.target ?? 0,
userId,
overrides.title_key ?? 'notification.test.title',
overrides.text_key ?? 'notification.test.body'
);
const r = db
.prepare(
`INSERT INTO notifications (type, scope, target, recipient_id, title_key, text_key, is_read)
VALUES (?, ?, ?, ?, ?, ?, 0)`,
)
.run(
overrides.type ?? 'simple',
overrides.scope ?? 'user',
overrides.target ?? 0,
userId,
overrides.title_key ?? 'notification.test.title',
overrides.text_key ?? 'notification.test.body',
);
return db.prepare('SELECT * FROM notifications WHERE id = ?').get(r.lastInsertRowid);
}
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
async function withResourceHarness(userId: number, fn: (h: McpHarness) => Promise<void>) {
const h = await createMcpHarness({ userId, withTools: false, withResources: true });
try { await fn(h); } finally { await h.cleanup(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -240,7 +254,11 @@ describe('Tool: mark_all_notifications_read', () => {
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
expect(data.count).toBe(3);
const unread = (testDb.prepare('SELECT COUNT(*) as c FROM notifications WHERE recipient_id = ? AND is_read = 0').get(user.id) as any).c;
const unread = (
testDb
.prepare('SELECT COUNT(*) as c FROM notifications WHERE recipient_id = ? AND is_read = 0')
.get(user.id) as any
).c;
expect(unread).toBe(0);
});
});
@@ -5,6 +5,12 @@
* set_packing_category_assignees, apply_packing_template, save_packing_template,
* bulk_import_packing.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser, createTrip, createPackingItem } 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -19,7 +25,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -36,12 +46,6 @@ vi.mock('../../../src/config', () => ({
const { broadcastMock } = vi.hoisted(() => ({ broadcastMock: vi.fn() }));
vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createPackingItem } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -59,7 +63,11 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -119,7 +127,9 @@ describe('Tool: list_packing_bags', () => {
it('returns bags that exist', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
testDb.prepare('INSERT INTO packing_bags (trip_id, name, color) VALUES (?, ?, ?)').run(trip.id, 'Carry-on', '#ff0000');
testDb
.prepare('INSERT INTO packing_bags (trip_id, name, color) VALUES (?, ?, ?)')
.run(trip.id, 'Carry-on', '#ff0000');
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'list_packing_bags',
@@ -187,7 +197,9 @@ describe('Tool: update_packing_bag', () => {
it('updates bag name and broadcasts', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const r = testDb.prepare('INSERT INTO packing_bags (trip_id, name, color) VALUES (?, ?, ?)').run(trip.id, 'Old Name', '#aabbcc');
const r = testDb
.prepare('INSERT INTO packing_bags (trip_id, name, color) VALUES (?, ?, ?)')
.run(trip.id, 'Old Name', '#aabbcc');
const bag = testDb.prepare('SELECT * FROM packing_bags WHERE id = ?').get(r.lastInsertRowid) as any;
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
@@ -223,7 +235,9 @@ describe('Tool: delete_packing_bag', () => {
it('deletes a bag and broadcasts', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const r = testDb.prepare('INSERT INTO packing_bags (trip_id, name, color) VALUES (?, ?, ?)').run(trip.id, 'Delete Me', '#000000');
const r = testDb
.prepare('INSERT INTO packing_bags (trip_id, name, color) VALUES (?, ?, ?)')
.run(trip.id, 'Delete Me', '#000000');
const bagId = r.lastInsertRowid as number;
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
@@ -259,7 +273,9 @@ describe('Tool: set_bag_members', () => {
it('sets bag members and broadcasts', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const r = testDb.prepare('INSERT INTO packing_bags (trip_id, name, color) VALUES (?, ?, ?)').run(trip.id, 'My Bag', '#123456');
const r = testDb
.prepare('INSERT INTO packing_bags (trip_id, name, color) VALUES (?, ?, ?)')
.run(trip.id, 'My Bag', '#123456');
const bagId = r.lastInsertRowid as number;
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
@@ -275,7 +291,9 @@ describe('Tool: set_bag_members', () => {
it('clears bag members when passed empty array', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const r = testDb.prepare('INSERT INTO packing_bags (trip_id, name, color) VALUES (?, ?, ?)').run(trip.id, 'My Bag', '#123456');
const r = testDb
.prepare('INSERT INTO packing_bags (trip_id, name, color) VALUES (?, ?, ?)')
.run(trip.id, 'My Bag', '#123456');
const bagId = r.lastInsertRowid as number;
testDb.prepare('INSERT OR IGNORE INTO packing_bag_members (bag_id, user_id) VALUES (?, ?)').run(bagId, user.id);
await withHarness(user.id, async (h) => {
@@ -330,7 +348,9 @@ describe('Tool: set_packing_category_assignees', () => {
it('clears assignees when passed empty array', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
testDb.prepare('INSERT INTO packing_category_assignees (trip_id, category_name, user_id) VALUES (?, ?, ?)').run(trip.id, 'Clothing', user.id);
testDb
.prepare('INSERT INTO packing_category_assignees (trip_id, category_name, user_id) VALUES (?, ?, ?)')
.run(trip.id, 'Clothing', user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_packing_category_assignees',
+60 -19
View File
@@ -2,6 +2,12 @@
* Unit tests for MCP packing tools: create_packing_item, update_packing_item,
* toggle_packing_item, delete_packing_item.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser, createTrip, createPackingItem } 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -16,7 +22,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -33,12 +43,6 @@ vi.mock('../../../src/config', () => ({
const { broadcastMock } = vi.hoisted(() => ({ broadcastMock: vi.fn() }));
vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createPackingItem } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -56,7 +60,11 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -106,7 +114,10 @@ describe('Tool: create_packing_item', () => {
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_packing_item', arguments: { tripId: trip.id, name: 'X' } });
const result = await h.client.callTool({
name: 'create_packing_item',
arguments: { tripId: trip.id, name: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -116,7 +127,10 @@ describe('Tool: create_packing_item', () => {
const { user } = createUser(testDb, { email: 'demo@nomad.app' });
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_packing_item', arguments: { tripId: trip.id, name: 'X' } });
const result = await h.client.callTool({
name: 'create_packing_item',
arguments: { tripId: trip.id, name: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -147,7 +161,10 @@ describe('Tool: update_packing_item', () => {
const trip = createTrip(testDb, user.id);
const item = createPackingItem(testDb, trip.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({ name: 'update_packing_item', arguments: { tripId: trip.id, itemId: item.id, name: 'Updated' } });
await h.client.callTool({
name: 'update_packing_item',
arguments: { tripId: trip.id, itemId: item.id, name: 'Updated' },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'packing:updated', expect.any(Object));
});
});
@@ -156,7 +173,10 @@ describe('Tool: update_packing_item', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_packing_item', arguments: { tripId: trip.id, itemId: 99999, name: 'X' } });
const result = await h.client.callTool({
name: 'update_packing_item',
arguments: { tripId: trip.id, itemId: 99999, name: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -167,7 +187,10 @@ describe('Tool: update_packing_item', () => {
const trip = createTrip(testDb, other.id);
const item = createPackingItem(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_packing_item', arguments: { tripId: trip.id, itemId: item.id, name: 'X' } });
const result = await h.client.callTool({
name: 'update_packing_item',
arguments: { tripId: trip.id, itemId: item.id, name: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -212,7 +235,10 @@ describe('Tool: toggle_packing_item', () => {
const trip = createTrip(testDb, user.id);
const item = createPackingItem(testDb, trip.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({ name: 'toggle_packing_item', arguments: { tripId: trip.id, itemId: item.id, checked: true } });
await h.client.callTool({
name: 'toggle_packing_item',
arguments: { tripId: trip.id, itemId: item.id, checked: true },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'packing:updated', expect.any(Object));
});
});
@@ -221,7 +247,10 @@ describe('Tool: toggle_packing_item', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'toggle_packing_item', arguments: { tripId: trip.id, itemId: 99999, checked: true } });
const result = await h.client.callTool({
name: 'toggle_packing_item',
arguments: { tripId: trip.id, itemId: 99999, checked: true },
});
expect(result.isError).toBe(true);
});
});
@@ -232,7 +261,10 @@ describe('Tool: toggle_packing_item', () => {
const trip = createTrip(testDb, other.id);
const item = createPackingItem(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'toggle_packing_item', arguments: { tripId: trip.id, itemId: item.id, checked: true } });
const result = await h.client.callTool({
name: 'toggle_packing_item',
arguments: { tripId: trip.id, itemId: item.id, checked: true },
});
expect(result.isError).toBe(true);
});
});
@@ -248,7 +280,10 @@ describe('Tool: delete_packing_item', () => {
const trip = createTrip(testDb, user.id);
const item = createPackingItem(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_packing_item', arguments: { tripId: trip.id, itemId: item.id } });
const result = await h.client.callTool({
name: 'delete_packing_item',
arguments: { tripId: trip.id, itemId: item.id },
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
expect(testDb.prepare('SELECT id FROM packing_items WHERE id = ?').get(item.id)).toBeUndefined();
@@ -269,7 +304,10 @@ describe('Tool: delete_packing_item', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_packing_item', arguments: { tripId: trip.id, itemId: 99999 } });
const result = await h.client.callTool({
name: 'delete_packing_item',
arguments: { tripId: trip.id, itemId: 99999 },
});
expect(result.isError).toBe(true);
});
});
@@ -280,7 +318,10 @@ describe('Tool: delete_packing_item', () => {
const trip = createTrip(testDb, other.id);
const item = createPackingItem(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_packing_item', arguments: { tripId: trip.id, itemId: item.id } });
const result = await h.client.callTool({
name: 'delete_packing_item',
arguments: { tripId: trip.id, itemId: item.id },
});
expect(result.isError).toBe(true);
});
});
+80 -21
View File
@@ -1,6 +1,12 @@
/**
* Unit tests for MCP place tools: create_place, update_place, delete_place, list_categories, search_place.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser, createTrip, createPlace, createDay } 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -14,13 +20,29 @@ const { testDb, dbMock } = vi.hoisted(() => {
closeDb: () => {},
reinitialize: () => {},
getPlaceWithTags: (placeId: number) => {
const place: any = db.prepare(`SELECT p.*, c.name as category_name, c.color as category_color, c.icon as category_icon FROM places p LEFT JOIN categories c ON p.category_id = c.id WHERE p.id = ?`).get(placeId);
const place: any = db
.prepare(
`SELECT p.*, c.name as category_name, c.color as category_color, c.icon as category_icon FROM places p LEFT JOIN categories c ON p.category_id = c.id WHERE p.id = ?`,
)
.get(placeId);
if (!place) return null;
const tags = db.prepare(`SELECT t.* FROM tags t JOIN place_tags pt ON t.id = pt.tag_id WHERE pt.place_id = ?`).all(placeId);
return { ...place, category: place.category_id ? { id: place.category_id, name: place.category_name, color: place.category_color, icon: place.category_icon } : null, tags };
const tags = db
.prepare(`SELECT t.* FROM tags t JOIN place_tags pt ON t.id = pt.tag_id WHERE pt.place_id = ?`)
.all(placeId);
return {
...place,
category: place.category_id
? { id: place.category_id, name: place.category_name, color: place.category_color, icon: place.category_icon }
: null,
tags,
};
},
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -40,12 +62,6 @@ vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
const { searchPlacesMock } = vi.hoisted(() => ({ searchPlacesMock: vi.fn() }));
vi.mock('../../../src/services/mapsService', () => ({ searchPlaces: searchPlacesMock }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createPlace, createDay } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -64,7 +80,11 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -171,7 +191,10 @@ describe('Tool: update_place', () => {
const trip = createTrip(testDb, user.id);
const place = createPlace(testDb, trip.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({ name: 'update_place', arguments: { tripId: trip.id, placeId: place.id, name: 'Updated' } });
await h.client.callTool({
name: 'update_place',
arguments: { tripId: trip.id, placeId: place.id, name: 'Updated' },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'place:updated', expect.any(Object));
});
});
@@ -191,7 +214,10 @@ describe('Tool: update_place', () => {
const trip = createTrip(testDb, other.id);
const place = createPlace(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_place', arguments: { tripId: trip.id, placeId: place.id, name: 'X' } });
const result = await h.client.callTool({
name: 'update_place',
arguments: { tripId: trip.id, placeId: place.id, name: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -207,7 +233,10 @@ describe('Tool: delete_place', () => {
const trip = createTrip(testDb, user.id);
const place = createPlace(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_place', arguments: { tripId: trip.id, placeId: place.id } });
const result = await h.client.callTool({
name: 'delete_place',
arguments: { tripId: trip.id, placeId: place.id },
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
expect(testDb.prepare('SELECT id FROM places WHERE id = ?').get(place.id)).toBeUndefined();
@@ -239,7 +268,10 @@ describe('Tool: delete_place', () => {
const trip = createTrip(testDb, other.id);
const place = createPlace(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_place', arguments: { tripId: trip.id, placeId: place.id } });
const result = await h.client.callTool({
name: 'delete_place',
arguments: { tripId: trip.id, placeId: place.id },
});
expect(result.isError).toBe(true);
});
});
@@ -276,7 +308,13 @@ describe('Tool: search_place', () => {
searchPlacesMock.mockResolvedValue({
source: 'openstreetmap',
places: [
{ osm_id: 'node:12345', name: 'Eiffel Tower', address: 'Eiffel Tower, Paris, France', lat: 48.8584, lng: 2.2945 },
{
osm_id: 'node:12345',
name: 'Eiffel Tower',
address: 'Eiffel Tower, Paris, France',
lat: 48.8584,
lng: 2.2945,
},
],
});
@@ -296,7 +334,16 @@ describe('Tool: search_place', () => {
searchPlacesMock.mockResolvedValue({
source: 'google',
places: [
{ google_place_id: 'ChIJD3uTd9hx5kcR1IQvGfr8dbk', name: 'Eiffel Tower', address: 'Champ de Mars, Paris', lat: 48.8584, lng: 2.2945, rating: 4.7, website: 'https://toureiffel.paris', phone: null },
{
google_place_id: 'ChIJD3uTd9hx5kcR1IQvGfr8dbk',
name: 'Eiffel Tower',
address: 'Champ de Mars, Paris',
lat: 48.8584,
lng: 2.2945,
rating: 4.7,
website: 'https://toureiffel.paris',
phone: null,
},
],
});
@@ -351,7 +398,10 @@ describe('Tool: list_places', () => {
testDb.prepare('INSERT INTO day_assignments (day_id, place_id) VALUES (?, ?)').run(day.id, assigned.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'list_places', arguments: { tripId: trip.id, assignment: 'unassigned' } });
const result = await h.client.callTool({
name: 'list_places',
arguments: { tripId: trip.id, assignment: 'unassigned' },
});
const data = parseToolResult(result) as any;
expect(data.places).toHaveLength(1);
expect(data.places[0].name).toBe('Orphan Place');
@@ -367,7 +417,10 @@ describe('Tool: list_places', () => {
testDb.prepare('INSERT INTO day_assignments (day_id, place_id) VALUES (?, ?)').run(day.id, assigned.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'list_places', arguments: { tripId: trip.id, assignment: 'assigned' } });
const result = await h.client.callTool({
name: 'list_places',
arguments: { tripId: trip.id, assignment: 'assigned' },
});
const data = parseToolResult(result) as any;
expect(data.places).toHaveLength(1);
expect(data.places[0].name).toBe('Assigned Place');
@@ -382,7 +435,10 @@ describe('Tool: list_places', () => {
testDb.prepare('INSERT INTO day_assignments (day_id, place_id) VALUES (?, ?)').run(day.id, place.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'list_places', arguments: { tripId: trip.id, assignment: 'unassigned' } });
const result = await h.client.callTool({
name: 'list_places',
arguments: { tripId: trip.id, assignment: 'unassigned' },
});
const data = parseToolResult(result) as any;
expect(data.places).toHaveLength(0);
});
@@ -397,7 +453,10 @@ describe('Tool: list_places', () => {
testDb.prepare('INSERT INTO day_assignments (day_id, place_id) VALUES (?, ?)').run(day.id, assigned.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'list_places', arguments: { tripId: trip.id, assignment: 'unassigned', search: 'Louvre' } });
const result = await h.client.callTool({
name: 'list_places',
arguments: { tripId: trip.id, assignment: 'unassigned', search: 'Louvre' },
});
const data = parseToolResult(result) as any;
expect(data.places).toHaveLength(1);
expect(data.places[0].name).toBe('Louvre Museum');
+29 -13
View File
@@ -6,9 +6,15 @@
* with the MCP client's type-safe getPrompt. We therefore test prompt callbacks
* directly via the registered prompt handlers on the server instance.
*/
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { registerMcpPrompts } from '../../../src/mcp/tools/prompts';
import { createUser, createTrip, addTripMember, createPackingItem, createBudgetItem } from '../../helpers/factories';
import { resetTestDb } from '../../helpers/test-db';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
const { testDb, dbMock } = vi.hoisted(() => {
const Database = require('better-sqlite3');
const db = new Database(':memory:');
@@ -21,7 +27,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -54,12 +64,6 @@ vi.mock('../../../src/services/tripService', () => ({
getTripSummary: mockGetTripSummary,
}));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, addTripMember, createPackingItem, createBudgetItem } from '../../helpers/factories';
import { registerMcpPrompts } from '../../../src/mcp/tools/prompts';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -76,18 +80,22 @@ beforeEach(() => {
mockGetTripSummary.mockImplementation((tripId: any) => {
const trip = testDb.prepare('SELECT * FROM trips WHERE id = ?').get(tripId) as any;
if (!trip) return null;
const members = testDb.prepare(`
const members = testDb
.prepare(
`
SELECT u.id, u.username as name, u.email
FROM trip_members m JOIN users u ON u.id = m.user_id
WHERE m.trip_id = ?
`).all(tripId) as any[];
`,
)
.all(tripId) as any[];
const budgetRows = testDb.prepare('SELECT * FROM budget_items WHERE trip_id = ?').all(tripId) as any[];
const packingRows = testDb.prepare('SELECT * FROM packing_items WHERE trip_id = ?').all(tripId) as any[];
return {
trip,
days: [],
members,
budget: budgetRows, // array shape expected by prompts.ts
budget: budgetRows, // array shape expected by prompts.ts
packing: packingRows, // array shape expected by prompts.ts
reservations: [],
collabNotes: [],
@@ -214,7 +222,15 @@ describe('Prompt: trip-summary', () => {
// Return summary with minimal trip fields (no title, no dates, no description)
mockGetTripSummary.mockReturnValueOnce({
trip: { id: trip.id, title: null, description: null, start_date: null, end_date: null, currency: null, user_id: user.id },
trip: {
id: trip.id,
title: null,
description: null,
start_date: null,
end_date: null,
currency: null,
user_id: user.id,
},
days: [],
members: [],
budget: [],
@@ -226,7 +242,7 @@ describe('Prompt: trip-summary', () => {
const server = buildServer(user.id);
const text = await invokePromptText(server, 'trip-summary', { tripId: trip.id });
expect(text).toContain('Untitled');
expect(text).toContain('?'); // start/end date fallback
expect(text).toContain('?'); // start/end date fallback
expect(text).toContain('EUR'); // currency fallback
});
});
+124 -27
View File
@@ -2,6 +2,19 @@
* Unit tests for MCP reservation tools: create_reservation, update_reservation,
* delete_reservation, link_hotel_accommodation.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import {
createUser,
createTrip,
createDay,
createPlace,
createReservation,
createDayAssignment,
} 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -16,7 +29,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -33,12 +50,6 @@ vi.mock('../../../src/config', () => ({
const { broadcastMock } = vi.hoisted(() => ({ broadcastMock: vi.fn() }));
vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createDay, createPlace, createReservation, createDayAssignment } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -56,7 +67,11 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -104,7 +119,9 @@ describe('Tool: create_reservation', () => {
expect(data.reservation.type).toBe('hotel');
expect(data.reservation.accommodation_id).not.toBeNull();
// accommodation was created
const acc = testDb.prepare('SELECT * FROM day_accommodations WHERE id = ?').get(data.reservation.accommodation_id) as any;
const acc = testDb
.prepare('SELECT * FROM day_accommodations WHERE id = ?')
.get(data.reservation.accommodation_id) as any;
expect(acc.place_id).toBe(hotel.id);
expect(acc.check_in).toBe('15:00');
});
@@ -144,7 +161,10 @@ describe('Tool: create_reservation', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({ name: 'create_reservation', arguments: { tripId: trip.id, title: 'Bus', type: 'other' } });
await h.client.callTool({
name: 'create_reservation',
arguments: { tripId: trip.id, title: 'Bus', type: 'other' },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'reservation:created', expect.any(Object));
});
});
@@ -158,7 +178,14 @@ describe('Tool: create_reservation', () => {
await withHarness(user.id, async (h) => {
await h.client.callTool({
name: 'create_reservation',
arguments: { tripId: trip.id, title: 'Hotel', type: 'hotel', place_id: hotel.id, start_day_id: day1.id, end_day_id: day2.id },
arguments: {
tripId: trip.id,
title: 'Hotel',
type: 'hotel',
place_id: hotel.id,
start_day_id: day1.id,
end_day_id: day2.id,
},
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'accommodation:created', expect.any(Object));
});
@@ -169,7 +196,10 @@ describe('Tool: create_reservation', () => {
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_reservation', arguments: { tripId: trip.id, title: 'X', type: 'flight' } });
const result = await h.client.callTool({
name: 'create_reservation',
arguments: { tripId: trip.id, title: 'X', type: 'flight' },
});
expect(result.isError).toBe(true);
});
});
@@ -214,7 +244,10 @@ describe('Tool: update_reservation', () => {
const trip = createTrip(testDb, user.id);
const reservation = createReservation(testDb, trip.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({ name: 'update_reservation', arguments: { tripId: trip.id, reservationId: reservation.id, title: 'Updated' } });
await h.client.callTool({
name: 'update_reservation',
arguments: { tripId: trip.id, reservationId: reservation.id, title: 'Updated' },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'reservation:updated', expect.any(Object));
});
});
@@ -223,7 +256,10 @@ describe('Tool: update_reservation', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_reservation', arguments: { tripId: trip.id, reservationId: 99999, title: 'X' } });
const result = await h.client.callTool({
name: 'update_reservation',
arguments: { tripId: trip.id, reservationId: 99999, title: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -249,7 +285,10 @@ describe('Tool: update_reservation', () => {
const trip = createTrip(testDb, other.id);
const reservation = createReservation(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_reservation', arguments: { tripId: trip.id, reservationId: reservation.id, title: 'X' } });
const result = await h.client.callTool({
name: 'update_reservation',
arguments: { tripId: trip.id, reservationId: reservation.id, title: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -265,7 +304,10 @@ describe('Tool: delete_reservation', () => {
const trip = createTrip(testDb, user.id);
const reservation = createReservation(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_reservation', arguments: { tripId: trip.id, reservationId: reservation.id } });
const result = await h.client.callTool({
name: 'delete_reservation',
arguments: { tripId: trip.id, reservationId: reservation.id },
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
expect(testDb.prepare('SELECT id FROM reservations WHERE id = ?').get(reservation.id)).toBeUndefined();
@@ -283,12 +325,20 @@ describe('Tool: delete_reservation', () => {
await withHarness(user.id, async (h) => {
const r = await h.client.callTool({
name: 'create_reservation',
arguments: { tripId: trip.id, title: 'Hotel', type: 'hotel', place_id: hotel.id, start_day_id: day1.id, end_day_id: day2.id },
arguments: {
tripId: trip.id,
title: 'Hotel',
type: 'hotel',
place_id: hotel.id,
start_day_id: day1.id,
end_day_id: day2.id,
},
});
reservationId = (parseToolResult(r) as any).reservation.id;
});
const accId = (testDb.prepare('SELECT accommodation_id FROM reservations WHERE id = ?').get(reservationId!) as any).accommodation_id;
const accId = (testDb.prepare('SELECT accommodation_id FROM reservations WHERE id = ?').get(reservationId!) as any)
.accommodation_id;
expect(accId).not.toBeNull();
await withHarness(user.id, async (h) => {
@@ -303,7 +353,10 @@ describe('Tool: delete_reservation', () => {
const trip = createTrip(testDb, user.id);
const reservation = createReservation(testDb, trip.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({ name: 'delete_reservation', arguments: { tripId: trip.id, reservationId: reservation.id } });
await h.client.callTool({
name: 'delete_reservation',
arguments: { tripId: trip.id, reservationId: reservation.id },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'reservation:deleted', expect.any(Object));
});
});
@@ -312,7 +365,10 @@ describe('Tool: delete_reservation', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_reservation', arguments: { tripId: trip.id, reservationId: 99999 } });
const result = await h.client.callTool({
name: 'delete_reservation',
arguments: { tripId: trip.id, reservationId: 99999 },
});
expect(result.isError).toBe(true);
});
});
@@ -323,7 +379,10 @@ describe('Tool: delete_reservation', () => {
const trip = createTrip(testDb, other.id);
const reservation = createReservation(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_reservation', arguments: { tripId: trip.id, reservationId: reservation.id } });
const result = await h.client.callTool({
name: 'delete_reservation',
arguments: { tripId: trip.id, reservationId: reservation.id },
});
expect(result.isError).toBe(true);
});
});
@@ -345,7 +404,15 @@ describe('Tool: link_hotel_accommodation', () => {
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'link_hotel_accommodation',
arguments: { tripId: trip.id, reservationId: reservation.id, place_id: hotel.id, start_day_id: day1.id, end_day_id: day2.id, check_in: '14:00', check_out: '12:00' },
arguments: {
tripId: trip.id,
reservationId: reservation.id,
place_id: hotel.id,
start_day_id: day1.id,
end_day_id: day2.id,
check_in: '14:00',
check_out: '12:00',
},
});
const data = parseToolResult(result) as any;
expect(data.reservation.accommodation_id).not.toBeNull();
@@ -368,7 +435,13 @@ describe('Tool: link_hotel_accommodation', () => {
await withHarness(user.id, async (h) => {
await h.client.callTool({
name: 'link_hotel_accommodation',
arguments: { tripId: trip.id, reservationId: reservation.id, place_id: hotel.id, start_day_id: day1.id, end_day_id: day2.id },
arguments: {
tripId: trip.id,
reservationId: reservation.id,
place_id: hotel.id,
start_day_id: day1.id,
end_day_id: day2.id,
},
});
});
@@ -376,7 +449,13 @@ describe('Tool: link_hotel_accommodation', () => {
await withHarness(user.id, async (h) => {
await h.client.callTool({
name: 'link_hotel_accommodation',
arguments: { tripId: trip.id, reservationId: reservation.id, place_id: hotel2.id, start_day_id: day2.id, end_day_id: day3.id },
arguments: {
tripId: trip.id,
reservationId: reservation.id,
place_id: hotel2.id,
start_day_id: day2.id,
end_day_id: day3.id,
},
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'accommodation:updated', expect.any(Object));
});
@@ -392,7 +471,13 @@ describe('Tool: link_hotel_accommodation', () => {
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'link_hotel_accommodation',
arguments: { tripId: trip.id, reservationId: reservation.id, place_id: place.id, start_day_id: day1.id, end_day_id: day2.id },
arguments: {
tripId: trip.id,
reservationId: reservation.id,
place_id: place.id,
start_day_id: day1.id,
end_day_id: day2.id,
},
});
expect(result.isError).toBe(true);
});
@@ -409,7 +494,13 @@ describe('Tool: link_hotel_accommodation', () => {
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'link_hotel_accommodation',
arguments: { tripId: trip1.id, reservationId: reservation.id, place_id: placeFromTrip2.id, start_day_id: day1.id, end_day_id: day2.id },
arguments: {
tripId: trip1.id,
reservationId: reservation.id,
place_id: placeFromTrip2.id,
start_day_id: day1.id,
end_day_id: day2.id,
},
});
expect(result.isError).toBe(true);
});
@@ -426,7 +517,13 @@ describe('Tool: link_hotel_accommodation', () => {
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'link_hotel_accommodation',
arguments: { tripId: trip.id, reservationId: reservation.id, place_id: place.id, start_day_id: day1.id, end_day_id: day2.id },
arguments: {
tripId: trip.id,
reservationId: reservation.id,
place_id: place.id,
start_day_id: day1.id,
end_day_id: day2.id,
},
});
expect(result.isError).toBe(true);
});
@@ -4,6 +4,13 @@
* get_place_details, reverse_geocode, resolve_maps_url,
* get_weather, get_detailed_weather.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import * as mapsService from '../../../src/services/mapsService';
import { createUser } 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -18,7 +25,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -47,13 +58,6 @@ vi.mock('../../../src/services/weatherService', () => ({
getDetailedWeather: vi.fn().mockResolvedValue({ hourly: [] }),
}));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
import * as mapsService from '../../../src/services/mapsService';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -71,7 +75,11 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -154,7 +162,9 @@ describe('Tool: create_tag', () => {
describe('Tool: update_tag', () => {
it('updates tag name and color', async () => {
const { user } = createUser(testDb);
const r = testDb.prepare('INSERT INTO tags (user_id, name, color) VALUES (?, ?, ?)').run(user.id, 'Old Name', '#aaaaaa');
const r = testDb
.prepare('INSERT INTO tags (user_id, name, color) VALUES (?, ?, ?)')
.run(user.id, 'Old Name', '#aaaaaa');
const tagId = r.lastInsertRowid as number;
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
@@ -187,7 +197,9 @@ describe('Tool: update_tag', () => {
describe('Tool: delete_tag', () => {
it('removes the tag row', async () => {
const { user } = createUser(testDb);
const r = testDb.prepare('INSERT INTO tags (user_id, name, color) VALUES (?, ?, ?)').run(user.id, 'To Delete', '#cccccc');
const r = testDb
.prepare('INSERT INTO tags (user_id, name, color) VALUES (?, ?, ?)')
.run(user.id, 'To Delete', '#cccccc');
const tagId = r.lastInsertRowid as number;
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
+47 -16
View File
@@ -3,6 +3,12 @@
* create_todo, update_todo, toggle_todo, delete_todo, reorder_todos,
* list_todos, get_todo_category_assignees, set_todo_category_assignees.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser, createTrip, createTodoItem } 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -17,7 +23,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -34,12 +44,6 @@ vi.mock('../../../src/config', () => ({
const { broadcastMock } = vi.hoisted(() => ({ broadcastMock: vi.fn() }));
vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createTodoItem } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -57,7 +61,11 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -195,8 +203,14 @@ describe('Tool: update_todo', () => {
it('clears due_date when passed null', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
testDb.prepare("INSERT INTO todo_items (trip_id, name, checked, sort_order, due_date) VALUES (?, 'Task', 0, 0, '2025-01-01')").run(trip.id);
const item = testDb.prepare('SELECT * FROM todo_items WHERE trip_id = ? ORDER BY id DESC LIMIT 1').get(trip.id) as any;
testDb
.prepare(
"INSERT INTO todo_items (trip_id, name, checked, sort_order, due_date) VALUES (?, 'Task', 0, 0, '2025-01-01')",
)
.run(trip.id);
const item = testDb
.prepare('SELECT * FROM todo_items WHERE trip_id = ? ORDER BY id DESC LIMIT 1')
.get(trip.id) as any;
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'update_todo',
@@ -212,7 +226,10 @@ describe('Tool: update_todo', () => {
const trip = createTrip(testDb, user.id);
const item = createTodoItem(testDb, trip.id);
await withHarness(user.id, async (h) => {
await h.client.callTool({ name: 'update_todo', arguments: { tripId: trip.id, itemId: item.id, name: 'Updated' } });
await h.client.callTool({
name: 'update_todo',
arguments: { tripId: trip.id, itemId: item.id, name: 'Updated' },
});
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'todo:updated', expect.any(Object));
});
});
@@ -221,7 +238,10 @@ describe('Tool: update_todo', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_todo', arguments: { tripId: trip.id, itemId: 99999, name: 'X' } });
const result = await h.client.callTool({
name: 'update_todo',
arguments: { tripId: trip.id, itemId: 99999, name: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -232,7 +252,10 @@ describe('Tool: update_todo', () => {
const trip = createTrip(testDb, other.id);
const item = createTodoItem(testDb, trip.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_todo', arguments: { tripId: trip.id, itemId: item.id, name: 'X' } });
const result = await h.client.callTool({
name: 'update_todo',
arguments: { tripId: trip.id, itemId: item.id, name: 'X' },
});
expect(result.isError).toBe(true);
});
});
@@ -285,7 +308,10 @@ describe('Tool: toggle_todo', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'toggle_todo', arguments: { tripId: trip.id, itemId: 99999, checked: true } });
const result = await h.client.callTool({
name: 'toggle_todo',
arguments: { tripId: trip.id, itemId: 99999, checked: true },
});
expect(result.isError).toBe(true);
});
});
@@ -367,7 +393,10 @@ describe('Tool: reorder_todos', () => {
const { user: other } = createUser(testDb);
const trip = createTrip(testDb, other.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'reorder_todos', arguments: { tripId: trip.id, orderedIds: [1] } });
const result = await h.client.callTool({
name: 'reorder_todos',
arguments: { tripId: trip.id, orderedIds: [1] },
});
expect(result.isError).toBe(true);
});
});
@@ -412,7 +441,9 @@ describe('Tool: set_todo_category_assignees', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
// Set then clear
testDb.prepare('INSERT INTO todo_category_assignees (trip_id, category_name, user_id) VALUES (?, ?, ?)').run(trip.id, 'Booking', user.id);
testDb
.prepare('INSERT INTO todo_category_assignees (trip_id, category_name, user_id) VALUES (?, ?, ?)')
.run(trip.id, 'Booking', user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'set_todo_category_assignees',
@@ -3,6 +3,12 @@
* list_trip_members, add_trip_member, remove_trip_member,
* copy_trip, export_trip_ics, get_share_link, create_share_link, delete_share_link.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser, createTrip, addTripMember } 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -17,7 +23,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -34,12 +44,6 @@ vi.mock('../../../src/config', () => ({
const { broadcastMock } = vi.hoisted(() => ({ broadcastMock: vi.fn() }));
vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, addTripMember } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -57,7 +61,11 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -179,7 +187,9 @@ describe('Tool: remove_trip_member', () => {
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
const row = testDb.prepare('SELECT * FROM trip_members WHERE trip_id = ? AND user_id = ?').get(trip.id, member.id);
const row = testDb
.prepare('SELECT * FROM trip_members WHERE trip_id = ? AND user_id = ?')
.get(trip.id, member.id);
expect(row).toBeUndefined();
});
});
@@ -306,9 +316,11 @@ describe('Tool: get_share_link', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
// Create a share link directly
testDb.prepare(
'INSERT INTO share_tokens (trip_id, token, created_by, share_map, share_bookings, share_packing, share_budget, share_collab) VALUES (?, ?, ?, 1, 1, 0, 0, 0)'
).run(trip.id, 'test-token-123', user.id);
testDb
.prepare(
'INSERT INTO share_tokens (trip_id, token, created_by, share_map, share_bookings, share_packing, share_budget, share_collab) VALUES (?, ?, ?, 1, 1, 0, 0, 0)',
)
.run(trip.id, 'test-token-123', user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'get_share_link', arguments: { tripId: trip.id } });
const data = parseToolResult(result) as any;
@@ -336,9 +348,11 @@ describe('Tool: create_share_link', () => {
it('updates existing share link permissions', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
testDb.prepare(
'INSERT INTO share_tokens (trip_id, token, created_by, share_map, share_bookings, share_packing, share_budget, share_collab) VALUES (?, ?, ?, 1, 1, 0, 0, 0)'
).run(trip.id, 'existing-token', user.id);
testDb
.prepare(
'INSERT INTO share_tokens (trip_id, token, created_by, share_map, share_bookings, share_packing, share_budget, share_collab) VALUES (?, ?, ?, 1, 1, 0, 0, 0)',
)
.run(trip.id, 'existing-token', user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
name: 'create_share_link',
@@ -364,9 +378,11 @@ describe('Tool: delete_share_link', () => {
it('revokes the share link', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
testDb.prepare(
'INSERT INTO share_tokens (trip_id, token, created_by, share_map, share_bookings, share_packing, share_budget, share_collab) VALUES (?, ?, ?, 1, 1, 0, 0, 0)'
).run(trip.id, 'to-delete', user.id);
testDb
.prepare(
'INSERT INTO share_tokens (trip_id, token, created_by, share_map, share_bookings, share_packing, share_budget, share_collab) VALUES (?, ?, ?, 1, 1, 0, 0, 0)',
)
.run(trip.id, 'to-delete', user.id);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'delete_share_link', arguments: { tripId: trip.id } });
const data = parseToolResult(result) as any;
+87 -42
View File
@@ -1,6 +1,25 @@
/**
* Unit tests for MCP trip tools: create_trip, update_trip, delete_trip, list_trips, get_trip_summary.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import {
createUser,
createTrip,
createDay,
createPlace,
addTripMember,
createBudgetItem,
createPackingItem,
createReservation,
createDayNote,
createCollabNote,
createDayAssignment,
createDayAccommodation,
} 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';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -15,7 +34,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -32,12 +55,6 @@ vi.mock('../../../src/config', () => ({
const { broadcastMock } = vi.hoisted(() => ({ broadcastMock: vi.fn() }));
vi.mock('../../../src/websocket', () => ({ broadcast: broadcastMock }));
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createDay, createPlace, addTripMember, createBudgetItem, createPackingItem, createReservation, createDayNote, createCollabNote, createDayAssignment, createDayAccommodation } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -55,7 +72,11 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -70,7 +91,9 @@ describe('Tool: create_trip', () => {
const data = parseToolResult(result) as any;
expect(data.trip).toBeTruthy();
expect(data.trip.title).toBe('Summer Escape');
const days = testDb.prepare('SELECT COUNT(*) as c FROM days WHERE trip_id = ?').get(data.trip.id) as { c: number };
const days = testDb.prepare('SELECT COUNT(*) as c FROM days WHERE trip_id = ?').get(data.trip.id) as {
c: number;
};
expect(days.c).toBe(7);
});
});
@@ -83,7 +106,9 @@ describe('Tool: create_trip', () => {
arguments: { title: 'Paris Trip', start_date: '2026-07-01', end_date: '2026-07-05' },
});
const data = parseToolResult(result) as any;
const days = testDb.prepare('SELECT COUNT(*) as c FROM days WHERE trip_id = ?').get(data.trip.id) as { c: number };
const days = testDb.prepare('SELECT COUNT(*) as c FROM days WHERE trip_id = ?').get(data.trip.id) as {
c: number;
};
expect(days.c).toBe(5);
});
});
@@ -96,7 +121,9 @@ describe('Tool: create_trip', () => {
arguments: { title: 'Long Trip', start_date: '2026-01-01', end_date: '2027-12-31' },
});
const data = parseToolResult(result) as any;
const days = testDb.prepare('SELECT COUNT(*) as c FROM days WHERE trip_id = ?').get(data.trip.id) as { c: number };
const days = testDb.prepare('SELECT COUNT(*) as c FROM days WHERE trip_id = ?').get(data.trip.id) as {
c: number;
};
expect(days.c).toBe(90);
});
});
@@ -104,7 +131,10 @@ describe('Tool: create_trip', () => {
it('returns error for invalid start_date format', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'create_trip', arguments: { title: 'Trip', start_date: 'not-a-date' } });
const result = await h.client.callTool({
name: 'create_trip',
arguments: { title: 'Trip', start_date: 'not-a-date' },
});
expect(result.isError).toBe(true);
});
});
@@ -139,7 +169,10 @@ describe('Tool: update_trip', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id, { title: 'Old Title' });
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_trip', arguments: { tripId: trip.id, title: 'New Title' } });
const result = await h.client.callTool({
name: 'update_trip',
arguments: { tripId: trip.id, title: 'New Title' },
});
const data = parseToolResult(result) as any;
expect(data.trip.title).toBe('New Title');
});
@@ -193,11 +226,15 @@ describe('Tool: update_trip', () => {
const planRes = testDb.prepare('INSERT INTO vacay_plans (owner_id) VALUES (?)').run(user.id);
const planId = Number(planRes.lastInsertRowid);
testDb.prepare('INSERT INTO vacay_years (plan_id, year) VALUES (?, ?)').run(planId, 2026);
testDb.prepare(
'INSERT INTO vacay_user_years (user_id, plan_id, year, vacation_days, carried_over) VALUES (?, ?, ?, 30, 0)'
).run(user.id, planId, 2026);
testDb
.prepare(
'INSERT INTO vacay_user_years (user_id, plan_id, year, vacation_days, carried_over) VALUES (?, ?, ?, 30, 0)',
)
.run(user.id, planId, 2026);
for (const d of ['2026-08-03', '2026-08-04', '2026-08-05', '2026-08-06', '2026-08-07']) {
testDb.prepare('INSERT INTO vacay_entries (plan_id, user_id, date, note) VALUES (?, ?, ?, ?)').run(planId, user.id, d, '');
testDb
.prepare('INSERT INTO vacay_entries (plan_id, user_id, date, note) VALUES (?, ?, ?, ?)')
.run(planId, user.id, d, '');
}
await withHarness(user.id, async (h) => {
@@ -210,21 +247,19 @@ describe('Tool: update_trip', () => {
expect(data.trip.end_date).toBe('2026-08-16');
});
const oldWindow = testDb.prepare(
"SELECT date FROM vacay_entries WHERE plan_id = ? AND user_id = ? AND date BETWEEN '2026-08-01' AND '2026-08-09'"
).all(planId, user.id) as { date: string }[];
const oldWindow = testDb
.prepare(
"SELECT date FROM vacay_entries WHERE plan_id = ? AND user_id = ? AND date BETWEEN '2026-08-01' AND '2026-08-09'",
)
.all(planId, user.id) as { date: string }[];
expect(oldWindow).toHaveLength(0);
const shifted = testDb.prepare(
"SELECT date FROM vacay_entries WHERE plan_id = ? AND user_id = ? AND date BETWEEN '2026-08-08' AND '2026-08-16' ORDER BY date"
).all(planId, user.id) as { date: string }[];
expect(shifted.map(r => r.date)).toEqual([
'2026-08-10',
'2026-08-11',
'2026-08-12',
'2026-08-13',
'2026-08-14',
]);
const shifted = testDb
.prepare(
"SELECT date FROM vacay_entries WHERE plan_id = ? AND user_id = ? AND date BETWEEN '2026-08-08' AND '2026-08-16' ORDER BY date",
)
.all(planId, user.id) as { date: string }[];
expect(shifted.map((r) => r.date)).toEqual(['2026-08-10', '2026-08-11', '2026-08-12', '2026-08-13', '2026-08-14']);
});
it('shifts entries from the owners own plan even if another vacay plan is active', async () => {
@@ -236,17 +271,23 @@ describe('Tool: update_trip', () => {
const ownPlanRes = testDb.prepare('INSERT INTO vacay_plans (owner_id) VALUES (?)').run(user.id);
const ownPlanId = Number(ownPlanRes.lastInsertRowid);
testDb.prepare('INSERT INTO vacay_years (plan_id, year) VALUES (?, ?)').run(ownPlanId, 2026);
testDb.prepare(
'INSERT INTO vacay_user_years (user_id, plan_id, year, vacation_days, carried_over) VALUES (?, ?, ?, 30, 0)'
).run(user.id, ownPlanId, 2026);
testDb
.prepare(
'INSERT INTO vacay_user_years (user_id, plan_id, year, vacation_days, carried_over) VALUES (?, ?, ?, 30, 0)',
)
.run(user.id, ownPlanId, 2026);
for (const d of ['2026-09-02', '2026-09-03']) {
testDb.prepare('INSERT INTO vacay_entries (plan_id, user_id, date, note) VALUES (?, ?, ?, ?)').run(ownPlanId, user.id, d, '');
testDb
.prepare('INSERT INTO vacay_entries (plan_id, user_id, date, note) VALUES (?, ?, ?, ?)')
.run(ownPlanId, user.id, d, '');
}
// Different accepted plan becomes "active" for the owner.
const foreignPlanRes = testDb.prepare('INSERT INTO vacay_plans (owner_id) VALUES (?)').run(otherOwner.id);
const foreignPlanId = Number(foreignPlanRes.lastInsertRowid);
testDb.prepare('INSERT INTO vacay_plan_members (plan_id, user_id, status) VALUES (?, ?, ?)').run(foreignPlanId, user.id, 'accepted');
testDb
.prepare('INSERT INTO vacay_plan_members (plan_id, user_id, status) VALUES (?, ?, ?)')
.run(foreignPlanId, user.id, 'accepted');
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({
@@ -256,15 +297,19 @@ describe('Tool: update_trip', () => {
expect(result.isError).toBeFalsy();
});
const oldWindow = testDb.prepare(
"SELECT date FROM vacay_entries WHERE plan_id = ? AND user_id = ? AND date BETWEEN '2026-09-01' AND '2026-09-07' ORDER BY date"
).all(ownPlanId, user.id) as { date: string }[];
const oldWindow = testDb
.prepare(
"SELECT date FROM vacay_entries WHERE plan_id = ? AND user_id = ? AND date BETWEEN '2026-09-01' AND '2026-09-07' ORDER BY date",
)
.all(ownPlanId, user.id) as { date: string }[];
expect(oldWindow).toHaveLength(0);
const shifted = testDb.prepare(
"SELECT date FROM vacay_entries WHERE plan_id = ? AND user_id = ? AND date BETWEEN '2026-09-08' AND '2026-09-14' ORDER BY date"
).all(ownPlanId, user.id) as { date: string }[];
expect(shifted.map(r => r.date)).toEqual(['2026-09-09', '2026-09-10']);
const shifted = testDb
.prepare(
"SELECT date FROM vacay_entries WHERE plan_id = ? AND user_id = ? AND date BETWEEN '2026-09-08' AND '2026-09-14' ORDER BY date",
)
.all(ownPlanId, user.id) as { date: string }[];
expect(shifted.map((r) => r.date)).toEqual(['2026-09-09', '2026-09-10']);
});
});
+34 -13
View File
@@ -8,6 +8,12 @@
* list_holiday_countries, list_holidays.
* Resources: trek://vacay/plan, trek://vacay/entries/{year}.
*/
import { runMigrations } from '../../../src/db/migrations';
import { createTables } from '../../../src/db/schema';
import { createUser } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, parseResourceResult, type McpHarness } from '../../helpers/mcp-harness';
import { resetTestDb } from '../../helpers/test-db';
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
const { testDb, dbMock } = vi.hoisted(() => {
@@ -22,7 +28,11 @@ const { testDb, dbMock } = vi.hoisted(() => {
reinitialize: () => {},
getPlaceWithTags: () => null,
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -46,7 +56,7 @@ vi.mock('../../../src/services/adminService', () => ({
// Mock async service functions that make external calls
vi.mock('../../../src/services/vacayService', async (importOriginal) => {
const original = await importOriginal() as Record<string, unknown>;
const original = (await importOriginal()) as Record<string, unknown>;
return {
...original,
updatePlan: vi.fn().mockResolvedValue(undefined),
@@ -55,12 +65,6 @@ vi.mock('../../../src/services/vacayService', async (importOriginal) => {
};
});
import { createTables } from '../../../src/db/schema';
import { runMigrations } from '../../../src/db/migrations';
import { resetTestDb } from '../../helpers/test-db';
import { createUser } from '../../helpers/factories';
import { createMcpHarness, parseToolResult, parseResourceResult, type McpHarness } from '../../helpers/mcp-harness';
beforeAll(() => {
createTables(testDb);
runMigrations(testDb);
@@ -78,12 +82,20 @@ afterAll(() => {
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(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
async function withResourceHarness(userId: number, fn: (h: McpHarness) => Promise<void>) {
const h = await createMcpHarness({ userId, withResources: true });
try { await fn(h); } finally { await h.cleanup(); }
try {
await fn(h);
} finally {
await h.cleanup();
}
}
// ---------------------------------------------------------------------------
@@ -309,7 +321,10 @@ describe('Tool: update_vacay_stats', () => {
it('updates vacation days allowance and returns success', async () => {
const { user } = createUser(testDb);
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_vacay_stats', arguments: { year: 2025, vacationDays: 25 } });
const result = await h.client.callTool({
name: 'update_vacay_stats',
arguments: { year: 2025, vacationDays: 25 },
});
const data = parseToolResult(result) as any;
expect(data.success).toBe(true);
});
@@ -319,7 +334,10 @@ describe('Tool: update_vacay_stats', () => {
process.env.DEMO_MODE = 'true';
const { user } = createUser(testDb, { email: 'demo@nomad.app' });
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_vacay_stats', arguments: { year: 2025, vacationDays: 20 } });
const result = await h.client.callTool({
name: 'update_vacay_stats',
arguments: { year: 2025, vacationDays: 20 },
});
expect(result.isError).toBe(true);
});
});
@@ -383,7 +401,10 @@ describe('Tool: update_holiday_calendar', () => {
process.env.DEMO_MODE = 'true';
const { user } = createUser(testDb, { email: 'demo@nomad.app' });
await withHarness(user.id, async (h) => {
const result = await h.client.callTool({ name: 'update_holiday_calendar', arguments: { calendarId: 1, label: 'X' } });
const result = await h.client.callTool({
name: 'update_holiday_calendar',
arguments: { calendarId: 1, label: 'X' },
});
expect(result.isError).toBe(true);
});
});