196 lines
7.2 KiB
TypeScript
196 lines
7.2 KiB
TypeScript
/**
|
|
* 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');
|
|
});
|
|
});
|
|
});
|