Files
ycode/scripts/ops/dev.test.mjs
T

69 lines
5.4 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { sameIdentity, ownedProcesses, rendererMatches, localUrls, moduleMatches, inspectHealth, verifyAttestation, runDev } from './dev.mjs';
const root = 'C:\\Users\\A Name\\YCode';
const p = (pid, parentPid, name, commandLine, created = '2026-10-21T00:00:01.000Z') => ({ pid, parentPid, name, commandLine, created });
const owner = p(10, 1, 'node.exe', `node "${root}\\scripts\\ops\\dev-host.mjs"`);
const vite = p(11, 10, 'node.exe', `node "${root}\\node_modules\\electron-vite\\bin\\electron-vite.js" dev`);
const main = p(12, 11, 'electron.exe', 'electron.exe .');
const renderer = p(13, 12, 'electron.exe', `electron.exe --type=renderer --app-path="${root}" --other`);
const snapshot = () => ({ processes: [owner, vite, main, renderer], windows: [{ pid: 12, title: 'YCode', responding: true }], listeners: [{ pid: 11, port: 5197 }] });
const state = { owner };
const source = 'export function ChatPane() { return "测试" }\n';
const fakeFetch = async url => new Response(url.search === '?raw' ? `export default ${JSON.stringify(source)};\n//# sourceURL=foo` : 'export function ChatPane() {}', { headers: { 'Content-Type': 'text/javascript' } });
test('identity rejects PID reuse, command changes, opaque timestamps', () => {
assert.equal(sameIdentity(owner, { ...owner }), true);
assert.equal(sameIdentity(owner, { ...owner, created: '2026-10-22T00:00:01.000Z' }), false);
assert.equal(sameIdentity(owner, { ...owner, commandLine: 'node other.js' }), false);
assert.equal(sameIdentity({ ...owner, created: '' }, { ...owner, created: '' }), false);
});
test('ownership excludes unrelated siblings and pre-parent timestamp PID artifacts', () => {
const snap = snapshot();
snap.processes.push(p(88, 1, 'node.exe', 'other repo'), p(89, 10, 'node.exe', 'old', '2026-10-20T00:00:00Z'), p(90, 10, 'system', '', ''));
assert.deepEqual(ownedProcesses(snap, owner).map(p => p.pid), [10, 11, 12, 13]);
assert.deepEqual(ownedProcesses(snap, { ...owner, created: '2025-01-01T00:00:00Z' }), []);
});
test('renderer app path is an exact Windows path, not a substring', () => {
assert.equal(rendererMatches(renderer.commandLine, root.toUpperCase()), true);
assert.equal(rendererMatches(renderer.commandLine.replace('YCode"', 'YCode-other"'), root), false);
assert.equal(rendererMatches(`electron.exe --type=renderer --app-path=C:/repo`, 'C:\\repo'), true);
assert.equal(rendererMatches(`electron.exe --app-path="${root}"`, root), false);
});
test('actual Local URL parses ANSI and variable ports without accepting remote hosts', () => {
assert.deepEqual(localUrls('\x1b[32mLocal: http://localhost:5197/\x1b[0m\nLocal: http://localhost:5198/\nLocal: http://evil:5173/'), ['http://localhost:5198/', 'http://localhost:5197/']);
});
test('HTTP requires exact source and real transformed JS, not generic 200 HTML', async () => {
assert.equal(await moduleMatches('http://localhost:5197/', source, fakeFetch), true);
assert.equal(await moduleMatches('http://localhost:5197/', source + '// changed', fakeFetch), false);
assert.equal(await moduleMatches('http://localhost:5197/', source, async () => new Response('<!doctype html><html>')), false);
assert.equal(await moduleMatches('http://localhost:5197/', source, async url => url.search ? fakeFetch(url) : new Response('<!doctype html>')), false);
});
test('health requires correct port ownership, target renderer, responsive owned main', async () => {
const check = snap => inspectHealth(snap, state, root, source, 'Local: http://localhost:5197/', fakeFetch);
assert.equal((await check(snapshot())).healthy, true);
assert.equal((await check(snapshot())).ownerAlive, true);
for (const change of [s => { s.windows[0].responding = false; }, s => { s.windows[0].pid = 99; }, s => { s.listeners[0].pid = 99; }, s => { s.listeners[0].port = 5173; }, s => { s.processes = s.processes.filter(p => p.pid !== 13); }, s => { s.processes[3] = { ...renderer, commandLine: renderer.commandLine.replace('YCode', 'Other') }; }]) {
const snap = snapshot(); snap.windows = snap.windows.map(x => ({ ...x })); snap.listeners = snap.listeners.map(x => ({ ...x }));
change(snap); assert.equal((await check(snap)).healthy, false);
}
});
test('non-Windows native dev is explicitly unsupported, never GUI PASS', { skip: process.platform === 'win32' }, async () => {
for (const action of ['dev', 'restart', 'status', 'stop']) {
const response = await runDev(action, { repoRoot: '/nonexistent/no-side-effects' });
assert.equal(response.status, 'UNSUPPORTED'); assert.equal(response.code, 'WINDOWS_REQUIRED');
}
});
test('renderer attestation requires fresh UUID-bound visible UI and exact process IDs', () => {
const now = Date.now();
const tracked = { runId: 'nonce', startedAt: new Date(now - 3000).toISOString() };
const health = { healthy: true, windowPid: 12, rendererPid: 13 };
const proof = { runId: 'nonce', ready: true, checkedAt: now - 500, pid: 12, rendererPid: 13 };
assert.equal(verifyAttestation(proof, tracked, health, root, now), true);
for (const change of [{ ready: false }, { runId: 'old-run' }, { pid: 99 }, { rendererPid: 99 }, { checkedAt: now - 6000 }, { checkedAt: now + 5000 }, { checkedAt: NaN }]) {
assert.equal(verifyAttestation({ ...proof, ...change }, tracked, health, root, now), false);
}
assert.equal(verifyAttestation(null, tracked, health, root, now), false);
assert.equal(verifyAttestation(proof, tracked, { ...health, healthy: false }, root, now), false);
});