Skip to content

Commit 10131b7

Browse files
committed
test: generate independent document fixtures for clean CI
1 parent 398f43e commit 10131b7

9 files changed

Lines changed: 68 additions & 14 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ tmp/
99
# Conformance fixtures are real work files — bring your own (see its README).
1010
tests/test-data/*
1111
!tests/test-data/README.md
12+
tests/generated/
1213

1314
# Internal design documents stay local; only the README ships publicly.
1415
docs/

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@
3535
"build:browser": "tsup --config tsup.browser.config.ts",
3636
"dev": "tsup --watch",
3737
"dev:browser": "tsup --config tsup.browser.config.ts --watch",
38-
"test": "vitest run",
38+
"test": "node scripts/generate-fixtures.mjs && vitest run",
3939
"test:unit": "vitest run tests/unit",
40-
"test:integration": "vitest run tests/integration",
40+
"test:integration": "node scripts/generate-fixtures.mjs && vitest run tests/integration",
4141
"test:browser": "vitest run tests/browser",
4242
"typecheck": "tsc --noEmit",
4343
"typecheck:browser": "tsc -p tsconfig.browser.json --noEmit",

scripts/browser-smoke.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import path from 'node:path';
2+
import './generate-fixtures.mjs';
23
import { fileURLToPath } from 'node:url';
34
import { createFakeCloud } from '../tests/browser/fake-cloud.mjs';
45

scripts/check-package-size.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { execFileSync, spawn } from 'node:child_process';
22
import fs from 'node:fs';
33
import os from 'node:os';
44
import path from 'node:path';
5+
import './generate-fixtures.mjs';
56

67
const MiB = 1024 * 1024;
78
const root = path.resolve(import.meta.dirname, '..');
@@ -34,7 +35,7 @@ try {
3435
const p95 = coldStarts[Math.ceil(coldStarts.length * 0.95) - 1];
3536
if (p95 > 500) throw new Error(`formats cold-start P95 is ${p95.toFixed(0)} ms; limit is 500 ms.`);
3637
const smokeArtifact = path.join(workspace, 'pdf-smoke');
37-
const measured = await runMeasured(cli, ['parse', path.join(root, 'tests/test-data/test.pdf'), '--engine', 'local', '--preflight', 'off', '--output', smokeArtifact, '--json']);
38+
const measured = await runMeasured(cli, ['parse', path.join(root, 'tests/generated/test.pdf'), '--engine', 'local', '--preflight', 'off', '--output', smokeArtifact, '--json']);
3839
const smoke = JSON.parse(measured.stdout);
3940
if (!smoke.ok || smoke.format !== 'pdf' || smoke.engine !== 'local') throw new Error('Strict production install failed the local PDF smoke test.');
4041
if (measured.maxRssBytes > 256 * MiB) throw new Error(`Local PDF smoke reached ${(measured.maxRssBytes / MiB).toFixed(2)} MiB RSS; limit is 256 MiB.`);

scripts/generate-fixtures.mjs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/** Deterministic synthetic documents for CI. Never reads or replaces user samples. */
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
import { zipSync, strToU8 } from 'fflate';
5+
6+
const directory = path.resolve(import.meta.dirname, '../tests/generated');
7+
fs.mkdirSync(directory, { recursive: true });
8+
const xml = (body) => `<?xml version="1.0" encoding="UTF-8"?>${body}`;
9+
const relNS = 'http://schemas.openxmlformats.org/package/2006/relationships';
10+
const officeNS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
11+
const typeNS = 'http://schemas.openxmlformats.org/package/2006/content-types';
12+
const archive = (name, entries) => fs.writeFileSync(path.join(directory, name), zipSync(Object.fromEntries(
13+
Object.entries(entries).map(([key, value]) => [key, [typeof value === 'string' ? strToU8(value) : value, { mtime: new Date('2020-01-01T00:00:00Z') }]])
14+
)));
15+
16+
const stream = 'BT /F1 18 Tf 40 140 Td (DeckOps deterministic migration document fixture) Tj ET\n';
17+
const objects = [
18+
'<< /Type /Catalog /Pages 2 0 R >>',
19+
'<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
20+
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>',
21+
'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
22+
`<< /Length ${Buffer.byteLength(stream)} >>\nstream\n${stream}endstream`,
23+
];
24+
let pdf = '%PDF-1.4\n';
25+
const offsets = [];
26+
objects.forEach((body, index) => { offsets.push(Buffer.byteLength(pdf)); pdf += `${index + 1} 0 obj\n${body}\nendobj\n`; });
27+
const xref = Buffer.byteLength(pdf);
28+
pdf += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n${offsets.map((offset) => `${String(offset).padStart(10, '0')} 00000 n \n`).join('')}trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
29+
fs.writeFileSync(path.join(directory, 'test.pdf'), pdf);
30+
31+
archive('test.docx', {
32+
'[Content_Types].xml': xml(`<Types xmlns="${typeNS}"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>`),
33+
'_rels/.rels': xml(`<Relationships xmlns="${relNS}"><Relationship Id="rId1" Type="${officeNS}/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="${officeNS}/extended-properties" Target="docProps/app.xml"/></Relationships>`),
34+
'word/document.xml': xml('<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r><w:t>DeckOps deterministic migration document fixture</w:t></w:r></w:p><w:sectPr><w:pgSz w:w="12240" w:h="15840"/></w:sectPr></w:body></w:document>'),
35+
'docProps/app.xml': xml('<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"><Pages>1</Pages></Properties>'),
36+
});
37+
archive('test.pptx', {
38+
'[Content_Types].xml': xml(`<Types xmlns="${typeNS}"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/><Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/></Types>`),
39+
'_rels/.rels': xml(`<Relationships xmlns="${relNS}"><Relationship Id="rId1" Type="${officeNS}/officeDocument" Target="ppt/presentation.xml"/></Relationships>`),
40+
'ppt/presentation.xml': xml(`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="${officeNS}"><p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst><p:sldSz cx="9144000" cy="6858000"/></p:presentation>`),
41+
'ppt/_rels/presentation.xml.rels': xml(`<Relationships xmlns="${relNS}"><Relationship Id="rId1" Type="${officeNS}/slide" Target="slides/slide1.xml"/></Relationships>`),
42+
'ppt/slides/slide1.xml': xml('<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><p:cSld><p:spTree><p:sp><p:nvSpPr><p:cNvPr id="2" name="Fixture"/></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>DeckOps deterministic migration presentation fixture</a:t></a:r></a:p></p:txBody></p:sp></p:spTree></p:cSld></p:sld>'),
43+
});
44+
// Minimal IWA identity/structure fixture: uncompressed frame, one document
45+
// ArchiveInfo object and one slide component. It is not a local Keynote parser.
46+
const iwa = new Uint8Array([1, 9, 0, 0, 8, 8, 1, 18, 4, 8, 1, 24, 0]);
47+
archive('test.key', {
48+
'Index/Document.iwa': iwa,
49+
'Index/Slide.iwa': iwa,
50+
'Metadata/Properties.plist': xml('<plist version="1.0"><dict><key>fileFormatVersion</key><string>1.0</string><key>isMultiPage</key><true/></dict></plist>'),
51+
});

tests/browser/fake-cloud.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export async function createFakeCloud({ staticRoot, port = 0 } = {}) {
4141
if (!staticRoot) return false;
4242
const file = pathname === '/' ? 'tests/browser/smoke.html'
4343
: pathname === '/smoke.js' ? 'tests/browser/smoke.js'
44-
: pathname === '/tests/test-data/test.pdf' ? pathname.slice(1)
44+
: pathname === '/tests/generated/test.pdf' ? pathname.slice(1)
4545
: /^\/dist\/(?:[a-zA-Z0-9_-]+\/)*[a-zA-Z0-9_-]+\.(?:js|wasm)$/.test(pathname) ? pathname.slice(1) : undefined;
4646
if (!file) return false;
4747
try {

tests/browser/smoke.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ button.addEventListener('click', async () => {
2626
assert(typeof createClient === 'function', 'Missing browser SDK export');
2727
}],
2828
['DeckProbe Worker validates a real PDF before upload', async () => {
29-
const bytes = await fetch('/tests/test-data/test.pdf').then((response) => response.blob());
29+
const bytes = await fetch('/tests/generated/test.pdf').then((response) => response.blob());
3030
const events = [];
3131
const doc = await client('preflight').parse(new File([bytes], 'test.pdf'), {
3232
onProgress: (event) => events.push(event),

tests/integration/local.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe('local community engine', () => {
1515
it.each([
1616
['test.pdf', 'pdf'], ['test.pptx', 'pptx'], ['test.docx', 'docx'],
1717
] as const)('parses and converts %s without a cloud client', async (fixture, format) => {
18-
const root = tmp(); const source = path.resolve('tests/test-data', fixture); const out = path.join(root, 'artifact');
18+
const root = tmp(); const source = path.resolve('tests/generated', fixture); const out = path.join(root, 'artifact');
1919
const parsed = await runParse({ input: await resolveInput(source), inputLabel: source, out, flags: {}, common: { engine: 'local' }, preflight: 'off' });
2020
expect(parsed).toMatchObject({ engine: 'local', format, reusedParse: false, irSchemaVersion: 'deckir.v1' });
2121
expect(parsed.irKey).toBeUndefined();
@@ -43,7 +43,7 @@ describe('local community engine', () => {
4343
}, 30_000);
4444

4545
it('invalidates auto cache entries when the recorded parser major changes', async () => {
46-
const root = tmp(); const source = path.resolve('tests/test-data/test.docx'); const out = path.join(root, 'artifact');
46+
const root = tmp(); const source = path.resolve('tests/generated/test.docx'); const out = path.join(root, 'artifact');
4747
await runParse({ input: await resolveInput(source), inputLabel: source, out, flags: {}, common: { engine: 'local' }, preflight: 'off' });
4848
const irFile = path.join(out, 'ir.json'); const manifestFile = path.join(out, 'manifest.json');
4949
const ir = JSON.parse(fs.readFileSync(irFile, 'utf-8')); const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'));
@@ -56,7 +56,7 @@ describe('local community engine', () => {
5656
}, 30_000);
5757

5858
it('invalidates old local renderer views and removes stale split-page files', async () => {
59-
const root = tmp(); const source = path.resolve('tests/test-data/test.pptx'); const out = path.join(root, 'artifact');
59+
const root = tmp(); const source = path.resolve('tests/generated/test.pptx'); const out = path.join(root, 'artifact');
6060
await runParse({ input: await resolveInput(source), inputLabel: source, out, flags: {}, common: { engine: 'local' }, preflight: 'off' });
6161
await runConvert({ input: await resolveInput(out), inputLabel: out, flags: { splitPages: true }, common: { engine: 'local' } });
6262
const page = path.join(out, 'views/markdown/001.md'); expect(fs.existsSync(page)).toBe(true);
@@ -69,21 +69,21 @@ describe('local community engine', () => {
6969
}, 30_000);
7070

7171
it('does not silently upload unsupported Keynote input', async () => {
72-
const source = path.resolve('tests/test-data/test.key'); let cloudCalls = 0;
72+
const source = path.resolve('tests/generated/test.key'); let cloudCalls = 0;
7373
await expect(runParse({ input: await resolveInput(source), inputLabel: source, out: path.join(tmp(), 'artifact'), flags: {},
7474
common: { engine: 'auto' }, preflight: 'off', cloud: async () => { cloudCalls += 1; throw new Error('must not upload'); } })).rejects.toMatchObject({ code: 'unsupported' });
7575
expect(cloudCalls).toBe(0);
7676
});
7777

7878
it('does not silently ignore cloud-only PDF profiles', async () => {
79-
const source = path.resolve('tests/test-data/test.pdf'); let cloudCalls = 0;
79+
const source = path.resolve('tests/generated/test.pdf'); let cloudCalls = 0;
8080
await expect(runParse({ input: await resolveInput(source), inputLabel: source, out: path.join(tmp(), 'artifact'), flags: { profile: 'quality' },
8181
common: { engine: 'local' }, preflight: 'off', cloud: async () => { cloudCalls += 1; throw new Error('must not upload'); } })).rejects.toMatchObject({ code: 'unsupported' });
8282
expect(cloudCalls).toBe(0);
8383
});
8484

8585
it('uses cloud for unsupported input only after auto upload authorization', async () => {
86-
const source = path.resolve('tests/test-data/test.key'); let cloudCalls = 0;
86+
const source = path.resolve('tests/generated/test.key'); let cloudCalls = 0;
8787
const client = {
8888
parse: async () => { cloudCalls += 1; return { taskId: 'cloud-1', type: 'keynote.parseTextAndImage', irKey: 'remote/key.json', irSchemaVersion: 'keynote.v1', ir: { slides: [{ text: 'Cloud result' }] } }; },
8989
convert: async () => { throw new Error('not used'); },
@@ -97,15 +97,15 @@ describe('local community engine', () => {
9797
it('makes zero network requests for local file parsing and conversion', async () => {
9898
const network = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('network forbidden'));
9999
try {
100-
const source = path.resolve('tests/test-data/test.docx'); const out = path.join(tmp(), 'artifact');
100+
const source = path.resolve('tests/generated/test.docx'); const out = path.join(tmp(), 'artifact');
101101
await runParse({ input: await resolveInput(source), inputLabel: source, out, flags: {}, common: { engine: 'local' }, preflight: 'off' });
102102
await runConvert({ input: await resolveInput(out), inputLabel: out, flags: {}, common: { engine: 'local' } });
103103
expect(network).not.toHaveBeenCalled();
104104
} finally { network.mockRestore(); }
105105
});
106106

107107
it('validates Node SDK flags before starting local or cloud work', async () => {
108-
const source = path.resolve('tests/test-data/test.docx'); const client = createClient();
108+
const source = path.resolve('tests/generated/test.docx'); const client = createClient();
109109
await expect(client.parseEnvelope(source, { trackedChanges: 'invalid' as never })).rejects.toMatchObject({ code: 'usage_error' });
110110
await expect(client.parseEnvelope(source, { allowUpload: true })).rejects.toMatchObject({ code: 'usage_error' });
111111
});

tests/integration/ops.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ describe('parse → artifact', () => {
273273
['test.key', 'keynote', 'key'],
274274
] as const)('uses the packaged Node DeckProbe adapter on the real %s fixture', async (fixture, driver, profile) => {
275275
const dir = tmp();
276-
const source = path.resolve('tests/test-data', fixture);
276+
const source = path.resolve('tests/generated', fixture);
277277
const out = path.join(dir, 'artifact');
278278
const envelope = await runParse({
279279
input: await resolveInput(source), inputLabel: source, out, flags: {}, common: cloudCommon(), client: fakeClient(),

0 commit comments

Comments
 (0)