/** * Trust Model Last-Mile Wiring Tests * * Validates that: * 1. InstallAuditStore is instantiated and wired at runtime * 2. Install operations record audit entries in the .mind DB * 3. Existing non-trust approvals remain backward-compatible */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import Database from 'better-sqlite3'; import { MindDB } from '@waggle/core'; import { buildLocalServer } from '../../src/local/index.js'; import type { FastifyInstance } from 'fastify'; import { injectWithAuth } from '../test-utils.js'; describe('Trust Model Runtime Wiring', () => { let server: FastifyInstance; let tmpDir: string; beforeAll(async () => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-trust-wire-')); // Create personal.mind const personalPath = path.join(tmpDir, 'personal.mind'); const mind = new MindDB(personalPath); mind.close(); // Create skills dir with marker to prevent auto-install of starter skills const skillsDir = path.join(tmpDir, 'skills'); fs.mkdirSync(skillsDir, { recursive: true }); fs.writeFileSync(path.join(skillsDir, '.starter-installed'), 'test'); server = await buildLocalServer({ dataDir: tmpDir }); }); afterAll(async () => { await server.close(); fs.rmSync(tmpDir, { recursive: true, force: true }); }); describe('Audit store wiring', () => { it('install_audit table exists after server startup', () => { const personalPath = path.join(tmpDir, 'personal.mind'); const db = new Database(personalPath); const table = db.prepare( "SELECT name FROM sqlite_master WHERE type='table' AND name='install_audit'", ).get() as { name: string } | undefined; db.close(); expect(table).toBeDefined(); expect(table!.name).toBe('install_audit'); }); it('installing a starter skill creates audit entries', async () => { // Remove skill if auto-installed, then install fresh await injectWithAuth(server, { method: 'DELETE', url: '/api/skills/brainstorm' }); const res = await injectWithAuth(server, { method: 'POST', url: '/api/skills/starter-pack/brainstorm', }); expect(res.statusCode).toBe(200); // Check audit trail — read through the server's own auditStore (the // writer connection). A fresh better-sqlite3 reader can miss WAL rows the // server hasn't checkpointed, which is flaky across platforms/boot timing. const entries = server.auditStore.getByCapability('brainstorm'); // API route records 1 installed entry (agent tool path records approved + installed) expect(entries.length).toBeGreaterThanOrEqual(1); expect(entries.some(e => e.action === 'installed')).toBe(true); // Verify trust metadata is populated const installed = entries.find(e => e.action === 'installed')!; expect(installed.risk_level).toBe('low'); expect(installed.trust_source).toBe('starter_pack'); expect(installed.approval_class).toBe('standard'); expect(installed.initiator).toBe('user'); expect(installed.detail).toContain('Install Center'); }); it('failed install records audit entry', async () => { const res = await injectWithAuth(server, { method: 'POST', url: '/api/skills/starter-pack/nonexistent-xyz', }); expect(res.statusCode).toBe(404); // The API route handler returns 404, but the skill-tools install_capability // path records failures. Check if any failed entries exist. // Read through the server's writer connection (avoids the cross-connection // WAL visibility race a fresh reader hits). const total = server.auditStore.getAll().length; // At least the brainstorm entry from the previous test should exist expect(total).toBeGreaterThanOrEqual(1); }); it('duplicate install does not create new audit entries', async () => { const personalPath = path.join(tmpDir, 'personal.mind'); const db1 = new Database(personalPath); const before = (db1.prepare("SELECT count(*) as c FROM install_audit").get() as { c: number }).c; db1.close(); // Try to install already-installed skill const res = await injectWithAuth(server, { method: 'POST', url: '/api/skills/starter-pack/brainstorm', }); expect(res.statusCode).toBe(409); const db2 = new Database(personalPath); const after = (db2.prepare("SELECT count(*) as c FROM install_audit").get() as { c: number }).c; db2.close(); // Server route handles duplicate before reaching skill-tools, // so no new audit entries should be created via the API route expect(after).toBe(before); }); }); describe('Audit REST endpoint', () => { it('GET /api/audit/installs returns recent install events', async () => { // brainstorm was already installed in the previous test block const res = await injectWithAuth(server, { method: 'GET', url: '/api/audit/installs', }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(body.entries).toBeDefined(); expect(body.entries.length).toBeGreaterThan(0); expect(body.entries[0].capabilityName).toBeDefined(); expect(body.entries[0].riskLevel).toBeDefined(); expect(body.entries[0].action).toBeDefined(); expect(body.entries[0].trustSource).toBeDefined(); expect(body.entries[0].timestamp).toBeDefined(); }); it('respects limit parameter', async () => { const res = await injectWithAuth(server, { method: 'GET', url: '/api/audit/installs?limit=1', }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(body.entries.length).toBeLessThanOrEqual(1); }); it('caps limit at 100', async () => { const res = await injectWithAuth(server, { method: 'GET', url: '/api/audit/installs?limit=999', }); expect(res.statusCode).toBe(200); // Just verify it doesn't error — the cap is internal const body = JSON.parse(res.body); expect(body.entries).toBeDefined(); }); }); describe('Backward compatibility', () => { it('starter-pack catalog still works', async () => { const res = await injectWithAuth(server, { method: 'GET', url: '/api/skills/starter-pack/catalog', }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(body.skills).toBeDefined(); expect(body.skills.length).toBeGreaterThan(0); }); it('capabilities status endpoint still works', async () => { const res = await injectWithAuth(server, { method: 'GET', url: '/api/capabilities/status', }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(body.skills).toBeDefined(); }); it('health endpoint still works', async () => { const res = await injectWithAuth(server, { method: 'GET', url: '/health', }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(body.mode).toBe('local'); }); }); });