This commit is contained in:
195
packages/server/tests/routes/trust-wiring.test.ts
Normal file
195
packages/server/tests/routes/trust-wiring.test.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user