Skip to main content
Glama
plans.test.js14.6 kB
const request = require('supertest'); const { createApp } = require('./server'); describe('Plans API', () => { let app; let db; beforeEach(async () => { const instance = await createApp({ skipMigration: true }); app = instance.app; db = instance.db; }); afterEach(() => { db.close(); }); test('POST /plans with valid title and description succeeds and persists', async () => { const response = await request(app) .post('/plans') .send({ title: 'Test Plan', description: 'Test description' }) .expect(201); expect(response.body).toHaveProperty('id', 1); expect(response.body).toHaveProperty('title', 'Test Plan'); expect(response.body).toHaveProperty('description', 'Test description'); expect(response.body).toHaveProperty('status', 'proposed'); // Verify persistence const getResponse = await request(app) .get('/plans') .expect(200); expect(getResponse.body).toHaveLength(1); expect(getResponse.body[0].id).toBe(1); expect(getResponse.body[0].title).toBe('Test Plan'); expect(getResponse.body[0].description).toBe('Test description'); expect(getResponse.body[0].status).toBe('proposed'); expect(getResponse.body[0].timestamp).toBeDefined(); }); test('POST /plans with missing title returns 400', async () => { await request(app) .post('/plans') .send({ description: 'Test description' }) .expect(400); }); test('POST /plans with empty description returns 400', async () => { await request(app) .post('/plans') .send({ title: 'Test Plan', description: '' }) .expect(400); }); test('POST /plans allows duplicate titles with incrementing ID', async () => { await request(app) .post('/plans') .send({ title: 'Duplicate Plan', description: 'Description 1' }) .expect(201); const secondResponse = await request(app) .post('/plans') .send({ title: 'Duplicate Plan', description: 'Description 2' }) .expect(201); expect(secondResponse.body.id).toBe(2); expect(secondResponse.body.title).toBe('Duplicate Plan'); expect(secondResponse.body.description).toBe('Description 2'); expect(secondResponse.body.status).toBe('proposed'); // Verify both persisted const getResponse = await request(app) .get('/plans') .expect(200); expect(getResponse.body).toHaveLength(2); expect(getResponse.body[0].title).toBe('Duplicate Plan'); expect(getResponse.body[1].title).toBe('Duplicate Plan'); expect(getResponse.body[1].id).toBe(2); }); test('Regression: POST /thoughts still works', async () => { const response = await request(app) .post('/thoughts') .send({ content: 'Test thought' }) .expect(201); expect(response.body).toHaveProperty('id', '1'); expect(response.body).toHaveProperty('content', 'Test thought'); expect(response.body).toHaveProperty('timestamp'); // Verify using GET /thoughts const getResponse = await request(app) .get('/thoughts') .expect(200); expect(getResponse.body).toHaveLength(1); expect(getResponse.body[0]).toEqual(response.body); }); test('PATCH /plans/:id updates status successfully', async () => { // Create a plan first const createResponse = await request(app) .post('/plans') .send({ title: 'Test Plan', description: 'Test description' }) .expect(201); const planId = createResponse.body.id; // Update status const updateResponse = await request(app) .patch(`/plans/${planId}`) .send({ status: 'in_progress' }) .expect(200); expect(updateResponse.body).toEqual({ status: 'in_progress' }); // Verify persistence with GET const getResponse = await request(app) .get(`/plans/${planId}`) .expect(200); expect(getResponse.body.status).toBe('in_progress'); expect(getResponse.body.id).toBe(planId); }); test('PATCH /plans/:id with invalid status returns 400', async () => { // Create a plan const createResponse = await request(app) .post('/plans') .send({ title: 'Test Plan', description: 'Test description' }) .expect(201); const planId = createResponse.body.id; await request(app) .patch(`/plans/${planId}`) .send({ status: 'invalid_status' }) .expect(400); }); test('PATCH /plans/:id for non-existent plan returns 404', async () => { await request(app) .patch('/plans/999') .send({ status: 'completed' }) .expect(404); }); test('PATCH /plans/:id without status does nothing but succeeds', async () => { // Create a plan const createResponse = await request(app) .post('/plans') .send({ title: 'Test Plan', description: 'Test description' }) .expect(201); const planId = createResponse.body.id; const originalStatus = createResponse.body.status; // PATCH without status const updateResponse = await request(app) .patch(`/plans/${planId}`) .send({}) .expect(200); expect(updateResponse.body.status).toBe(originalStatus); // Verify no change const getResponse = await request(app) .get(`/plans/${planId}`) .expect(200); expect(getResponse.body.status).toBe(originalStatus); }); test('GET /plans returns empty array when no plans exist', async () => { const response = await request(app) .get('/plans') .expect(200); expect(response.body).toEqual([]); }); test('GET /plans returns plans sorted chronologically ascending', async () => { // Create plans with different timestamps (order implies ascending) await request(app) .post('/plans') .send({ title: 'First Plan', description: 'First description' }) .expect(201); await request(app) .post('/plans') .send({ title: 'Second Plan', description: 'Second description' }) .expect(201); const response = await request(app) .get('/plans') .expect(200); expect(response.body).toHaveLength(2); expect(response.body[0].title).toBe('First Plan'); expect(response.body[1].title).toBe('Second Plan'); // Verify timestamps ascending const timestamps = response.body.map(p => new Date(p.timestamp)); expect(timestamps[0] < timestamps[1]).toBe(true); }); test('GET /plans integrates with POST and PATCH, returns sorted with updated status', async () => { // Create first plan const firstResponse = await request(app) .post('/plans') .send({ title: 'Plan A', description: 'Desc A' }) .expect(201); // Create second plan const secondResponse = await request(app) .post('/plans') .send({ title: 'Plan B', description: 'Desc B' }) .expect(201); // Update second plan status await request(app) .patch(`/plans/${secondResponse.body.id}`) .send({ status: 'in_progress' }) .expect(200); // GET all plans const getResponse = await request(app) .get('/plans') .expect(200); expect(getResponse.body).toHaveLength(2); expect(getResponse.body[0].title).toBe('Plan A'); expect(getResponse.body[0].status).toBe('proposed'); expect(getResponse.body[1].title).toBe('Plan B'); expect(getResponse.body[1].status).toBe('in_progress'); expect(getResponse.body[1].description).toBe('Desc B'); expect(getResponse.body[1].timestamp).toBeDefined(); // Timestamps ascending const timestamps = getResponse.body.map(p => new Date(p.timestamp)); expect(timestamps[0] < timestamps[1]).toBe(true); }); }); describe('Plan Thoughts Linking', () => { let app; let db; beforeEach(async () => { const instance = await createApp({ skipMigration: true }); app = instance.app; db = instance.db; }); afterEach(() => { db.close(); }); test('GET /plans/:id/thoughts returns linked thoughts sorted by timestamp', async () => { // Create plan const planResponse = await request(app) .post('/plans') .send({ title: 'Test Plan', description: 'Test desc' }) .expect(201); const planId = planResponse.body.id.toString(); // Create two thoughts linked to plan await request(app) .post('/thoughts') .send({ content: 'First linked thought', plan_id: planId }) .expect(201); const secondThought = await request(app) .post('/thoughts') .send({ content: 'Second linked thought', plan_id: planId }) .expect(201); // GET linked thoughts const thoughtsResponse = await request(app) .get(`/plans/${planId}/thoughts`) .expect(200); expect(thoughtsResponse.body).toHaveLength(2); expect(thoughtsResponse.body[0].content).toBe('First linked thought'); expect(thoughtsResponse.body[1].content).toBe('Second linked thought'); // Verify timestamps ascending const timestamps = thoughtsResponse.body.map(t => new Date(t.timestamp)); expect(timestamps[0] < timestamps[1]).toBe(true); }); test('GET /plans/:id/thoughts returns empty array if no linked thoughts', async () => { // Create plan const planResponse = await request(app) .post('/plans') .send({ title: 'Test Plan', description: 'Test desc' }) .expect(201); const planId = planResponse.body.id.toString(); // Create a thought without plan_id await request(app) .post('/thoughts') .send({ content: 'Unlinked thought' }) .expect(201); // GET linked thoughts const thoughtsResponse = await request(app) .get(`/plans/${planId}/thoughts`) .expect(200); expect(thoughtsResponse.body).toEqual([]); }); test('GET /plans/:id/thoughts returns 200 [] for non-existent plan', async () => { const response = await request(app) .get('/plans/999/thoughts') .expect(200); expect(response.body).toEqual([]); }); test('GET /plans/:id/thoughts excludes thoughts with different plan_id', async () => { // Create plan 1 const plan1Response = await request(app) .post('/plans') .send({ title: 'Plan 1', description: 'Desc 1' }) .expect(201); const plan1Id = plan1Response.body.id.toString(); // Create plan 2 const plan2Response = await request(app) .post('/plans') .send({ title: 'Plan 2', description: 'Desc 2' }) .expect(201); const plan2Id = plan2Response.body.id.toString(); // Create thought for plan 2 await request(app) .post('/thoughts') .send({ content: 'Thought for plan 2', plan_id: plan2Id }) .expect(201); // GET for plan 1 const thoughtsResponse = await request(app) .get(`/plans/${plan1Id}/thoughts`) .expect(200); expect(thoughtsResponse.body).toEqual([]); }); }); describe('Plan Changelog', () => { let app; let db; beforeEach(async () => { const instance = await createApp({ skipMigration: true }); app = instance.app; db = instance.db; }); afterEach(() => { db.close(); }); test('PATCH /plans/:id/changelog appends entry with timestamp and returns updated plan', async () => { // Create plan const createResponse = await request(app) .post('/plans') .send({ title: 'Test Plan', description: 'Test desc' }) .expect(201); const planId = createResponse.body.id; // Append changelog const change = 'Initial change'; const updateResponse = await request(app) .patch(`/plans/${planId}/changelog`) .send({ change }) .expect(200); expect(updateResponse.body.changelog).toHaveLength(1); expect(updateResponse.body.changelog[0].change).toBe(change); expect(updateResponse.body.changelog[0].timestamp).toBeDefined(); expect(updateResponse.body.id).toBe(planId); // Verify full plan returned expect(updateResponse.body.title).toBe('Test Plan'); }); test('Multiple appends to changelog accumulate in order', async () => { const createResponse = await request(app) .post('/plans') .send({ title: 'Test Plan', description: 'Test desc' }) .expect(201); const planId = createResponse.body.id; const changes = ['First change', 'Second change']; let timestamps = []; // First append const firstResponse = await request(app) .patch(`/plans/${planId}/changelog`) .send({ change: changes[0] }) .expect(200); timestamps.push(firstResponse.body.changelog[0].timestamp); // Second append const secondResponse = await request(app) .patch(`/plans/${planId}/changelog`) .send({ change: changes[1] }) .expect(200); timestamps.push(secondResponse.body.changelog[1].timestamp); expect(secondResponse.body.changelog).toHaveLength(2); expect(secondResponse.body.changelog[0].change).toBe(changes[0]); expect(secondResponse.body.changelog[1].change).toBe(changes[1]); expect(new Date(timestamps[0]) < new Date(timestamps[1])).toBe(true); }); test('PATCH /plans/:id/changelog for non-existent plan returns 404', async () => { await request(app) .patch('/plans/999/changelog') .send({ change: 'Test change' }) .expect(404); }); test('PATCH /plans/:id/changelog with empty entry returns 400', async () => { const createResponse = await request(app) .post('/plans') .send({ title: 'Test Plan', description: 'Test desc' }) .expect(201); const planId = createResponse.body.id; await request(app) .patch(`/plans/${planId}/changelog`) .send({ change: '' }) .expect(400); }); test('Integration: Changelog appears in GET /plans and does not affect GET /plans/:id/thoughts', async () => { // Create plan const createResponse = await request(app) .post('/plans') .send({ title: 'Test Plan', description: 'Test desc' }) .expect(201); const planId = createResponse.body.id.toString(); // Append changelog await request(app) .patch(`/plans/${planId}/changelog`) .send({ change: 'Test changelog change' }) .expect(200); // GET /plans const plansResponse = await request(app) .get('/plans') .expect(200); const plan = plansResponse.body.find(p => p.id === parseInt(planId)); expect(plan.changelog).toHaveLength(1); expect(plan.changelog[0].change).toBe('Test changelog change'); // GET /plans/:id/thoughts (should be empty, unaffected) const thoughtsResponse = await request(app) .get(`/plans/${planId}/thoughts`) .expect(200); expect(thoughtsResponse.body).toEqual([]); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/suttonwilliamd/tpc-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server