Files
waggle-os/packages/server/tests/routes/persistence.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

255 lines
9.3 KiB
TypeScript

import { describe, it, expect, 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';
/** Skill ID used for persistence tests */
const TEST_SKILL = 'retrospective';
/** Minimal shape of a catalog/skills-list entry as returned over the JSON API. */
type CatalogSkillEntry = { id: string; name: string };
describe('Capability Persistence & Cross-Surface Agreement', () => {
const tmpDirs: string[] = [];
function makeTmpDir(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-persist-test-'));
// Create skills dir with marker to prevent auto-install of starter skills
const skillsDir = path.join(dir, 'skills');
fs.mkdirSync(skillsDir, { recursive: true });
fs.writeFileSync(path.join(skillsDir, '.starter-installed'), 'test');
tmpDirs.push(dir);
return dir;
}
function initMind(dataDir: string): void {
const personalPath = path.join(dataDir, 'personal.mind');
const mind = new MindDB(personalPath);
mind.close();
}
afterAll(() => {
for (const dir of tmpDirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
// ── Restart Coherence ──────────────────────────────────────────────
it('installed skill survives server restart', async () => {
const dataDir = makeTmpDir();
initMind(dataDir);
// 1. Build server, install a skill
const server1 = await buildLocalServer({ dataDir });
const installRes = await injectWithAuth(server1, {
method: 'POST',
url: `/api/skills/starter-pack/${TEST_SKILL}`,
});
expect(installRes.statusCode).toBe(200);
// Verify skill file exists on disk
const skillPath = path.join(dataDir, 'skills', `${TEST_SKILL}.md`);
expect(fs.existsSync(skillPath)).toBe(true);
// 2. Close server
await server1.close();
// 3. Rebuild with same dataDir
const server2 = await buildLocalServer({ dataDir });
// 4. Verify skill is active after restart
const catalogRes = await injectWithAuth(server2, {
method: 'GET',
url: '/api/skills/starter-pack/catalog',
});
expect(catalogRes.statusCode).toBe(200);
const catalog = catalogRes.json();
const skill = catalog.skills.find((s: CatalogSkillEntry) => s.id === TEST_SKILL);
expect(skill, `${TEST_SKILL} should exist in catalog after restart`).toBeDefined();
expect(skill.state).toBe('active');
await server2.close();
});
it('multiple installed skills all survive restart', async () => {
const dataDir = makeTmpDir();
initMind(dataDir);
const skillsToInstall = ['retrospective', 'daily-plan', 'brainstorm'];
// Install skills
const server1 = await buildLocalServer({ dataDir });
for (const id of skillsToInstall) {
const res = await injectWithAuth(server1, {
method: 'POST',
url: `/api/skills/starter-pack/${id}`,
});
expect(res.statusCode).toBe(200);
}
await server1.close();
// Restart and verify all are active
const server2 = await buildLocalServer({ dataDir });
const catalogRes = await injectWithAuth(server2, {
method: 'GET',
url: '/api/skills/starter-pack/catalog',
});
const catalog = catalogRes.json();
for (const id of skillsToInstall) {
const skill = catalog.skills.find((s: CatalogSkillEntry) => s.id === id);
expect(skill, `${id} should exist after restart`).toBeDefined();
expect(skill.state, `${id} should be active after restart`).toBe('active');
}
await server2.close();
});
// ── Cross-Surface Agreement ────────────────────────────────────────
it('all endpoints agree on skill state after install', async () => {
const dataDir = makeTmpDir();
initMind(dataDir);
const server = await buildLocalServer({ dataDir });
// Install a skill
const installRes = await injectWithAuth(server, {
method: 'POST',
url: `/api/skills/starter-pack/${TEST_SKILL}`,
});
expect(installRes.statusCode).toBe(200);
// Surface 1: Catalog endpoint
const catalogRes = await injectWithAuth(server, {
method: 'GET',
url: '/api/skills/starter-pack/catalog',
});
const catalog = catalogRes.json();
const catalogSkill = catalog.skills.find((s: CatalogSkillEntry) => s.id === TEST_SKILL);
expect(catalogSkill, 'catalog should contain installed skill').toBeDefined();
expect(catalogSkill.state).toBe('active');
// Surface 2: Skills list endpoint
const skillsRes = await injectWithAuth(server, {
method: 'GET',
url: '/api/skills',
});
const skillsList = skillsRes.json();
const listedSkill = skillsList.skills.find((s: CatalogSkillEntry) => s.name === TEST_SKILL);
expect(listedSkill, '/api/skills should contain installed skill').toBeDefined();
// Surface 3: Capabilities status endpoint
const capsRes = await injectWithAuth(server, {
method: 'GET',
url: '/api/capabilities/status',
});
const caps = capsRes.json();
const capSkill = caps.skills.find((s: CatalogSkillEntry) => s.name === TEST_SKILL);
expect(capSkill, '/api/capabilities/status should contain installed skill').toBeDefined();
// Surface 4: In-memory agentState
const agentSkill = server.agentState.skills.find(s => s.name === TEST_SKILL);
expect(agentSkill, 'agentState.skills should contain installed skill').toBeDefined();
await server.close();
});
it('all endpoints agree after restart', async () => {
const dataDir = makeTmpDir();
initMind(dataDir);
// Install, then restart
const server1 = await buildLocalServer({ dataDir });
await injectWithAuth(server1, {
method: 'POST',
url: `/api/skills/starter-pack/${TEST_SKILL}`,
});
await server1.close();
// Verify agreement after restart
const server2 = await buildLocalServer({ dataDir });
const [catalogRes, skillsRes, capsRes] = await Promise.all([
injectWithAuth(server2, { method: 'GET', url: '/api/skills/starter-pack/catalog' }),
injectWithAuth(server2, { method: 'GET', url: '/api/skills' }),
injectWithAuth(server2, { method: 'GET', url: '/api/capabilities/status' }),
]);
const catalog = catalogRes.json();
const skills = skillsRes.json();
const caps = capsRes.json();
// All surfaces report the skill
expect(catalog.skills.find((s: CatalogSkillEntry) => s.id === TEST_SKILL)?.state).toBe('active');
expect(skills.skills.find((s: CatalogSkillEntry) => s.name === TEST_SKILL)).toBeDefined();
expect(caps.skills.find((s: CatalogSkillEntry) => s.name === TEST_SKILL)).toBeDefined();
expect(server2.agentState.skills.find(s => s.name === TEST_SKILL)).toBeDefined();
await server2.close();
});
// ── dataDir Isolation ──────────────────────────────────────────────
it('different dataDirs have independent skill state', async () => {
const dataDir1 = makeTmpDir();
const dataDir2 = makeTmpDir();
initMind(dataDir1);
initMind(dataDir2);
// Install a skill in dataDir1 only
const server1 = await buildLocalServer({ dataDir: dataDir1 });
await injectWithAuth(server1, {
method: 'POST',
url: `/api/skills/starter-pack/${TEST_SKILL}`,
});
// Verify installed in dataDir1
const cat1 = await injectWithAuth(server1, { method: 'GET', url: '/api/skills/starter-pack/catalog' });
expect(cat1.json().skills.find((s: CatalogSkillEntry) => s.id === TEST_SKILL)?.state).toBe('active');
await server1.close();
// Verify NOT installed in dataDir2
const server2 = await buildLocalServer({ dataDir: dataDir2 });
const cat2 = await injectWithAuth(server2, { method: 'GET', url: '/api/skills/starter-pack/catalog' });
expect(cat2.json().skills.find((s: CatalogSkillEntry) => s.id === TEST_SKILL)?.state).toBe('available');
await server2.close();
});
// ── Filesystem is canonical ────────────────────────────────────────
it('deleting skill file from disk makes it unavailable after restart', async () => {
const dataDir = makeTmpDir();
initMind(dataDir);
// Install
const server1 = await buildLocalServer({ dataDir });
await injectWithAuth(server1, {
method: 'POST',
url: `/api/skills/starter-pack/${TEST_SKILL}`,
});
await server1.close();
// Delete the file manually
const skillPath = path.join(dataDir, 'skills', `${TEST_SKILL}.md`);
expect(fs.existsSync(skillPath)).toBe(true);
fs.unlinkSync(skillPath);
// Restart — skill should be back to available
const server2 = await buildLocalServer({ dataDir });
const catalogRes = await injectWithAuth(server2, {
method: 'GET',
url: '/api/skills/starter-pack/catalog',
});
const skill = catalogRes.json().skills.find((s: CatalogSkillEntry) => s.id === TEST_SKILL);
expect(skill?.state).toBe('available');
await server2.close();
});
});