This commit is contained in:
201
packages/server/tests/workspace-templates.test.ts
Normal file
201
packages/server/tests/workspace-templates.test.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user