202 lines
6.8 KiB
TypeScript
202 lines
6.8 KiB
TypeScript
/**
|
|
* Workspace Templates API Tests
|
|
*
|
|
* Validates GET (built-in + user-created) and POST (create custom) endpoints.
|
|
* Uses the standard pattern: tmpDir + buildLocalServer + server.inject().
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import os from 'node:os';
|
|
import { MindDB } from '@waggle/core';
|
|
import { buildLocalServer } from '../src/local/index.js';
|
|
import type { FastifyInstance } from 'fastify';
|
|
import { injectWithAuth } from './test-utils.js';
|
|
|
|
interface TemplateResponse {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
persona: string;
|
|
connectors: string[];
|
|
suggestedCommands: string[];
|
|
starterMemory: string[];
|
|
builtIn: boolean;
|
|
}
|
|
|
|
describe('Workspace Templates API', () => {
|
|
let server: FastifyInstance;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-ws-templates-'));
|
|
|
|
// Create personal.mind (required by buildLocalServer)
|
|
const personalPath = path.join(tmpDir, 'personal.mind');
|
|
const mind = new MindDB(personalPath);
|
|
mind.close();
|
|
|
|
server = await buildLocalServer({ dataDir: tmpDir });
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await server.close();
|
|
try {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
} catch {
|
|
// Non-critical — OS will clean temp dir
|
|
}
|
|
});
|
|
|
|
it('GET /api/workspace-templates returns 15 built-in templates', async () => {
|
|
const res = await injectWithAuth(server, {
|
|
method: 'GET',
|
|
url: '/api/workspace-templates',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
const body = JSON.parse(res.body) as { templates: TemplateResponse[]; count: number };
|
|
expect(body.templates).toBeDefined();
|
|
expect(body.count).toBe(15);
|
|
expect(body.templates.length).toBe(15);
|
|
});
|
|
|
|
it('each built-in template has all required fields', async () => {
|
|
const res = await injectWithAuth(server, {
|
|
method: 'GET',
|
|
url: '/api/workspace-templates',
|
|
});
|
|
|
|
const body = JSON.parse(res.body) as { templates: TemplateResponse[] };
|
|
for (const template of body.templates) {
|
|
expect(template.id).toBeDefined();
|
|
expect(typeof template.name).toBe('string');
|
|
expect(template.name.length).toBeGreaterThan(0);
|
|
expect(typeof template.description).toBe('string');
|
|
expect(template.description.length).toBeGreaterThan(0);
|
|
expect(typeof template.persona).toBe('string');
|
|
expect(template.persona.length).toBeGreaterThan(0);
|
|
expect(Array.isArray(template.connectors)).toBe(true);
|
|
expect(Array.isArray(template.suggestedCommands)).toBe(true);
|
|
expect(template.suggestedCommands.length).toBeGreaterThan(0);
|
|
expect(Array.isArray(template.starterMemory)).toBe(true);
|
|
// `blank` template intentionally starts with empty starterMemory;
|
|
// all other templates must seed some context.
|
|
if (template.id !== 'blank') {
|
|
expect(template.starterMemory.length).toBeGreaterThan(0);
|
|
}
|
|
expect(template.builtIn).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('built-in templates have correct IDs', async () => {
|
|
const res = await injectWithAuth(server, {
|
|
method: 'GET',
|
|
url: '/api/workspace-templates',
|
|
});
|
|
|
|
const body = JSON.parse(res.body) as { templates: TemplateResponse[] };
|
|
const ids = body.templates.map(t => t.id);
|
|
expect(ids).toContain('sales-pipeline');
|
|
expect(ids).toContain('research-project');
|
|
expect(ids).toContain('code-review');
|
|
expect(ids).toContain('marketing-campaign');
|
|
expect(ids).toContain('product-launch');
|
|
expect(ids).toContain('legal-review');
|
|
});
|
|
|
|
it('built-in templates map to valid persona IDs', async () => {
|
|
// Kept in sync with BUILT_IN_TEMPLATES — expanded to 15 templates
|
|
// following the Template/Persona Architecture expansion (see CLAUDE.md §5).
|
|
const validPersonaIds = [
|
|
'researcher', 'writer', 'analyst', 'coder',
|
|
'project-manager', 'executive-assistant', 'sales-rep', 'marketer',
|
|
'support-agent', 'finance-owner', 'hr-manager', 'ops-manager',
|
|
'data-engineer', 'recruiter', 'creative-director', 'general-purpose',
|
|
];
|
|
|
|
const res = await injectWithAuth(server, {
|
|
method: 'GET',
|
|
url: '/api/workspace-templates',
|
|
});
|
|
|
|
const body = JSON.parse(res.body) as { templates: TemplateResponse[] };
|
|
for (const template of body.templates) {
|
|
expect(validPersonaIds).toContain(template.persona);
|
|
}
|
|
});
|
|
|
|
it('POST /api/workspace-templates creates a custom template', async () => {
|
|
const res = await injectWithAuth(server, {
|
|
method: 'POST',
|
|
url: '/api/workspace-templates',
|
|
payload: {
|
|
name: 'My Custom Template',
|
|
description: 'A test template for custom workflows.',
|
|
persona: 'writer',
|
|
connectors: ['slack'],
|
|
suggestedCommands: ['/draft'],
|
|
starterMemory: ['This workspace uses a custom template.'],
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
const body = JSON.parse(res.body) as TemplateResponse;
|
|
expect(body.id).toMatch(/^custom-/);
|
|
expect(body.name).toBe('My Custom Template');
|
|
expect(body.description).toBe('A test template for custom workflows.');
|
|
expect(body.persona).toBe('writer');
|
|
expect(body.connectors).toEqual(['slack']);
|
|
expect(body.builtIn).toBe(false);
|
|
});
|
|
|
|
it('custom template appears in GET after POST', async () => {
|
|
const res = await injectWithAuth(server, {
|
|
method: 'GET',
|
|
url: '/api/workspace-templates',
|
|
});
|
|
|
|
const body = JSON.parse(res.body) as { templates: TemplateResponse[]; count: number };
|
|
// 15 built-in + 1 custom from previous test
|
|
expect(body.count).toBe(16);
|
|
const custom = body.templates.find(t => t.name === 'My Custom Template');
|
|
expect(custom).toBeDefined();
|
|
expect(custom!.builtIn).toBe(false);
|
|
});
|
|
|
|
it('POST /api/workspace-templates rejects missing required fields', async () => {
|
|
const res = await injectWithAuth(server, {
|
|
method: 'POST',
|
|
url: '/api/workspace-templates',
|
|
payload: {
|
|
name: 'Incomplete Template',
|
|
// missing description, persona, connectors, suggestedCommands, starterMemory
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(400);
|
|
const body = JSON.parse(res.body) as { error: string };
|
|
expect(body.error).toBeDefined();
|
|
});
|
|
|
|
it('POST /api/workspace-templates rejects invalid field types', async () => {
|
|
const res = await injectWithAuth(server, {
|
|
method: 'POST',
|
|
url: '/api/workspace-templates',
|
|
payload: {
|
|
name: 'Bad Template',
|
|
description: 'A template with bad types.',
|
|
persona: 'writer',
|
|
connectors: 'not-an-array', // should be array
|
|
suggestedCommands: ['/help'],
|
|
starterMemory: ['test'],
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(400);
|
|
const body = JSON.parse(res.body) as { error: string };
|
|
expect(body.error).toContain('connectors');
|
|
});
|
|
});
|