"use strict";
/**
 * scan-limits.test.ts — resource caps for customer-side `ai-governance scan`.
 *
 * Step 0 probe: scan walks + reads every source file in a customer repo with
 *   no bound. A pathological tree (millions of files / a multi-GB file) could
 *   spike CPU/memory on the customer's box (Invariants 2 + 10).
 * Step 0 evidence: agent-dir-scanner.ts walkDir/analyseFile and scan.ts walk
 *   had no file-count / depth / size / time cap before this change.
 * Step 0 customer-path: customer runs `ai-governance scan <repo>` →
 *   scanAgentDirectory walks the tree → unbounded read/analyse.
 */
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;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_test_1 = require("node:test");
const strict_1 = __importDefault(require("node:assert/strict"));
const fs = __importStar(require("node:fs"));
const os = __importStar(require("node:os"));
const path = __importStar(require("node:path"));
const scan_limits_1 = require("./scan-limits");
const agent_dir_scanner_1 = require("./agent-dir-scanner");
(0, node_test_1.describe)('ScanBudget', () => {
    (0, node_test_1.it)('stops continuing once the file-count cap is reached', () => {
        const b = new scan_limits_1.ScanBudget({ maxFiles: 3, maxDepth: 50, maxFileBytes: 1e9, timeBudgetMs: 1e9 });
        strict_1.default.equal(b.canContinue(), true);
        b.countVisit();
        b.countVisit();
        b.countVisit();
        strict_1.default.equal(b.canContinue(), false);
        strict_1.default.equal(b.report().truncatedByFileCap, true);
        strict_1.default.equal(b.report().capped, true);
    });
    (0, node_test_1.it)('expires when the wall-clock budget passes (no timers)', () => {
        const start = 1_000_000;
        const b = new scan_limits_1.ScanBudget({ maxFiles: 1e9, maxDepth: 50, maxFileBytes: 1e9, timeBudgetMs: 100 }, start);
        strict_1.default.equal(b.canContinue(start + 50), true);
        strict_1.default.equal(b.canContinue(start + 150), false);
        strict_1.default.equal(b.report().truncatedByTimeBudget, true);
    });
    (0, node_test_1.it)('rejects oversize files and records the skip', () => {
        const b = new scan_limits_1.ScanBudget({ maxFiles: 1e9, maxDepth: 50, maxFileBytes: 1024, timeBudgetMs: 1e9 });
        strict_1.default.equal(b.allowFileSize(500), true);
        strict_1.default.equal(b.allowFileSize(2048), false);
        strict_1.default.equal(b.report().filesSkippedOversize, 1);
        strict_1.default.equal(b.report().capped, true);
    });
    (0, node_test_1.it)('caps recursion depth', () => {
        const b = new scan_limits_1.ScanBudget({ maxFiles: 1e9, maxDepth: 2, maxFileBytes: 1e9, timeBudgetMs: 1e9 });
        strict_1.default.equal(b.withinDepth(2), true);
        strict_1.default.equal(b.withinDepth(3), false);
        strict_1.default.equal(b.report().truncatedByDepthCap, true);
    });
    (0, node_test_1.it)('default limits are positive and finite', () => {
        const l = (0, scan_limits_1.defaultScanLimits)();
        for (const v of [l.maxFiles, l.maxDepth, l.maxFileBytes, l.timeBudgetMs]) {
            strict_1.default.ok(Number.isFinite(v) && v > 0);
        }
    });
});
(0, node_test_1.describe)('scanAgentDirectory honours the ScanBudget (wiring)', () => {
    let dir;
    (0, node_test_1.before)(() => {
        dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cxni-scan-cap-'));
        // 12 detectable agent files (each imports an AI provider).
        for (let i = 0; i < 12; i++) {
            fs.writeFileSync(path.join(dir, `agent-${i}.ts`), `import Anthropic from '@anthropic-ai/sdk';\nconst c = new Anthropic();\n`);
        }
    });
    (0, node_test_1.after)(() => fs.rmSync(dir, { recursive: true, force: true }));
    (0, node_test_1.it)('stops visiting after the file-count cap and reports truncation', () => {
        const budget = new scan_limits_1.ScanBudget({ maxFiles: 4, maxDepth: 50, maxFileBytes: 1e9, timeBudgetMs: 1e9 });
        const results = (0, agent_dir_scanner_1.scanAgentDirectory)(dir, budget);
        // No more than the cap of files may be visited/returned.
        strict_1.default.ok(results.length <= 4, `expected <= 4 detected, got ${results.length}`);
        strict_1.default.equal(budget.report().truncatedByFileCap, true);
        strict_1.default.equal(budget.report().capped, true);
    });
    (0, node_test_1.it)('skips oversize files instead of reading them', () => {
        const big = path.join(dir, 'huge-agent.ts');
        fs.writeFileSync(big, `import OpenAI from 'openai';\n` + 'x'.repeat(5000));
        const budget = new scan_limits_1.ScanBudget({ maxFiles: 1e9, maxDepth: 50, maxFileBytes: 1024, timeBudgetMs: 1e9 });
        (0, agent_dir_scanner_1.scanAgentDirectory)(dir, budget);
        strict_1.default.ok(budget.report().filesSkippedOversize >= 1, 'oversize file must be skipped');
    });
    (0, node_test_1.it)('does not run signature regexes over a multi-MB single line (ReDoS/CPU guard)', () => {
        // A provider import that lives ONLY on a line longer than the per-line cap
        // is intentionally NOT analysed — that line is skipped, so no O(line)×N
        // regex pass happens. (A real import line is always short.)
        const giantOnly = fs.mkdtempSync(path.join(os.tmpdir(), 'cxni-scan-giant-'));
        try {
            fs.writeFileSync(path.join(giantOnly, 'minified-only.ts'), `import Anthropic from '@anthropic-ai/sdk';` + ' '.repeat(50_000) + ';\n');
            const budget = new scan_limits_1.ScanBudget({ maxFiles: 1e9, maxDepth: 50, maxFileBytes: 1e9, timeBudgetMs: 1e9 });
            const results = (0, agent_dir_scanner_1.scanAgentDirectory)(giantOnly, budget);
            strict_1.default.equal(results.length, 0, 'an import that exists only on an over-cap line must not be detected (line skipped)');
        }
        finally {
            fs.rmSync(giantOnly, { recursive: true, force: true });
        }
    });
    (0, node_test_1.it)('a short import on a file that also has a giant line is still detected', () => {
        const mixedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cxni-scan-mixed-'));
        try {
            fs.writeFileSync(path.join(mixedDir, 'mixed.ts'), `import OpenAI from 'openai';\n` + 'x'.repeat(80_000) + '\n');
            const budget = new scan_limits_1.ScanBudget({ maxFiles: 1e9, maxDepth: 50, maxFileBytes: 1e9, timeBudgetMs: 1e9 });
            const results = (0, agent_dir_scanner_1.scanAgentDirectory)(mixedDir, budget);
            strict_1.default.equal(results.length, 1, 'short import line must still be detected past a giant line');
        }
        finally {
            fs.rmSync(mixedDir, { recursive: true, force: true });
        }
    });
    (0, node_test_1.it)('a symlink cycle does not cause an unbounded walk', () => {
        const loopDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cxni-scan-loop-'));
        try {
            fs.writeFileSync(path.join(loopDir, 'a.ts'), `import OpenAI from 'openai';\n`);
            // self-referential symlink: loopDir/loop -> loopDir
            try {
                fs.symlinkSync(loopDir, path.join(loopDir, 'loop'), 'dir');
            }
            catch {
                return; // platform without symlink permission — skip
            }
            const budget = new scan_limits_1.ScanBudget({ maxFiles: 1000, maxDepth: 50, maxFileBytes: 1e9, timeBudgetMs: 5000 });
            const results = (0, agent_dir_scanner_1.scanAgentDirectory)(loopDir, budget);
            // Terminates (no hang) and does not re-descend the cycle: the single real
            // file is found exactly once, not thousands of times.
            strict_1.default.equal(results.length, 1, 'symlink target must not be re-walked');
            strict_1.default.equal(budget.report().truncatedByFileCap, false, 'cycle must not exhaust the file cap');
        }
        finally {
            fs.rmSync(loopDir, { recursive: true, force: true });
        }
    });
});
//# sourceMappingURL=scan-limits.test.js.map