import assert from 'node:assert/strict'; import { spawn, spawnSync } from 'node:child_process'; import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { createServer } from 'node:net'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; import { runInNewContext } from 'node:vm'; import { assertCleanLog, assertNiriLog, assertPureService, assertSafeCapture, captureUiFailure, childEnvironment, finishUiRun, replayRequest, stopChild } from './ui-test.mjs'; import { assertSubscription, pngSize } from './test-fixtures/ui/niri-integration.mjs'; import { taskOverlayFixtures } from './test-fixtures/ui/tasks.mjs'; test('named replay defaults to all and rejects invalid selections before execution', () => { assert.deepEqual(replayRequest([]), { version: 1, fixture: 'all', command: 'just ui-test all' }); for (const fixture of ['all', 'controls', 'popouts', 'privilege', 'noko', 'niri', 'tasks']) { assert.deepEqual(replayRequest([fixture]), { version: 1, fixture, command: `just ui-test ${fixture}` }); } for (const args of [[''], ['unknown'], ['../controls'], ['all', 'noko'], ['--help']]) { assert.throws(() => replayRequest(args), /usage: just ui-test/); } }); test('task geometry and service overlays remain explicit and isolated', () => { for (const size of ['1366x768', '2560x1440']) assert.deepEqual(replayRequest(['tasks', size]), { version: 1, fixture: 'tasks', size, command: `just ui-test tasks ${size}` }); for (const args of [['tasks', '0x0'], ['tasks', '../host'], ['all', '1366x768'], ['tasks', '1366x768', 'extra']]) assert.throws(() => replayRequest(args)); assert.equal(new Set(taskOverlayFixtures.map(row => row.target)).size, taskOverlayFixtures.length); for (const { source, target } of taskOverlayFixtures) { assert.match(source, /^test-fixtures\/ui\/(?:task-)?services\/\w+\.qml$/); assert.match(target, /^\w+\.qml$/); assertPureService(readFileSync(source, 'utf8')); } const source = readFileSync('test-fixtures/ui/tasks.qml', 'utf8'); assert.match(source, /N\.Bar\s*\{/); assert.match(source, /hasInput.*networkInput\.text\.length > 0/); assert.match(source, /knownDraft.*receivedKnownDraft/); assert.doesNotMatch(source, /\"(?:text|draft|passphrase)\":\s*(?:networkInput|notificationInput)\.text/); const runner = readFileSync('ui-test.mjs', 'utf8'); assert.match(runner, /for \(const \[target, text\] of taskOriginals\)[\s\S]*?await prepareIntegration\(\)/); }); test('subscription termination requires an exact native command and direct QML parent', () => { const command = '/usr/bin/niri\0msg\0--json\0event-stream\0'; assert.doesNotThrow(() => assertSubscription(42, 30, 'Name:\tniri\nPPid:\t30\n', command)); for (const pid of [0, 1, -1, null, '42', 42.5]) { assert.throws(() => assertSubscription(pid, 30, 'PPid:\t30\n', command)); } for (const [parent, status, args] of [ [1, 'PPid:\t1\n', command], [30, 'PPid:\t99\n', command], [30, 'Name:\tniri\n', command], [30, 'PPid:\t30\n', '/usr/bin/niri\0'], [30, 'PPid:\t30\n', '/other/niri\0msg\0--json\0event-stream\0'], [30, 'PPid:\t30\n', command.replace('event-stream', 'outputs')], [30, 'PPid:\t30\n', command + 'extra\0'] ]) assert.throws(() => assertSubscription(42, parent, status, args)); }); test('capture dimensions reject missing, non-PNG, truncated and zero-size headers', () => { const bytes = Buffer.alloc(40); Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(bytes); bytes.write('IHDR', 12); bytes.writeUInt32BE(1280, 16); bytes.writeUInt32BE(720, 20); assert.deepEqual(pngSize(bytes), { width: 1280, height: 720 }); for (const invalid of [Buffer.alloc(0), bytes.subarray(0, 32), Buffer.alloc(40), Buffer.from(bytes).fill(0, 16, 20), Buffer.from(bytes).fill(0, 20, 24)]) { assert.throws(() => pngSize(invalid)); } }); test('niri fixture has a pure vault sink and the real Bar/Niri/Screenshots route', () => { assertPureService(readFileSync('test-fixtures/ui/niri-integration-vault.qml', 'utf8')); const fixture = readFileSync('test-fixtures/ui/niri-integration.qml', 'utf8'); for (const type of ['Bar', 'Niri', 'Screenshots']) assert(fixture.includes(`N.${type} {`)); assert.match(fixture, /function fixtureOutputs\(enabled: bool\)/); const runner = readFileSync('ui-test.mjs', 'utf8'); assert.match(runner, /const integration = replay.fixture === 'all' \|\| replay.fixture === 'niri'/); assert.match(runner, /await runNiriIntegration\(/); }); test('failure screenshots allow masked input but never revealed input or unknown auth state', () => { assert.doesNotThrow(() => assertSafeCapture({ auth: { visible: true, hasInput: true, inputMasked: true } })); assert.doesNotThrow(() => assertSafeCapture({ auth: { visible: true, hasInput: false, inputMasked: false } })); assert.throws(() => assertSafeCapture({ auth: { visible: true, hasInput: true, inputMasked: false } }), /unmasked input/); assert.throws(() => assertSafeCapture({ auth: { visible: true } }), /unmasked input/); assert.throws(() => assertSafeCapture(null), /snapshot unavailable/); }); test('failure evidence retains the original error, named phase and last safe snapshot', async () => { const calls = []; const snapshot = { auth: { visible: true, hasInput: true, inputMasked: true } }; const error = new Error('original assertion'); const record = await captureUiFailure(error, { evidence: '/evidence', fixture: 'privilege', phase: 'privilege', snapshot, screenshot: async name => { calls.push(name); return '/evidence/failure.png'; }, write: (path, text) => calls.push([path, JSON.parse(text)]) }); assert.equal(record.error, error.stack); assert.equal(record.fixture, 'privilege'); assert.equal(record.phase, 'privilege'); assert.deepEqual(record.snapshot, snapshot); assert.equal(record.screenshot, '/evidence/failure.png'); assert.deepEqual(record.captureErrors, []); assert.equal(calls[0], 'failure'); assert.deepEqual(calls[1], ['/evidence/failure.json', record]); const failedCapture = await captureUiFailure(error, { evidence: '/evidence', fixture: 'all', phase: 'controls', snapshot, screenshot: async () => { throw new Error('capture socket gone'); }, write: () => {} }); assert.equal(failedCapture.error, error.stack); assert.equal(failedCapture.screenshot, null); assert.match(failedCapture.captureErrors[0], /capture socket gone/); assert.deepEqual(failedCapture.snapshot, snapshot); }); test('runtime gate is captured before teardown; all children and complete logs are still checked', async () => { for (const original of [null, new Error('original interaction')]) { const calls = []; const diagnostic = new Error('WARN private login1'); await assert.rejects(finishUiRun(original, { children: ['weston', 'niri', 'fixture'], checkLogs: () => { calls.push('logs'); throw diagnostic; }, capture: async error => { calls.push(['capture', error]); }, stop: async child => { calls.push(['stop', child]); } }), error => error === (original || diagnostic)); assert.deepEqual(calls, ['logs', ['capture', original || diagnostic], ['stop', 'fixture'], ['stop', 'niri'], ['stop', 'weston'], 'logs']); } }); test('clean completion checks full logs on both sides of teardown without failure capture', async () => { const calls = []; const children = ['weston', 'niri']; await finishUiRun(null, { children, checkLogs: () => calls.push('logs'), capture: async () => assert.fail('unexpected failure capture'), stop: async child => calls.push(child) }); assert.deepEqual(calls, ['logs', 'niri', 'weston', 'logs']); assert.deepEqual(children, ['weston', 'niri']); }); test('capture and teardown errors cannot replace the original error or skip sibling cleanup', async () => { const original = new Error('original assertion'); const stopped = []; const secondary = []; await assert.rejects(finishUiRun(original, { children: ['weston', 'niri', 'fixture'], checkLogs: () => {}, capture: async () => { throw new Error('evidence unwritable'); }, stop: async child => { stopped.push(child); if (child === 'fixture') throw new Error('teardown error'); }, report: error => secondary.push(error.message) }), error => error === original); assert.deepEqual(stopped, ['fixture', 'niri', 'weston']); assert.deepEqual(secondary, ['evidence unwritable', 'teardown error']); }); test('diagnostics produced only during teardown still fail the run', async () => { const diagnostic = new Error('ERROR during shutdown'); let stopped = false; let captured; await assert.rejects(finishUiRun(null, { children: ['niri'], checkLogs: () => { if (stopped) throw diagnostic; }, capture: async error => { captured = error; }, stop: async () => { stopped = true; } }), error => error === diagnostic); assert.equal(captured, diagnostic); }); test('fixture snapshots serialize predicates, never editable contents', () => { const controls = readFileSync('ui-test.qml', 'utf8'); assert(!/query: root\.query|text: field\.text/.test(controls)); assert.match(controls, /queryMatchesInput: root\.query === field\.text/); const refactor = readFileSync('test-fixtures/ui/refactor.qml', 'utf8'); assert.match(refactor, /child\.echoMode === undefined/); assert.match(refactor, /hasInput: input \? input\.text\.length > 0 : false/); assert.match(refactor, /inputMasked: input \? input\.echoMode === TextInput\.Password : false/); }); test('snapshot predicates and visual-text selection redact hostile editable values', () => { const sentinel = 'fixture-only\"\\nhttps://private.invalid/account'; const controls = readFileSync('ui-test.qml', 'utf8'); const body = controls.match(/function snapshot\(\): string \{([\s\S]*?)\n \}/)[1]; const field = { text: sentinel, input: {} }; const state = JSON.parse(runInNewContext(`(function () { ${body} })()`, { root: { query: sentinel, rect: () => ({ x: 1, y: 2, width: 3, height: 4 }) }, field, window: {}, mute: {}, slider: { Accessible: {} }, surface: {}, actionCard: {}, nestedAction: {}, iconAction: { Accessible: {} }, disabledAction: {}, longAction: {}, iconField: { input: { text: sentinel } } })); assert.equal(state.queryMatchesInput, true); assert.equal(state.queryIsHeadphones, false); assert(!JSON.stringify(state).includes('fixture-only')); const refactor = readFileSync('test-fixtures/ui/refactor.qml', 'utf8'); const visibleText = refactor.match(/function visibleText\(item: var, window: var\): var \{([\s\S]*?)\n \}/)[1]; for (const echoMode of [0, 1, 2, 3]) { const rows = runInNewContext(`(function (item, window) { ${visibleText} })(null, null)`, { tree: () => [{ visible: true, font: {}, text: sentinel, echoMode }, { visible: true, font: {}, text: 'safe label' }], rect: () => ({ x: 1, y: 2, width: 3, height: 4 }) }); assert.equal(rows.length, 1); assert.equal(rows[0].text, 'safe label'); assert(!JSON.stringify(rows).includes('fixture-only')); } }); test('input targets must be real sockets inside the owned runtime', async () => { const runtime = mkdtempSync(join(tmpdir(), 'ui-boundary-')); const outside = mkdtempSync(join(tmpdir(), 'ui-parent-')); const servers = []; async function socket(path) { const server = createServer(); await new Promise((resolve, reject) => { server.once('error', reject); server.listen(path, resolve); }); servers.push(server); return path; } try { const display = await socket(join(runtime, 'wayland-1')); const ipc = await socket(join(runtime, 'niri.wayland-1.42.sock')); const parent = await socket(join(outside, 'wayland-0')); const weston = await socket(join(runtime, 'weston-headless')); const env = childEnvironment(runtime, display, ipc); assert.equal(env.WAYLAND_DISPLAY, display); assert.equal(env.NIRI_SOCKET, ipc); assert.equal(env.XDG_RUNTIME_DIR, runtime); assert.throws(() => childEnvironment(runtime, parent, ipc), /refusing non-child socket/); assert.throws(() => childEnvironment(runtime, weston, ipc), /regular expression/); const file = join(runtime, 'wayland-2'); writeFileSync(file, 'not a socket'); assert.throws(() => childEnvironment(runtime, file, ipc), /must be a socket/); assert.throws(() => childEnvironment(runtime, ipc, display), /regular expression/); } finally { for (const server of servers) await new Promise(resolve => server.close(resolve)); rmSync(runtime, { recursive: true }); rmSync(outside, { recursive: true }); } }); test('runtime diagnostics fail rather than being filtered or downgraded', () => { assert.doesNotThrow(() => assertCleanLog('INFO: Configuration Loaded\n')); for (const diagnostic of ['WARN: unresolved binding', 'WARNING: unresolved binding', 'warning: deprecated API', 'Could not bind', 'failed to load', 'ERROR: creation failed', 'ReferenceError: missing', 'TypeError: wrong value', 'binding loop detected', 'Error parsing file']) { assert.throws(() => assertCleanLog(diagnostic)); } }); const niriRecord = (level, source, message) => `2026-09-06T14:51:45.642004Z ${level} ${source}: ${message}\n`; const softwareFallback = niriRecord('DEBUG', 'niri::backend::winit', 'failed building default dmabuf feedback, falling back to v3: error getting EGL device render node\n\nCaused by:\n None of the following EGL extensions is supported by the underlying EGL implementation, at least one is required: ["EGL_EXT_device_drm"]'); const failedDestination = niriRecord('DEBUG', 'niri::niri', 'saving screenshot to "/evidence/missing-directory/native-failure.png"'); const nativeWriteWarning = niriRecord('WARN', 'niri::niri', 'error saving screenshot image: Os { code: 2, kind: NotFound, message: "No such file or directory" }'); test('only the exact Niri software fallback at DEBUG is a sandbox diagnostic exception', () => { assert.deepEqual(assertNiriLog(''), { softwareFallbacks: 0, expectedWriteFailures: 0 }); assert.deepEqual(assertNiriLog(softwareFallback), { softwareFallbacks: 1, expectedWriteFailures: 0 }); assert.deepEqual(assertNiriLog(softwareFallback.replace('DEBUG', '\x1b[34mDEBUG\x1b[0m')), { softwareFallbacks: 1, expectedWriteFailures: 0 }); for (const invalid of [ softwareFallback.replace('DEBUG', 'WARN'), softwareFallback.replace('DEBUG', 'ERROR'), softwareFallback.replace('niri::backend::winit', 'niri::renderer'), softwareFallback.replace('EGL_EXT_device_drm', 'EGL_other_extension'), softwareFallback.replace('falling back to v3', 'aborting'), softwareFallback + softwareFallback, softwareFallback + 'WARN: unrelated\n', niriRecord('DEBUG', 'niri::backend::winit', 'failed to initialize renderer'), niriRecord('WARN', 'niri::dbus::freedesktop_login1', 'service absent'), niriRecord('WARN', 'niri::dbus::freedesktop_locale1', 'service absent') ]) assert.throws(() => assertNiriLog(invalid)); // Production/default log checking has no exceptions. assert.throws(() => assertCleanLog(softwareFallback)); assert.throws(() => assertCleanLog(nativeWriteWarning)); }); test('native failed-write warning is required exactly once inside its deliberate action interval', () => { const before = softwareFallback + niriRecord('INFO', 'niri', 'ready'); const interval = failedDestination + nativeWriteWarning; const after = niriRecord('INFO', 'niri', 'stopped'); const expected = { start: before.length, end: before.length + interval.length }; assert.deepEqual(assertNiriLog(before + interval + after, expected), { softwareFallbacks: 1, expectedWriteFailures: 1 }); for (const body of [ '', failedDestination, nativeWriteWarning, interval + nativeWriteWarning, interval + interval, interval.replace('WARN', 'ERROR'), interval.replace('code: 2', 'code: 13'), interval.replace('NotFound', 'PermissionDenied'), interval.replace('niri::niri: error', 'other::source: error'), interval.replace('missing-directory', 'different-directory'), failedDestination + niriRecord('INFO', 'niri', 'different action') + nativeWriteWarning, interval + 'TypeError: unrelated failure\n' ]) assert.throws(() => assertNiriLog(before + body + after, { start: before.length, end: before.length + body.length })); assert.throws(() => assertNiriLog(before + interval + after)); assert.throws(() => assertNiriLog(nativeWriteWarning + before + interval, { start: nativeWriteWarning.length + before.length, end: nativeWriteWarning.length + before.length + interval.length })); assert.throws(() => assertNiriLog(before + interval + nativeWriteWarning, expected)); for (const range of [{ start: -1, end: 1 }, { start: 0, end: 1e9 }, { start: 1, end: 2 }, { start: 0.5, end: 1 }, { start: 0, end: 0 }]) assert.throws(() => assertNiriLog(interval, range)); }); test('native diagnostic exceptions apply only to the child compositor and fixtures precede its launch', () => { const runner = readFileSync('ui-test.mjs', 'utf8'); assert.match(runner, /log === join\(evidence, 'niri.log'\)\) diagnostics = assertNiriLog\(text, expectedWriteFailure\)/); assert.match(runner, /else assertCleanLog\(text\)/); assert(runner.indexOf("await waitFor('private login/locale fixtures'") < runner.indexOf("const compositor = start('niri'")); assert.match(runner, /Niri consumed private login\/locale properties/); const fixture = readFileSync('test-fixtures/ui/session-bus.c', 'utf8'); assert.match(fixture, /sd_bus_set_address\(bus, address\)/); assert.match(fixture, /strcmp\(address, getenv\("DBUS_SYSTEM_BUS_ADDRESS"\)\)/); assert(!/SD_BUS_METHOD|SD_BUS_WRITABLE_PROPERTY|sd_bus_open_system/.test(fixture), 'fixture exports no mutation or implicit system bus access'); }); test('overlay service doubles are pure QtQuick Item state/functions without privileged APIs', () => { for (const name of ['CalDav', 'Resources', 'Privilege', 'Noko', 'Niri', 'Audio', 'Network', 'Vault']) { assertPureService(readFileSync(`test-fixtures/ui/services/${name}.qml`, 'utf8')); } for (const body of ['Process {}', 'FileView {}', 'PolkitAgent {}', 'PamContext {}', 'WorkerScript {}', 'function bad() { Qt.openUrlExternally("test"); }', 'function bad() { new XMLHttpRequest(); }', 'function bad() { fetch("test"); }', 'function bad() { eval("test"); }', 'function bad() { Quickshell.execDetached([]); }']) { assert.throws(() => assertPureService(`import QtQuick\nItem { ${body} }`)); } assert.throws(() => assertPureService('import QtQuick\nimport Quickshell.Io\nItem {}')); assert.throws(() => assertPureService('import "../../"\nItem {}')); }); test('fixture replacement reaps only its own process, including an uncooperative child', { timeout: 8000 }, async () => { const sibling = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)']); try { for (const ignoreTerm of [false, true]) { const child = spawn(process.execPath, ['-e', `${ignoreTerm ? 'process.on("SIGTERM", () => {});' : ''} console.log("ready"); setInterval(() => {}, 1000);`]); try { await new Promise(resolve => child.stdout.once('data', resolve)); await stopChild(child); assert.equal(child.signalCode, ignoreTerm ? 'SIGKILL' : 'SIGTERM'); assert.equal(sibling.exitCode, null); assert.equal(sibling.signalCode, null); await stopChild(child); } finally { await stopChild(child); } } } finally { await stopChild(sibling); } }); test('an unsuccessful spawn has nothing to reap', async () => { const child = spawn('/nonexistent-nuguland-fixture-command'); await new Promise(resolve => child.once('error', resolve)); await stopChild(child); }); test('invalid invocation and unsandboxed internal invocation fail before launching a compositor', () => { for (const args of [['unexpected'], ['--inside'], ['controls', 'noko'], ['--inside', 'invalid']]) { const result = spawnSync(process.execPath, ['ui-test.mjs', ...args], { encoding: 'utf8', env: { PATH: process.env.PATH }, timeout: 5000 }); assert.equal(result.status, 1); assert.match(result.stderr, args.length === 1 && args[0] === '--inside' ? /internal mode requires sandbox/ : /usage: just ui-test/); assert(!result.stdout.includes('evidence:')); } }); test('named selections reach the sandbox and its original failure evidence is never overwritten', () => { const dir = mkdtempSync(join(tmpdir(), 'ui-replay-')); try { writeFileSync(join(dir, 'bwrap'), `#!${process.execPath} const fs = require('node:fs'); const args = process.argv.slice(2); const evidence = args[args.indexOf('/evidence') - 1]; fs.writeFileSync(evidence + '/failure.json', JSON.stringify({ error: 'original inner assertion', fixture: args.at(-1) })); process.exit(7); `, { mode: 0o755 }); for (const fixture of ['all', 'controls', 'popouts', 'privilege', 'noko', 'niri', 'tasks']) { const result = spawnSync(process.execPath, ['ui-test.mjs', fixture], { env: { PATH: dir }, encoding: 'utf8', timeout: 5000 }); const evidence = result.stdout.match(/^evidence: (.+)$/m)?.[1]; assert(evidence, result.stderr); try { assert.equal(result.status, 1); assert.deepEqual(JSON.parse(readFileSync(join(evidence, 'failure.json'), 'utf8')), { error: 'original inner assertion', fixture }); assert.deepEqual(JSON.parse(readFileSync(join(evidence, 'replay.json'), 'utf8')), { ...replayRequest([fixture]), phase: 'startup' }); const { command } = JSON.parse(readFileSync(join(evidence, 'command.json'), 'utf8')); assert.deepEqual(command.slice(-2), ['--inside', fixture]); assert(!existsSync(command[command.indexOf('/run/user/1000') - 1])); } finally { rmSync(evidence, { recursive: true }); } } } finally { rmSync(dir, { recursive: true }); } }); test('sandbox failure, spawn error, and interruption remove the owned runtime and fail', { timeout: 10_000 }, async () => { const dir = mkdtempSync(join(tmpdir(), 'ui-lifecycle-')); try { for (const mode of ['failure', 'interrupt', 'stubborn', 'missing']) { const executable = join(dir, 'bwrap'); if (mode === 'missing') rmSync(executable); else writeFileSync(executable, `#!${process.execPath}\n${mode === 'failure' ? 'process.exit(7)' : `process.on("SIGTERM", () => { ${mode === 'stubborn' ? '' : 'process.exit(0)'} }); console.log("sandbox-ready"); setInterval(() => {}, 1000)`}\n`, { mode: 0o755 }); const child = spawn(process.execPath, ['ui-test.mjs'], { env: { PATH: dir }, stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; child.stdout.on('data', data => { stdout += data; if (['interrupt', 'stubborn'].includes(mode) && stdout.includes('sandbox-ready')) child.kill('SIGINT'); }); child.stderr.on('data', data => { stderr += data; }); const code = await new Promise((resolve, reject) => { child.once('error', reject); child.once('close', resolve); }); const evidence = stdout.match(/^evidence: (.+)$/m)?.[1]; assert(evidence, `${mode}: no evidence directory`); try { assert.equal(code, 1, `${mode}: ${stderr}`); const failure = JSON.parse(readFileSync(join(evidence, 'failure.json'), 'utf8')); assert.equal(failure.fixture, 'all'); assert.equal(failure.phase, 'startup'); assert.equal(failure.snapshot, null); assert.equal(failure.screenshot, null); assert.match(failure.captureErrors[0], /sandbox unavailable/); assert.match(failure.error, mode === 'missing' ? /ENOENT/ : ['interrupt', 'stubborn'].includes(mode) ? /interrupted/ : /nested UI test failed/); const replay = JSON.parse(readFileSync(join(evidence, 'replay.json'), 'utf8')); assert.deepEqual(replay, { ...replayRequest([]), phase: 'startup' }); const { command } = JSON.parse(readFileSync(join(evidence, 'command.json'), 'utf8')); assert.deepEqual(command.slice(-2), ['--inside', 'all']); const runtime = command[command.indexOf('/run/user/1000') - 1]; assert(runtime.startsWith(join(tmpdir(), 'nuguland-ui-runtime-'))); assert(!existsSync(runtime), `${mode}: runtime leaked`); assert(!command.includes('WAYLAND_DISPLAY'), `${mode}: inherited display configuration`); assert(!command.includes('/run/parent-display'), `${mode}: exposed host compositor`); if (['interrupt', 'stubborn'].includes(mode)) assert.match(stderr, /interrupted/); } finally { rmSync(evidence, { recursive: true }); } } } finally { rmSync(dir, { recursive: true }); } });