285 lines
11 KiB
JavaScript
285 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { _electron as electron } from '@playwright/test';
|
|
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
import { createServer } from 'node:net';
|
|
import { tmpdir } from 'node:os';
|
|
import { dirname, join, resolve } from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
import { performance } from 'node:perf_hooks';
|
|
|
|
import { defaultProductExecutable } from './lib/pi-product-artifact.mjs';
|
|
|
|
function parseArgs(argv, projectRoot = process.cwd()) {
|
|
const options = {
|
|
projectRoot: resolve(projectRoot),
|
|
runtimeRoot: undefined,
|
|
electronExecutable: undefined,
|
|
reportPath: undefined,
|
|
verifyCleanupFailure: true,
|
|
};
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const argument = argv[index];
|
|
const value = argv[index + 1];
|
|
if (!value || value.startsWith('--')) throw new Error(`${argument} requires a value`);
|
|
if (argument === '--runtime-root') options.runtimeRoot = resolve(value);
|
|
else if (argument === '--electron-executable') options.electronExecutable = resolve(value);
|
|
else if (argument === '--report') options.reportPath = resolve(value);
|
|
else throw new Error(`Unknown argument: ${argument}`);
|
|
index += 1;
|
|
}
|
|
options.electronExecutable ??= defaultProductExecutable(options.projectRoot);
|
|
return options;
|
|
}
|
|
|
|
async function allocatePort() {
|
|
return await new Promise((resolvePort, reject) => {
|
|
const server = createServer();
|
|
server.once('error', reject);
|
|
server.listen(0, '127.0.0.1', () => {
|
|
const address = server.address();
|
|
if (!address || typeof address === 'string') {
|
|
server.close(() => reject(new Error('Failed to allocate PI proof Host API port')));
|
|
return;
|
|
}
|
|
server.close((error) => error ? reject(error) : resolvePort(address.port));
|
|
});
|
|
});
|
|
}
|
|
|
|
function assertPackagedMain(result) {
|
|
if (result?.packagedMain?.isPackaged !== true || result?.packagedMain?.appPathUsesAsar !== true) {
|
|
throw new Error(`PI proof did not execute from packaged app.asar Main: ${JSON.stringify(result?.packagedMain)}`);
|
|
}
|
|
}
|
|
|
|
function assertActivePressure(pressure) {
|
|
const expected = {
|
|
parentWorkers: 4,
|
|
childWorkers: 4,
|
|
parentProcesses: 4,
|
|
childProcesses: 4,
|
|
liveProcesses: 8,
|
|
parentProviderRequests: 4,
|
|
childProviderRequests: 4,
|
|
processBudget: 8,
|
|
childPermits: 4,
|
|
dispatches: 4,
|
|
writeLeases: 4,
|
|
};
|
|
const actual = {
|
|
parentWorkers: pressure?.parentWorkers,
|
|
childWorkers: pressure?.childWorkers,
|
|
parentProcesses: pressure?.parentProcessIds?.length,
|
|
childProcesses: pressure?.childProcessIds?.length,
|
|
liveProcesses: pressure?.liveProcessIds?.length,
|
|
parentProviderRequests: pressure?.providerRequests?.parent,
|
|
childProviderRequests: pressure?.providerRequests?.child,
|
|
processBudget: pressure?.processBudget?.active,
|
|
childPermits: pressure?.childPermits?.active,
|
|
dispatches: pressure?.dispatches?.active,
|
|
writeLeases: pressure?.writeLeases?.active,
|
|
};
|
|
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
throw new Error(`PI pressure did not reach 4 parent + 4 child state: ${JSON.stringify(actual)}`);
|
|
}
|
|
}
|
|
|
|
function assertReleasedPressure(pressure) {
|
|
const counts = [
|
|
pressure?.parentWorkers,
|
|
pressure?.childWorkers,
|
|
pressure?.parentProcessIds?.length,
|
|
pressure?.childProcessIds?.length,
|
|
pressure?.liveProcessIds?.length,
|
|
pressure?.providerRequests?.parent,
|
|
pressure?.providerRequests?.child,
|
|
pressure?.processBudget?.active,
|
|
pressure?.processBudget?.waiting,
|
|
pressure?.childPermits?.active,
|
|
pressure?.childPermits?.waiting,
|
|
pressure?.dispatches?.active,
|
|
pressure?.dispatches?.parents,
|
|
pressure?.writeLeases?.active,
|
|
pressure?.writeLeases?.waiting,
|
|
];
|
|
if (counts.some((value) => value !== 0)) {
|
|
throw new Error(`PI pressure resources were not fully released: ${JSON.stringify(pressure)}`);
|
|
}
|
|
}
|
|
|
|
function assertManagedTurns(extension) {
|
|
const expectedMilestones = 'worker.queue_wait,resources.ready,worker.spawn,rpc.ready,session.open,prompt.accepted,agent.start,provider.first_event,agent.settled';
|
|
const expectedDelayMs = extension?.providerFirstEventDelayMs;
|
|
if (!Number.isFinite(expectedDelayMs) || expectedDelayMs < 1) {
|
|
throw new Error(`PI proof did not expose a controlled Provider first-event delay: ${expectedDelayMs}`);
|
|
}
|
|
for (const turn of extension?.managedTurns ?? []) {
|
|
if (turn.milestones.map(({ milestone }) => milestone).join(',') !== expectedMilestones) {
|
|
throw new Error(`PI managed turn timeline is incomplete: ${JSON.stringify(turn)}`);
|
|
}
|
|
const agentStart = turn.milestones.find(({ milestone }) => milestone === 'agent.start');
|
|
const providerFirstEvent = turn.milestones.find(({ milestone }) => milestone === 'provider.first_event');
|
|
if (agentStart?.source !== 'pi.agent_start'
|
|
|| providerFirstEvent?.source !== 'pi.assistant_message_start'
|
|
|| providerFirstEvent.at <= agentStart.at
|
|
|| providerFirstEvent.durationMs < Math.floor(expectedDelayMs * 0.75)) {
|
|
throw new Error(`PI Provider first-event milestone is not Provider-response-backed: ${JSON.stringify(turn)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function evaluateProof(electronApplication, action) {
|
|
return await electronApplication.evaluate(async (_electron, requestedAction) => {
|
|
const proof = globalThis.__niancodeRunPiReleaseProofE2E;
|
|
if (typeof proof !== 'function') throw new Error('Packaged Main has no PI release proof entry');
|
|
return await proof(requestedAction);
|
|
}, action);
|
|
}
|
|
|
|
async function closeApplication(electronApplication) {
|
|
await Promise.race([
|
|
electronApplication.close().catch(() => undefined),
|
|
new Promise((resolveTimeout) => setTimeout(resolveTimeout, 5_000)),
|
|
]);
|
|
}
|
|
|
|
export async function runPackagedProductProof(options) {
|
|
const scratchRoot = await mkdtemp(join(tmpdir(), 'makelore-pi-final-product-proof-'));
|
|
const homeDir = join(scratchRoot, 'home');
|
|
const userDataDir = join(scratchRoot, 'user-data');
|
|
await Promise.all([
|
|
mkdir(join(homeDir, '.config'), { recursive: true }),
|
|
mkdir(join(homeDir, 'AppData', 'Local'), { recursive: true }),
|
|
mkdir(join(homeDir, 'AppData', 'Roaming'), { recursive: true }),
|
|
mkdir(userDataDir, { recursive: true }),
|
|
]);
|
|
const hostApiPort = await allocatePort();
|
|
let electronApplication;
|
|
let pressureActive = false;
|
|
try {
|
|
electronApplication = await electron.launch({
|
|
executablePath: options.electronExecutable,
|
|
env: {
|
|
...process.env,
|
|
HOME: homeDir,
|
|
USERPROFILE: homeDir,
|
|
APPDATA: join(homeDir, 'AppData', 'Roaming'),
|
|
LOCALAPPDATA: join(homeDir, 'AppData', 'Local'),
|
|
XDG_CONFIG_HOME: join(homeDir, '.config'),
|
|
NIANCODE_E2E: '1',
|
|
NIANCODE_E2E_SKIP_SETUP: '1',
|
|
NIANCODE_USER_DATA_DIR: userDataDir,
|
|
NIANCODE_PORT_NIANCODE_HOST_API: String(hostApiPort),
|
|
...(process.platform === 'linux' ? { ELECTRON_DISABLE_SANDBOX: '1' } : {}),
|
|
},
|
|
timeout: 90_000,
|
|
});
|
|
const page = await electronApplication.firstWindow();
|
|
await page.waitForLoadState('domcontentloaded');
|
|
|
|
const extension = await evaluateProof(electronApplication, 'extension');
|
|
assertPackagedMain(extension);
|
|
assertManagedTurns(extension.extension);
|
|
if (extension.extension?.subagentStatus !== 'complete'
|
|
|| extension.extension?.childToolNames?.join(',') !== 'find,grep,ls,read'
|
|
|| !extension.extension?.parentToolNames?.includes('subagent')
|
|
|| extension.extension?.parentProcessIds?.length < 2
|
|
|| extension.extension?.childProcessIds?.length !== 1
|
|
|| extension.extension?.providerRequests?.child !== 1
|
|
|| extension.extension?.managedTurns?.length !== 2) {
|
|
throw new Error(`Final ASAR extension/subagent proof failed: ${JSON.stringify(extension.extension)}`);
|
|
}
|
|
|
|
const pressureStart = await evaluateProof(electronApplication, 'pressure.start');
|
|
pressureActive = true;
|
|
assertPackagedMain(pressureStart);
|
|
assertActivePressure(pressureStart.pressure);
|
|
|
|
const uiStartedAt = performance.now();
|
|
await page.getByTestId('ai-module-option-programming').click();
|
|
await page.getByTestId('main-layout').waitFor({ state: 'visible', timeout: 10_000 });
|
|
const uiInteractiveMs = Math.round(performance.now() - uiStartedAt);
|
|
|
|
const pressureFinish = await evaluateProof(electronApplication, 'pressure.finish');
|
|
pressureActive = false;
|
|
assertPackagedMain(pressureFinish);
|
|
assertReleasedPressure(pressureFinish.pressure);
|
|
|
|
let failureCleanup;
|
|
if (options.verifyCleanupFailure === true) {
|
|
const failureStart = await evaluateProof(electronApplication, 'pressure.start');
|
|
pressureActive = true;
|
|
assertPackagedMain(failureStart);
|
|
assertActivePressure(failureStart.pressure);
|
|
let injectedFailure;
|
|
try {
|
|
await evaluateProof(electronApplication, 'pressure.finish.inject-failure');
|
|
} catch (error) {
|
|
injectedFailure = error;
|
|
}
|
|
if (!injectedFailure
|
|
|| !String(injectedFailure).includes('PI release pressure cleanup failed: parents.settle')) {
|
|
throw new Error(`PI pressure cleanup failure injection did not fail as expected: ${String(injectedFailure)}`);
|
|
}
|
|
const retryFinish = await evaluateProof(electronApplication, 'pressure.finish');
|
|
pressureActive = false;
|
|
assertPackagedMain(retryFinish);
|
|
assertReleasedPressure(retryFinish.pressure);
|
|
failureCleanup = {
|
|
injectedAt: 'parents.settle',
|
|
firstFinish: 'failed-as-injected',
|
|
retry: 'pass',
|
|
released: retryFinish.pressure,
|
|
};
|
|
}
|
|
|
|
const report = {
|
|
schemaVersion: 1,
|
|
generatedAt: new Date().toISOString(),
|
|
executable: options.electronExecutable,
|
|
runtimeRoot: options.runtimeRoot ?? null,
|
|
packagedMain: extension.packagedMain,
|
|
extension: extension.extension,
|
|
pressure: {
|
|
processKind: 'final-product-executable-with-ELECTRON_RUN_AS_NODE',
|
|
active: pressureStart.pressure,
|
|
ui: {
|
|
action: 'select Makelore Code and render main layout',
|
|
interactive: true,
|
|
durationMs: uiInteractiveMs,
|
|
},
|
|
released: pressureFinish.pressure,
|
|
...(failureCleanup ? { failureCleanup } : {}),
|
|
},
|
|
result: 'pass',
|
|
};
|
|
if (options.reportPath) {
|
|
await mkdir(dirname(options.reportPath), { recursive: true });
|
|
await writeFile(options.reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
}
|
|
return report;
|
|
} finally {
|
|
if (electronApplication && pressureActive) {
|
|
await evaluateProof(electronApplication, 'pressure.finish').catch(() => undefined);
|
|
}
|
|
if (electronApplication) await closeApplication(electronApplication);
|
|
await rm(scratchRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const options = parseArgs(process.argv.slice(2));
|
|
const report = await runPackagedProductProof(options);
|
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
}
|
|
|
|
const isMain = process.argv[1]
|
|
&& pathToFileURL(resolve(process.argv[1])).href === import.meta.url;
|
|
if (isMain) {
|
|
main().catch((error) => {
|
|
process.stderr.write(`${error.stack ?? error.message}\n`);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|