"use strict";
/**
 * Tests for PackEvaluator (GAP-S3-01)
 *
 * @connexum/ai-governance
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
const node_test_1 = require("node:test");
const assert = __importStar(require("node:assert/strict"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const pack_evaluator_1 = require("./pack-evaluator");
const index_1 = require("../packs/index");
const event_bus_1 = require("../audit/event-bus");
const index_2 = require("../adapters/index");
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function makeTmpEventDir() {
    return fs.mkdtempSync(path.join(os.tmpdir(), 'pe-test-'));
}
function makeEventBus(dir) {
    return new event_bus_1.EventBus({ eventDir: dir, agentName: 'test-evaluator' });
}
function makePackRegistry(packs) {
    const registry = new index_1.PackRegistry();
    for (const p of packs) {
        registry.register(p);
        registry.activate(p.id);
    }
    return registry;
}
function makeActionContext(toolName, input = {}) {
    return (0, index_2.createActionContext)(toolName, input, { name: 'test-agent', orgId: 'tenant-x' });
}
function makePack(id, categoryActions, overrides = {}) {
    return {
        id,
        name: `Pack ${id}`,
        version: '1.0.0',
        description: `Test pack ${id}`,
        authority: 'Test',
        industries: ['test'],
        requiredModules: [],
        moduleConfig: {},
        providerPolicy: { blockedProviders: [], requiredCapabilities: [], blockReasons: {} },
        dataPolicy: {
            scanCategories: Object.keys(categoryActions),
            categoryActions,
            encryptionRequired: false,
        },
        retentionPolicy: {
            auditLogRetentionDays: 90,
            sessionRetentionDays: 30,
            incidentRetentionDays: 365,
            reference: 'Test',
        },
        incidentPolicy: {
            notificationDeadlineHours: 72,
            reference: 'Test',
            requiredRecipients: [],
            formalNotificationRequired: false,
        },
        validators: [],
        ...overrides,
    };
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
(0, node_test_1.describe)('PackEvaluator', () => {
    // (a) Cold-cache evaluate -> resolves from registry, caches, returns verdict
    (0, node_test_1.it)('(a) cold-cache: resolves from registry and caches result', () => {
        const dir = makeTmpEventDir();
        const bus = makeEventBus(dir);
        const pack = makePack('test-hipaa', { PHI: 'BLOCK' });
        const registry = makePackRegistry([pack]);
        const evaluator = new pack_evaluator_1.PackEvaluator(registry, bus);
        // Deactivate the pack after caching -- a warm cache won't see this.
        const verdict1 = evaluator.evaluate('tenant-1', 'agent-1', makeActionContext('Write', { content: 'phi data' }));
        // Should produce DENY because PHI is in input text and action is BLOCK
        assert.equal(verdict1.verdict, 'DENY');
        assert.ok(verdict1.triggeringPackIds.includes('test-hipaa'));
        assert.ok(verdict1.triggeringClassifierIds.includes('PHI'));
    });
    // (b) Warm-cache: uses cache, skips registry
    (0, node_test_1.it)('(b) warm-cache: second call uses cached pack set', () => {
        const dir = makeTmpEventDir();
        const bus = makeEventBus(dir);
        const pack = makePack('test-soc2', { PII: 'BLOCK' });
        const registry = makePackRegistry([pack]);
        const evaluator = new pack_evaluator_1.PackEvaluator(registry, bus, { ttlMs: 60_000, enableCache: true });
        // First call -- cold cache
        const v1 = evaluator.evaluate('tenant-warm', 'agent-1', makeActionContext('Write', { content: 'pii data' }));
        assert.equal(v1.verdict, 'DENY');
        // Manually deactivate the pack in the registry.
        // A warm cache should still return DENY because it uses the cached pack set.
        registry.deactivate('test-soc2');
        const v2 = evaluator.evaluate('tenant-warm', 'agent-1', makeActionContext('Write', { content: 'pii data' }));
        assert.equal(v2.verdict, 'DENY', 'warm cache should still produce DENY from cached pack set');
    });
    // (c) pack:amended event -> invalidates cache for affected tenant only
    (0, node_test_1.it)('(c) pack:amended invalidates cache for affected tenant only', () => {
        const dir = makeTmpEventDir();
        const bus = makeEventBus(dir);
        const pack = makePack('test-amended', { CREDIT: 'BLOCK' });
        const registry = makePackRegistry([pack]);
        const evaluator = new pack_evaluator_1.PackEvaluator(registry, bus, { ttlMs: 60_000 });
        // Warm up both tenants
        const ctx = makeActionContext('Write', { content: 'credit card data' });
        const vt1Before = evaluator.evaluate('tenant-alpha', 'agent-1', ctx);
        const vt2Before = evaluator.evaluate('tenant-beta', 'agent-1', ctx);
        assert.equal(vt1Before.verdict, 'DENY');
        assert.equal(vt2Before.verdict, 'DENY');
        // Deactivate pack in registry (simulates pack amendment)
        registry.deactivate('test-amended');
        // Emit pack:amended for tenant-alpha -- should flush only tenant-alpha
        bus.emit('pack:amended', { tenantId: 'tenant-alpha' });
        // orgId field path: eventBus.emit sets event.orgId from options...
        // Our bus.on handler checks event.orgId ?? event.data.tenantId
        // The emit signature is: emit(type, data, target?) -- orgId is not directly set.
        // We set tenantId in data above.
        // tenant-alpha: cache flushed, resolves fresh (no active packs now -> ALLOW)
        const vt1After = evaluator.evaluate('tenant-alpha', 'agent-1', ctx);
        assert.equal(vt1After.verdict, 'ALLOW', 'tenant-alpha cache should be flushed');
        // tenant-beta: cache untouched, still DENY
        const vt2After = evaluator.evaluate('tenant-beta', 'agent-1', ctx);
        assert.equal(vt2After.verdict, 'DENY', 'tenant-beta cache should be untouched');
    });
    // (d) flushTenant() -> invalidates one tenant
    (0, node_test_1.it)('(d) flushTenant() invalidates exactly one tenant', () => {
        const dir = makeTmpEventDir();
        const bus = makeEventBus(dir);
        const pack = makePack('test-flush', { SSN: 'BLOCK' });
        const registry = makePackRegistry([pack]);
        const evaluator = new pack_evaluator_1.PackEvaluator(registry, bus, { ttlMs: 60_000 });
        const ctx = makeActionContext('Write', { content: 'ssn 123-45-6789' });
        evaluator.evaluate('t1', 'a1', ctx); // warm t1
        evaluator.evaluate('t2', 'a1', ctx); // warm t2
        // Remove the pack from registry before flushing
        registry.deactivate('test-flush');
        evaluator.flushTenant('t1');
        const v1 = evaluator.evaluate('t1', 'a1', ctx);
        const v2 = evaluator.evaluate('t2', 'a1', ctx);
        assert.equal(v1.verdict, 'ALLOW', 't1 flushed -> resolves fresh with no packs');
        assert.equal(v2.verdict, 'DENY', 't2 still cached');
    });
    // (e) flushAll() -> invalidates everything
    (0, node_test_1.it)('(e) flushAll() invalidates all tenants', () => {
        const dir = makeTmpEventDir();
        const bus = makeEventBus(dir);
        const pack = makePack('test-flush-all', { PAN: 'BLOCK' });
        const registry = makePackRegistry([pack]);
        const evaluator = new pack_evaluator_1.PackEvaluator(registry, bus, { ttlMs: 60_000 });
        const ctx = makeActionContext('Write', { content: 'pan 4111111111111111' });
        evaluator.evaluate('ta', 'a1', ctx);
        evaluator.evaluate('tb', 'a1', ctx);
        registry.deactivate('test-flush-all');
        evaluator.flushAll();
        const va = evaluator.evaluate('ta', 'a1', ctx);
        const vb = evaluator.evaluate('tb', 'a1', ctx);
        assert.equal(va.verdict, 'ALLOW', 'ta should re-resolve with no packs');
        assert.equal(vb.verdict, 'ALLOW', 'tb should re-resolve with no packs');
    });
    // (f) BLOCK action -> DENY verdict
    (0, node_test_1.it)('(f) BLOCK categoryAction produces DENY verdict', () => {
        const dir = makeTmpEventDir();
        const bus = makeEventBus(dir);
        const pack = makePack('block-pack', { BIOMETRIC: 'BLOCK' });
        const registry = makePackRegistry([pack]);
        const evaluator = new pack_evaluator_1.PackEvaluator(registry, bus);
        const v = evaluator.evaluate('tenant-f', 'a1', makeActionContext('Write', { content: 'biometric fingerprint data' }));
        assert.equal(v.verdict, 'DENY');
        assert.ok(v.triggeringPackIds.includes('block-pack'));
    });
    // (g) WARN action -> PENDING verdict
    (0, node_test_1.it)('(g) WARN categoryAction produces PENDING verdict', () => {
        const dir = makeTmpEventDir();
        const bus = makeEventBus(dir);
        const pack = makePack('warn-pack', { HEALTH: 'WARN' });
        const registry = makePackRegistry([pack]);
        const evaluator = new pack_evaluator_1.PackEvaluator(registry, bus);
        const v = evaluator.evaluate('tenant-g', 'a1', makeActionContext('Write', { content: 'health record data' }));
        assert.equal(v.verdict, 'PENDING');
        assert.ok(v.triggeringPackIds.includes('warn-pack'));
    });
    // (h) No classifier hit -> ALLOW verdict
    (0, node_test_1.it)('(h) no classifier hit produces ALLOW verdict', () => {
        const dir = makeTmpEventDir();
        const bus = makeEventBus(dir);
        const pack = makePack('benign-pack', { NUCLEAR_LAUNCH_CODES: 'BLOCK' });
        const registry = makePackRegistry([pack]);
        const evaluator = new pack_evaluator_1.PackEvaluator(registry, bus);
        // A completely innocuous action with no sensitive content
        const v = evaluator.evaluate('tenant-h', 'a1', makeActionContext('Read', { path: '/readme.txt' }));
        assert.equal(v.verdict, 'ALLOW');
        assert.equal(v.triggeringPackIds.length, 0);
    });
    // (i) Multiple packs hitting same action -> strictest-wins (DENY > PENDING > ALLOW)
    (0, node_test_1.it)('(i) multiple packs: strictest verdict wins (DENY > PENDING > ALLOW)', () => {
        const dir = makeTmpEventDir();
        const bus = makeEventBus(dir);
        // Pack A: WARN on PHI
        // Pack B: BLOCK on PHI
        // Strictest-wins should produce DENY.
        const packA = makePack('pack-a', { PHI: 'WARN' });
        const packB = makePack('pack-b', { PHI: 'BLOCK' });
        const registry = makePackRegistry([packA, packB]);
        const evaluator = new pack_evaluator_1.PackEvaluator(registry, bus);
        const v = evaluator.evaluate('tenant-i', 'a1', makeActionContext('Write', { content: 'phi medical record' }));
        assert.equal(v.verdict, 'DENY', 'BLOCK from pack-b should dominate WARN from pack-a');
        assert.ok(v.triggeringPackIds.includes('pack-b'), 'pack-b should be in triggering packs');
    });
    // (j) Latency: 1,000 sequential evaluate() calls with 5-pack tenant config -- p99 <= 50ms
    (0, node_test_1.it)('(j) latency: p99 <= 50ms over 1,000 sequential calls with 5-pack tenant config', () => {
        const dir = makeTmpEventDir();
        const bus = makeEventBus(dir);
        // Build 5 packs each with 10 categories
        const packs = [];
        for (let i = 0; i < 5; i++) {
            const categoryActions = {};
            for (let j = 0; j < 10; j++) {
                categoryActions[`CATEGORY_${i}_${j}`] = j % 2 === 0 ? 'BLOCK' : 'WARN';
            }
            packs.push(makePack(`perf-pack-${i}`, categoryActions));
        }
        const registry = makePackRegistry(packs);
        const evaluator = new pack_evaluator_1.PackEvaluator(registry, bus, { ttlMs: 60_000, enableCache: true });
        const ctx = makeActionContext('Read', { path: '/safe/path.txt' });
        const latencies = [];
        // Warm the cache with one call first (not counted in p99)
        evaluator.evaluate('tenant-j', 'a1', ctx);
        for (let i = 0; i < 1000; i++) {
            const t0 = performance.now();
            evaluator.evaluate('tenant-j', 'a1', ctx);
            latencies.push(performance.now() - t0);
        }
        latencies.sort((a, b) => a - b);
        const p99Index = Math.floor(latencies.length * 0.99);
        const p99Ms = latencies[p99Index];
        console.log(`  [perf] p50=${latencies[Math.floor(latencies.length * 0.50)].toFixed(3)}ms  p99=${p99Ms.toFixed(3)}ms`);
        assert.ok(p99Ms <= 50, `p99 latency must be <= 50ms (GAP-S3-01 gate). Measured: ${p99Ms.toFixed(3)}ms`);
    });
});
//# sourceMappingURL=pack-evaluator.test.js.map