Files
waggle-os/packages/server/tests/routes/starter-catalog.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

183 lines
6.2 KiB
TypeScript

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);
});
});