This commit is contained in:
254
packages/core/tests/compliance/template-store.test.ts
Normal file
254
packages/core/tests/compliance/template-store.test.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user