Skip to content

Commit 4cb9b3a

Browse files
committed
feat(slidedeck): add @corbits/pdf, deck checks and agent deck tools
1 parent 89a478e commit 4cb9b3a

19 files changed

Lines changed: 4329 additions & 8 deletions

‎apps/hub/package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
"@corbits/mcp": "0.4.1",
2828
"@corbits/oauth-core": "0.3.4",
2929
"@corbits/openai-responses": "0.2.3",
30+
"@corbits/slidedeck": "workspace:*",
3031
"@corbits/xai-provider": "0.1.6",
32+
"@corbits/zip": "workspace:*",
3133
"@intx/agent": "workspace:*",
3234
"@intx/authz": "workspace:*",
3335
"@intx/crypto": "0.5.0",
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import { expect, test } from "bun:test";
2+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { unzip } from "@corbits/zip";
6+
7+
import {
8+
DECK_JSON,
9+
DECK_PDF,
10+
DECK_PPTX,
11+
checkDeckInWorkspace,
12+
createDeckTools,
13+
writeDeckToWorkspace,
14+
} from "./deck-tools.ts";
15+
16+
const sampleDeck = {
17+
theme: {
18+
colors: {
19+
primary: "#6D28D9",
20+
secondary: "#0EA5E9",
21+
accent: "#F59E0B",
22+
background: "#FFFFFF",
23+
text: "#1B1B1F",
24+
muted: "#6B7280",
25+
},
26+
fonts: { heading: "Georgia", body: "Helvetica Neue" },
27+
},
28+
footer: { text: "Builder · Deck Tools", slideNumbers: true },
29+
slides: [
30+
{
31+
layout: "title" as const,
32+
title: "Quarter Review",
33+
subtitle: "Deterministic slides and PDF",
34+
},
35+
{
36+
layout: "content" as const,
37+
title: "Key results",
38+
body: [
39+
{
40+
type: "paragraph" as const,
41+
text: "Every check runs in pure TypeScript.",
42+
},
43+
{
44+
type: "list" as const,
45+
ordered: true,
46+
items: [
47+
"Validate schema",
48+
"Check overflow and contrast",
49+
"Render PPTX and PDF",
50+
],
51+
},
52+
],
53+
},
54+
],
55+
};
56+
57+
const toolEnv = (toolCwd: string) =>
58+
({ toolCwd }) as unknown as Parameters<ReturnType<typeof createDeckTools>>[0];
59+
60+
test("write_deck and check_deck write .builder/deck.json, .builder/deck.pptx and .builder/deck.pdf and verify issues", async () => {
61+
const workspace = await mkdtemp(join(tmpdir(), "deck-tools-"));
62+
try {
63+
const tool = createDeckTools()(toolEnv(workspace));
64+
expect(tool.definitions.map((d) => d.name)).toEqual([
65+
"write_deck",
66+
"check_deck",
67+
]);
68+
69+
const writeRes = await tool.run(
70+
{
71+
id: "call-1",
72+
name: "write_deck",
73+
arguments: { deck: sampleDeck },
74+
},
75+
new AbortController().signal,
76+
);
77+
expect(writeRes.isError).toBeUndefined();
78+
const written = JSON.parse(String(writeRes.content));
79+
expect(written).toEqual({
80+
slideCount: 2,
81+
pptxBytes: expect.any(Number),
82+
pdfBytes: expect.any(Number),
83+
passed: true,
84+
issues: [],
85+
});
86+
87+
const pptxBytes = new Uint8Array(
88+
await readFile(join(workspace, DECK_PPTX)),
89+
);
90+
const pdfBytes = new Uint8Array(await readFile(join(workspace, DECK_PDF)));
91+
expect(pptxBytes.byteLength).toBe(written.pptxBytes);
92+
expect(pdfBytes.byteLength).toBe(written.pdfBytes);
93+
const entries = await unzip(pptxBytes);
94+
expect(entries.some((e) => e.path === "ppt/slides/slide1.xml")).toBe(true);
95+
expect(entries.some((e) => e.path === "ppt/slides/slide2.xml")).toBe(true);
96+
expect(new TextDecoder("latin1").decode(pdfBytes)).toContain(
97+
"(Quarter Review) Tj",
98+
);
99+
100+
const checkRes = await tool.run(
101+
{
102+
id: "call-2",
103+
name: "check_deck",
104+
arguments: {},
105+
},
106+
new AbortController().signal,
107+
);
108+
expect(checkRes.isError).toBeUndefined();
109+
expect(JSON.parse(String(checkRes.content))).toEqual({
110+
passed: true,
111+
issues: [],
112+
});
113+
} finally {
114+
await rm(workspace, { recursive: true, force: true });
115+
}
116+
});
117+
118+
test("write_deck and check_deck return isError: true naming the slide and field on validation or check failures", async () => {
119+
const workspace = await mkdtemp(join(tmpdir(), "deck-tools-err-"));
120+
try {
121+
const tool = createDeckTools()(toolEnv(workspace));
122+
123+
const invalidRes = await tool.run(
124+
{
125+
id: "call-bad",
126+
name: "write_deck",
127+
arguments: {
128+
deck: {
129+
...sampleDeck,
130+
slides: [{ layout: "content", body: [] }],
131+
},
132+
},
133+
},
134+
new AbortController().signal,
135+
);
136+
expect(invalidRes.isError).toBe(true);
137+
const invalidBody = JSON.parse(String(invalidRes.content));
138+
expect(invalidBody.passed).toBe(false);
139+
expect(invalidBody.issues).toEqual([
140+
expect.objectContaining({
141+
slide: 0,
142+
path: "slides[0].title",
143+
}),
144+
]);
145+
146+
// Write a deck with low contrast and empty content to .builder/deck.json and run check_deck.
147+
await writeDeckToWorkspace(workspace, {
148+
...sampleDeck,
149+
slides: [
150+
{
151+
layout: "content",
152+
title: "Low contrast slide",
153+
body: [
154+
{
155+
type: "paragraph",
156+
text: [{ text: "Unreadable light text", color: "#E5E7EB" }],
157+
},
158+
],
159+
},
160+
],
161+
});
162+
const directCheck = await checkDeckInWorkspace(workspace);
163+
expect(directCheck.passed).toBe(false);
164+
165+
const checkRes = await tool.run(
166+
{
167+
id: "call-check-fail",
168+
name: "check_deck",
169+
arguments: {},
170+
},
171+
new AbortController().signal,
172+
);
173+
expect(checkRes.isError).toBe(true);
174+
const checkBody = JSON.parse(String(checkRes.content));
175+
expect(checkBody.passed).toBe(false);
176+
expect(checkBody.issues).toEqual([
177+
expect.objectContaining({
178+
slide: 0,
179+
path: "slides[0].body[0].text[0]",
180+
message: expect.stringContaining("contrast"),
181+
}),
182+
]);
183+
184+
// Also verify missing image asset in .builder/deck.json fails check_deck.
185+
await mkdir(join(workspace, ".builder"), { recursive: true });
186+
await writeFile(
187+
join(workspace, DECK_JSON),
188+
JSON.stringify({
189+
...sampleDeck,
190+
slides: [
191+
{
192+
layout: "image",
193+
title: "Missing image",
194+
image: { path: "does-not-exist.png" },
195+
},
196+
],
197+
}),
198+
);
199+
const missingImgRes = await tool.run(
200+
{
201+
id: "call-missing-img",
202+
name: "check_deck",
203+
arguments: {},
204+
},
205+
new AbortController().signal,
206+
);
207+
expect(missingImgRes.isError).toBe(true);
208+
expect(JSON.parse(String(missingImgRes.content)).issues).toEqual([
209+
expect.objectContaining({
210+
slide: 0,
211+
path: "slides[0].image",
212+
}),
213+
]);
214+
} finally {
215+
await rm(workspace, { recursive: true, force: true });
216+
}
217+
});

0 commit comments

Comments
 (0)