@zenodinh/pi-render 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +20 -6
  2. package/index.ts +29 -33
  3. package/package.json +6 -2
  4. package/src/core/paint.ts +17 -2
  5. package/src/core/types/host.ts +15 -18
  6. package/src/core/types.ts +0 -1
  7. package/src/renderers/content/artifacts/cards.ts +13 -6
  8. package/src/renderers/content/code-panel.ts +53 -31
  9. package/src/renderers/content/index.ts +70 -21
  10. package/src/renderers/content/json-panel.ts +9 -17
  11. package/src/renderers/content/link-paragraph.ts +153 -0
  12. package/src/renderers/content/table.ts +4 -1
  13. package/src/renderers/tool/runtime.ts +44 -5
  14. package/src/renderers/tool/specs/search.ts +18 -10
  15. package/src/renderers/tool/types.ts +6 -1
  16. package/src/commands/canvas.test.ts +0 -150
  17. package/src/commands/canvas.ts +0 -57
  18. package/src/core/code-theme.test.ts +0 -266
  19. package/src/core/log.test.ts +0 -127
  20. package/src/core/paint.test.ts +0 -322
  21. package/src/core/registry.test.ts +0 -307
  22. package/src/core/settings.test.ts +0 -183
  23. package/src/renderers/content/artifacts/artifacts.test.ts +0 -199
  24. package/src/renderers/content/artifacts/cards.test.ts +0 -216
  25. package/src/renderers/content/artifacts/engines-extra.test.ts +0 -556
  26. package/src/renderers/content/artifacts/local-binary.test.ts +0 -207
  27. package/src/renderers/content/image-card.test.ts +0 -170
  28. package/src/renderers/content/panels.test.ts +0 -188
  29. package/src/renderers/content/table.test.ts +0 -209
  30. package/src/renderers/content/transformer.test.ts +0 -254
  31. package/src/renderers/tool/resolver.test.ts +0 -257
  32. package/src/renderers/tool/runtime.test.ts +0 -313
  33. package/src/renderers/tool/specs/bash.test.ts +0 -110
  34. package/src/renderers/tool/specs/codemode.test.ts +0 -212
  35. package/src/renderers/tool/specs/edit.test.ts +0 -260
  36. package/src/renderers/tool/specs/ls.test.ts +0 -173
  37. package/src/renderers/tool/specs/read.test.ts +0 -340
  38. package/src/renderers/tool/specs/search.test.ts +0 -197
  39. package/src/renderers/tool/specs/write.test.ts +0 -145
  40. package/themes/themes.test.ts +0 -251
@@ -1,209 +0,0 @@
1
- /**
2
- * table.test.ts — the table surface: banded geometry, theme-derived frame, contrast gate.
3
- *
4
- * Every test enters at the exported Surface (table.rewrite) with markdown in and markdown out, and
5
- * reads the frame through the marker paint double, so a leaked real escape — the #22 defect class —
6
- * is visible from the string alone. No host package is imported here.
7
- */
8
-
9
- // shape: none — dispatch object does not apply: one entry point and no discriminator, so the cases
10
- // differ by fixture data and never by behavior.
11
-
12
- import { describe, expect, it } from "vitest";
13
- import { assertNoRealEscapes, markerPaint } from "../../../test/fakes/index.ts";
14
- import { createContentPaint } from "../../core/paint.ts";
15
- import type { TransformContext } from "../../core/types/host.ts";
16
- import { table } from "./table.ts";
17
-
18
- const ESC = String.fromCharCode(27);
19
- const ESCAPE = new RegExp(`${ESC}\\[[0-9;]*m`, "g");
20
-
21
- type Rgb = readonly [number, number, number];
22
-
23
- /** One sRGB channel of a WCAG relative-luminance computation. */
24
- function channel(value: number): number {
25
- const srgb = value / 255;
26
- return srgb <= 0.04045 ? srgb / 12.92 : ((srgb + 0.055) / 1.055) ** 2.4;
27
- }
28
-
29
- function luminance([red, green, blue]: Rgb): number {
30
- return 0.2126 * channel(red) + 0.7152 * channel(green) + 0.0722 * channel(blue);
31
- }
32
-
33
- /** WCAG 2.x contrast ratio, lighter over darker. */
34
- function contrastRatio(a: Rgb, b: Rgb): number {
35
- const [lighter, darker]: [Rgb, Rgb] = luminance(a) >= luminance(b) ? [a, b] : [b, a];
36
- return (luminance(lighter) + 0.05) / (luminance(darker) + 0.05);
37
- }
38
-
39
- const CTX: TransformContext = { messageType: "assistant", isStreaming: false, availableWidth: 60 };
40
-
41
- /** Painted through a sentinel, so the assertions compare against the fake's own pair, not a copy. */
42
- const SENTINEL = "\u0000";
43
- const [RULE_OPEN = "", RULE_CLOSE = ""] = markerPaint().rule(SENTINEL).split(SENTINEL);
44
-
45
- const plain = (text: string): string => text.replace(ESCAPE, "");
46
-
47
- /** One entry per markdown code span (the fence stripped); markers are kept for the escape assertions. */
48
- const splitRows = (output: string): string[] =>
49
- output
50
- .replace(/\n$/, "")
51
- .split(" \n")
52
- .map((row) => row.slice(1, -1));
53
-
54
- const rowsOf = (output: string): string[] => splitRows(output).map(plain);
55
-
56
- // invented: a canonical three-column GFM table with a wrapping cell; its plain rows were diffed
57
- // against the predecessor's renderTable output on the same token (probe, 2026-10-06) — identical.
58
- const CANONICAL = [
59
- "| Tool | Purpose | Notes |",
60
- "|---|---|---|",
61
- "| bash | run a shell command in the working directory | collapses to a single line; ctrl+o expands the row |",
62
- "| read | read a file | none |",
63
- "",
64
- ].join("\n");
65
-
66
- // recorded-from: pi-pretty-tui/src/features/canvas/table.test.ts (2026-10-06) — its long-cell case;
67
- // the path is one unbroken word, so it only fits by wrapping mid-token.
68
- const LONG_PATH =
69
- "~/.pi/agent/pi-pretty-tui-cache/118fe853b8b6cf94b9300c0909baa93c9abadffb41e819fc52f60a1bd963d914.svg";
70
- const WIDE = [
71
- "| Gate | State |",
72
- "|---|---|",
73
- `| sa-validate | PASS — 16/16, 495 lines (first run caught a real gap: ${LONG_PATH}) |`,
74
- "",
75
- ].join("\n");
76
-
77
- // invented: one cell per inline shape the host tokenizer emits, so each role's handling is pinned.
78
- const INLINE = ["| A | B |", "|---|---|", "| `bash` | **bold** and *em* and [link](http://x) |", ""].join("\n");
79
-
80
- /** the golden: the predecessor's banded geometry, pinned as literals (AC-1). */
81
- const GOLDEN_TOP = "┌────────┬────────────────────────┬────────────────────────┐";
82
- const GOLDEN_ROWS = [
83
- GOLDEN_TOP,
84
- "│ Tool │ Purpose │ Notes │",
85
- "├────────┼────────────────────────┼────────────────────────┤",
86
- "│ bash │ run a shell command in │ collapses to a single │",
87
- "│ │ the working directory │ line; ctrl+o expands │",
88
- "│ │ │ the row │",
89
- "├────────┼────────────────────────┼────────────────────────┤",
90
- "│ read │ read a file │ none │",
91
- "└────────┴────────────────────────┴────────────────────────┘",
92
- ];
93
-
94
- describe("table.rewrite — geometry", () => {
95
- it("AC-1: the canonical table matches the golden banded layout", () => {
96
- const output = table.rewrite(CANONICAL, CTX, markerPaint());
97
- // fails_when: a rule, a column width, or a wrap regresses against the predecessor's table
98
- expect(rowsOf(output)).toEqual(GOLDEN_ROWS);
99
- });
100
-
101
- it("AC-1: every row is emitted as one code span, so the host cannot reflow the box", () => {
102
- const output = table.rewrite(CANONICAL, CTX, markerPaint());
103
- // fails_when: raw box-drawing goes back into markdown, where the host reflows or recolors it
104
- for (const row of plain(output).replace(/\n$/, "").split(" \n")) {
105
- expect(row.startsWith("`")).toBe(true);
106
- expect(row.endsWith("`")).toBe(true);
107
- }
108
- expect(plain(output).endsWith("`\n")).toBe(true);
109
- });
110
-
111
- it("AC-1: nothing is elided — a wrapped cell stays reachable in full", () => {
112
- const output = table.rewrite(WIDE, CTX, markerPaint());
113
- // fails_when: a cell is truncated instead of wrapped (geometry only survives if it wraps)
114
- expect(plain(output)).not.toContain("...");
115
- expect(plain(output)).toContain("963d914.svg");
116
- expect(rowsOf(output)).toHaveLength(9);
117
- });
118
-
119
- it("AC-4: no row exceeds availableWidth", () => {
120
- const wide = table.rewrite(WIDE, CTX, markerPaint());
121
- const narrow = table.rewrite(CANONICAL, { ...CTX, availableWidth: 40 }, markerPaint());
122
- // fails_when: an overflowing row reaches the terminal
123
- for (const row of rowsOf(wide)) expect(row.length).toBeLessThanOrEqual(60);
124
- for (const row of rowsOf(narrow)) expect(row.length).toBeLessThanOrEqual(40);
125
- });
126
-
127
- it("returns markdown that holds no table byte-identically", () => {
128
- const paragraph = "A paragraph with a | pipe but no table row.\n";
129
- // recorded-from: executed probe 2026-10-06 — Marked normalizes \r\n to \n, so joining the
130
- // tokens' own raw text is NOT byte-exact; only the input itself preserves the bytes.
131
- const crlf = "line one\r\nline two with | pipe\r\n";
132
- // fails_when: a pipe-bearing block with no table is rebuilt from lexed tokens
133
- expect(table.rewrite(paragraph, CTX, markerPaint())).toBe(paragraph);
134
- expect(table.rewrite(crlf, CTX, markerPaint())).toBe(crlf);
135
- });
136
-
137
- it("rewrites the table block and leaves the surrounding blocks alone", () => {
138
- const output = table.rewrite(`Before the table.\n\n${CANONICAL}\nAfter the table.\n`, CTX, markerPaint());
139
- // fails_when: the surrounding blocks are dropped or rewritten
140
- expect(plain(output).startsWith("Before the table.\n\n")).toBe(true);
141
- expect(plain(output).endsWith("\n\nAfter the table.\n")).toBe(true);
142
- expect(plain(output).split(GOLDEN_TOP).length - 1).toBe(1);
143
- });
144
- });
145
-
146
- describe("table.rewrite — the #22 fix", () => {
147
- it("AC-2: the frame is painted by the rule role and nothing else", () => {
148
- const output = table.rewrite(CANONICAL, CTX, markerPaint());
149
- const marked = splitRows(output);
150
- // fails_when: a hardcoded frame color (a real escape survives the marker-only detector)
151
- assertNoRealEscapes(output);
152
- // fails_when: any escape in the frame that is not a rule marker
153
- expect([...new Set(output.match(ESCAPE) ?? [])].sort()).toEqual([RULE_OPEN, RULE_CLOSE].sort());
154
- for (const row of marked) {
155
- if (row.includes("│")) {
156
- // fails_when: a bar drawn outside a rule pair
157
- expect(row.split(`${RULE_OPEN}│${RULE_CLOSE}`)).toHaveLength(row.split("│").length);
158
- } else {
159
- // fails_when: a rule line drawn outside a rule pair
160
- expect(row.startsWith(RULE_OPEN)).toBe(true);
161
- expect(row.endsWith(RULE_CLOSE)).toBe(true);
162
- }
163
- }
164
- });
165
-
166
- it("AC-2: every frame segment comes from the live theme's hr, never from a literal", () => {
167
- const calls: string[] = [];
168
- const paint = createContentPaint({
169
- hr: (text) => {
170
- calls.push(text);
171
- return text;
172
- },
173
- });
174
- const output = table.rewrite(CANONICAL, CTX, paint);
175
- // fails_when: the frame carries a hardcoded color instead of the theme's own token
176
- expect(rowsOf(output)).toEqual(GOLDEN_ROWS);
177
- expect(output).not.toContain(ESC);
178
- expect(calls).toContain("│");
179
- expect(calls.filter((text) => text.includes("─"))).toHaveLength(4);
180
- });
181
-
182
- it("AC-2: inline code goes through the code role; emphasis keeps its text", () => {
183
- const output = table.rewrite(INLINE, CTX, markerPaint());
184
- const codePair = markerPaint().code(SENTINEL).split(SENTINEL);
185
- assertNoRealEscapes(output);
186
- // fails_when: inline code renders as its plain text (the role is never called)
187
- expect(output).toContain(`${codePair[0]}bash${codePair[1]}`);
188
- // fails_when: an emphasis wrapper leaks as a literal escape, or its text is dropped
189
- expect(rowsOf(output)).toContain("│ bash │ bold and em and link │");
190
- });
191
- });
192
-
193
- describe("table.rewrite — the frame token's contrast (issue #22)", () => {
194
- // recorded-from: sa-reviewer-delta-verdict.md point 7 (executed getResolvedThemeColors + WCAG
195
- // computation, 2026-10-06) — mdHr resolves to #9da5a9 dark / #677176 light; the backgrounds are
196
- // the pinned fixtures the design computes against (the terminal background is not reachable here).
197
- const THEMES = ["dark", "light"] as const;
198
- const RECORDED: Record<(typeof THEMES)[number], { mdHr: Rgb; background: Rgb; ratio: number }> = {
199
- dark: { mdHr: [0x9d, 0xa5, 0xa9], background: [0x1e, 0x1e, 0x2e], ratio: 6.55 },
200
- light: { mdHr: [0x67, 0x71, 0x76], background: [0xff, 0xff, 0xff], ratio: 5.0 },
201
- };
202
-
203
- it.each(THEMES)("AC-3: mdHr on the %s theme clears the 4.5:1 gate", (theme) => {
204
- const { mdHr, background, ratio } = RECORDED[theme];
205
- // fails_when: either theme measures below the gate — then the token, not the test, is wrong
206
- expect(contrastRatio(mdHr, background)).toBeGreaterThanOrEqual(4.5);
207
- expect(contrastRatio(mdHr, background)).toBeCloseTo(ratio, 1);
208
- });
209
- });
@@ -1,254 +0,0 @@
1
- /**
2
- * transformer through its seam — createContentTransformer(registry, surfaces, deps), driven with fake
3
- * surfaces, a fake registry, the marker paint, and a recording logger.
4
- *
5
- * Every scenario's first meaningful action is a factory call; nothing below the seam is imported.
6
- *
7
- * invented: the fake surface / registry / logger doubles stand in for T-22..T-24, core/registry and
8
- * core/log — they satisfy the structural contracts and record every call.
9
- */
10
- import { describe, expect, test } from "vitest";
11
- import { markerPaint } from "../../../test/fakes/index.ts";
12
- import type { TransformContext } from "../../core/types/host.ts";
13
- import type { LogEntry, Logger } from "../../core/types/log.ts";
14
- import type { ContentPaint } from "../../core/types/paint.ts";
15
- import type { Registry } from "../../core/types/registry.ts";
16
- import { type ContentSurfaces, createContentTransformer } from "./index.ts";
17
- import type { Surface } from "./types.ts";
18
-
19
- const PAINT = markerPaint();
20
- const ASSISTANT: TransformContext = { messageType: "assistant", isStreaming: false, availableWidth: 80 };
21
-
22
- // invented: a two-row table and a one-line fence are the smallest inputs that name two different
23
- // surfaces; T-22/T-23 own the real table and panel shapes, and this ticket pins dispatch, not rendering.
24
- const TABLE = "| a | b |\n| - | - |\n| 1 | 2 |\n";
25
- const FENCE = "```ts\nconst x = 1;\n```\n";
26
-
27
- // recorded-from: @earendil-works/pi-coding-agent dist, createMermaidMarkdownTransformer({getMode: () =>
28
- // "unicode"}), width 100, executed 2026-10-07 — its output for "```mermaid\ngraph TD; A-->B;\n```".
29
- const HOST_MERMAID_ROWS = "` ┌───┐` \n` │ A │` \n` └─┬─┘` \n` │` \n` ▼` \n` ┌───┐` \n` │ B │` \n` └───┘`\n";
30
-
31
- /** One recorded rewrite call. */
32
- interface SurfaceCall {
33
- markdown: string;
34
- ctx: TransformContext;
35
- paint: ContentPaint;
36
- }
37
-
38
- interface FakeSurface extends Surface {
39
- /** Every call this surface received, in order; a skipped surface leaves no entry. Required. */
40
- readonly calls: SurfaceCall[];
41
- }
42
-
43
- function fakeSurface(transform: (markdown: string) => string): FakeSurface {
44
- const calls: SurfaceCall[] = [];
45
- return {
46
- calls,
47
- rewrite(markdown, ctx, paint) {
48
- calls.push({ markdown, ctx, paint });
49
- return transform(markdown);
50
- },
51
- };
52
- }
53
-
54
- function fakeRegistry(disabled: readonly string[]): Registry {
55
- return {
56
- defineModule: () => {},
57
- isEnabled: (key) => !disabled.includes(key),
58
- getSettings: () => ({}),
59
- setSettings: () => {},
60
- };
61
- }
62
-
63
- function fakeLog(): { log: Logger; entries: LogEntry[] } {
64
- const entries: LogEntry[] = [];
65
- const seen = new Set<string>();
66
- return {
67
- entries,
68
- log: {
69
- logLine(scope, message) {
70
- entries.push({ scope, message, time: 0 });
71
- },
72
- logOnce(key, scope, message) {
73
- if (seen.has(key)) return;
74
- seen.add(key);
75
- entries.push({ scope, message, time: 0 });
76
- },
77
- drain: () => entries.splice(0, entries.length),
78
- },
79
- };
80
- }
81
-
82
- type Slot = keyof ContentSurfaces;
83
-
84
- interface Harness {
85
- readonly transformer: (markdown: string, ctx: TransformContext) => string;
86
- readonly table: FakeSurface;
87
- readonly codePanel: FakeSurface;
88
- readonly jsonPanel: FakeSurface;
89
- readonly imageCard: FakeSurface;
90
- readonly artifacts: FakeSurface | undefined;
91
- readonly entries: LogEntry[];
92
- }
93
-
94
- function harness(
95
- config: {
96
- /** Registry keys to disable, e.g. "content.table". */
97
- disabled?: readonly string[];
98
- /** Per-slot rewrites; a slot named here but absent from `rewrite` passes its input through. */
99
- rewrite?: Partial<Record<Slot, (markdown: string) => string>>;
100
- /** Slots whose rewrite throws — the never-throw path. */
101
- throwing?: readonly Slot[];
102
- /** Inject the artifacts fence pass, as T-28 will once T-25/T-26 land. */
103
- withArtifacts?: boolean;
104
- } = {},
105
- ): Harness {
106
- const { disabled = [], rewrite = {}, throwing = [], withArtifacts = false } = config;
107
- const make = (slot: Slot): FakeSurface => {
108
- if (throwing.includes(slot)) {
109
- return fakeSurface(() => {
110
- throw new Error(`${slot} exploded`);
111
- });
112
- }
113
- return fakeSurface(rewrite[slot] ?? ((markdown) => markdown));
114
- };
115
-
116
- const table = make("table");
117
- const codePanel = make("codePanel");
118
- const jsonPanel = make("jsonPanel");
119
- const imageCard = make("imageCard");
120
- const artifacts = withArtifacts ? make("artifacts") : undefined;
121
- const { log, entries } = fakeLog();
122
- const surfaces: ContentSurfaces =
123
- artifacts === undefined
124
- ? { table, codePanel, jsonPanel, imageCard }
125
- : { table, codePanel, jsonPanel, imageCard, artifacts };
126
-
127
- return {
128
- table,
129
- codePanel,
130
- jsonPanel,
131
- imageCard,
132
- artifacts,
133
- entries,
134
- transformer: createContentTransformer(fakeRegistry(disabled), surfaces, { paint: PAINT, log }),
135
- };
136
- }
137
-
138
- /** Every rewrite the transformer dispatched, across all slots — a gate or skip leaves none. */
139
- function allCalls(h: Harness): SurfaceCall[] {
140
- return [h.table, h.codePanel, h.jsonPanel, h.imageCard, h.artifacts]
141
- .filter((surface) => surface !== undefined)
142
- .flatMap((surface) => surface.calls);
143
- }
144
-
145
- describe("content transformer", () => {
146
- // spec: transformer(markdown, {messageType:"user"|"assistant-thinking"}) -> markdown, zero surface
147
- // calls; transformer(markdown, {isStreaming:true}) -> markdown, zero calls; transformer(markdown,
148
- // settled assistant) -> the table surface's output.
149
- // fails_when: streaming or user content is transformed.
150
- test("AC-1 the gate keeps every non-final-assistant message by reference", () => {
151
- const h = harness({ rewrite: { table: (markdown) => markdown.replace(TABLE, "TABLE\n") } });
152
- const markdown = TABLE;
153
-
154
- expect(h.transformer(markdown, { ...ASSISTANT, messageType: "user" })).toBe(markdown);
155
- expect(h.transformer(markdown, { ...ASSISTANT, messageType: "assistant-thinking" })).toBe(markdown);
156
- expect(h.transformer(markdown, { ...ASSISTANT, isStreaming: true })).toBe(markdown);
157
- expect(allCalls(h)).toEqual([]);
158
-
159
- // The gate assertion above is not vacuous: the same markdown on a settled assistant pass rewrites.
160
- expect(h.transformer(markdown, ASSISTANT)).toBe("TABLE\n");
161
- expect(h.table.calls).toHaveLength(1);
162
- });
163
-
164
- // spec: registry disables content.table -> the table text passes through unchanged while
165
- // content.codePanel still rewrites the fence; enabling both rewrites both.
166
- // fails_when: one disabled surface blocks the rest or transforms anyway.
167
- test("AC-2 a disabled surface is skipped while the other surfaces still rewrite", () => {
168
- const rewrite = {
169
- table: (markdown: string) => markdown.replace(TABLE, "TABLE\n"),
170
- codePanel: (markdown: string) => markdown.replace(FENCE, "PANEL\n"),
171
- };
172
- const markdown = TABLE + FENCE;
173
-
174
- const disabled = harness({ disabled: ["content.table"], rewrite });
175
- expect(disabled.transformer(markdown, ASSISTANT)).toBe(`${TABLE}PANEL\n`);
176
- expect(disabled.table.calls).toEqual([]);
177
- expect(disabled.codePanel.calls).toHaveLength(1);
178
- expect(disabled.codePanel.calls[0]?.markdown).toBe(markdown);
179
-
180
- // The enabled control is what makes the pass-through above meaningful.
181
- const enabled = harness({ rewrite });
182
- expect(enabled.transformer(markdown, ASSISTANT)).toBe("TABLE\nPANEL\n");
183
- expect(enabled.table.calls[0]?.ctx).toBe(ASSISTANT);
184
- expect(enabled.table.calls[0]?.paint).toBe(PAINT);
185
- });
186
-
187
- // spec: all four surfaces enabled, each returning its input by reference -> prose matching no
188
- // surface returns the same string while a table the table surface rewrites does not.
189
- // fails_when: a no-op pass is turned into a rewrite (a changed string where nothing matched).
190
- test("AC-3 markdown matching no surface returns the input unchanged", () => {
191
- const h = harness({ rewrite: { table: (markdown) => markdown.replace(TABLE, "TABLE\n") } });
192
- const prose = "Just prose: no table, no fence, no image reference.\n";
193
-
194
- expect(h.transformer(prose, ASSISTANT)).toBe(prose);
195
- // Every slot was dispatched: the identity survives a full pass, not a skipped one.
196
- expect(allCalls(h).map((call) => call.markdown)).toEqual([prose, prose, prose, prose]);
197
-
198
- expect(h.transformer(TABLE, ASSISTANT)).toBe("TABLE\n");
199
- });
200
-
201
- // spec: the content.codePanel surface throws -> the ORIGINAL input returns (the table surface's rewrite
202
- // is rolled back) and exactly one entry with scope "content" and message
203
- // "transform failed: codePanel exploded" is logged; a second failing pass logs nothing more.
204
- // fails_when: a failing surface kills the answer render.
205
- test("AC-4 a throwing surface returns the input unchanged with one keyed log line", () => {
206
- const h = harness({
207
- rewrite: { table: (markdown) => markdown.replace(TABLE, "TABLE\n") },
208
- throwing: ["codePanel"],
209
- });
210
- const markdown = TABLE + FENCE;
211
-
212
- expect(h.transformer(markdown, ASSISTANT)).toBe(markdown);
213
- expect(h.entries).toEqual([{ scope: "content", message: "transform failed: codePanel exploded", time: 0 }]);
214
-
215
- expect(h.transformer(markdown, ASSISTANT)).toBe(markdown);
216
- expect(h.entries).toHaveLength(1);
217
- });
218
-
219
- // spec: input carrying the host's already-transformed mermaid rows alone -> the same string returns;
220
- // input carrying those rows plus a table -> the rows survive byte-for-byte and the table alone
221
- // rewrites, with the rows reaching the surface verbatim.
222
- // fails_when: double-transformation of host-owned content.
223
- test("AC-5 already-transformed mermaid passes through untouched", () => {
224
- const untouched = harness();
225
- expect(untouched.transformer(HOST_MERMAID_ROWS, ASSISTANT)).toBe(HOST_MERMAID_ROWS);
226
-
227
- const h = harness({ rewrite: { table: (markdown) => markdown.replace(TABLE, "TABLE\n") } });
228
- const message = `Diagram:\n\n${HOST_MERMAID_ROWS}${TABLE}`;
229
-
230
- expect(h.transformer(message, ASSISTANT)).toBe(`Diagram:\n\n${HOST_MERMAID_ROWS}TABLE\n`);
231
- expect(h.table.calls[0]?.markdown).toBe(message);
232
- });
233
-
234
- // spec: artifacts injected -> its rewrite runs AFTER the four surfaces (it receives their output) and
235
- // its card replaces the fence; content.artifacts disabled -> the fence pass is skipped and no call
236
- // lands.
237
- // fails_when: the fence pass runs while disabled, or before the markdown surfaces.
238
- test("the artifacts fence pass runs last, only while content.artifacts is enabled", () => {
239
- const rewrite = {
240
- table: (markdown: string) => markdown.replace(TABLE, "TABLE\n"),
241
- artifacts: (markdown: string) => markdown.replace(FENCE, "CARD\n"),
242
- };
243
- const markdown = TABLE + FENCE;
244
-
245
- const enabled = harness({ rewrite, withArtifacts: true });
246
- expect(enabled.transformer(markdown, ASSISTANT)).toBe("TABLE\nCARD\n");
247
- // Received the table surface's output, fence still raw: the pass runs last.
248
- expect(enabled.artifacts?.calls[0]?.markdown).toBe(`TABLE\n${FENCE}`);
249
-
250
- const disabled = harness({ disabled: ["content.artifacts"], rewrite, withArtifacts: true });
251
- expect(disabled.transformer(markdown, ASSISTANT)).toBe(`TABLE\n${FENCE}`);
252
- expect(disabled.artifacts?.calls).toEqual([]);
253
- });
254
- });