255 lines
8.7 KiB
TypeScript
255 lines
8.7 KiB
TypeScript
/**
|
|
* ComplianceTemplateStore unit tests (M-03)
|
|
*
|
|
* Covers CRUD + section merge semantics + default section fill-in + risk-class CHECK
|
|
* constraint + deletion. Following HarvestRunStore test shape (in-memory MindDB).
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { MindDB } from '@waggle/hive-mind-core';
|
|
import { ComplianceTemplateStore, KVARK_TEMPLATE_NAME } from '../../src/compliance/template-store.js';
|
|
import type { ComplianceTemplateSections } from '../../src/compliance/types.js';
|
|
|
|
const ALL_ON: ComplianceTemplateSections = {
|
|
interactions: true,
|
|
oversight: true,
|
|
models: true,
|
|
provenance: true,
|
|
riskAssessment: true,
|
|
fria: true,
|
|
};
|
|
const ALL_OFF: ComplianceTemplateSections = {
|
|
interactions: false,
|
|
oversight: false,
|
|
models: false,
|
|
provenance: false,
|
|
riskAssessment: false,
|
|
fria: false,
|
|
};
|
|
|
|
describe('ComplianceTemplateStore', () => {
|
|
let db: MindDB;
|
|
let store: ComplianceTemplateStore;
|
|
|
|
beforeEach(() => {
|
|
db = new MindDB(':memory:');
|
|
store = new ComplianceTemplateStore(db);
|
|
});
|
|
|
|
afterEach(() => {
|
|
db.close();
|
|
});
|
|
|
|
describe('create', () => {
|
|
it('persists a template with all fields', () => {
|
|
const t = store.create({
|
|
name: 'KVARK enterprise',
|
|
description: 'Full AI Act package for enterprise on-prem',
|
|
sections: ALL_ON,
|
|
riskClassification: 'high-risk',
|
|
orgName: 'KVARK Sovereign',
|
|
footerText: 'Confidential — internal only',
|
|
});
|
|
expect(t.id).toBeGreaterThan(0);
|
|
expect(t.name).toBe('KVARK enterprise');
|
|
expect(t.description).toBe('Full AI Act package for enterprise on-prem');
|
|
expect(t.sections).toEqual(ALL_ON);
|
|
expect(t.riskClassification).toBe('high-risk');
|
|
expect(t.orgName).toBe('KVARK Sovereign');
|
|
expect(t.footerText).toBe('Confidential — internal only');
|
|
expect(t.createdAt).toBeTruthy();
|
|
expect(t.updatedAt).toBeTruthy();
|
|
});
|
|
|
|
it('defaults optional fields to null', () => {
|
|
const t = store.create({ name: 'Bare', sections: ALL_OFF });
|
|
expect(t.description).toBeNull();
|
|
expect(t.riskClassification).toBeNull();
|
|
expect(t.orgName).toBeNull();
|
|
expect(t.footerText).toBeNull();
|
|
});
|
|
|
|
it('trims whitespace from name', () => {
|
|
const t = store.create({ name: ' Spaced ', sections: ALL_OFF });
|
|
expect(t.name).toBe('Spaced');
|
|
});
|
|
|
|
it('rejects empty name', () => {
|
|
expect(() => store.create({ name: ' ', sections: ALL_OFF })).toThrow(/name is required/i);
|
|
});
|
|
|
|
it('fills in missing section keys with defaults', () => {
|
|
const t = store.create({
|
|
name: 'Partial',
|
|
// Only interactions specified; others should fall back to DEFAULT_SECTIONS.
|
|
sections: { interactions: false } as ComplianceTemplateSections,
|
|
});
|
|
expect(t.sections.interactions).toBe(false);
|
|
expect(t.sections.oversight).toBe(true); // default
|
|
expect(t.sections.fria).toBe(false); // default
|
|
});
|
|
});
|
|
|
|
describe('getById', () => {
|
|
it('returns null for missing id', () => {
|
|
expect(store.getById(999)).toBeNull();
|
|
});
|
|
|
|
it('round-trips a created template', () => {
|
|
const a = store.create({ name: 'A', sections: ALL_ON, riskClassification: 'limited' });
|
|
const b = store.getById(a.id);
|
|
expect(b).not.toBeNull();
|
|
expect(b?.name).toBe('A');
|
|
expect(b?.sections).toEqual(ALL_ON);
|
|
expect(b?.riskClassification).toBe('limited');
|
|
});
|
|
});
|
|
|
|
describe('list', () => {
|
|
it('returns empty array when none exist', () => {
|
|
expect(store.list()).toEqual([]);
|
|
});
|
|
|
|
it('returns templates newest-updated first', async () => {
|
|
store.create({ name: 'Old', sections: ALL_OFF });
|
|
await new Promise(resolve => setTimeout(resolve, 1100)); // datetime('now') is second-resolution
|
|
store.create({ name: 'New', sections: ALL_OFF });
|
|
const list = store.list();
|
|
expect(list).toHaveLength(2);
|
|
expect(list[0].name).toBe('New');
|
|
expect(list[1].name).toBe('Old');
|
|
});
|
|
});
|
|
|
|
describe('update', () => {
|
|
it('returns null for missing id', () => {
|
|
expect(store.update(999, { name: 'nope' })).toBeNull();
|
|
});
|
|
|
|
it('updates only the fields provided', () => {
|
|
const t = store.create({
|
|
name: 'Orig',
|
|
description: 'desc',
|
|
sections: ALL_ON,
|
|
orgName: 'Acme',
|
|
});
|
|
const updated = store.update(t.id, { name: 'Renamed' });
|
|
expect(updated?.name).toBe('Renamed');
|
|
expect(updated?.description).toBe('desc'); // preserved
|
|
expect(updated?.sections).toEqual(ALL_ON); // preserved
|
|
expect(updated?.orgName).toBe('Acme'); // preserved
|
|
});
|
|
|
|
it('clears a field when explicitly set to null', () => {
|
|
const t = store.create({
|
|
name: 'T',
|
|
sections: ALL_ON,
|
|
orgName: 'To remove',
|
|
footerText: 'also remove',
|
|
});
|
|
const updated = store.update(t.id, { orgName: null, footerText: null });
|
|
expect(updated?.orgName).toBeNull();
|
|
expect(updated?.footerText).toBeNull();
|
|
});
|
|
|
|
it('replaces sections wholesale', () => {
|
|
const t = store.create({ name: 'T', sections: ALL_ON });
|
|
const updated = store.update(t.id, { sections: ALL_OFF });
|
|
expect(updated?.sections).toEqual(ALL_OFF);
|
|
});
|
|
|
|
it('rejects an empty rename', () => {
|
|
const t = store.create({ name: 'T', sections: ALL_OFF });
|
|
expect(() => store.update(t.id, { name: ' ' })).toThrow(/name is required/i);
|
|
});
|
|
|
|
it('bumps updated_at', async () => {
|
|
const t = store.create({ name: 'T', sections: ALL_OFF });
|
|
await new Promise(resolve => setTimeout(resolve, 1100));
|
|
const updated = store.update(t.id, { name: 'T2' });
|
|
expect(updated?.updatedAt).not.toBe(t.updatedAt);
|
|
});
|
|
});
|
|
|
|
describe('delete', () => {
|
|
it('returns false for missing id', () => {
|
|
expect(store.delete(999)).toBe(false);
|
|
});
|
|
|
|
it('removes an existing row and returns true', () => {
|
|
const t = store.create({ name: 'T', sections: ALL_OFF });
|
|
expect(store.delete(t.id)).toBe(true);
|
|
expect(store.getById(t.id)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('risk_classification CHECK constraint', () => {
|
|
it('accepts all four AIActRiskLevel values', () => {
|
|
for (const level of ['minimal', 'limited', 'high-risk', 'unacceptable'] as const) {
|
|
const t = store.create({ name: `T-${level}`, sections: ALL_OFF, riskClassification: level });
|
|
expect(t.riskClassification).toBe(level);
|
|
}
|
|
});
|
|
|
|
it('rejects an invalid risk class at the SQL layer', () => {
|
|
expect(() =>
|
|
store.create({
|
|
name: 'Bad',
|
|
sections: ALL_OFF,
|
|
riskClassification: 'super-dangerous' as never,
|
|
}),
|
|
).toThrow(); // better-sqlite3 surfaces the CHECK violation
|
|
});
|
|
});
|
|
|
|
describe('seedKvarkTemplateIfMissing (M-06)', () => {
|
|
it('creates the KVARK template on first run', () => {
|
|
const seeded = store.seedKvarkTemplateIfMissing();
|
|
expect(seeded).not.toBeNull();
|
|
expect(seeded!.name).toBe(KVARK_TEMPLATE_NAME);
|
|
expect(seeded!.riskClassification).toBe('high-risk');
|
|
expect(seeded!.sections.fria).toBe(true);
|
|
expect(seeded!.orgName).toContain('KVARK');
|
|
expect(seeded!.footerText).toContain('sovereign');
|
|
});
|
|
|
|
it('is idempotent — second call returns null', () => {
|
|
store.seedKvarkTemplateIfMissing();
|
|
expect(store.seedKvarkTemplateIfMissing()).toBeNull();
|
|
// Only one KVARK row exists.
|
|
const all = store.list().filter(t => t.name === KVARK_TEMPLATE_NAME);
|
|
expect(all).toHaveLength(1);
|
|
});
|
|
|
|
it('does not seed when the user already created a same-named template', () => {
|
|
store.create({
|
|
name: KVARK_TEMPLATE_NAME,
|
|
sections: ALL_OFF,
|
|
});
|
|
expect(store.seedKvarkTemplateIfMissing()).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('mergeSections (static)', () => {
|
|
it('unions template + runtime flags', () => {
|
|
const template: ComplianceTemplateSections = { ...ALL_OFF, interactions: true, fria: true };
|
|
const runtime: ComplianceTemplateSections = { ...ALL_OFF, oversight: true };
|
|
const merged = ComplianceTemplateStore.mergeSections(template, runtime);
|
|
expect(merged.interactions).toBe(true); // from template
|
|
expect(merged.oversight).toBe(true); // from runtime
|
|
expect(merged.fria).toBe(true); // from template
|
|
expect(merged.models).toBe(false);
|
|
});
|
|
|
|
it('never hides a section the runtime requested (union semantics)', () => {
|
|
const merged = ComplianceTemplateStore.mergeSections(ALL_OFF, ALL_ON);
|
|
expect(merged).toEqual(ALL_ON);
|
|
});
|
|
|
|
it('never hides a section the template requested', () => {
|
|
const merged = ComplianceTemplateStore.mergeSections(ALL_ON, ALL_OFF);
|
|
expect(merged).toEqual(ALL_ON);
|
|
});
|
|
});
|
|
});
|