Ctrl K
ComunidadGuías…

tests/pipeline.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import assert from 'node:assert/strict';
import test from 'node:test';
import { runPipeline } from '../src/application/pipeline.js';
import { configSchema } from '../src/domain/config.js';
import type { Runner } from '../src/ports/runner.js';
import { RetryableError, StaleTaskError } from '../src/shared/control.js';
import { agent, answer, result, store, testCase } from './helpers.js';
const base = new Map([['.repopilot/policy.json', JSON.stringify({ rules: [{ id: 'bad', forbiddenText: 'BAD', extensions: ['.ts'], message: 'bad source' }] })], ['src/a.ts', 'GOOD']]);
const head = new Map(base); head.set('src/a.ts', 'BAD');
const input = { base, head, baseSha: 'a'.repeat(40), headSha: 'b'.repeat(40) };
const config = configSchema.parse({ repository: 'example/project', agent: { enabled: true, repair: true } });
const runner: Runner = { run: async files => result('passed', [testCase(), ...(files.has('test/generated.test.js') ? [testCase('test/generated.test.js')] : [])]) };
test('static violation repairs after planning, verification and deduplication', async () => {
  const saved = await store();
  const report = await runPipeline(input, config, saved, runner, agent());
  assert.equal(report.status, 'verified'); assert.equal(report.evidence.length, 5); assert.equal(report.changes.length, 2);
  assert.ok(report.plan); assert.equal(report.repairs[0]?.accepted, true);
  const again = await runPipeline(input, config, saved, { run: async () => { throw new Error('must dedupe'); } }, agent());
  assert.equal(again.id, report.id); assert.equal(again.status, 'verified');
});
test('missing tests and environment failures do not trigger repair', async () => {
  const missing = await runPipeline(input, config, await store());
  assert.equal(missing.status, 'needs_attention'); assert.equal(missing.attempts, 0);
  const error = await runPipeline(input, config, await store(), { run: async () => result('error') }, agent());
  assert.equal(error.status, 'needs_attention'); assert.equal(error.attempts, 0);
});
test('historical test failure blocks automatic repair', async () => {
  const report = await runPipeline(input, config, await store(), { run: async () => result('failed') }, agent());
  assert.equal(report.attempts, 0); assert.match(report.notes.join(), /Base tests already fail/);
});
test('policy edits require review', async () => {
  const candidate = new Map(head); candidate.delete('.repopilot/policy.json');
  const report = await runPipeline({ ...input, head: candidate }, config, await store(), runner, agent());
  assert.equal(report.status, 'needs_attention'); assert.equal(report.attempts, 0);
});
test('invalid patches terminate after configured attempts', async () => {
  const bad = agent(); bad.repair = async () => answer([{ path: 'package.json', content: '{}' }]);
  const report = await runPipeline(input, config, await store(), runner, bad);
  assert.equal(report.status, 'needs_attention'); assert.equal(report.attempts, 2); assert.equal(report.changes.length, 0);
});
test('new tests execute even when original suite passes; stable regression is repaired', async () => {
  const generated: Runner = { run: async files => {
    const cases = [testCase()];
    if (files.has('test/generated.test.js')) cases.push(testCase('test/generated.test.js', files.get('src/a.ts') === 'BAD' ? 'failed' : 'passed'));
    return result(cases.some(c => c.status === 'failed') ? 'failed' : 'passed', cases);
  } };
  const report = await runPipeline(input, config, await store(), generated, agent());
  assert.equal(report.status, 'verified'); assert.equal(report.evidence.find(e => e.phase === 'repeat-head')?.result.status, 'failed');
});
test('undiscovered generated tests fail closed', async () => {
  const report = await runPipeline(input, config, await store(), { run: async () => result() }, agent());
  assert.equal(report.status, 'error'); assert.equal(report.attempts, 0); assert.match(report.notes.join(), /not fully executed/);
});
test('changing failure fingerprint blocks repair', async () => {
  const flaky: Runner = { run: async (files, label) => {
    const fail = files.has('test/generated.test.js') && files.get('src/a.ts') === 'BAD';
    return result(fail ? 'failed' : 'passed', [testCase(), ...(files.has('test/generated.test.js')
      ? [testCase('test/generated.test.js', fail ? 'failed' : 'passed', label === 'repeat-head' ? 'different failure' : 'assert expected 1')] : [])]);
  } };
  const report = await runPipeline(input, config, await store(), flaky, agent());
  assert.equal(report.attempts, 0); assert.match(report.notes.join(), /fingerprints changed/);
});
test('repair cannot remove generated test discovery or edit frozen tests', async () => {
  const missing: Runner = { run: async (files, label) => label === 'repair' ? result() : runner.run(files, label) };
  const report = await runPipeline(input, config, await store(), missing, agent());
  assert.equal(report.status, 'needs_attention'); assert.match(report.notes.join(), /same test identities/);
  const bad = agent(); bad.repair = async () => answer([{ path: 'test/generated.test.js', content: '' }]);
  const frozen = await runPipeline(input, config, await store(), runner, bad);
  assert.equal(frozen.status, 'needs_attention'); assert.match(frozen.notes.join(), /frozen tests/);
});
test('description edits produce a new task without changing commits', async () => {
  const saved = await store();
  const a = await runPipeline({ ...input, description: 'first' }, config, saved);
  const b = await runPipeline({ ...input, description: 'second' }, config, saved);
  assert.notEqual(a.id, b.id); assert.notEqual(a.descriptionHash, b.descriptionHash);
});
test('stale cancellation interrupts active runner and persists state', async () => {
  const controller = new AbortController();
  const blocked: Runner = { run: async (_files, _label, signal) => new Promise((_resolve, reject) => {
    signal!.addEventListener('abort', () => reject(signal!.reason), { once: true });
    controller.abort(new StaleTaskError());
  }) };
  const report = await runPipeline(input, config, await store(), blocked, undefined, controller.signal);
  assert.equal(report.status, 'stale'); assert.equal(report.attempts, 0);
});
test('retryable task errors have bounded durable executions', async () => {
  const saved = await store(), retryConfig = configSchema.parse({ ...config, retry: { ...config.retry, baseDelayMs: 0, maxTaskExecutions: 2 } });
  let calls = 0; const bad = agent(); bad.review = async () => { calls++; throw new RetryableError('temporary'); };
  const one = await runPipeline(input, retryConfig, saved, runner, bad);
  const two = await runPipeline(input, retryConfig, saved, runner, bad);
  const three = await runPipeline(input, retryConfig, saved, runner, bad);
  assert.equal(one.executions, 1); assert.equal(two.executions, 2); assert.equal(three.executions, 2); assert.equal(calls, 2);
});
test('controller lock prevents concurrent writers and releases', async () => {
  const s = await store(), release = await s.acquire();
  await assert.rejects(s.acquire(), /lock exists/); await release(); await (await s.acquire())();
});