mirror of
https://github.com/Heretek-AI/Agent-Proof.git
synced 2026-11-07 10:46:10 -10:00
166 lines
6.3 KiB
TypeScript
166 lines
6.3 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import * as os from 'node:os';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { unlockGovernance } from '../src/installer/index.js';
|
|
import { formatDiagnostics } from '../src/formatter/index.js';
|
|
|
|
describe('E2E Repository Hard-Gate Initialization & Slop Interception', () => {
|
|
let tempDir: string;
|
|
const rootDir = path.resolve(__dirname, '..');
|
|
const binScriptPath = path.join(rootDir, 'bin', 'agent-gate.js');
|
|
|
|
beforeEach(() => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-gate-e2e-test-'));
|
|
|
|
// 1. Initialize git in mock repo
|
|
fs.mkdirSync(path.join(tempDir, '.git'), { recursive: true });
|
|
|
|
// 2. Setup mock multi-stack repository files
|
|
// JS/TS stack
|
|
fs.writeFileSync(
|
|
path.join(tempDir, 'package.json'),
|
|
JSON.stringify({
|
|
name: 'mock-polyglot-repo',
|
|
version: '1.0.0',
|
|
scripts: { build: 'tsc' },
|
|
devDependencies: { typescript: '^5.0.0' },
|
|
}, null, 2)
|
|
);
|
|
fs.writeFileSync(path.join(tempDir, 'tsconfig.json'), '{}');
|
|
|
|
// Python stack
|
|
fs.writeFileSync(
|
|
path.join(tempDir, 'pyproject.toml'),
|
|
'[project]\nname = "mock-backend"\nversion = "0.1.0"\n'
|
|
);
|
|
|
|
// GitHub Workflows & Infra
|
|
const wfDir = path.join(tempDir, '.github', 'workflows');
|
|
fs.mkdirSync(wfDir, { recursive: true });
|
|
fs.writeFileSync(path.join(wfDir, 'ci.yml'), 'name: CI\non: [push]\n');
|
|
|
|
// Agent Harness
|
|
const skillsDir = path.join(tempDir, '.claude', 'skills');
|
|
fs.mkdirSync(skillsDir, { recursive: true });
|
|
fs.writeFileSync(path.join(skillsDir, 'test-runner.md'), '---\nname: "test-runner"\ndescription: "Run tests"\n---\n');
|
|
fs.writeFileSync(path.join(tempDir, 'SKILL.md'), '---\nname: "agent-root"\ndescription: "Root skill"\n---\n');
|
|
|
|
// Create simulated slop code files
|
|
const srcDir = path.join(tempDir, 'src');
|
|
fs.mkdirSync(srcDir, { recursive: true });
|
|
|
|
// Simulated TypeScript slop: empty catch block & any cast
|
|
fs.writeFileSync(
|
|
path.join(srcDir, 'auth_slop.ts'),
|
|
`export async function login(token: any) {
|
|
try {
|
|
const payload = JSON.parse(token);
|
|
return payload;
|
|
} catch (e) {
|
|
// Slop: swallowed error
|
|
}
|
|
}`
|
|
);
|
|
|
|
// Simulated Python slop: bare except & unused import
|
|
fs.writeFileSync(
|
|
path.join(tempDir, 'backend_slop.py'),
|
|
`import os
|
|
import sys
|
|
|
|
def process_data(data):
|
|
try:
|
|
return data["value"]
|
|
except:
|
|
pass
|
|
`
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
try {
|
|
unlockGovernance(tempDir);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('runs npx create-agent-gate (bin/agent-gate.js init), generates configs, and locks gates', () => {
|
|
// Execute CLI in mock repository
|
|
const output = execFileSync(process.execPath, [binScriptPath, 'init', tempDir], {
|
|
encoding: 'utf-8',
|
|
});
|
|
|
|
expect(output).toContain('Agent-Proof Mechanical Hard-Gate Initializer');
|
|
expect(output).toContain('Detected Stacks: JavaScript/TypeScript, Python, Workflows/Infra, Agent Harness');
|
|
expect(output).toContain('Emitted 6 configs');
|
|
expect(output).toContain('Repository is now Agent-Proof!');
|
|
|
|
// Verify files exist on disk
|
|
expect(fs.existsSync(path.join(tempDir, 'lefthook.yml'))).toBe(true);
|
|
expect(fs.existsSync(path.join(tempDir, '.claude', 'hooks.json'))).toBe(true);
|
|
expect(fs.existsSync(path.join(tempDir, '.claude', 'settings.json'))).toBe(true);
|
|
expect(fs.existsSync(path.join(tempDir, 'biome.json'))).toBe(true);
|
|
expect(fs.existsSync(path.join(tempDir, 'ruff.toml'))).toBe(true);
|
|
expect(fs.existsSync(path.join(tempDir, '.aislop', 'config.yml'))).toBe(true);
|
|
|
|
// Verify git hooks installed
|
|
expect(fs.existsSync(path.join(tempDir, '.git', 'hooks', 'pre-commit'))).toBe(true);
|
|
expect(fs.existsSync(path.join(tempDir, '.git', 'hooks', 'pre-push'))).toBe(true);
|
|
|
|
// Verify immutable permissions applied
|
|
const hooksStats = fs.statSync(path.join(tempDir, '.claude', 'hooks.json'));
|
|
// Read-only check (user write bit is not set: 0o200)
|
|
expect((hooksStats.mode & 0o200) === 0).toBe(true);
|
|
});
|
|
|
|
it('intercepts simulated slop and produces an LSP-compliant diagnostic envelope with repair tokens', () => {
|
|
// Simulate raw failure output from aislop detecting the empty catch in auth_slop.ts
|
|
const simulatedAislopStderr = `src/auth_slop.ts:6:5: [AI_SLOP_SWALLOWED_ERROR] Empty catch block silently suppresses authentication error.`;
|
|
|
|
const envelope = formatDiagnostics(simulatedAislopStderr, {
|
|
toolName: 'aislop',
|
|
stage: 'PreCommit',
|
|
});
|
|
|
|
expect(envelope.$schema).toBe('https://json.schemastore.org/lsif.json');
|
|
expect(envelope.status).toBe('GATE_FAILED');
|
|
expect(envelope.summary.total_errors).toBe(1);
|
|
expect(envelope.summary.gate_stage).toBe('PreCommit');
|
|
|
|
const diag = envelope.diagnostics[0];
|
|
expect(diag.source).toBe('aislop');
|
|
expect(diag.rule_id).toBe('AI_SLOP_SWALLOWED_ERROR');
|
|
expect(diag.file_path).toBe('src/auth_slop.ts');
|
|
expect(diag.range?.start.line).toBe(6);
|
|
expect(diag.repair_instruction?.action).toBe('REWRITE_BLOCK');
|
|
expect(diag.repair_instruction?.repair_tokens.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('intercepts simulated Python slop and produces Ruff diagnostics with repair tokens', () => {
|
|
// Simulate raw failure output from ruff detecting bare except and unused imports in backend_slop.py
|
|
const simulatedRuffStderr = `backend_slop.py:1:1: F401 [*] 'os' imported but unused
|
|
backend_slop.py:2:1: F401 [*] 'sys' imported but unused
|
|
backend_slop.py:7:5: E722 Do not use bare 'except'`;
|
|
|
|
const envelope = formatDiagnostics(simulatedRuffStderr, {
|
|
toolName: 'ruff',
|
|
stage: 'PreCommit',
|
|
});
|
|
|
|
expect(envelope.status).toBe('GATE_FAILED');
|
|
expect(envelope.summary.total_errors).toBe(3);
|
|
|
|
const f401 = envelope.diagnostics.find(d => d.rule_id === 'F401');
|
|
expect(f401?.file_path).toBe('backend_slop.py');
|
|
expect(f401?.repair_instruction?.action).toBe('DELETE_LINE');
|
|
|
|
const e722 = envelope.diagnostics.find(d => d.rule_id === 'E722');
|
|
expect(e722?.repair_instruction?.action).toBe('REWRITE_BLOCK');
|
|
expect(e722?.repair_instruction?.repair_tokens[0]).toContain('except Exception');
|
|
});
|
|
});
|