Files
learnharness/tests/pdf-ops.test.ts
T
amosblomqvist 9a90dd51d9 feat(T2b): sources v0 — read tool + pdf-reader sub-agent
Learner source files, on the teacher's own terms (nothing auto-injects):

- read tool (src/agent/tools/read.ts): pi-read port — path/offset/limit,
  2000-line/50KB truncation with use-offset continuation notices, image
  files (jpg/png/gif/webp/bmp) returned as native image content parts
  (new LhToolImageResult escape hatch in lh-tool.ts). Always-on in both
  the prober and teacher registries (user-decided 2026-09).
- pdf-reader sub-agent (JobKind 'pdf', toggleable, ack-and-steer): triage
  via pdf_info (pdfjs text layer: per-page text/image/math-density),
  targeted pdf_extract, regex pdf_search (hits count as consultation),
  and pdf_render — pages painted by pdfjs onto a canvas shim in headless
  Chrome (the pdfium viewer doesn't paint headless), screenshot fed back
  image-in-the-loop (visual-maker idiom, providerSafePng applied). The
  unread-finish gate bounces a zero-consultation brief once. Teacher gets
  a text brief with page citations only; renders never leave the loop.
- MODELS.pdfReader = qwen/qwen3.8-flash (user override 2026-09-19: the
  look-at-renders workload wants the vision-proven model; catalog listed
  glm-5.3-flash as image-capable but qwen is the known-good daily driver).
- UI: PdfCompleteBadge + PdfBriefCard (steer-card, job-card), dispatch
  card + activity-dock kind entries (type-forced, flagged for UI review).
- Prompts: 'Learner source files' section in teacher/30-jobs.md; read
  line in prober/10-probe-doctrine.md. Doctrine rides tool descriptions
  per the self-contained-tools principle.
2026-09-19 15:24:26 +03:00

67 lines
2.8 KiB
TypeScript

/**
* pdf-ops (sources v0) — the pdfjs text layer against a committed fixture
* (tests/fixtures/sample-3page.pdf: 3 pages, distinct subjects, "force" on
* page 2 only). The render path (headless Chrome) is covered by the
* pdf-reader loop tests via the headlessOverride fake.
*/
import assert from "node:assert/strict";
import * as path from "node:path";
import { describe, it } from "node:test";
import { parsePageSpec, pdfExtract, pdfInfo, pdfProbe, pdfSearch } from "../src/agent/subagents/pdf-ops.ts";
const FIXTURE = path.join(import.meta.dirname, "fixtures", "sample-3page.pdf");
describe("pdf-ops — the pdfjs text layer", () => {
it("pdfInfo triages every page (count, text lengths, image counts, math density)", async () => {
const info = await pdfInfo(FIXTURE);
assert.equal(info.page_count, 3);
assert.equal(info.pages.length, 3);
assert.equal(info.pages[0]!.page, 1);
assert.ok(info.pages.every((p) => p.text_length > 0));
assert.ok(info.mean_text_length > 0);
// fixture has no images
assert.ok(info.pages.every((p) => p.image_count === 0));
});
it("pdfExtract heads pages and honors the spec", async () => {
const two = await pdfExtract(FIXTURE, "1-2");
assert.match(two, /^--- Page 1 ---/);
assert.match(two, /--- Page 2 ---/);
assert.ok(!two.includes("--- Page 3 ---"), "page 3 not requested");
assert.match(two, /Calculus/);
const picks = await pdfExtract(FIXTURE, "1,3");
assert.match(picks, /--- Page 1 ---/);
assert.ok(!picks.includes("--- Page 2 ---"));
assert.match(picks, /Chemistry/);
});
it("pdfSearch finds hits with page/line + context, literal and regex", async () => {
const lit = await pdfSearch(FIXTURE, "force", { literal: true });
assert.equal(lit.hits.length, 1);
assert.equal(lit.hits[0]!.page, 2);
assert.match(lit.hits[0]!.context, />>> /);
// regex with case-insensitivity
const re = await pdfSearch(FIXTURE, "H2O|water");
assert.equal(re.hits.length, 1);
assert.equal(re.hits[0]!.page, 3);
// no-hit silence
const none = await pdfSearch(FIXTURE, "zymurgy", { literal: true });
assert.equal(none.hits.length, 0);
// invalid regex throws the skill's error shape
await assert.rejects(() => pdfSearch(FIXTURE, "([unclosed"), /Invalid regex/);
});
it("pdfProbe accepts the fixture and rejects garbage", async () => {
await pdfProbe(FIXTURE); // resolves
const garbage = path.join(import.meta.dirname, "fixtures", "not-a-pdf.txt");
await assert.rejects(() => pdfProbe(garbage));
});
it("parsePageSpec: all / ranges / lists / clamping", () => {
assert.deepEqual(parsePageSpec("all", 3), [1, 2, 3]);
assert.deepEqual(parsePageSpec("1-5", 3), [1, 2, 3]);
assert.deepEqual(parsePageSpec("1,3,7", 3), [1, 3]);
assert.deepEqual(parsePageSpec("3", 3), [3]);
assert.deepEqual(parsePageSpec(" 2 , 1 ", 3), [1, 2]);
});
});