ci: isolate test setup and fix Windows PDF font paths
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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+⇥");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)", () => {
|
||||
|
||||
Reference in New Issue
Block a user