Peer review: one "different" no longer sends a function back. It stays in review until three agents agree: three "same" send it to human review, three "different" back to its author; mixed votes let further reviewers in. When a function does go back (peers or a human), only that function goes; the rest of its batch carries on and its approved part merges without it. Public "Active agents" (who is working on what right now) and "Users" (merged, submitted, peer reviews) sections, from /api/crew. No token labels or account details. Updates: deploy/update.sh copies and syntax-checks a new release next to the running one, switches the /opt/togetherdecomp symlink in one step and only then restarts what changed (the worker when idle); pages are live without a restart. tools/deploy.sh ships HEAD (and with --td the td binaries). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNwtXPYqFu3oKaaXwYwNWW
563 lines
33 KiB
JavaScript
563 lines
33 KiB
JavaScript
import { test, before, after } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import crypto from 'node:crypto';
|
|
import { execFileSync, execFile } from 'node:child_process';
|
|
import { promisify } from 'node:util';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { fixture, FN_ADDR, CALLEE_ADDR, GLOBAL_ADDR } from './helpers.js';
|
|
import { openServer, createApp } from '../src/server/app.js';
|
|
import { Store, now } from '../src/server/db.js';
|
|
import { STATUS } from '../src/shared/compare.js';
|
|
|
|
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
let dataDir, ctx, server, base;
|
|
const quiet = { info() {}, error() {}, warn() {} };
|
|
|
|
before(async () => {
|
|
dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'td-test-'));
|
|
fs.writeFileSync(path.join(dataDir, 'server.json'), JSON.stringify({ listen: { host: '127.0.0.1', port: 0 }, verifyConcurrency: 1 }));
|
|
const repoDir = path.join(dataDir, 'repo');
|
|
fs.cpSync(path.join(ROOT, 'template'), repoDir, { recursive: true });
|
|
const git = (...a) => execFileSync('git', a, { cwd: repoDir, stdio: 'pipe' });
|
|
git('init', '-q', '-b', 'main');
|
|
git('add', '-A');
|
|
git('-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-q', '-m', 'init');
|
|
|
|
const store = new Store(path.join(dataDir, 'td.sqlite'));
|
|
const exe = path.join(dataDir, 'target.exe');
|
|
const pe = fixture().pe;
|
|
fs.writeFileSync(exe, pe);
|
|
store.run('INSERT INTO versions (id, sha256, file, size, is_current, created_at) VALUES (?, ?, ?, ?, 1, ?)', '1.0', crypto.createHash('sha256').update(pe).digest('hex'), exe, pe.length, now());
|
|
store.ensureSymbol({ kind: 'function', versionId: '1.0', addr: FN_ADDR, size: 15 });
|
|
store.ensureSymbol({ kind: 'function', versionId: '1.0', addr: CALLEE_ADDR, size: 1, name: 'Callee' });
|
|
store.db.close();
|
|
|
|
ctx = openServer(dataDir, { log: quiet });
|
|
ctx.tasks.generate();
|
|
ctx.verifier.start();
|
|
server = createApp(ctx);
|
|
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
base = `http://127.0.0.1:${server.address().port}`;
|
|
ctx.config.publicUrl = base;
|
|
});
|
|
|
|
after(() => {
|
|
ctx.verifier.stop();
|
|
server.close();
|
|
ctx.store.db.close();
|
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
});
|
|
|
|
async function request(method, p, body, { token, cookie, csrf = true } = {}) {
|
|
const headers = { 'content-type': 'application/json' };
|
|
if (token) headers.authorization = `Bearer ${token}`;
|
|
if (cookie) headers.cookie = cookie;
|
|
if (cookie && csrf) headers['x-td-csrf'] = '1';
|
|
const res = await fetch(base + p, { method, headers, body: body ? JSON.stringify(body) : undefined });
|
|
return { status: res.status, body: await res.json(), setCookie: res.headers.get('set-cookie') };
|
|
}
|
|
const call = (method, p, body, token) => request(method, p, body, { token });
|
|
|
|
const PASSWORD = 'correct horse battery';
|
|
/** Creates an approved account and returns an API token for it. */
|
|
async function activeUser(name, role = 'contributor') {
|
|
await ctx.accounts.createUser(name, role, PASSWORD);
|
|
return ctx.accounts.createApiToken(ctx.accounts.byName(name).id, 'test').token;
|
|
}
|
|
async function login(name, password = PASSWORD) {
|
|
const r = await request('POST', '/api/login', { name, password });
|
|
assert.equal(r.status, 200, JSON.stringify(r.body));
|
|
return r.setCookie.split(';')[0];
|
|
}
|
|
|
|
async function waitFor(fn, ms = 5000) {
|
|
const end = Date.now() + ms;
|
|
for (;;) {
|
|
const v = await fn();
|
|
if (v) return v;
|
|
if (Date.now() > end) throw new Error('timed out');
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
}
|
|
}
|
|
|
|
test('public info, symbol snapshot, and the target is not served by default', async () => {
|
|
const info = (await call('GET', '/api/info')).body;
|
|
assert.equal(info.version.id, '1.0');
|
|
assert.match(info.version.sha256, /^[0-9a-f]{64}$/);
|
|
assert.equal(info.version.download, null);
|
|
assert.equal((await fetch(`${base}/api/target/1.0`)).status, 404);
|
|
|
|
// "contributors": approved accounts only.
|
|
ctx.config.distributeTarget = 'contributors';
|
|
assert.equal((await fetch(`${base}/api/target/1.0`)).status, 401);
|
|
const token = await activeUser('downloader');
|
|
const res = await fetch(`${base}/api/target/1.0`, { headers: { authorization: `Bearer ${token}` } });
|
|
assert.equal(res.status, 200);
|
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
assert.equal(crypto.createHash('sha256').update(buf).digest('hex'), info.version.sha256);
|
|
ctx.config.distributeTarget = false;
|
|
const snap = (await call('GET', '/api/symbols')).body;
|
|
assert.deepEqual(snap.symbols.map((s) => s[0]).sort(), ['fn_00001', 'fn_00002']);
|
|
});
|
|
|
|
test('the repository can be cloned over HTTP but not pushed to', async () => {
|
|
const dest = path.join(dataDir, 'clone');
|
|
await promisify(execFile)("git", ["clone", "-q", `${base}/repo.git`, dest]);
|
|
assert.ok(fs.existsSync(path.join(dest, 'decomp.json')));
|
|
const res = await fetch(`${base}/repo.git/info/refs?service=git-receive-pack`);
|
|
assert.equal(res.status, 403);
|
|
});
|
|
|
|
test('claim and static-gate rejection', async () => {
|
|
const token = await activeUser('alice');
|
|
assert.equal((await call('GET', '/api/me', null, token)).body.name, 'alice');
|
|
assert.equal((await call('GET', '/api/me', null, 'td_bogus')).status, 401);
|
|
|
|
const claim = await call('POST', '/api/tasks/claim', {}, token);
|
|
assert.equal(claim.status, 200);
|
|
const ctxBody = claim.body;
|
|
assert.equal(ctxBody.annotation, `// FUNCTION: ${ctxBody.symbol.id}`);
|
|
assert.ok(ctxBody.allowedPaths.includes('include/**'));
|
|
|
|
const head = ctx.repo.head();
|
|
const sub = await call('POST', `/api/tasks/${ctxBody.task.id}/submit`, { baseCommit: head, files: { 'src/player.cpp': `// FUNCTION: ${ctxBody.symbol.id}\nvoid f() { __asm nop }\n` } }, token);
|
|
assert.equal(sub.status, 200);
|
|
|
|
const result = await waitFor(async () => {
|
|
const r = (await call('GET', `/api/attempts/${sub.body.attemptId}`, null, token)).body;
|
|
return ['rejected', 'needs_cleanup', 'in_review'].includes(r.state) && r;
|
|
});
|
|
assert.equal(result.state, 'rejected');
|
|
assert.ok(result.feedback.lintErrors.some((e) => e.message.includes('inline assembly')));
|
|
assert.equal(ctx.tasks.get(ctxBody.task.id).state, 'open');
|
|
|
|
// Lease is gone after submitting.
|
|
assert.equal((await call('POST', `/api/tasks/${ctxBody.task.id}/heartbeat`, {}, token)).status, 409);
|
|
});
|
|
|
|
test('symbol allocation returns stable IDs', async () => {
|
|
const token = await activeUser('bob');
|
|
const a = (await call('POST', '/api/symbols', { kind: 'global', addr: '0x' + GLOBAL_ADDR.toString(16), name: 'g_val' }, token)).body;
|
|
assert.match(a.id, /^gl_\d{5}$/);
|
|
assert.equal(a.annotation, `// GLOBAL: ${a.id}`);
|
|
const again = (await call('POST', '/api/symbols', { kind: 'global', addr: '0x' + GLOBAL_ADDR.toString(16) }, token)).body;
|
|
assert.equal(again.id, a.id);
|
|
assert.equal(again.created, false);
|
|
assert.equal((await call('POST', '/api/symbols', { kind: 'global', addr: '0x401000' }, token)).status, 400);
|
|
assert.equal((await call('POST', '/api/symbols', { kind: 'global', addr: '0x900000' }, token)).status, 400);
|
|
});
|
|
|
|
test('review rules: no self review, approval moves to merge queue', async () => {
|
|
const author = ctx.store.get(`SELECT id FROM users WHERE name = 'alice'`);
|
|
const task = ctx.store.get(`SELECT * FROM tasks WHERE state = 'open' LIMIT 1`);
|
|
const attemptId = Number(
|
|
ctx.store.run(`INSERT INTO attempts (task_id, user_id, base_commit, files_json, state, result_json, created_at) VALUES (?, ?, ?, '{}', 'in_review', '{}', ?)`, task.id, author.id, ctx.repo.head(), now()).lastInsertRowid,
|
|
);
|
|
ctx.store.run(`UPDATE tasks SET state = 'in_review' WHERE id = ?`, task.id);
|
|
|
|
const alice = ctx.store.get(`SELECT * FROM users WHERE name = 'alice'`);
|
|
ctx.store.run(`UPDATE users SET role = 'reviewer' WHERE id = ?`, alice.id);
|
|
assert.throws(() => ctx.merger.review({ ...alice, role: 'reviewer' }, attemptId, 'approve'), /own attempt/);
|
|
|
|
ctx.merger.pump = async () => {}; // merging needs a real compiler
|
|
const carolToken = await activeUser('carol', 'reviewer');
|
|
// Reviews need a browser session; an API token is not enough.
|
|
assert.equal((await call('GET', '/api/review/queue', null, carolToken)).status, 403);
|
|
const cookie = await login('carol');
|
|
const queue = await request('GET', '/api/review/queue', null, { cookie });
|
|
assert.ok(queue.body.some((q) => q.id === attemptId));
|
|
const r = await request('POST', `/api/attempts/${attemptId}/review`, { action: 'approve' }, { cookie });
|
|
assert.equal(r.body.state, 'approved');
|
|
assert.equal(ctx.tasks.get(task.id).state, 'approved');
|
|
// Stop the merger from picking it up (it would need a compiler).
|
|
ctx.store.run(`UPDATE tasks SET state = 'done' WHERE id = ?`, task.id);
|
|
});
|
|
|
|
test('evaluate routes results', () => {
|
|
const fn = (status, score) => ({ id: 'fn_00001', status, score, relocations: [] });
|
|
const check = (f, extra = {}) => ({ annotationErrors: [], units: [{ file: 'src/a.cpp', compiled: true, lintErrors: [], functions: f ? [f] : [], unannotated: [], ...extra }] });
|
|
const rec = { conflicts: [], unknown: [], learned: [] };
|
|
const ev = (c, r = rec) => ctx.verifier.evaluate({ check: c, rec: r, targetId: 'fn_00001', files: { 'src/a.cpp': '' }, changedHeaders: [] });
|
|
|
|
assert.equal(ev(check(fn(STATUS.EXACT, 1))).state, 'approved', 'byte-identical is approved without a human');
|
|
assert.equal(ev(check(fn(STATUS.UNRESOLVED, 1))).state, 'approved');
|
|
assert.equal(ev(check(fn(STATUS.MISMATCH, 0.8))).state, 'peer_review');
|
|
assert.equal(ev(check(fn(STATUS.MISSING, 0))).state, 'needs_cleanup');
|
|
assert.equal(ev(check(null)).state, 'needs_cleanup');
|
|
assert.equal(ev(check(fn(STATUS.EXACT, 1), { compiled: false, log: 'error C2065' })).state, 'needs_cleanup');
|
|
assert.equal(ev(check(fn(STATUS.EXACT, 1), { lintErrors: [{ message: 'x' }] })).state, 'rejected');
|
|
// A definitively wrong reference of the target bounces; an ambiguity elsewhere does not.
|
|
const withRef = { ...fn(STATUS.UNRESOLVED, 1), relocations: [{ symbol: '?Foo@@YAXXZ', ok: true, unresolved: true }] };
|
|
assert.equal(ev(check(withRef), { ...rec, conflicts: [{ symbol: '?Foo@@YAXXZ', hard: true, message: 'Foo is elsewhere' }] }).state, 'needs_cleanup');
|
|
assert.equal(ev(check(withRef), { ...rec, conflicts: [{ symbol: '?Bar@@YAXXZ', hard: true, message: 'Bar is elsewhere' }] }).state, 'approved');
|
|
assert.equal(ev(check(withRef), { ...rec, conflicts: [{ symbol: '?Foo@@YAXXZ', message: 'Foo seen twice' }] }).state, 'approved');
|
|
});
|
|
|
|
test('regressions are detected against func_status', () => {
|
|
ctx.store.run(`INSERT INTO func_status (symbol_id, file, status, score, commit_hash, updated_at) VALUES ('fn_00002', 'src/a.cpp', 'exact', 1, 'x', 0)`);
|
|
const check = { annotationErrors: [], units: [{ file: 'src/a.cpp', compiled: true, lintErrors: [], functions: [{ id: 'fn_00002', status: STATUS.MISMATCH, score: 0.5 }], unannotated: [] }] };
|
|
assert.deepEqual(ctx.verifier.regressions(check).map((r) => r.id), ['fn_00002']);
|
|
ctx.store.run(`DELETE FROM func_status`);
|
|
});
|
|
|
|
test('expired leases return to the pool', async () => {
|
|
const token = await activeUser('dave');
|
|
const c = (await call('POST', '/api/tasks/claim', {}, token)).body;
|
|
ctx.store.run('UPDATE tasks SET lease_expires = 1 WHERE id = ?', c.task.id);
|
|
assert.equal(ctx.tasks.sweepExpired(), 1);
|
|
assert.notEqual(ctx.tasks.get(c.task.id).state, 'leased');
|
|
});
|
|
|
|
test('registration needs admin approval before anything works', async () => {
|
|
const reg = await request('POST', '/api/register', { name: 'erin', password: PASSWORD });
|
|
assert.equal(reg.status, 200);
|
|
assert.equal(reg.body.status, 'pending');
|
|
assert.equal(reg.body.token, undefined);
|
|
assert.equal((await request('POST', '/api/register', { name: 'ERIN', password: PASSWORD })).status, 409, 'names are case-insensitive');
|
|
assert.equal((await request('POST', '/api/register', { name: 'shorty', password: 'short' })).status, 400);
|
|
|
|
const pending = await request('POST', '/api/login', { name: 'erin', password: PASSWORD });
|
|
assert.equal(pending.status, 403);
|
|
assert.match(pending.body.error, /approval/);
|
|
assert.equal((await request('POST', '/api/login', { name: 'erin', password: 'wrong password!!' })).status, 401);
|
|
|
|
await ctx.accounts.createUser('root', 'admin', PASSWORD);
|
|
const admin = await login('root');
|
|
const list = await request('GET', '/api/admin/users?status=pending', null, { cookie: admin });
|
|
assert.deepEqual(list.body.users.map((u) => u.name), ['erin']);
|
|
assert.equal((await request('POST', '/api/admin/users/erin/approve', {}, { cookie: admin, csrf: false })).status, 403, 'CSRF header required');
|
|
assert.equal((await request('POST', '/api/admin/users/erin/approve', {}, { cookie: admin })).status, 200);
|
|
|
|
const erin = await login('erin');
|
|
const me = await request('GET', '/api/me', null, { cookie: erin });
|
|
assert.equal(me.body.via, 'session');
|
|
assert.equal((await request('GET', '/api/admin/users', null, { cookie: erin })).status, 403, 'contributors are not admins');
|
|
});
|
|
|
|
test('API tokens: created in the browser, limited scope, revocable', async () => {
|
|
const erin = await login('erin');
|
|
const t = await request('POST', '/api/me/tokens', { label: 'laptop' }, { cookie: erin });
|
|
assert.equal(t.status, 200);
|
|
assert.match(t.body.token, /^td_/);
|
|
const token = t.body.token;
|
|
|
|
assert.equal((await call('GET', '/api/me', null, token)).body.name, 'erin');
|
|
assert.equal((await call('GET', '/api/tasks', null, token)).status, 200);
|
|
// A token cannot mint more tokens or touch accounts.
|
|
assert.equal((await call('POST', '/api/me/tokens', { label: 'x' }, token)).status, 403);
|
|
assert.equal((await call('POST', '/api/me/password', { current: PASSWORD, password: 'something else 123' }, token)).status, 403);
|
|
|
|
const list = (await request('GET', '/api/me/tokens', null, { cookie: erin })).body;
|
|
assert.equal(list[0].label, 'laptop');
|
|
assert.ok(list[0].last_used_at, 'last use is recorded');
|
|
assert.equal((await request('POST', `/api/me/tokens/${t.body.id}/revoke`, {}, { cookie: erin })).status, 200);
|
|
assert.equal((await call('GET', '/api/me', null, token)).status, 401, 'revoked token stops working at once');
|
|
});
|
|
|
|
test('revoking an account kills its tokens, sessions and leases', async () => {
|
|
const token = await activeUser('frank');
|
|
const session = await login('frank');
|
|
const claim = await call('POST', '/api/tasks/claim', {}, token);
|
|
assert.equal(claim.status, 200);
|
|
|
|
const admin = await login('root');
|
|
const r = await request('POST', '/api/admin/users/frank/revoke', {}, { cookie: admin });
|
|
assert.equal(r.status, 200);
|
|
assert.equal(r.body.releasedLeases, 1);
|
|
assert.equal((await call('GET', '/api/me', null, token)).status, 401);
|
|
assert.equal((await request('GET', '/api/me', null, { cookie: session })).status, 401);
|
|
assert.equal((await request('POST', '/api/login', { name: 'frank', password: PASSWORD })).status, 403);
|
|
assert.notEqual(ctx.tasks.get(claim.body.task.id).state, 'leased');
|
|
|
|
// Reinstating does not bring old tokens back.
|
|
assert.equal((await request('POST', '/api/admin/users/frank/reinstate', {}, { cookie: admin })).status, 200);
|
|
assert.equal((await call('GET', '/api/me', null, token)).status, 401);
|
|
await login('frank');
|
|
});
|
|
|
|
test('the last admin cannot be revoked or demoted', async () => {
|
|
const admin = await login('root');
|
|
assert.equal((await request('POST', '/api/admin/users/root/revoke', {}, { cookie: admin })).status, 409);
|
|
assert.equal((await request('POST', '/api/admin/users/root/role', { role: 'contributor' }, { cookie: admin })).status, 409);
|
|
});
|
|
|
|
test('changing the password logs out browser sessions', async () => {
|
|
const s = await login('erin');
|
|
const r = await request('POST', '/api/me/password', { current: PASSWORD, password: 'a brand new password' }, { cookie: s });
|
|
assert.equal(r.status, 200);
|
|
assert.equal((await request('GET', '/api/me', null, { cookie: s })).status, 401);
|
|
await login('erin', 'a brand new password');
|
|
});
|
|
|
|
test('peer review: three other agents must agree, by reading only', async () => {
|
|
const author = await activeUser('pauthor');
|
|
const r1 = await activeUser('peer1');
|
|
const r2 = await activeUser('peer2');
|
|
const r3 = await activeUser('peer3');
|
|
const r4 = await activeUser('peer4');
|
|
ctx.merger.pump = async () => {};
|
|
|
|
const authorId = ctx.accounts.byName('pauthor').id;
|
|
const task = ctx.store.get(`SELECT * FROM tasks WHERE state IN ('open', 'needs_cleanup') LIMIT 1`);
|
|
const feedback = { summary: 'not byte-identical', target: { score: 0.8, diffRegions: [{ offset: 4 }] } };
|
|
const attempt = Number(
|
|
ctx.store.run(
|
|
`INSERT INTO attempts (task_id, user_id, base_commit, files_json, state, score, result_json, created_at, finished_at) VALUES (?, ?, ?, ?, 'peer_review', 0.8, ?, ?, ?)`,
|
|
task.id, authorId, ctx.repo.head(), JSON.stringify({ 'src/a.cpp': '// FUNCTION: fn_00001\nint f() { return 1; }\n' }), JSON.stringify({ feedback }), now(), now(),
|
|
).lastInsertRowid,
|
|
);
|
|
ctx.store.run(`UPDATE tasks SET state = 'peer_review' WHERE id = ?`, task.id);
|
|
|
|
assert.ok(!(await call('GET', '/api/peer-reviews', null, author)).body.some((x) => x.attempt_id === attempt), 'authors do not see their own attempts');
|
|
const claim = await call('POST', '/api/peer-reviews/claim', { attemptId: attempt }, r1);
|
|
assert.equal(claim.status, 200);
|
|
assert.equal(claim.body.required, 3);
|
|
assert.ok(claim.body.instructions.some((i) => /Do not compile/.test(i)));
|
|
assert.equal(JSON.stringify(claim.body).includes('diffRegions'), false, 'no machine verdict in the review context');
|
|
assert.equal(claim.body.original.read[0], `td_disassemble ${task.symbol_id}`);
|
|
|
|
await call('POST', '/api/peer-reviews/claim', { attemptId: attempt }, r2);
|
|
await call('POST', '/api/peer-reviews/claim', { attemptId: attempt }, r3);
|
|
assert.equal((await call('POST', '/api/peer-reviews/claim', { attemptId: attempt }, r4)).status, 409, 'no more reviewers than needed');
|
|
|
|
assert.equal((await call('POST', `/api/peer-reviews/${attempt}/verdict`, { verdict: 'same', reason: 'short' }, r1)).status, 400);
|
|
assert.equal((await call('POST', `/api/peer-reviews/${attempt}/verdict`, { verdict: 'same', reason: 'all branches and calls agree' }, author)).status, 403);
|
|
assert.equal((await call('POST', `/api/peer-reviews/${attempt}/verdict`, { verdict: 'same', reason: 'all branches and calls agree' }, r1)).body.state, 'peer_review');
|
|
assert.equal((await call('POST', `/api/peer-reviews/${attempt}/verdict`, { verdict: 'same', reason: 'same loop bounds and the same return' }, r1)).status, 409, 'one verdict per account');
|
|
assert.equal((await call('POST', `/api/peer-reviews/${attempt}/verdict`, { verdict: 'same', reason: 'checked every condition and field access' }, r2)).body.state, 'peer_review');
|
|
const done = await call('POST', `/api/peer-reviews/${attempt}/verdict`, { verdict: 'same', reason: 'identical behavior, only register use differs' }, r3);
|
|
assert.equal(done.body.state, 'in_review');
|
|
assert.equal(ctx.tasks.get(task.id).state, 'in_review');
|
|
assert.ok(ctx.peers.confirmed(attempt));
|
|
|
|
// One "different" is not enough: it stays in review until three agree, here after a mixed round.
|
|
const second = Number(
|
|
ctx.store.run(`INSERT INTO attempts (task_id, user_id, base_commit, files_json, state, created_at) VALUES (?, ?, ?, '{}', 'peer_review', ?)`, task.id, authorId, ctx.repo.head(), now()).lastInsertRowid,
|
|
);
|
|
const verdict = async (reviewer, v) => {
|
|
await call('POST', '/api/peer-reviews/claim', { attemptId: second }, reviewer);
|
|
return (await call('POST', `/api/peer-reviews/${second}/verdict`, { verdict: v, reason: v === 'same' ? 'the same branches, calls and returns' : 'the original checks the flag before the loop, this does not' }, reviewer)).body;
|
|
};
|
|
assert.equal((await verdict(r4, 'different')).state, 'peer_review', 'one objection does not send it back');
|
|
assert.equal((await verdict(r1, 'different')).state, 'peer_review');
|
|
const mixed = await verdict(r2, 'same');
|
|
assert.deepEqual([mixed.state, mixed.same, mixed.different], ['peer_review', 1, 2], 'a split vote lets another reviewer in');
|
|
const no = await verdict(r3, 'different');
|
|
assert.equal(no.state, 'needs_cleanup', 'three objections send it back');
|
|
assert.equal(ctx.peers.verdicts(second)[0].reason.untrusted, true);
|
|
ctx.store.run(`UPDATE tasks SET state = 'done' WHERE id = ?`, task.id);
|
|
});
|
|
|
|
test('the crew view is public and shows no account details', async () => {
|
|
const token = await activeUser('crewview');
|
|
await call('GET', '/api/tasks', null, token); // the agent checks in
|
|
const r = await request('GET', '/api/crew');
|
|
assert.equal(r.status, 200);
|
|
assert.ok(r.body.users.some((u) => u.name === 'crewview'));
|
|
assert.ok(r.body.active.some((a) => a.name === 'crewview'), 'an agent that just called in is active');
|
|
const text = JSON.stringify(r.body);
|
|
for (const secret of ['label', 'token', 'password', 'status']) assert.equal(text.includes(`"${secret}"`), false, `no ${secret} field`);
|
|
});
|
|
|
|
test('remote checks are queued in the database and run by the worker', async () => {
|
|
const user = await activeUser('rcheck');
|
|
const realCheck = ctx.verifier.check;
|
|
ctx.verifier.check = async () => ({
|
|
check: { annotationErrors: [], units: [{ file: 'src/rc.cpp', compiled: true, lintErrors: [], unannotated: [], bindings: {}, inferred: [], includes: [], errors: [], functions: [] }] },
|
|
rec: { conflicts: [], unknown: [], learned: [], learnedAddresses: [] },
|
|
});
|
|
ctx.checks.start();
|
|
try {
|
|
const r = await call('POST', '/api/check', { baseCommit: ctx.repo.head(), files: { 'src/rc.cpp': 'int rc() { return 0; }\n' } }, user);
|
|
assert.equal(r.status, 200, JSON.stringify(r.body));
|
|
assert.equal(r.body.gate, 'ok');
|
|
assert.equal(r.body.units[0].file, 'src/rc.cpp');
|
|
assert.equal(ctx.store.get(`SELECT state FROM check_jobs ORDER BY id DESC LIMIT 1`).state, 'done');
|
|
} finally {
|
|
ctx.checks.stop();
|
|
ctx.verifier.check = realCheck;
|
|
}
|
|
});
|
|
|
|
test('maintainers can decide a function still in peer review; reviewers cannot', async () => {
|
|
await ctx.accounts.createUser('pmaint', 'maintainer', PASSWORD);
|
|
await ctx.accounts.createUser('previewer', 'reviewer', PASSWORD);
|
|
await activeUser('pmauthor');
|
|
const author = ctx.accounts.byName('pmauthor');
|
|
const v = ctx.store.currentVersion();
|
|
const sym = ctx.store.ensureSymbol({ kind: 'function', versionId: v.id, addr: 0x401600, size: 8, unit: 'src/peer.cpp' }).symbol.id;
|
|
ctx.tasks.generate();
|
|
const task = ctx.store.get('SELECT id FROM tasks WHERE symbol_id = ?', sym).id;
|
|
ctx.store.run(`UPDATE tasks SET state = 'peer_review' WHERE id = ?`, task);
|
|
const attempt = Number(ctx.store.run(`INSERT INTO attempts (task_id, user_id, base_commit, files_json, state, created_at) VALUES (?, ?, ?, '{}', 'peer_review', ?)`, task, author.id, ctx.repo.head(), now()).lastInsertRowid);
|
|
|
|
assert.throws(() => ctx.merger.review(ctx.accounts.byName('previewer'), attempt, 'approve'), /attempt is peer_review/);
|
|
assert.equal(ctx.merger.review(ctx.accounts.byName('pmaint'), attempt, 'approve').state, 'approved');
|
|
assert.equal(ctx.store.get('SELECT state FROM tasks WHERE id = ?', task).state, 'approved');
|
|
assert.ok(ctx.store.get(`SELECT 1 FROM events WHERE type = 'review' AND data LIKE '%skippedPeerReview%'`));
|
|
});
|
|
|
|
test('split: the approved part of a batch merges while the rest waits for review', async () => {
|
|
const author = await activeUser('sauthor');
|
|
const peers = [await activeUser('speer1'), await activeUser('speer2'), await activeUser('speer3')];
|
|
await ctx.accounts.createUser('sreviewer', 'reviewer', PASSWORD);
|
|
const reviewer = await login('sreviewer');
|
|
delete ctx.merger.pump;
|
|
|
|
const v = ctx.store.currentVersion();
|
|
const c = ctx.store.ensureSymbol({ kind: 'function', versionId: v.id, addr: 0x401500, size: 8, unit: 'src/split.cpp' }).symbol.id;
|
|
const d = ctx.store.ensureSymbol({ kind: 'function', versionId: v.id, addr: 0x401540, size: 8, unit: 'src/split.cpp' }).symbol.id;
|
|
ctx.tasks.generate();
|
|
|
|
// Stubbed compiler: C is byte-identical, D is not; a function is only compiled when its code is in the file.
|
|
const realCheck = ctx.verifier.check;
|
|
ctx.verifier.check = async (files) => {
|
|
const text = files['src/split.cpp'] ?? '';
|
|
const functions = [];
|
|
if (text.includes('int fc()')) functions.push({ id: c, status: STATUS.EXACT, score: 1, file: 'src/split.cpp', codeHash: 'hash-c', relocations: [], size: { ours: 8, original: 8 } });
|
|
if (text.includes('int fd()')) functions.push({ id: d, status: STATUS.MISMATCH, score: 0.9, file: 'src/split.cpp', codeHash: 'hash-d', relocations: [], size: { ours: 8, original: 8 } });
|
|
return {
|
|
check: { annotationErrors: [], units: [{ file: 'src/split.cpp', compiled: true, lintErrors: [], unannotated: [], bindings: {}, inferred: [], includes: [], errors: [], functions }] },
|
|
rec: { conflicts: [], unknown: [], learned: [], learnedAddresses: [] },
|
|
changedHeaders: [],
|
|
units: ['src/split.cpp'],
|
|
};
|
|
};
|
|
|
|
const claim = await call('POST', '/api/tasks/claim-batch', { unit: 'src/split.cpp' }, author);
|
|
const taskIds = claim.body.tasks.map((t) => t.task);
|
|
const source = `#include "x.h"\n\n// FUNCTION: ${c}\nint fc() {\n return 1;\n}\n\n// FUNCTION: ${d}\nint fd() {\n if (x) { return 2; }\n return 3;\n}\n`;
|
|
const s = await call('POST', '/api/batches', { taskIds, baseCommit: ctx.repo.head(), files: { 'src/split.cpp': source } }, author);
|
|
assert.equal(s.status, 200, JSON.stringify(s.body));
|
|
const batchState = async (id) => (await call('GET', `/api/batches/${id}`, null, author)).body;
|
|
await waitFor(async () => !['queued', 'verifying'].includes((await batchState(s.body.batchId)).state));
|
|
|
|
// C merges on its own; D stays behind in the original batch, rebased onto that commit.
|
|
await ctx.merger.pump();
|
|
const head1 = ctx.repo.head();
|
|
const onMain = ctx.repo.show(head1, 'src/split.cpp');
|
|
assert.match(onMain, /int fc\(\)/);
|
|
assert.doesNotMatch(onMain, /int fd\(\)/, 'the function still in review is not merged');
|
|
assert.equal(ctx.store.get('SELECT state FROM tasks WHERE symbol_id = ?', c).state, 'done');
|
|
const rest = ctx.store.get('SELECT * FROM batches WHERE id = ?', s.body.batchId);
|
|
assert.equal(rest.state, 'open');
|
|
assert.equal(rest.base_commit, head1);
|
|
assert.equal(JSON.parse(rest.files_json)['src/split.cpp'], source);
|
|
|
|
// D passes review later and merges on top.
|
|
const att = (await batchState(s.body.batchId)).functions.find((f) => f.symbol === d).attemptId;
|
|
for (const t of peers) {
|
|
await call('POST', '/api/peer-reviews/claim', { attemptId: att }, t);
|
|
await call('POST', `/api/peer-reviews/${att}/verdict`, { verdict: 'same', reason: 'same branches and return values' }, t);
|
|
}
|
|
assert.equal((await request('POST', `/api/batches/${s.body.batchId}/review`, { action: 'approve' }, { cookie: reviewer })).status, 200);
|
|
await ctx.merger.pump();
|
|
assert.equal(ctx.store.get('SELECT state FROM batches WHERE id = ?', s.body.batchId).state, 'merged');
|
|
assert.equal(ctx.repo.show(ctx.repo.head(), 'src/split.cpp'), source);
|
|
|
|
ctx.verifier.check = realCheck;
|
|
});
|
|
|
|
test('batches: one compile, auto-approval, peer review, one function sent back, carry-over', async () => {
|
|
const author = await activeUser('bauthor');
|
|
const peers = [await activeUser('bpeer1'), await activeUser('bpeer2'), await activeUser('bpeer3')];
|
|
await ctx.accounts.createUser('breviewer', 'reviewer', PASSWORD);
|
|
const reviewer = await login('breviewer');
|
|
delete ctx.merger.pump; // an earlier test stubbed it on the instance; use the real one here
|
|
|
|
// Two fresh functions in one unit.
|
|
const v = ctx.store.currentVersion();
|
|
const a = ctx.store.ensureSymbol({ kind: 'function', versionId: v.id, addr: 0x401300, size: 8, unit: 'src/batch.cpp' }).symbol.id;
|
|
const b = ctx.store.ensureSymbol({ kind: 'function', versionId: v.id, addr: 0x401340, size: 8, unit: 'src/batch.cpp' }).symbol.id;
|
|
ctx.tasks.generate();
|
|
|
|
// The compiler is stubbed: A byte-identical, B not; B's code fingerprint decides carry-over.
|
|
let bHash = 'hash-b-1';
|
|
const realCheck = ctx.verifier.check;
|
|
// A function is only compiled when its code is in the file.
|
|
ctx.verifier.check = async (files) => ({
|
|
check: {
|
|
annotationErrors: [],
|
|
units: [{
|
|
file: 'src/batch.cpp', compiled: true, lintErrors: [], unannotated: [], bindings: {}, inferred: [], includes: [], errors: [],
|
|
functions: [
|
|
{ id: a, status: STATUS.EXACT, score: 1, file: 'src/batch.cpp', codeHash: 'hash-a', relocations: [], size: { ours: 8, original: 8 } },
|
|
{ id: b, status: STATUS.MISMATCH, score: 0.9, file: 'src/batch.cpp', codeHash: bHash, relocations: [], size: { ours: 8, original: 8 } },
|
|
].filter((f) => (files['src/batch.cpp'] ?? '').includes(f.id === a ? 'int fa()' : 'int fb()')),
|
|
}],
|
|
},
|
|
rec: { conflicts: [], unknown: [], learned: [], learnedAddresses: [] },
|
|
changedHeaders: [],
|
|
units: ['src/batch.cpp'],
|
|
});
|
|
|
|
const claim = await call('POST', '/api/tasks/claim-batch', { unit: 'src/batch.cpp' }, author);
|
|
assert.equal(claim.status, 200, JSON.stringify(claim.body));
|
|
assert.deepEqual(claim.body.tasks.map((t) => t.symbol).sort(), [a, b].sort());
|
|
const taskIds = claim.body.tasks.map((t) => t.task);
|
|
const source = `// FUNCTION: ${a}\nint fa() { return 1; }\n// FUNCTION: ${b}\nint fb() { return 2; }\n`;
|
|
const submit = () => call('POST', '/api/batches', { taskIds, baseCommit: ctx.repo.head(), files: { 'src/batch.cpp': source } }, author);
|
|
const batchState = async (id) => (await call('GET', `/api/batches/${id}`, null, author)).body;
|
|
const settle = (id) => waitFor(async () => { const st = await batchState(id); return !['queued', 'verifying'].includes(st.state) && st; });
|
|
|
|
const s1 = await submit();
|
|
assert.equal(s1.status, 200, JSON.stringify(s1.body));
|
|
const r1 = Object.fromEntries((await settle(s1.body.batchId)).functions.map((f) => [f.symbol, f]));
|
|
assert.equal(r1[a].state, 'approved', 'byte-identical needs no human');
|
|
assert.equal(r1[b].state, 'peer_review');
|
|
|
|
// Round 1: a peer claim covers the batch's waiting functions; all three find a difference.
|
|
const pc = await call('POST', '/api/peer-reviews/claim', { attemptId: r1[b].attemptId }, peers[0]);
|
|
assert.deepEqual(pc.body.functions.map((f) => f.symbol), [b]);
|
|
for (const t of peers) {
|
|
await call('POST', '/api/peer-reviews/claim', { attemptId: r1[b].attemptId }, t);
|
|
await call('POST', `/api/peer-reviews/${r1[b].attemptId}/verdict`, { verdict: 'different', reason: 'the original checks the flag first, this does not' }, t);
|
|
}
|
|
// Only B goes back; A merges on its own and the batch is over.
|
|
const back = Object.fromEntries((await batchState(s1.body.batchId)).functions.map((f) => [f.symbol, f]));
|
|
assert.equal(back[b].state, 'needs_cleanup');
|
|
assert.equal(back[a].state, 'approved', 'the rest of the batch is not sent back');
|
|
await ctx.merger.pump();
|
|
assert.doesNotMatch(ctx.repo.show(ctx.repo.head(), 'src/batch.cpp'), /int fb\(\)/, 'the function sent back is not merged');
|
|
assert.equal(ctx.store.get('SELECT state FROM tasks WHERE symbol_id = ?', a).state, 'done');
|
|
assert.equal((await batchState(s1.body.batchId)).state, 'returned');
|
|
|
|
// Round 2: the author fixes B (new fingerprint) and resubmits it; three peers confirm.
|
|
bHash = 'hash-b-2';
|
|
const taskOfB = claim.body.tasks.find((t) => t.symbol === b).task;
|
|
assert.equal((await call('POST', '/api/tasks/claim', { taskId: taskOfB }, author)).status, 200);
|
|
const s2 = await call('POST', '/api/batches', { taskIds: [taskOfB], baseCommit: ctx.repo.head(), files: { 'src/batch.cpp': source } }, author);
|
|
const r2 = Object.fromEntries((await settle(s2.body.batchId)).functions.map((f) => [f.symbol, f]));
|
|
assert.equal(r2[b].state, 'peer_review', 'changed code gets reviewed again');
|
|
for (const t of peers) {
|
|
await call('POST', '/api/peer-reviews/claim', { attemptId: r2[b].attemptId }, t);
|
|
await call('POST', `/api/peer-reviews/${r2[b].attemptId}/verdict`, { verdict: 'same', reason: 'now checks the flag first like the original' }, t);
|
|
}
|
|
assert.equal((await batchState(s2.body.batchId)).functions.find((f) => f.symbol === b).state, 'in_review');
|
|
|
|
// One human click for the batch; one merge commit.
|
|
const rev = await request('POST', `/api/batches/${s2.body.batchId}/review`, { action: 'approve' }, { cookie: reviewer });
|
|
assert.equal(rev.status, 200, JSON.stringify(rev.body));
|
|
await ctx.merger.pump();
|
|
const merged = await batchState(s2.body.batchId);
|
|
assert.equal(merged.state, 'merged', JSON.stringify(merged));
|
|
assert.match(ctx.repo.git(['log', '-1', '--format=%B']), new RegExp(`Decompile ${b}[\\s\\S]*Peer-reviewed-by`));
|
|
assert.equal(ctx.store.get('SELECT confirmed FROM func_status WHERE symbol_id = ?', b).confirmed, 1);
|
|
assert.equal(ctx.store.get('SELECT code_hash FROM func_status WHERE symbol_id = ?', a).code_hash, 'hash-a');
|
|
|
|
// Carry-over: an unchanged, peer-confirmed function keeps its verdicts in a resubmission.
|
|
const taskB = ctx.store.get('SELECT task_id FROM attempts WHERE id = ?', r2[b].attemptId).task_id;
|
|
const next = { task_id: taskB, id: Number(ctx.store.run(`INSERT INTO attempts (task_id, user_id, base_commit, files_json, state, created_at) VALUES (?, ?, ?, '{}', 'verifying', ?)`, taskB, ctx.accounts.byName('bauthor').id, ctx.repo.head(), now()).lastInsertRowid) };
|
|
ctx.store.run('UPDATE attempts SET result_json = ? WHERE id = ?', JSON.stringify({ feedback: { target: { codeHash: 'hash-b-2' } } }), r2[b].attemptId);
|
|
assert.equal(ctx.verifier.carryOver(next, { codeHash: 'hash-b-2' }), true);
|
|
assert.equal(ctx.verifier.carryOver(next, { codeHash: 'hash-b-3' }), false);
|
|
|
|
// Guard: a submission may not change functions it does not hold.
|
|
const outside = ctx.verifier.changedOutside({ units: [{ functions: [{ id: a, file: 'src/batch.cpp', codeHash: 'hash-a-changed' }] }] }, { 'src/batch.cpp': '' }, new Set([b]));
|
|
assert.deepEqual(outside.map((o) => o.id), [a]);
|
|
|
|
ctx.verifier.check = realCheck;
|
|
});
|