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.
67 lines
2.8 KiB
TypeScript
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]);
|
|
});
|
|
}); |