veritas-kanban/scripts/native-ui/shutdown.mjs

113 lines
3.9 KiB
JavaScript

import assert from 'node:assert/strict';
import { once } from 'node:events';
import { readFile, writeFile } from 'node:fs/promises';
import net from 'node:net';
import path from 'node:path';
import { setTimeout as delay } from 'node:timers/promises';
import { parseArgs } from 'node:util';
import { createNativeSession } from './session.mjs';
const { values } = parseArgs({
options: {
app: { type: 'string' },
commit: { type: 'string' },
version: { type: 'string' },
output: { type: 'string' },
},
});
for (const key of ['app', 'commit', 'version', 'output'])
assert(values[key], `--${key} is required`);
const session = await createNativeSession({
packagePath: values.app,
commit: values.commit,
version: values.version,
});
const report = {
commit: values.commit,
version: values.version,
profile: session.profile,
status: 'running',
cycles: [],
};
try {
// Reuse only this synthetic profile to exercise quit and subsequent relaunch.
for (let cycle = 0; cycle < 2; cycle += 1) {
const { app, page } = await session.launch();
let socket;
let closing;
const result = { cycle, httpFailures: [] };
report.cycles.push(result);
page.on('response', (response) => {
if (response.status() >= 500)
result.httpFailures.push({
status: response.status(),
path: new URL(response.url()).pathname,
});
});
try {
assert.equal(await page.evaluate(async () => (await fetch('/api/tasks')).status), 200);
// Electron uses its own session partition; browser-context cookies are
// not authoritative here. Keep this synthetic credential in memory only.
const cookies = await app.evaluate(
async ({ BrowserWindow }, origin) =>
BrowserWindow.getAllWindows()[0].webContents.session.cookies.get({ url: origin }),
session.origin
);
assert(
cookies.some((cookie) => cookie.name === 'veritas_session'),
'Missing isolated login'
);
const cookie = cookies.map((item) => `${item.name}=${item.value}`).join('; ');
socket = net.connect(Number(new URL(session.origin).port), '127.0.0.1');
await once(socket, 'connect');
const chunks = [];
socket.on('data', (chunk) => chunks.push(chunk));
const ended = once(socket, 'end');
void ended.catch(() => {}); // Awaited below; handle early socket errors too.
socket.setTimeout(15_000, () => socket.destroy(new Error('Diagnostic request timed out')));
socket.write(
`GET /api/tasks HTTP/1.1\r\nHost: ${new URL(session.origin).host}\r\nCookie: ${cookie}\r\nConnection: close\r\n`
);
await delay(100);
const started = Date.now();
closing = app.close();
void closing.catch(() => {});
await delay(100);
socket.write('\r\n');
await ended;
await closing;
result.elapsedMs = Date.now() - started;
result.statusLine = Buffer.concat(chunks).toString().split('\r\n')[0];
assert.match(
result.statusLine,
/^HTTP\/1.1 200 /,
'Connected task request outlived its storage'
);
assert.deepEqual(result.httpFailures, []);
} finally {
socket?.destroy();
await (closing ?? app.close());
}
}
const log = await readFile(
path.join(session.profile, 'profiles/native-ui-conformance/workspaces/local/logs/server.log'),
'utf8'
);
assert(
!/Forced shutdown|Graceful shutdown failed|Unhandled error/.test(log),
'Server shutdown logged a failure'
);
assert.equal(
(log.match(/\[desktop\] exited code=0 signal=null/g) ?? []).length,
2,
'Both server exits must be clean'
);
report.status = 'passed';
} catch (error) {
report.status = 'failed';
report.error = error.message;
} finally {
await writeFile(values.output, `${JSON.stringify(report, null, 2)}\n`);
}
console.log(`Native shutdown ${report.status}: ${values.output}`);
process.exitCode = report.status === 'passed' ? 0 : 1;