This commit is contained in:
182
packages/server/tests/routes/starter-catalog.test.ts
Normal file
182
packages/server/tests/routes/starter-catalog.test.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } 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';
|
||||
|
||||
/** Skill ID used for install tests — cleaned up between tests to avoid 409 conflicts */
|
||||
const INSTALL_TEST_SKILL = 'retrospective';
|
||||
|
||||
describe('Starter Skill Catalog', () => {
|
||||
let server: FastifyInstance;
|
||||
let tmpDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-catalog-test-'));
|
||||
const personalPath = path.join(tmpDir, 'personal.mind');
|
||||
const mind = new MindDB(personalPath);
|
||||
mind.close();
|
||||
server = await buildLocalServer({ dataDir: tmpDir });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('GET /api/skills/starter-pack/catalog returns all starter skills', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: '/api/skills/starter-pack/catalog',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = res.json();
|
||||
expect(body.skills).toBeDefined();
|
||||
expect(Array.isArray(body.skills)).toBe(true);
|
||||
expect(body.skills.length).toBe(18);
|
||||
});
|
||||
|
||||
it('each skill has the correct shape', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: '/api/skills/starter-pack/catalog',
|
||||
});
|
||||
|
||||
const body = res.json();
|
||||
for (const skill of body.skills) {
|
||||
expect(skill).toHaveProperty('id');
|
||||
expect(skill).toHaveProperty('name');
|
||||
expect(skill).toHaveProperty('description');
|
||||
expect(skill).toHaveProperty('family');
|
||||
expect(skill).toHaveProperty('familyLabel');
|
||||
expect(skill).toHaveProperty('state');
|
||||
expect(skill).toHaveProperty('isWorkflow');
|
||||
expect(typeof skill.id).toBe('string');
|
||||
expect(typeof skill.name).toBe('string');
|
||||
expect(typeof skill.description).toBe('string');
|
||||
expect(typeof skill.family).toBe('string');
|
||||
expect(typeof skill.familyLabel).toBe('string');
|
||||
expect(['active', 'installed', 'available']).toContain(skill.state);
|
||||
expect(typeof skill.isWorkflow).toBe('boolean');
|
||||
}
|
||||
});
|
||||
|
||||
it('families array is populated with correct shape', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: '/api/skills/starter-pack/catalog',
|
||||
});
|
||||
|
||||
const body = res.json();
|
||||
expect(body.families).toBeDefined();
|
||||
expect(Array.isArray(body.families)).toBe(true);
|
||||
expect(body.families.length).toBeGreaterThan(0);
|
||||
for (const family of body.families) {
|
||||
expect(family).toHaveProperty('id');
|
||||
expect(family).toHaveProperty('label');
|
||||
expect(typeof family.id).toBe('string');
|
||||
expect(typeof family.label).toBe('string');
|
||||
}
|
||||
});
|
||||
|
||||
it('workflow skills have isWorkflow: true', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: '/api/skills/starter-pack/catalog',
|
||||
});
|
||||
|
||||
const body = res.json();
|
||||
const workflowIds = ['research-team', 'review-pair', 'plan-execute'];
|
||||
for (const wfId of workflowIds) {
|
||||
const skill = body.skills.find((s: { id: string }) => s.id === wfId);
|
||||
expect(skill, `workflow skill ${wfId} should exist`).toBeDefined();
|
||||
expect(skill.isWorkflow).toBe(true);
|
||||
}
|
||||
|
||||
// Non-workflow skills should have isWorkflow: false
|
||||
const nonWorkflow = body.skills.filter(
|
||||
(s: { id: string }) => !workflowIds.includes(s.id),
|
||||
);
|
||||
for (const skill of nonWorkflow) {
|
||||
expect(skill.isWorkflow).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('all skills map to a known family (no "other")', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: '/api/skills/starter-pack/catalog',
|
||||
});
|
||||
|
||||
const body = res.json();
|
||||
const knownFamilies = ['writing', 'research', 'decision', 'planning', 'communication', 'code', 'creative'];
|
||||
for (const skill of body.skills) {
|
||||
expect(
|
||||
knownFamilies,
|
||||
`skill "${skill.id}" has unknown family "${skill.family}"`,
|
||||
).toContain(skill.family);
|
||||
}
|
||||
});
|
||||
|
||||
it('POST /api/skills/starter-pack/:id installs a single skill', async () => {
|
||||
// Ensure the skill is not already installed (now writes to tmpDir/skills/)
|
||||
const skillsDir = path.join(tmpDir, 'skills');
|
||||
const targetPath = path.join(skillsDir, `${INSTALL_TEST_SKILL}.md`);
|
||||
if (fs.existsSync(targetPath)) fs.unlinkSync(targetPath);
|
||||
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: `/api/skills/starter-pack/${INSTALL_TEST_SKILL}`,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.payload);
|
||||
expect(body.ok).toBe(true);
|
||||
expect(body.skill.id).toBe(INSTALL_TEST_SKILL);
|
||||
expect(['active', 'installed']).toContain(body.skill.state);
|
||||
});
|
||||
|
||||
it('POST /api/skills/starter-pack/:id returns 409 for already installed', async () => {
|
||||
// INSTALL_TEST_SKILL was installed in previous test
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: `/api/skills/starter-pack/${INSTALL_TEST_SKILL}`,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(409);
|
||||
});
|
||||
|
||||
it('POST /api/skills/starter-pack/:id returns 404 for nonexistent skill', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/skills/starter-pack/nonexistent-skill-xyz',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('POST /api/skills/starter-pack/:id returns 400 for path traversal', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/skills/starter-pack/..%2F..%2Fetc%2Fpasswd',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('catalog reflects installed state after single install', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: '/api/skills/starter-pack/catalog',
|
||||
});
|
||||
|
||||
const body = JSON.parse(res.payload);
|
||||
const installed = body.skills.find((s: { id: string }) => s.id === INSTALL_TEST_SKILL);
|
||||
expect(installed).toBeDefined();
|
||||
expect(['active', 'installed']).toContain(installed.state);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user