ci: isolate test setup and fix Windows PDF font paths

This commit is contained in:
amosblomqvist
2026-10-11 10:41:30 +03:00
parent e266c3c028
commit 0540bf697c
9 changed files with 32 additions and 45 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ jobs:
cache: npm
- run: npm ci
- name: Native renderer tests on this OS
run: npx electron --import tsx --test tests/pdf-native-render.test.ts tests/svg-native-render.test.ts tests/browser-service.test.ts
run: npx electron --import tsx --test tests/pdf-native-render.test.ts tests/pdf-ops.test.ts tests/svg-native-render.test.ts tests/browser-service.test.ts
env:
NODE_ENV: test
ELECTRON_RUN_AS_NODE: "1"
+1 -1
View File
@@ -115,7 +115,7 @@ jobs:
cache: npm
- run: npm ci
- name: Native renderer tests on this OS
run: npx electron --import tsx --test tests/pdf-native-render.test.ts tests/svg-native-render.test.ts tests/browser-service.test.ts
run: npx electron --import tsx --test tests/pdf-native-render.test.ts tests/pdf-ops.test.ts tests/svg-native-render.test.ts tests/browser-service.test.ts
env:
NODE_ENV: test
ELECTRON_RUN_AS_NODE: "1"
+1 -18
View File
@@ -8,24 +8,7 @@ This block is written and re-added by `next dev` — verify at `node_modules/nex
<!-- END:nextjs-agent-rules -->
<!-- BEGIN:lh-workflow -->
# Workflow: you are one parallel development stream
This repo is developed in parallel using git worktrees. You are one stream; the master session merges.
- **Read `SCOPE.md` at the repo root first** — it holds your mission, your dev-server port, your owned files, and your done-protocol. It is untracked (never commit it).
- **Stay in your lane.** Only touch files SCOPE.md lists as yours. If work outside your lane is needed, write it up in your final summary instead of doing it.
- **Contracts are split by domain** (`src/core/contracts/{plan,graph,chat,jobs,probe,models}.ts`): each stream owns its domain's contract file outright. Schema changes in a domain you don't own go through the master session — never change another domain's contract in your branch.
- **Gates before every commit:** `npm run typecheck` and `npm test` and `npm run lint` all pass; `npm run build` before declaring done.
- **Dev server:** always `PORT=<your SCOPE.md port> npm run dev` — port 3000 belongs to the master tree and Next refuses to start a second server on it.
- `.env.local` is a symlink to the master tree's copy (secrets only — never edit it). Models + reasoning budgets live in `config.ts` MODELS.
- Your `data/` is disposable scratch (chat records + graph ledgers). The master tree holds the real data. Never copy data back into the master tree; a one-way throwaway copy of master's `data/graph` + `data/chats` into your worktree for testing is fine.
- **Don't merge yourself.** When your wave is done: stop, summarize (what changed, what you deliberately didn't touch, open questions), and wait for the master to merge and rebase you.
<!-- END:lh-workflow -->
# No guessing (master directive — applies to every stream)
# No guessing
All design authority belongs to the human. **Never guess a design or implementation decision.** If anything is ambiguous — naming, behavior, data shapes, UX, defaults, scope boundaries, prompt wording — STOP and ask with the ask-user-question tool before proceeding. If you are "a little unsure," you are unsure: ask. A wrong guess is far more expensive than a question. Ask early, ask specific, one question per call.
+2 -9
View File
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { standardFontsDir } from "./pdf-ops";
/**
* Verification engines for sub-agents: demos use the app's own Electron
@@ -483,9 +484,7 @@ async function renderPdfPages(
// Standard-font data as a directory path (live-smoke finding 2026-09-19:
// without it pdfjs renders non-embedded standard fonts via fallback);
// verbosity 0 = errors only (JBIG2 noise on scanned PDFs).
const standardFontDataUrl =
path.join(PATHS_nodeModules(), "pdfjs-dist", "standard_fonts") + path.sep;
const loadingTask = pdfjs.getDocument({ data, standardFontDataUrl, verbosity: 0 });
const loadingTask = pdfjs.getDocument({ data, standardFontDataUrl: standardFontsDir(), verbosity: 0 });
const doc = await loadingTask.promise;
try {
const out: RenderedPdfPage[] = [];
@@ -515,12 +514,6 @@ async function renderPdfPages(
}
}
/** node_modules — resolved from this module's location. */
function PATHS_nodeModules(): string {
// import.meta.url = .../src/agent/subagents/headless.ts → three up + node_modules
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "node_modules");
}
// ─── the real API ────────────────────────────────────────────────────────────
export const realHeadless: HeadlessApi = {
+6 -5
View File
@@ -8,8 +8,8 @@ import { fileURLToPath } from "node:url";
* pdf_search / pdf_render), on the user-chosen hybrid stack:
*
* - TEXT (info / extract / search): pdfjs-dist — pure JS, no native deps.
* - RENDER: the shared headless-Chrome infra (subagents/headless.ts,
* puppeteer + system Chrome) screenshots pdfium-rendered pages.
* - RENDER: subagents/headless.ts uses the same pdfjs engine with
* @napi-rs/canvas (Skia), in-process — no browser needed.
*
* Doctrine (the skill's reading strategy, mirrored in the sub-agent's system
* prompt): triage with pdf_info FIRST, then size-based strategy — short PDFs
@@ -45,10 +45,11 @@ async function loadPdfjs() {
/** Standard-font data dir (live-smoke finding 2026-09-19: without it pdfjs
* warns and falls back on non-embedded standard fonts — Helvetica, Times…).
* The node side takes a plain filesystem path. Resolved from this module's
* location, the headless.ts PATHS_nodeModules idiom. */
* The node side takes a plain filesystem path, shared by text and rendering.
* pdfjs checks endsWith("/") even on Windows: use a literal trailing slash,
* NOT path.sep ("\\" on Windows). Node fs accepts this path on every OS. */
export function standardFontsDir(): string {
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "node_modules", "pdfjs-dist", "standard_fonts") + path.sep;
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "node_modules", "pdfjs-dist", "standard_fonts") + "/";
}
async function openDocument(filePath: string) {
+2 -1
View File
@@ -1598,7 +1598,8 @@ describe("duplicate provider tool-call ids (the Lacan fix)", () => {
() => fauxAssistantMessage([fauxText("Right — and that is where Lacan begins.")]),
]);
const chat = chatStore.createChat("Lacan: duplicate call_0 turns");
const chat = createConfiguredChat("Lacan: duplicate call_0 turns");
createdChats.push(chat.id);
// Turn 1: the ask question.
await drainTurn(post(chat.id, { type: "text", text: "teach me Lacan" }), agentFactory(engine));
let saved = chatStore.getChat(chat.id)!;
+4 -3
View File
@@ -4,6 +4,7 @@ import {
actionForBinding,
bindingForAction,
formatBinding,
IS_MAC,
kbdEventForAction,
KEYBOARD_ACTIONS,
} from "../src/core/keyboard.ts";
@@ -40,7 +41,7 @@ describe("the ⌘B chain — keypress → binding → action → event", () => {
it("mod+b is the toggle-sidebar action", () => {
assert.equal(actionForBinding("mod+b"), "toggle-sidebar");
assert.equal(bindingForAction("toggle-sidebar"), "mod+b");
assert.equal(formatBinding("mod+b"), "⌘B");
assert.equal(formatBinding("mod+b"), IS_MAC ? "⌘B" : "Ctrl+B");
});
it("the event the chain ends in is the one the sidebar answers on", () => {
@@ -51,9 +52,9 @@ describe("the ⌘B chain — keypress → binding → action → event", () => {
});
describe("the thinking pill suffix — ⇧⇥ via the registry", () => {
it("shift+tab formats as ⇧⇥ and maps to cycle-thinking", () => {
it("shift+tab uses the platform's modifier label and maps to cycle-thinking", () => {
assert.equal(actionForBinding("shift+tab"), "cycle-thinking");
assert.equal(formatBinding("shift+tab"), "⇧⇥");
assert.equal(formatBinding("shift+tab"), IS_MAC ? "⇧⇥" : "Shift+⇥");
});
});
+8
View File
@@ -7,9 +7,11 @@
* this process).
*/
import assert from "node:assert/strict";
import * as fs from "node:fs";
import * as path from "node:path";
import { describe, it } from "node:test";
import { realHeadless } from "../src/agent/subagents/headless.ts";
import { standardFontsDir } from "../src/agent/subagents/pdf-ops.ts";
const FIXTURE = path.join(import.meta.dirname, "fixtures", "sample-3page.pdf");
@@ -30,6 +32,12 @@ function pngFacts(buf: Buffer): { w: number; h: number; idats: number } {
}
describe("native pdf_render (pdfjs paints, @napi-rs/canvas hosts)", () => {
it("the shared font directory ends in pdfjs's required slash on every OS", () => {
const fonts = standardFontsDir();
assert.ok(fonts.endsWith("/"), "pdfjs rejects a trailing Windows backslash");
assert.ok(fs.statSync(fonts).isDirectory(), "the filesystem accepts the shared font path");
});
it("renders fixture pages as canonical single-IDAT PNGs", async () => {
const pages = await realHeadless.renderPdfPages(FIXTURE, [1, 3], { dpi: 150 });
assert.equal(pages.length, 2, "requested pages 1 and 3");
+7 -7
View File
@@ -3,10 +3,9 @@
* icon/pill controls ride the styled Radix tooltip via Hint, the shortcut
* appended as an em-dash suffix off the LIVE registry (a rebind changes
* the suffix); truncation-reveals keep native title=; the copy carries no
* internal vocabulary. Node has no navigator → IS_MAC is false → the mod
* glyph renders "Ctrl+" here; the composition logic is the pin, not the
* platform glyph. (Node 26 ships navigator.platform — on this Mac the real
* ⌘/⇧ glyphs render, so they're pinned as-is.)
* internal vocabulary. Node 26 exposes the host platform through navigator,
* so expectations cover Mac glyphs AND Ctrl+/Shift+ elsewhere. Composition
* is the pin; running on a Mac must not be a test prerequisite.
*/
import React from "react";
@@ -19,12 +18,13 @@ import { renderToStaticMarkup } from "react-dom/server";
import { Hint, hintText } from "../components/ui/hint.tsx";
import { TOOLTIP_DELAY_MS } from "../components/ui/tooltip.tsx";
import { AUTO_UPDATE_HINT } from "../components/settings/settings-dialog.tsx";
import { IS_MAC } from "../src/core/keyboard.ts";
describe("hintText — the parenthesized composition", () => {
it("a registry action appends the LIVE binding (override-aware source)", () => {
assert.equal(hintText("Dictate", "toggle-dictation"), "Dictate (⌘D)");
assert.equal(hintText("Settings", "open-settings"), "Settings (⌘,)");
assert.equal(hintText("Search sessions", "sessions-search"), "Search sessions (⌘⇧F)");
assert.equal(hintText("Dictate", "toggle-dictation"), IS_MAC ? "Dictate (⌘D)" : "Dictate (Ctrl+D)");
assert.equal(hintText("Settings", "open-settings"), IS_MAC ? "Settings (⌘,)" : "Settings (Ctrl+,)");
assert.equal(hintText("Search sessions", "sessions-search"), IS_MAC ? "Search sessions (⌘⇧F)" : "Search sessions (Ctrl+Shift+F)");
});
it("surface-owned keys ride the binding prop (no registry entry)", () => {