110 lines
6.4 KiB
JavaScript
110 lines
6.4 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { EventEmitter } from 'node:events';
|
|
import path from 'node:path';
|
|
import vm from 'node:vm';
|
|
import { transform } from 'esbuild';
|
|
|
|
// Compile the real producer; Electron, disk, timers and renderer are isolated mocks.
|
|
// No GUI, application profile or repository runtime files are touched.
|
|
const source = await readFile(new URL('../../src/main/dev-ops-health.ts', import.meta.url), 'utf8');
|
|
const compiled = (await transform(source, { loader: 'ts', format: 'cjs', target: 'node20' })).code;
|
|
const runId = '12345678-1234-1234-1234-123456789abc';
|
|
const flush = async () => { for (let i = 0; i < 8; i++) await new Promise(resolve => setImmediate(resolve)); };
|
|
function fixture(options = {}) {
|
|
const files = new Map(), writes = [], renames = [], intervals = new Set();
|
|
const directory = path.join(path.resolve('/fixture/ycode'), '.tmp', 'ops');
|
|
const destination = path.join(directory, 'renderer-health.json');
|
|
const window = new EventEmitter(), contents = new EventEmitter();
|
|
let sequence = 0, releaseWrite, hold = !!options.holdFirstWrite;
|
|
const dom = { complete: true, shell: true, button: true, width: 800, height: 600, display: 'block', visibility: 'visible' };
|
|
const fs = {
|
|
realpath: async value => options.linkedDirectory ? `${value}-elsewhere` : value,
|
|
writeFile: async (file, data, flags) => {
|
|
writes.push({ file, data: JSON.parse(data), flags });
|
|
if (hold) { hold = false; await new Promise(resolve => { releaseWrite = resolve; }); }
|
|
assert.equal(flags.flag, 'wx'); assert.equal(flags.mode, 0o600);
|
|
assert.equal(files.has(file), false); files.set(file, data);
|
|
},
|
|
rename: async (from, to) => { assert.equal(files.has(from), true); files.set(to, files.get(from)); files.delete(from); renames.push(JSON.parse(files.get(to))); },
|
|
rm: async file => { files.delete(file); }
|
|
};
|
|
Object.assign(contents, {
|
|
isDestroyed: () => false, getOSProcessId: () => 5678,
|
|
isLoadingMainFrame: () => !!options.loading,
|
|
executeJavaScript: async code => {
|
|
if (options.rejectProbe) throw new Error('renderer unavailable');
|
|
const shell = { querySelector: () => dom.button ? {} : null, getBoundingClientRect: () => ({ width: dom.width, height: dom.height }) };
|
|
return vm.runInNewContext(code, {
|
|
document: { readyState: dom.complete ? 'complete' : 'loading', querySelector: () => dom.shell ? shell : null },
|
|
getComputedStyle: () => ({ display: dom.display, visibility: dom.visibility })
|
|
});
|
|
}
|
|
});
|
|
Object.assign(window, { webContents: contents, isDestroyed: () => false });
|
|
const module = { exports: {} };
|
|
vm.runInNewContext(compiled, {
|
|
module, exports: module.exports,
|
|
require: name => {
|
|
if (name === 'electron') return { app: { isPackaged: !!options.packaged } };
|
|
if (name === 'node:fs') return { promises: fs };
|
|
if (name === 'node:path') return path;
|
|
if (name === 'node:crypto') return { randomUUID: () => `mock-${++sequence}` };
|
|
throw new Error(`Unexpected dependency: ${name}`);
|
|
},
|
|
process: { env: { YCODE_OPS_RUN_ID: options.id === undefined ? runId : options.id }, cwd: () => '/fixture/ycode', pid: 1234 },
|
|
setInterval: callback => { const timer = { callback, unref() {} }; intervals.add(timer); return timer; },
|
|
clearInterval: timer => intervals.delete(timer), Date
|
|
});
|
|
return { window, contents, dom, files, writes, renames, intervals, destination,
|
|
attach: () => module.exports.attachDevOpsHealth(window),
|
|
tick: () => { for (const timer of intervals) timer.callback(); },
|
|
release: () => { assert.ok(releaseWrite, 'expected delayed write'); releaseWrite(); },
|
|
proof: () => files.has(destination) ? JSON.parse(files.get(destination)) : null };
|
|
}
|
|
|
|
test('producer opt-in gates packaged, missing and invalid run IDs without timers or writes', async () => {
|
|
for (const options of [{ packaged: true }, { id: '' }, { id: 'not-a-uuid' }]) {
|
|
const f = fixture(options); f.attach(); await flush();
|
|
assert.equal(f.writes.length, 0); assert.equal(f.intervals.size, 0);
|
|
}
|
|
});
|
|
test('producer writes fresh run and native PID proof only for a complete visible shell with button', async () => {
|
|
const f = fixture(); f.attach(); await flush();
|
|
const proof = f.proof();
|
|
assert.equal(proof.ready, true); assert.equal(proof.runId, runId);
|
|
assert.equal(proof.pid, 1234); assert.equal(proof.rendererPid, 5678);
|
|
assert.ok(Math.abs(Date.now() - proof.checkedAt) < 5000);
|
|
assert.equal(f.files.size, 1); assert.equal(f.files.has(f.destination), true);
|
|
for (const [key, value] of [['complete', false], ['shell', false], ['button', false], ['width', 0], ['height', 0], ['display', 'none'], ['visibility', 'hidden']]) {
|
|
const prior = f.dom[key]; f.dom[key] = value; f.tick(); await flush();
|
|
assert.equal(f.proof().ready, false, `${key} must fail readiness`); f.dom[key] = prior;
|
|
}
|
|
f.tick(); await flush(); assert.equal(f.proof().ready, true);
|
|
});
|
|
test('loading, rejected renderer and linked output directory cannot produce ready proof', async () => {
|
|
for (const options of [{ loading: true }, { rejectProbe: true }, { linkedDirectory: true }]) {
|
|
const f = fixture(options); f.attach(); await flush();
|
|
assert.notEqual(f.proof()?.ready, true);
|
|
if (options.linkedDirectory) assert.equal(f.writes.length, 0);
|
|
}
|
|
});
|
|
test('invalidation while a true proof write is pending cannot leave a stale true result', async () => {
|
|
const f = fixture({ holdFirstWrite: true }); f.attach(); await flush();
|
|
assert.equal(f.writes[0].data.ready, true);
|
|
f.contents.emit('did-start-loading'); await flush();
|
|
f.release(); await flush();
|
|
assert.equal(f.proof().ready, false);
|
|
// Once invalidation is published, no older queued true may replace it.
|
|
const firstFalse = f.renames.findIndex(proof => proof.ready === false);
|
|
assert.ok(firstFalse >= 0); assert.ok(f.renames.slice(firstFalse).every(proof => proof.ready === false));
|
|
});
|
|
test('unresponsive and closed windows invalidate proof; close cancels periodic probes', async () => {
|
|
const f = fixture(); f.attach(); await flush();
|
|
f.window.emit('unresponsive'); await flush(); assert.equal(f.proof().ready, false);
|
|
f.tick(); await flush(); assert.equal(f.proof().ready, false);
|
|
f.window.emit('responsive'); await flush(); assert.equal(f.proof().ready, true);
|
|
f.window.emit('closed'); await flush(); assert.equal(f.proof().ready, false); assert.equal(f.intervals.size, 0);
|
|
});
|