ci: isolate test setup and fix Windows PDF font paths
This commit is contained in:
@@ -58,7 +58,7 @@ jobs:
|
|||||||
cache: npm
|
cache: npm
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Native renderer tests on this OS
|
- 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:
|
env:
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
ELECTRON_RUN_AS_NODE: "1"
|
ELECTRON_RUN_AS_NODE: "1"
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
cache: npm
|
cache: npm
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Native renderer tests on this OS
|
- 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:
|
env:
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
ELECTRON_RUN_AS_NODE: "1"
|
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 -->
|
<!-- END:nextjs-agent-rules -->
|
||||||
|
|
||||||
<!-- BEGIN:lh-workflow -->
|
# No guessing
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
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.
|
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 os from "node:os";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
|
import { standardFontsDir } from "./pdf-ops";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verification engines for sub-agents: demos use the app's own Electron
|
* 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:
|
// Standard-font data as a directory path (live-smoke finding 2026-09-19:
|
||||||
// without it pdfjs renders non-embedded standard fonts via fallback);
|
// without it pdfjs renders non-embedded standard fonts via fallback);
|
||||||
// verbosity 0 = errors only (JBIG2 noise on scanned PDFs).
|
// verbosity 0 = errors only (JBIG2 noise on scanned PDFs).
|
||||||
const standardFontDataUrl =
|
const loadingTask = pdfjs.getDocument({ data, standardFontDataUrl: standardFontsDir(), verbosity: 0 });
|
||||||
path.join(PATHS_nodeModules(), "pdfjs-dist", "standard_fonts") + path.sep;
|
|
||||||
const loadingTask = pdfjs.getDocument({ data, standardFontDataUrl, verbosity: 0 });
|
|
||||||
const doc = await loadingTask.promise;
|
const doc = await loadingTask.promise;
|
||||||
try {
|
try {
|
||||||
const out: RenderedPdfPage[] = [];
|
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 ────────────────────────────────────────────────────────────
|
// ─── the real API ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const realHeadless: HeadlessApi = {
|
export const realHeadless: HeadlessApi = {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { fileURLToPath } from "node:url";
|
|||||||
* pdf_search / pdf_render), on the user-chosen hybrid stack:
|
* pdf_search / pdf_render), on the user-chosen hybrid stack:
|
||||||
*
|
*
|
||||||
* - TEXT (info / extract / search): pdfjs-dist — pure JS, no native deps.
|
* - TEXT (info / extract / search): pdfjs-dist — pure JS, no native deps.
|
||||||
* - RENDER: the shared headless-Chrome infra (subagents/headless.ts,
|
* - RENDER: subagents/headless.ts uses the same pdfjs engine with
|
||||||
* puppeteer + system Chrome) screenshots pdfium-rendered pages.
|
* @napi-rs/canvas (Skia), in-process — no browser needed.
|
||||||
*
|
*
|
||||||
* Doctrine (the skill's reading strategy, mirrored in the sub-agent's system
|
* 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
|
* 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
|
/** Standard-font data dir (live-smoke finding 2026-09-19: without it pdfjs
|
||||||
* warns and falls back on non-embedded standard fonts — Helvetica, Times…).
|
* warns and falls back on non-embedded standard fonts — Helvetica, Times…).
|
||||||
* The node side takes a plain filesystem path. Resolved from this module's
|
* The node side takes a plain filesystem path, shared by text and rendering.
|
||||||
* location, the headless.ts PATHS_nodeModules idiom. */
|
* 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 {
|
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) {
|
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.")]),
|
() => 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.
|
// Turn 1: the ask question.
|
||||||
await drainTurn(post(chat.id, { type: "text", text: "teach me Lacan" }), agentFactory(engine));
|
await drainTurn(post(chat.id, { type: "text", text: "teach me Lacan" }), agentFactory(engine));
|
||||||
let saved = chatStore.getChat(chat.id)!;
|
let saved = chatStore.getChat(chat.id)!;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
actionForBinding,
|
actionForBinding,
|
||||||
bindingForAction,
|
bindingForAction,
|
||||||
formatBinding,
|
formatBinding,
|
||||||
|
IS_MAC,
|
||||||
kbdEventForAction,
|
kbdEventForAction,
|
||||||
KEYBOARD_ACTIONS,
|
KEYBOARD_ACTIONS,
|
||||||
} from "../src/core/keyboard.ts";
|
} 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", () => {
|
it("mod+b is the toggle-sidebar action", () => {
|
||||||
assert.equal(actionForBinding("mod+b"), "toggle-sidebar");
|
assert.equal(actionForBinding("mod+b"), "toggle-sidebar");
|
||||||
assert.equal(bindingForAction("toggle-sidebar"), "mod+b");
|
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", () => {
|
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", () => {
|
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(actionForBinding("shift+tab"), "cycle-thinking");
|
||||||
assert.equal(formatBinding("shift+tab"), "⇧⇥");
|
assert.equal(formatBinding("shift+tab"), IS_MAC ? "⇧⇥" : "Shift+⇥");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
* this process).
|
* this process).
|
||||||
*/
|
*/
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
import * as fs from "node:fs";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
import { realHeadless } from "../src/agent/subagents/headless.ts";
|
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");
|
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)", () => {
|
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 () => {
|
it("renders fixture pages as canonical single-IDAT PNGs", async () => {
|
||||||
const pages = await realHeadless.renderPdfPages(FIXTURE, [1, 3], { dpi: 150 });
|
const pages = await realHeadless.renderPdfPages(FIXTURE, [1, 3], { dpi: 150 });
|
||||||
assert.equal(pages.length, 2, "requested pages 1 and 3");
|
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
|
* 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
|
* appended as an em-dash suffix off the LIVE registry (a rebind changes
|
||||||
* the suffix); truncation-reveals keep native title=; the copy carries no
|
* the suffix); truncation-reveals keep native title=; the copy carries no
|
||||||
* internal vocabulary. Node has no navigator → IS_MAC is false → the mod
|
* internal vocabulary. Node 26 exposes the host platform through navigator,
|
||||||
* glyph renders "Ctrl+" here; the composition logic is the pin, not the
|
* so expectations cover Mac glyphs AND Ctrl+/Shift+ elsewhere. Composition
|
||||||
* platform glyph. (Node 26 ships navigator.platform — on this Mac the real
|
* is the pin; running on a Mac must not be a test prerequisite.
|
||||||
* ⌘/⇧ glyphs render, so they're pinned as-is.)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -19,12 +18,13 @@ import { renderToStaticMarkup } from "react-dom/server";
|
|||||||
import { Hint, hintText } from "../components/ui/hint.tsx";
|
import { Hint, hintText } from "../components/ui/hint.tsx";
|
||||||
import { TOOLTIP_DELAY_MS } from "../components/ui/tooltip.tsx";
|
import { TOOLTIP_DELAY_MS } from "../components/ui/tooltip.tsx";
|
||||||
import { AUTO_UPDATE_HINT } from "../components/settings/settings-dialog.tsx";
|
import { AUTO_UPDATE_HINT } from "../components/settings/settings-dialog.tsx";
|
||||||
|
import { IS_MAC } from "../src/core/keyboard.ts";
|
||||||
|
|
||||||
describe("hintText — the parenthesized composition", () => {
|
describe("hintText — the parenthesized composition", () => {
|
||||||
it("a registry action appends the LIVE binding (override-aware source)", () => {
|
it("a registry action appends the LIVE binding (override-aware source)", () => {
|
||||||
assert.equal(hintText("Dictate", "toggle-dictation"), "Dictate (⌘D)");
|
assert.equal(hintText("Dictate", "toggle-dictation"), IS_MAC ? "Dictate (⌘D)" : "Dictate (Ctrl+D)");
|
||||||
assert.equal(hintText("Settings", "open-settings"), "Settings (⌘,)");
|
assert.equal(hintText("Settings", "open-settings"), IS_MAC ? "Settings (⌘,)" : "Settings (Ctrl+,)");
|
||||||
assert.equal(hintText("Search sessions", "sessions-search"), "Search sessions (⌘⇧F)");
|
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)", () => {
|
it("surface-owned keys ride the binding prop (no registry entry)", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user