__tests__/resight.test.js
Rama main · réplica de lectura del repositorio original.
/*
* RE-SIGHTING THE OLD RUNS, AND THE WAYS IT COULD MINT A WRONG PAGE.
*
* A re-sight attaches a page fetched today to a decision taken months ago. The danger is not that it
* fails - a failed fetch leaves the turn blind, as it was - but that it succeeds on the WRONG page:
* a read after a click nobody recorded, a 404 the same length as an article, a redirect to a login
* wall. Each test here is one of those.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
import resight from '../src/resight.js';
const { candidates, accept, contentFor, pick, left, load, save, setOn, state, urlOfStep } = resight;
const step = (kind, text, extra = {}) => ({ kind, text, at: '2026-09-01T00:00:00Z', ...extra });
const blind = (n) => step('read', `read the page (${n} characters)`);
describe('which reads can be re-taken', () => {
it('takes the address from the open before the read', () => {
const job = { steps: [step('tool', 'open(https://example.org/a)'), step('open', 'https://example.org/a'), step('tool', 'read()'), blind(911)] };
expect(candidates(job)).toEqual([{ index: 3, url: 'https://example.org/a', expected: 911 }]);
});
it('never re-sights a read after a click, because nobody knows where the click went', () => {
const job = { steps: [step('open', 'https://example.org/a'), step('click', 'clicked [3]'), blind(911)] };
expect(candidates(job)).toEqual([]);
});
it('knows the address again after a later open, and from a dig that named its url', () => {
const job = { steps: [
step('open', 'https://example.org/a'), step('click', 'clicked [3]'), blind(100),
step('read', 'read https://example.org/b (2000 characters)', { url: 'https://example.org/b' }), blind(2100),
] };
expect(candidates(job)).toEqual([{ index: 4, url: 'https://example.org/b', expected: 2100 }]);
});
it('leaves alone a read that already carries its page, or was already tried', () => {
const job = { steps: [
step('open', 'https://example.org/a'),
{ ...blind(500), content: 'You are on: ...' },
{ ...blind(500), resighted: { rejected: 'empty' } },
blind(500),
] };
expect(candidates(job).map((c) => c.index)).toEqual([3]);
});
it('reads "back to" and "now using" as addresses too', () => {
expect(urlOfStep(step('open', 'back to https://example.org/x'))).toBe('https://example.org/x');
expect(urlOfStep(step('open', 'now using "hn" — https://news.ycombinator.com/'))).toBe('https://news.ycombinator.com/');
expect(urlOfStep(step('look', 'Example — 12 things to click'))).toBeNull();
});
});
describe('whether the page fetched today is the page read then', () => {
it('accepts a length within a third of the record, on the same site', () => {
expect(accept({ expected: 1000, got: 1200, asked: 'https://a.org/x', landed: 'https://www.a.org/x' }).ok).toBe(true);
});
it('refuses an empty page, a redirect elsewhere, and a page of a different size', () => {
expect(accept({ expected: 1000, got: 12 }).ok).toBe(false);
expect(accept({ expected: 1000, got: 1000, asked: 'https://a.org/x', landed: 'https://login.b.org/' }).why).toMatch(/redirected/);
expect(accept({ expected: 1000, got: 300 }).why).toMatch(/300 characters today against 1000/);
expect(accept({ expected: 1000, got: 4000 }).ok).toBe(false);
});
it('hands the model exactly what the read tool would have', () => {
const c = contentFor('https://a.org/x', 'one\n\n\n\ntwo');
expect(c).toBe('You are on: https://a.org/x\n\nPage text:\none\n\ntwo');
expect(contentFor('u', 'x'.repeat(9000)).length).toBeLessThan(5100);
});
});
describe('the order and the bookkeeping', () => {
let dir;
beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gb-resight-')); process.env.PROFILE_DIR = dir; });
afterEach(() => { delete process.env.PROFILE_DIR; try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* gone */ } });
const run = (id, tier, n = 2) => ({ id, status: 'done', createdAt: `2026-09-0${n}T00:00:00Z`, verdict: { tier },
steps: [step('open', `https://${id}.org/`), blind(800), step('scroll', 'down'), blind(1600)] });
it('takes gold runs first, skips void ones and runs already done, and never overfills a batch', () => {
const jobs = [run('silver1', 'silver'), run('gold1', 'gold'), run('void1', 'void'), run('gold2', 'gold', 3)];
const b = pick(jobs, { doneJobs: { gold2: 'x' } }, { max: 3 });
expect(b.map((x) => x.job.id)).toEqual(['gold1', 'silver1']);
expect(b[0].cands.length).toBe(2);
expect(b[1].cands.length).toBe(1);
expect(left(jobs, { doneJobs: { gold2: 'x' } })).toBe(4);
});
it('scans the runs on disk in slices, and orders what it found gold first', () => {
const { scan, order, loadJob } = resight;
const jdir = path.join(dir, 'jobs');
fs.mkdirSync(jdir, { recursive: true });
for (const j of [run('silver1', 'silver'), run('gold1', 'gold'), run('void1', 'void'), run('gold2', 'gold', 3)]) {
fs.writeFileSync(path.join(jdir, `${j.id}.json`), JSON.stringify(j));
}
fs.writeFileSync(path.join(jdir, 'broken.json'), '{not json');
const a = scan(jdir, { doneJobs: {} }, { from: 0, limit: 2 });
expect(a.done).toBe(false);
expect(a.next).toBe(2);
const b = scan(jdir, { doneJobs: {} }, { from: a.next, limit: 100 });
expect(b.done).toBe(true);
const all = order([...a.found, ...b.found]);
expect(all.map((e) => e.id)).toEqual(['gold2', 'gold1', 'silver1']);
expect(all.every((e) => e.n === 2)).toBe(true);
expect(loadJob(jdir, 'gold1').id).toBe('gold1');
expect(loadJob(jdir, 'nope')).toBeNull();
});
it('keeps its tallies and its switch on disk', () => {
expect(load().on).toBe(false);
setOn(true);
const st = load(); st.accepted = 7; st.rejected = 2; st.left = 40; st.doneJobs.a = 'x'; save(st);
expect(state()).toMatchObject({ on: true, accepted: 7, rejected: 2, left: 40, doneJobs: 1 });
setOn(false);
expect(state().on).toBe(false);
});
});