mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-22 17:45:15 +02:00
feat(plan-canvas): download artifacts as PDF
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const { EventEmitter } = require('events');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const {
|
||||
assertLoopbackUrl,
|
||||
exportPdf,
|
||||
isCompletePdf,
|
||||
pdfFileName,
|
||||
resolveChromiumExecutable
|
||||
} = require('../../scripts/lib/plan-canvas/pdf');
|
||||
|
||||
const results = [];
|
||||
|
||||
async function test(name, fn) {
|
||||
try {
|
||||
await fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
results.push(true);
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` ${error.stack || error.message}`);
|
||||
results.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
function fakeChild(onSpawn) {
|
||||
const child = new EventEmitter();
|
||||
child.stderr = new EventEmitter();
|
||||
child.exitCode = null;
|
||||
child.signalCode = null;
|
||||
child.kill = signal => {
|
||||
child.signalCode = signal;
|
||||
setImmediate(() => {
|
||||
child.exitCode = 0;
|
||||
child.emit('close', 0, signal);
|
||||
});
|
||||
return true;
|
||||
};
|
||||
onSpawn(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('\n=== Testing Plan Canvas PDF export ===\n');
|
||||
|
||||
await test('builds safe, useful PDF filenames', () => {
|
||||
assert.strictEqual(pdfFileName('/tmp/feature-fleet-2.2.plan.md'), 'feature-fleet-2.2.pdf');
|
||||
assert.strictEqual(pdfFileName('/tmp/release-preview.html'), 'release-preview.pdf');
|
||||
assert.strictEqual(pdfFileName('/tmp/bad:name?.md'), 'bad-name-.pdf');
|
||||
assert.strictEqual(pdfFileName(''), 'plan.pdf');
|
||||
});
|
||||
|
||||
await test('restricts the renderer to loopback artifact URLs', () => {
|
||||
assert.strictEqual(
|
||||
assertLoopbackUrl('http://127.0.0.1:4518/artifact/abc/?pdf=1'),
|
||||
'http://127.0.0.1:4518/artifact/abc/?pdf=1'
|
||||
);
|
||||
assert.throws(() => assertLoopbackUrl('https://127.0.0.1/artifact/abc'), { code: 'PDF_EXPORT_INVALID_URL' });
|
||||
assert.throws(() => assertLoopbackUrl('http://example.com/artifact/abc'), { code: 'PDF_EXPORT_INVALID_URL' });
|
||||
});
|
||||
|
||||
await test('honors an explicit Chromium executable override', () => {
|
||||
const fsImpl = {
|
||||
accessSync(file) { assert.strictEqual(file, '/opt/test/chrome'); },
|
||||
statSync() { return { isFile: () => true }; }
|
||||
};
|
||||
assert.strictEqual(
|
||||
resolveChromiumExecutable({
|
||||
env: { ECC_PLAN_CANVAS_CHROME_PATH: '/opt/test/chrome', PATH: '' },
|
||||
platform: 'linux',
|
||||
fsImpl
|
||||
}),
|
||||
'/opt/test/chrome'
|
||||
);
|
||||
});
|
||||
|
||||
await test('fails actionably when no local PDF renderer is installed', async () => {
|
||||
await assert.rejects(
|
||||
exportPdf({
|
||||
url: 'http://127.0.0.1:4517/artifact/abc123/?pdf=1',
|
||||
artifactFile: '/workspace/launch.plan.md',
|
||||
env: { PATH: '' },
|
||||
platform: 'linux'
|
||||
}),
|
||||
error => error.code === 'PDF_BROWSER_NOT_FOUND' && error.message.includes('ECC_PLAN_CANVAS_CHROME_PATH')
|
||||
);
|
||||
});
|
||||
|
||||
await test('recognizes only complete PDF output', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'plan-canvas-pdf-complete-'));
|
||||
const file = path.join(tmp, 'artifact.pdf');
|
||||
fs.writeFileSync(file, '%PDF-1.4\npartial');
|
||||
assert.strictEqual(isCompletePdf(file), false);
|
||||
fs.appendFileSync(file, '\n%%EOF\n');
|
||||
assert.strictEqual(isCompletePdf(file), true);
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
await test('renders, terminates its private browser, and removes temporary state', async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'plan-canvas-pdf-test-'));
|
||||
let spawned = null;
|
||||
let child = null;
|
||||
const spawnImpl = (command, args, options) => {
|
||||
spawned = { command, args, options };
|
||||
const outputFile = args.find(arg => arg.startsWith('--print-to-pdf=')).slice('--print-to-pdf='.length);
|
||||
child = fakeChild(() => {
|
||||
setTimeout(() => fs.writeFileSync(outputFile, '%PDF-1.4\nlocal plan\n%%EOF\n'), 20);
|
||||
});
|
||||
return child;
|
||||
};
|
||||
|
||||
const result = await exportPdf({
|
||||
url: 'http://localhost:4517/artifact/abc123/?pdf=1',
|
||||
artifactFile: '/workspace/launch.plan.md',
|
||||
executable: '/opt/test/chrome',
|
||||
timeoutMs: 1000,
|
||||
spawnImpl,
|
||||
osImpl: { tmpdir: () => tempRoot }
|
||||
});
|
||||
|
||||
assert.strictEqual(result.filename, 'launch.pdf');
|
||||
assert.ok(result.buffer.subarray(0, 5).equals(Buffer.from('%PDF-')));
|
||||
assert.strictEqual(spawned.command, '/opt/test/chrome');
|
||||
assert.strictEqual(spawned.options.shell, false);
|
||||
assert.ok(spawned.args.includes('--headless=new'));
|
||||
assert.ok(spawned.args.includes('http://localhost:4517/artifact/abc123/?pdf=1'));
|
||||
assert.strictEqual(child.signalCode, 'SIGTERM');
|
||||
assert.deepStrictEqual(fs.readdirSync(tempRoot), []);
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const passed = results.filter(Boolean).length;
|
||||
const failed = results.length - passed;
|
||||
console.log('\n========================================');
|
||||
console.log(`Passed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
console.log('========================================');
|
||||
process.exit(failed ? 1 : 0);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -98,12 +98,17 @@ async function main() {
|
||||
fs.writeFileSync(path.join(outsideDir, 'secret.txt'), 'secret');
|
||||
|
||||
const store = createSessionStore({ stateDir: path.join(tmp, 'state') });
|
||||
const pdfRequests = [];
|
||||
let idleFired = false;
|
||||
const canvas = createPlanCanvasServer({
|
||||
store,
|
||||
version: '9.9.9-test',
|
||||
heartbeatMs: 25,
|
||||
idleTimeoutMs: 0,
|
||||
pdfExporter: async options => {
|
||||
pdfRequests.push(options);
|
||||
return { buffer: Buffer.from('%PDF-1.4\n%%EOF\n'), filename: 'demo.pdf' };
|
||||
},
|
||||
onIdleShutdown: () => {
|
||||
idleFired = true;
|
||||
}
|
||||
@@ -119,7 +124,7 @@ async function main() {
|
||||
ok: true,
|
||||
app: 'ecc-plan-canvas',
|
||||
version: '9.9.9-test',
|
||||
protocolVersion: 3,
|
||||
protocolVersion: 4,
|
||||
runtimeId: PLAN_CANVAS_RUNTIME_ID
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
@@ -155,6 +160,7 @@ async function main() {
|
||||
assert.ok(res.body.includes('Plan Canvas'));
|
||||
assert.ok(res.body.includes('pc-session'));
|
||||
assert.ok(res.body.includes('Approve plan'));
|
||||
assert.ok(res.body.includes('Download PDF'));
|
||||
assert.ok(res.body.includes('sandbox="allow-scripts allow-forms allow-popups"'));
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -209,6 +215,22 @@ async function main() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await test('Download PDF fetches a generated PDF and starts a browser download', async () => {
|
||||
const client = await request(port, 'GET', '/client.js');
|
||||
assert.ok(client.body.includes("'/api/session/' + key + '/pdf'"));
|
||||
assert.ok(client.body.includes('URL.createObjectURL'));
|
||||
|
||||
const res = await request(port, 'GET', `/api/session/${key}/pdf`);
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
assert.strictEqual(res.headers['content-type'], 'application/pdf');
|
||||
assert.match(res.headers['content-disposition'], /^attachment;/);
|
||||
assert.strictEqual(res.headers['x-plan-canvas-filename'], 'demo.pdf');
|
||||
assert.ok(res.body.startsWith('%PDF-1.4'));
|
||||
assert.strictEqual(pdfRequests.length, 1);
|
||||
assert.strictEqual(pdfRequests[0].artifactFile, fs.realpathSync(artifact));
|
||||
assert.strictEqual(pdfRequests[0].url, `http://127.0.0.1:${port}/artifact/${key}/?pdf=1`);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await test('browser client uses finite polling instead of one permanent connection per canvas', async () => {
|
||||
const res = await request(port, 'GET', '/client.js');
|
||||
assert.ok(res.body.includes("'/api/session/' + key + '/state'"));
|
||||
|
||||
Reference in New Issue
Block a user