mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
chore: move day backend to proper DI
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
/**
|
||||
* Unit tests for MCP resources (resources.ts).
|
||||
* Tests all 9 legacy-registrar resources via InMemoryTransport + Client
|
||||
* Tests all 7 legacy-registrar resources via InMemoryTransport + Client
|
||||
* (trek://categories moved to the DI-discovered CategoriesMcp — see
|
||||
* tools-categories.test.ts; trek://trips/{tripId}/todos moved to TodoMcp —
|
||||
* see tools-todos.test.ts; trek://trips/{tripId}/packing and .../packing/bags
|
||||
* moved to PackingMcp — see tools-packing.test.ts;
|
||||
* trek://trips/{tripId}/days/{dayId}/notes moved to DayNotesMcp — see
|
||||
* tools-notes.test.ts; trek://trips/{tripId}/collab-notes moved to CollabMcp —
|
||||
* see tools-notes.test.ts).
|
||||
* see tools-notes.test.ts; trek://trips/{tripId}/days and
|
||||
* .../accommodations moved to DaysMcp — see tools-days.test.ts and
|
||||
* tools-days-accommodations.test.ts).
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll, afterEach } from 'vitest';
|
||||
|
||||
@@ -46,7 +48,7 @@ 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, createReservation, createCollabNote, createBucketListItem, createVisitedCountry, createDayAssignment, createDayAccommodation } from '../../helpers/factories';
|
||||
import { createUser, createTrip, createPlace, addTripMember, createBudgetItem, createReservation, createCollabNote, createBucketListItem, createVisitedCountry } from '../../helpers/factories';
|
||||
import { createMcpHarness, parseResourceResult, type McpHarness } from '../../helpers/mcp-harness';
|
||||
|
||||
beforeAll(() => {
|
||||
@@ -151,38 +153,8 @@ describe('Resource: trek://trips/{tripId}', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resource: trek://trips/{tripId}/days', () => {
|
||||
it('returns days with assignments in order', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const day1 = createDay(testDb, trip.id, { day_number: 1 });
|
||||
const day2 = createDay(testDb, trip.id, { day_number: 2 });
|
||||
const place = createPlace(testDb, trip.id);
|
||||
createDayAssignment(testDb, day1.id, place.id);
|
||||
|
||||
await withHarness(user.id, async (harness) => {
|
||||
const result = await harness.client.readResource({ uri: `trek://trips/${trip.id}/days` });
|
||||
const days = parseResourceResult(result) as any[];
|
||||
expect(days).toHaveLength(2);
|
||||
expect(days[0].day_number).toBe(1);
|
||||
expect(days[0].assignments).toHaveLength(1);
|
||||
expect(days[1].day_number).toBe(2);
|
||||
expect(days[1].assignments).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns access denied for unauthorized trip', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const { user: other } = createUser(testDb);
|
||||
const trip = createTrip(testDb, other.id);
|
||||
|
||||
await withHarness(user.id, async (harness) => {
|
||||
const result = await harness.client.readResource({ uri: `trek://trips/${trip.id}/days` });
|
||||
const data = parseResourceResult(result) as any;
|
||||
expect(data.error).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
// The trek://trips/{tripId}/days resource moved to the DI-discovered
|
||||
// DaysMcp — see tools-days.test.ts.
|
||||
|
||||
describe('Resource: trek://trips/{tripId}/places', () => {
|
||||
it('returns all places for a trip', async () => {
|
||||
@@ -250,35 +222,8 @@ describe('Resource: trek://trips/{tripId}/budget', () => {
|
||||
// The trek://trips/{tripId}/days/{dayId}/notes resource moved to the
|
||||
// DI-discovered DayNotesMcp — see tools-notes.test.ts.
|
||||
|
||||
describe('Resource: trek://trips/{tripId}/accommodations', () => {
|
||||
it('returns accommodations for a trip', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const day1 = createDay(testDb, trip.id, { day_number: 1 });
|
||||
const day2 = createDay(testDb, trip.id, { day_number: 2 });
|
||||
const place = createPlace(testDb, trip.id, { name: 'Grand Hotel' });
|
||||
createDayAccommodation(testDb, trip.id, place.id, day1.id, day2.id);
|
||||
|
||||
await withHarness(user.id, async (harness) => {
|
||||
const result = await harness.client.readResource({ uri: `trek://trips/${trip.id}/accommodations` });
|
||||
const items = parseResourceResult(result) as any[];
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0].place_name).toBe('Grand Hotel');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns access denied for unauthorized trip', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const { user: other } = createUser(testDb);
|
||||
const trip = createTrip(testDb, other.id);
|
||||
|
||||
await withHarness(user.id, async (harness) => {
|
||||
const result = await harness.client.readResource({ uri: `trek://trips/${trip.id}/accommodations` });
|
||||
const data = parseResourceResult(result) as any;
|
||||
expect(data.error).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
// The trek://trips/{tripId}/accommodations resource moved to the
|
||||
// DI-discovered DaysMcp — see tools-days-accommodations.test.ts.
|
||||
|
||||
describe('Resource: trek://trips/{tripId}/members', () => {
|
||||
it('returns owner and collaborators', async () => {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/**
|
||||
* Unit tests for MCP day and accommodation tools:
|
||||
* create_day, delete_day,
|
||||
* create_accommodation, update_accommodation, delete_accommodation.
|
||||
* create_accommodation, create_place_accommodation, update_accommodation,
|
||||
* delete_accommodation — plus the trek://trips/{tripId}/accommodations
|
||||
* resource (moved from resources.test.ts when the legacy registrar was
|
||||
* ported to the DI-discovered DaysMcp).
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
||||
|
||||
@@ -15,7 +18,18 @@ const { testDb, dbMock } = vi.hoisted(() => {
|
||||
db,
|
||||
closeDb: () => {},
|
||||
reinitialize: () => {},
|
||||
getPlaceWithTags: () => null,
|
||||
// Real-SQL implementation: create_place_accommodation's createPlace
|
||||
// returns getPlaceWithTags(placeId) — a null stub would make the tool's
|
||||
// atomic branch throw before the accommodation insert.
|
||||
getPlaceWithTags: (placeId: number | string) => {
|
||||
const place = 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) as { category_id: number | null; category_name: string; category_color: string; category_icon: string } | undefined;
|
||||
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 };
|
||||
},
|
||||
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),
|
||||
isOwner: (tripId: any, userId: number) =>
|
||||
@@ -38,7 +52,7 @@ 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';
|
||||
import { createMcpHarness, parseToolResult, parseResourceResult, type McpHarness } from '../../helpers/mcp-harness';
|
||||
|
||||
beforeAll(() => {
|
||||
createTables(testDb);
|
||||
@@ -294,3 +308,79 @@ describe('Tool: delete_accommodation', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// create_place_accommodation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Tool: create_place_accommodation', () => {
|
||||
it('creates the place and the accommodation atomically and broadcasts both', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const day = createDay(testDb, trip.id);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.callTool({
|
||||
name: 'create_place_accommodation',
|
||||
arguments: {
|
||||
tripId: trip.id, name: 'Ryokan Sakura', start_day_id: day.id, end_day_id: day.id,
|
||||
check_in: '15:00', accommodation_notes: 'Tatami room',
|
||||
},
|
||||
});
|
||||
const data = parseToolResult(result) as { place: { id: number; name: string }; accommodation: { id: number } };
|
||||
expect(data.place.name).toBe('Ryokan Sakura');
|
||||
expect(data.accommodation).toMatchObject({ place_id: data.place.id, start_day_id: day.id, end_day_id: day.id, notes: 'Tatami room' });
|
||||
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'place:created', expect.objectContaining({ place: expect.anything() }));
|
||||
expect(broadcastMock).toHaveBeenCalledWith(trip.id, 'accommodation:created', expect.objectContaining({ accommodation: expect.anything() }));
|
||||
// The partner hotel reservation rides along.
|
||||
const linked = testDb.prepare('SELECT type FROM reservations WHERE accommodation_id = ?').get(data.accommodation.id) as { type: string };
|
||||
expect(linked.type).toBe('hotel');
|
||||
});
|
||||
});
|
||||
|
||||
it('reports the day-validation errors without creating anything', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.callTool({
|
||||
name: 'create_place_accommodation',
|
||||
arguments: { tripId: trip.id, name: 'Nowhere Inn', start_day_id: 99999, end_day_id: 99999 },
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
expect(testDb.prepare('SELECT COUNT(*) as n FROM places WHERE trip_id = ?').get(trip.id)).toMatchObject({ n: 0 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// trek://trips/{tripId}/accommodations resource (moved from resources.test.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Resource: trek://trips/{tripId}/accommodations', () => {
|
||||
it('returns accommodations for a trip', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const day1 = createDay(testDb, trip.id, { day_number: 1 });
|
||||
const day2 = createDay(testDb, trip.id, { day_number: 2 });
|
||||
const place = createPlace(testDb, trip.id, { name: 'Grand Hotel' });
|
||||
createDayAccommodation(testDb, trip.id, place.id, day1.id, day2.id);
|
||||
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.readResource({ uri: `trek://trips/${trip.id}/accommodations` });
|
||||
const items = parseResourceResult(result) as any[];
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0].place_name).toBe('Grand Hotel');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns access denied for unauthorized trip', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const { user: other } = createUser(testDb);
|
||||
const trip = createTrip(testDb, other.id);
|
||||
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.readResource({ uri: `trek://trips/${trip.id}/accommodations` });
|
||||
const data = parseResourceResult(result) as any;
|
||||
expect(data.error).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/**
|
||||
* Unit tests for MCP day tools: update_day.
|
||||
* Unit tests for the DI-discovered DaysMcp: the update_day tool and the
|
||||
* trek://trips/{tripId}/days resource (moved from resources.test.ts when the
|
||||
* legacy registrar was ported).
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
||||
|
||||
@@ -35,8 +37,8 @@ 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';
|
||||
import { createUser, createTrip, createDay, createPlace, createDayAssignment } from '../../helpers/factories';
|
||||
import { createMcpHarness, parseToolResult, parseResourceResult, type McpHarness } from '../../helpers/mcp-harness';
|
||||
|
||||
beforeAll(() => {
|
||||
createTables(testDb);
|
||||
@@ -140,3 +142,41 @@ describe('Tool: update_day', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// trek://trips/{tripId}/days resource (moved from resources.test.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Resource: trek://trips/{tripId}/days', () => {
|
||||
it('returns days with assignments in order', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const day1 = createDay(testDb, trip.id, { day_number: 1 });
|
||||
const day2 = createDay(testDb, trip.id, { day_number: 2 });
|
||||
const place = createPlace(testDb, trip.id);
|
||||
createDayAssignment(testDb, day1.id, place.id);
|
||||
void day2;
|
||||
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.readResource({ uri: `trek://trips/${trip.id}/days` });
|
||||
const days = parseResourceResult(result) as any[];
|
||||
expect(days).toHaveLength(2);
|
||||
expect(days[0].day_number).toBe(1);
|
||||
expect(days[0].assignments).toHaveLength(1);
|
||||
expect(days[1].day_number).toBe(2);
|
||||
expect(days[1].assignments).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns access denied for unauthorized trip', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const { user: other } = createUser(testDb);
|
||||
const trip = createTrip(testDb, other.id);
|
||||
|
||||
await withHarness(user.id, async (h) => {
|
||||
const result = await h.client.readResource({ uri: `trek://trips/${trip.id}/days` });
|
||||
const data = parseResourceResult(result) as any;
|
||||
expect(data.error).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user