mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-09-30 08:53:18 +00:00
120 lines
4.8 KiB
TypeScript
120 lines
4.8 KiB
TypeScript
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
import { InMemoryTasksClient } from '../src/dev-client.js';
|
||
|
|
|
||
|
|
const LAUREN = 'lauren@seahavenind.com';
|
||
|
|
const ADAM = 'adam@seahavenind.com';
|
||
|
|
|
||
|
|
describe('InMemoryTasksClient', () => {
|
||
|
|
describe('createTask', () => {
|
||
|
|
it('returns a task with a generated id and completed:false', async () => {
|
||
|
|
const client = new InMemoryTasksClient();
|
||
|
|
const task = await client.createTask({ sub: LAUREN, title: 'Buy gloves' });
|
||
|
|
expect(task.taskId).toMatch(/^task-\d{4}$/);
|
||
|
|
expect(task.sub).toBe(LAUREN);
|
||
|
|
expect(task.title).toBe('Buy gloves');
|
||
|
|
expect(task.completed).toBe(false);
|
||
|
|
expect(task.createdAt).toBeDefined();
|
||
|
|
expect(task.description).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('carries an optional description through', async () => {
|
||
|
|
const client = new InMemoryTasksClient();
|
||
|
|
const task = await client.createTask({ sub: LAUREN, title: 'T', description: 'detail' });
|
||
|
|
expect(task.description).toBe('detail');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('a task created for sub A is not visible to sub B (partitioned)', async () => {
|
||
|
|
const client = new InMemoryTasksClient();
|
||
|
|
const task = await client.createTask({ sub: LAUREN, title: 'Lauren only' });
|
||
|
|
const adamTasks = await client.listTasks({ sub: ADAM, includeCompleted: true });
|
||
|
|
expect(adamTasks.map((t) => t.taskId)).not.toContain(task.taskId);
|
||
|
|
const laurenTasks = await client.listTasks({ sub: LAUREN, includeCompleted: true });
|
||
|
|
expect(laurenTasks.map((t) => t.taskId)).toContain(task.taskId);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('creates a task for a previously-unknown sub', async () => {
|
||
|
|
const client = new InMemoryTasksClient();
|
||
|
|
const task = await client.createTask({ sub: 'fresh@seahavenind.com', title: 'First' });
|
||
|
|
const tasks = await client.listTasks({
|
||
|
|
sub: 'fresh@seahavenind.com',
|
||
|
|
includeCompleted: true,
|
||
|
|
});
|
||
|
|
expect(tasks.map((t) => t.taskId)).toEqual([task.taskId]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('listTasks', () => {
|
||
|
|
it('omits completed tasks by default', async () => {
|
||
|
|
const client = new InMemoryTasksClient();
|
||
|
|
const tasks = await client.listTasks({ sub: LAUREN });
|
||
|
|
const ids = tasks.map((t) => t.taskId);
|
||
|
|
expect(ids).toContain('task-0001'); // open
|
||
|
|
expect(ids).not.toContain('task-0002'); // completed
|
||
|
|
});
|
||
|
|
|
||
|
|
it('includes completed tasks when includeCompleted is true', async () => {
|
||
|
|
const client = new InMemoryTasksClient();
|
||
|
|
const tasks = await client.listTasks({ sub: LAUREN, includeCompleted: true });
|
||
|
|
const ids = tasks.map((t) => t.taskId);
|
||
|
|
expect(ids).toContain('task-0001');
|
||
|
|
expect(ids).toContain('task-0002');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns an empty list for an unknown sub', async () => {
|
||
|
|
const client = new InMemoryTasksClient();
|
||
|
|
expect(await client.listTasks({ sub: 'nobody@example.com' })).toEqual([]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('completeTask', () => {
|
||
|
|
it('sets completed and completedAt', async () => {
|
||
|
|
const client = new InMemoryTasksClient();
|
||
|
|
const completed = await client.completeTask({ sub: LAUREN, taskId: 'task-0001' });
|
||
|
|
expect(completed.completed).toBe(true);
|
||
|
|
expect(completed.completedAt).toBeDefined();
|
||
|
|
// now omitted from the default (open-only) listing
|
||
|
|
const open = await client.listTasks({ sub: LAUREN });
|
||
|
|
expect(open.map((t) => t.taskId)).not.toContain('task-0001');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('throws for an unknown taskId', async () => {
|
||
|
|
const client = new InMemoryTasksClient();
|
||
|
|
await expect(client.completeTask({ sub: LAUREN, taskId: 'task-missing' })).rejects.toThrow(
|
||
|
|
/not found/,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('throws when the task belongs to another sub', async () => {
|
||
|
|
const client = new InMemoryTasksClient();
|
||
|
|
await expect(client.completeTask({ sub: ADAM, taskId: 'task-0001' })).rejects.toThrow(
|
||
|
|
/not found/,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('deleteTask', () => {
|
||
|
|
it('removes the task so subsequent listings omit it', async () => {
|
||
|
|
const client = new InMemoryTasksClient();
|
||
|
|
await client.deleteTask({ sub: LAUREN, taskId: 'task-0001' });
|
||
|
|
const tasks = await client.listTasks({ sub: LAUREN, includeCompleted: true });
|
||
|
|
expect(tasks.map((t) => t.taskId)).not.toContain('task-0001');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('is a no-op for an unknown taskId', async () => {
|
||
|
|
const client = new InMemoryTasksClient();
|
||
|
|
await expect(
|
||
|
|
client.deleteTask({ sub: LAUREN, taskId: 'task-missing' }),
|
||
|
|
).resolves.toBeUndefined();
|
||
|
|
const tasks = await client.listTasks({ sub: LAUREN, includeCompleted: true });
|
||
|
|
expect(tasks).toHaveLength(2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('is a no-op for an unknown sub', async () => {
|
||
|
|
const client = new InMemoryTasksClient();
|
||
|
|
await expect(
|
||
|
|
client.deleteTask({ sub: 'nobody@example.com', taskId: 'task-0001' }),
|
||
|
|
).resolves.toBeUndefined();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|