import assert from 'node:assert/strict'; import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { setTimeout as delay } from 'node:timers/promises'; // Verify a kernel-owned direct child, never a process-name search or parent PID. export function assertSubscription(pid, qmlPid, status, command) { assert(Number.isSafeInteger(pid) && pid > 1 && Number.isSafeInteger(qmlPid) && qmlPid > 1); assert.equal(Number(status.match(/^PPid:\s+(\d+)$/m)?.[1]), qmlPid, 'subscription must be a direct QML child'); assert.deepEqual(command.split('\0').filter(Boolean), ['/usr/bin/niri', 'msg', '--json', 'event-stream']); } export function pngSize(bytes) { assert(bytes.length > 32, 'capture is not a complete PNG header'); assert.deepEqual(bytes.subarray(0, 8), Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])); assert.equal(bytes.toString('ascii', 12, 16), 'IHDR'); const size = { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) }; assert(size.width > 0 && size.height > 0); return size; } export async function prepareNiriIntegration({ run, start, waitFor, runtime, nativeNiri, nativeSocket, assertPureService }) { writeFileSync('/project/Niri.qml', nativeNiri); const vault = readFileSync('/project/test-fixtures/ui/niri-integration-vault.qml', 'utf8'); assertPureService(vault); writeFileSync('/project/Vault.qml', vault); writeFileSync('/project/niri-integration-ui-test.qml', readFileSync('/project/test-fixtures/ui/niri-integration.qml', 'utf8').replace('import "../.." as N', 'import "." as N')); const protocol = '/usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml'; run('wayland-scanner', ['client-header', protocol, '/tmp/niri-integration-xdg.h']); run('wayland-scanner', ['private-code', protocol, '/tmp/niri-integration-xdg.c']); run('cc', ['-Wall', '-Wextra', '-Werror', '-I/tmp', '/project/test-fixtures/ui/niri-integration-client.c', '/tmp/niri-integration-xdg.c', '-lwayland-client', '-o', '/tmp/niri-integration-client']); const socket = join(runtime, 'niri.integration.sock'); const modeFile = join(runtime, 'niri-integration-outputs.json'); writeFileSync(modeFile, 'null'); start('node', ['/project/test-fixtures/ui/niri-integration-proxy.mjs', socket, nativeSocket, modeFile], 'niri-integration-proxy'); await waitFor('output fixture socket', () => existsSync(socket)); return { socket, modeFile }; } export async function runNiriIntegration({ run, start, check, snapshot, ipc, screenshot, click, input, key, compositorKey, stop, evidence, runtime, modeFile }) { const action = (...args) => run('niri', ['msg', 'action', ...args.map(String)]); const query = name => JSON.parse(run('niri', ['msg', '--json', name])); const file = name => join(evidence, `${name}.png`); async function closeClient(client) { action('close-window', '--id', client.id); let timeout; try { if (client.child.exitCode === null) await new Promise((resolve, reject) => { timeout = setTimeout(() => reject(new Error('native client did not acknowledge close')), 3000); client.child.once('exit', resolve); }); assert.equal(client.child.exitCode, 0, 'xdg close must exit normally'); assert.equal(client.child.signalCode, null); await stop(client.child); } finally { clearTimeout(timeout); } } const saved = async (kind, name) => { const path = file(name); const before = snapshot().captures.recentCount; const submitted = JSON.parse(ipc('capture', kind, path)); assert.equal(submitted.lastAction.status, 'pending'); assert.equal(submitted.recentCount, before, 'dispatch must not record success'); const state = await check(`${kind} saves only on exact native completion: ${name}`, s => s.captures.recentCount === before + 1 && s.captures.recent[0].path === path && s.captures.recent[0].saved); const bytes = readFileSync(path); const dimensions = pngSize(bytes); // Decode the entire image independently, not just its existence/header. run('identify', ['-regard-warnings', path]); appendFileSync(join(evidence, 'niri-files.jsonl'), JSON.stringify({ path, ...dimensions, size: bytes.length, id: state.captures.recent[0].id }) + '\n'); return dimensions; }; let state = await check('real stream initial state and actual Bar pills', s => s.niri.ready && s.niri.outputCount === 1 && s.niri.windowCount === 0 && s.pills.length === 1 && s.pills[0].active); const firstWorkspace = state.niri.workspaces.find(row => row.is_focused).idx; async function client(name, title, app) { const fifo = join(runtime, `niri-integration-${name}.fifo`); run('mkfifo', [fifo]); const child = start('/tmp/niri-integration-client', [fifo, title, app], `niri-client-${name}`); await check(`native xdg window opens: ${name}`, s => s.niri.focusedTitle === title && s.niri.focusedAppId === app); return { child, fifo, id: snapshot().niri.focusedWindow }; } const alpha = await client('alpha', 'alpha initial', 'synthetic.alpha'); writeFileSync(alpha.fifo, 'title alpha renamed\n'); await check('focused title delta replaces existing record', s => s.niri.windowCount === 1 && s.niri.focusedTitle === 'alpha renamed' && s.text.includes('focus: alpha renamed · synthetic.alpha')); writeFileSync(alpha.fifo, 'app synthetic.changed\n'); state = await check('focused app-id delta reaches visible context', s => s.niri.focusedAppId === 'synthetic.changed'); click(state.vault.rect); await check('actual Bar forwards focused app to safe vault sink', s => s.vault.opens === 1 && s.vault.app === 'synthetic.changed' && s.vault.opened === 'vault'); const beta = await client('beta', 'beta window', 'synthetic.beta'); action('move-window-to-workspace', '2'); state = await check('workspace deltas add populated second pill', s => s.pills.length === 2 && s.niri.workspaces.some(row => row.idx === 2 && row.active_window_id === beta.id)); click(state.pills.find(row => row.index === String(firstWorkspace)).rect); state = await check('actual Bar pill focuses first workspace and alpha', s => s.niri.focusedWindow === alpha.id && s.pills.find(row => row.index === String(firstWorkspace))?.active); click(state.vault.rect); await check('vault context updates on each opening after external focus', s => s.vault.opens === 2 && s.vault.app === 'synthetic.changed'); action('focus-window', '--id', beta.id); state = await check('external focus reaches second workspace and beta', s => s.niri.focusedWindow === beta.id && s.pills.find(row => row.index === '2')?.active); click(state.vault.rect); await check('vault receives changed focused app, not retained prior context', s => s.vault.opens === 3 && s.vault.app === 'synthetic.beta'); await screenshot('niri-bar-two-workspaces'); action('toggle-overview'); await check('native overview opens through stream', s => s.niri.overviewState === 'open'); await compositorKey('Escape'); await check('keyboard closes native overview', s => s.niri.overviewState === 'closed'); // Only the subscription dies. Mutate a client before the bounded retry fires. state = snapshot(); const pid = state.subscriptionPid; assertSubscription(pid, state.qmlPid, readFileSync(`/proc/${pid}/status`, 'utf8'), readFileSync(`/proc/${pid}/cmdline`, 'utf8')); const transitionCount = state.readiness.length; const originalQmlPid = state.qmlPid; writeFileSync(join(evidence, 'niri-subscription.json'), JSON.stringify({ pid, qmlPid: state.qmlPid, command: ['/usr/bin/niri', 'msg', '--json', 'event-stream'] }, null, 2)); process.kill(pid, 'SIGTERM'); writeFileSync(beta.fifo, 'title beta during disconnect\n'); state = await check('subscription disconnect clears readiness and reconnects with fresh state', s => s.readiness.slice(transitionCount).some(row => !row.ready) && s.niri.ready && s.subscriptionPid !== pid && s.niri.focusedTitle === 'beta during disconnect'); assert.equal(state.qmlPid, originalQmlPid); assert(state.readiness.slice(transitionCount).find(row => !row.ready).windows === 0); const original = query('outputs'); const outputName = Object.keys(original)[0]; run('niri', ['msg', 'output', outputName, 'position', 'set', '400', '120']); const repositioned = query('outputs'); writeFileSync(join(evidence, 'niri-reposition.json'), JSON.stringify({ original, repositioned }, null, 2)); assert.equal(repositioned[outputName].logical.x, 400); assert.equal(repositioned[outputName].logical.y, 120); await check('real output reposition reaches outputs-only poll', s => s.niri.outputs[outputName].logical.x === 400 && s.niri.outputs[outputName].logical.y === 120); run('niri', ['msg', 'output', outputName, 'position', 'set', '0', '0']); const stable = snapshot().niri; const extra = { ...original[outputName], name: 'fixture-hotplug', logical: { ...original[outputName].logical, x: -1280 } }; assert.equal(ipc('fixtureOutputs', 'true'), 'true'); writeFileSync(modeFile, JSON.stringify({ Ok: { Outputs: { ...original, 'fixture-hotplug': extra } } })); await check('fixture hotplug enters production outputs query without resetting stream', s => s.niri.outputCount === 2 && s.niri.outputs['fixture-hotplug'].logical.x === -1280 && s.niri.focusedTitle === stable.focusedTitle && s.niri.windowCount === stable.windowCount); writeFileSync(modeFile, JSON.stringify({ Err: 'controlled output query rejection' })); await check('rejected outputs query preserves last inventory and stream readiness', s => s.niri.outputError.length > 0 && s.niri.outputCount === 2 && s.niri.ready); writeFileSync(modeFile, 'null'); await check('fixture unplug restores real inventory through periodic polling', s => s.niri.outputCount === 1 && s.niri.outputError === '' && !s.niri.outputs['fixture-hotplug']); assert.equal(ipc('fixtureOutputs', 'false'), 'true'); assert.deepEqual(await saved('focused-screen', 'native-screen'), { width: 1280, height: 720 }); assert.deepEqual(await saved('focused-window', 'native-window'), { width: 480, height: 280 }); let submitted = JSON.parse(ipc('capture', 'interactive', file('native-interactive'))); assert.equal(submitted.lastAction.saved, false); assert.equal(submitted.recentCount, 2); await delay(350); input('mousemove', '100', '100'); input('mousedown', '1'); input('mousemove', '500', '300'); input('mouseup', '1'); await screenshot('niri-selection-pending'); await compositorKey('Return'); await check('keyboard confirms native interactive save', s => s.captures.recentCount === 3 && s.captures.recent[0].path === file('native-interactive') && s.captures.pendingCount === 0); assert.deepEqual(pngSize(readFileSync(file('native-interactive'))), { width: 401, height: 201 }); run('identify', ['-regard-warnings', file('native-interactive')]); await screenshot('niri-saved-history'); submitted = JSON.parse(ipc('capture', 'interactive', file('native-cancelled'))); const completions = snapshot().completions.length; await delay(350); await compositorKey('Escape'); await delay(350); state = await check('native cancellation is unconfirmed, never fictitious saved history', s => s.captures.pendingCount === 1 && s.captures.recentCount === 3 && s.completions.length === completions); assert(!existsSync(file('native-cancelled'))); assert.equal(state.captures.pending[0].id, submitted.lastAction.id); ipc('capture', 'interactive', file('native-clipboard-only')); await delay(350); input('mousemove', '100', '100'); input('mousedown', '1'); input('mousemove', '500', '300'); input('mouseup', '1'); await compositorKey('ctrl+c'); await check('native clipboard-only null completion cannot consume a pending save', s => s.completions.at(-1) === null && s.captures.recentCount === 3 && s.captures.pendingCount === 2); assert(!existsSync(file('native-clipboard-only'))); // wl-paste is a reader on the isolated compositor, never a second capture backend. assert(run('wl-paste', ['--list-types']).includes('image/png')); run('sh', ['-c', 'wl-paste --type image/png > /evidence/native-clipboard.png']); assert.deepEqual(pngSize(readFileSync(file('native-clipboard'))), { width: 401, height: 201 }); run('identify', ['-regard-warnings', file('native-clipboard')]); const unavailable = JSON.parse(ipc('unavailable')); assert.equal(unavailable.lastAction.status, 'unavailable'); assert.equal(unavailable.pendingCount, 2); assert(!existsSync(file('unavailable'))); await saved('focused-screen', 'native-repeat'); await saved('focused-screen', 'native-repeat'); state = snapshot(); assert.notEqual(state.captures.recent[0].id, state.captures.recent[1].id); assert.equal(state.captures.pendingCount, 2); await screenshot('niri-repeated-history'); // This single warning is required, not hidden: record its action-local log // interval so the complete-log gate rejects missing, duplicate or stray warnings. const nulls = state.completions.filter(path => path === null).length; const nativeLog = join(evidence, 'niri.log'); const writeFailure = { start: readFileSync(nativeLog, 'utf8').length }; ipc('capture', 'focused-screen', '/evidence/missing-directory/native-failure.png'); await check('native failed write emits null and leaves pending/history honest', s => s.completions.filter(path => path === null).length > nulls && s.captures.pendingCount === 3 && s.captures.recentCount === 5 && readFileSync(nativeLog, 'utf8').slice(writeFailure.start).includes('error saving screenshot image: Os { code: 2, kind: NotFound, message: "No such file or directory" }\n')); writeFailure.end = readFileSync(nativeLog, 'utf8').length; assert(!existsSync('/evidence/missing-directory/native-failure.png')); state = snapshot(); for (const record of state.captures.recent) { assert(existsSync(record.path)); assert(state.text.includes(`${record.kind} · ${record.path}`)); } assert(state.text.includes('saved: 5 · unconfirmed: 3')); await screenshot('niri-unconfirmed-history'); submitted = JSON.parse(ipc('burst')); assert.equal(submitted.recentCount, 5, 'a synchronous burst cannot fabricate completion'); assert.equal(submitted.pendingCount, 5); await check('rapid native requests both dispatch without terminating one another', s => s.captures.recentCount === 7 && s.captures.pendingCount === 3); for (const name of ['native-burst-screen', 'native-burst-window']) { pngSize(readFileSync(file(name))); run('identify', ['-regard-warnings', file(name)]); } submitted = JSON.parse(ipc('completeSelection', file('native-complete-selection'))); assert.equal(submitted.selection.phase, 'pending'); assert.equal(submitted.selection.completionFlash, false); await delay(350); input('mousemove', '100', '100'); input('mousedown', '1'); input('mousemove', '500', '300'); input('mouseup', '1'); await compositorKey('Return'); await check('completeSelection flashes only after its native save', s => s.captures.recentCount === 8 && s.captures.pendingCount === 3 && s.captures.selection.phase === 'completed' && s.captures.selection.completionFlash); assert.deepEqual(pngSize(readFileSync(file('native-complete-selection'))), { width: 401, height: 201 }); run('identify', ['-regard-warnings', file('native-complete-selection')]); await closeClient(beta); await check('native close removes beta from the streamed window inventory', s => !s.niri.windows.some(row => row.id === beta.id) && s.niri.windowCount === 1); action('focus-window', '--id', alpha.id); await check('remaining window regains focus', s => s.niri.focusedWindow === alpha.id); await closeClient(alpha); await check('closing last native window clears title/app and retains one active pill', s => s.niri.windowCount === 0 && s.niri.focusedTitle === '' && s.niri.focusedAppId === '' && s.pills.length === 1); ipc('clear'); await check('clear forgets saved and unconfirmed history visibly', s => s.captures.recentCount === 0 && s.captures.pendingCount === 0 && s.text.includes('saved: 0 · unconfirmed: 0')); await screenshot('niri-cleared-history'); ipc('pasteFixture'); state = await check('synthetic paste destination gains native focus', s => s.niri.focusedTitle === 'paste destination' && s.paste.destinationFocused && s.paste.destinationEmpty); const destinationId = state.niri.focusedWindow; click(state.paste.chip); state = await check('clipboard search owns popup focus before paste', s => s.paste.open && s.paste.searchFocused && s.paste.target === destinationId && s.paste.button); input('type', 'focus-safe'); state = await check('clipboard destination search is retained', s => s.paste.queryPreserved && s.paste.destinationEmpty); click(state.paste.button); await check('pointer paste releases popup and types exactly once into destination', s => !s.paste.open && !s.paste.pending && s.paste.destinationMatches && s.paste.queryPreserved && s.niri.focusedWindow === destinationId && s.paste.error === ''); await screenshot('clipboard-paste-pointer'); key('ctrl+a'); key('BackSpace'); state = await check('destination editor remains keyboard operable after paste', s => s.paste.destinationEmpty); click(state.paste.chip); state = await check('clipboard reopen restores search before keyboard paste', s => s.paste.open && s.paste.searchFocused && s.paste.queryPreserved); key('Return'); await check('keyboard paste returns focus and inserts exactly once', s => !s.paste.open && !s.paste.pending && s.paste.destinationMatches && s.paste.queryPreserved && s.niri.focusedWindow === destinationId && s.paste.error === ''); await screenshot('clipboard-paste-keyboard'); console.log('niri interaction assertions completed; complete runtime-log gate remains mandatory'); return writeFailure; }