feat(runtime): add native office preview and download plugin

This commit is contained in:
2026-10-17 03:03:32 +08:00
parent 65d118a145
commit bbdee632f6
18 changed files with 476 additions and 6 deletions
+50
View File
@@ -0,0 +1,50 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, writeFile, readFile, rm, access } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createConverter, runProcess } from './native-office.mjs';
import { patchOffice, target } from './install-office-preview.mjs';
test('patch is exact, version gated and refuses repeat application', () => {
const patched = patchOffice(target, '0.2.0-rc.2', '0.2.0-rc.2');
assert.match(patched, /dsh-native-office/);
for (const source of ['', target + target, patched]) assert.throws(() => patchOffice(source, '0.2.0-rc.2', '0.2.0-rc.2'));
assert.throws(() => patchOffice(target, 'next', '0.2.0-rc.2'));
assert.throws(() => patchOffice(target, '0.2.0-rc.2', 'next'));
});
test('process cancellation, missing binary and failure are classified', async () => {
const signal = new AbortController().signal;
await assert.rejects(runProcess('/not-an-office-binary', [], signal), { code: 'unavailable' });
await assert.rejects(runProcess(process.execPath, ['-e', 'process.exit(2)'], signal), { code: 'invalid-document' });
const controller = new AbortController();
const reason = new Error('cancel test');
const pending = runProcess(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], controller.signal);
controller.abort(reason);
await assert.rejects(pending, error => error === reason);
});
test('input bounds, corruption, exclusive output, abort and disposal', async () => {
const directory = await mkdtemp(join(tmpdir(), 'office-test-'));
const inputPath = join(directory, 'source.docx');
const outputPath = join(directory, 'output.pdf');
const converter = await createConverter();
try {
await writeFile(inputPath, 'invalid');
await assert.rejects(converter.render({ inputPath, outputPath }), { code: 'invalid-document' });
await assert.rejects(access(outputPath));
const small = await createConverter({ maxInputBytes: 1 });
await assert.rejects(small.render({ inputPath, outputPath }), { code: 'input-too-large' });
await small.dispose();
await writeFile(outputPath, 'preserve');
await assert.rejects(converter.render({ inputPath, outputPath }), { code: 'EEXIST' });
assert.equal(await readFile(outputPath, 'utf8'), 'preserve');
const controller = new AbortController();
controller.abort(new Error('cancelled'));
await assert.rejects(converter.render({ inputPath, outputPath }, controller.signal), /cancelled/);
await converter.dispose();
await assert.rejects(converter.render({ inputPath, outputPath }), { code: 'unavailable' });
await assert.rejects(createConverter({ fontDirectories: [] }), { code: 'unavailable' });
} finally { await converter.dispose(); await rm(directory, { recursive: true, force: true }); }
});