test(web): record headed lifecycle activation control

Add an opt-in owned-X-display diagnostic with window restoration and bounded foreground observations. Preserve the failed bare-Xvfb result: resumed timers do not establish restored visibility or Pulse convergence.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-08 07:52:15 +01:00
parent 73fd4da42f
commit ad8f7af6c1
2 changed files with 82 additions and 11 deletions
@@ -97,3 +97,43 @@ observations. Repeating this activation unchanged, synthetically dispatching a
visibility event, or enabling forced focus would not resolve the evidence gap.
This diagnostic is not a release gate and proves nothing about installed
incident state or destination receipt.
## Headed window control — 8 September 2026
The opt-in headed diagnostic uses an owned display (never a user's desktop):
```sh
pulse-heavy-run -- xvfb-run -a -s '-screen 0 1280x800x24 -nolisten tcp' node scripts/check-browser-single-session-control.mjs --headed-window
```
[Playwright's headed Linux guidance](https://playwright.dev/docs/ci#running-headed)
identifies Xvfb as the display prerequisite. This is not proof of window-manager
activation. This mode launches real headed Chromium with one raw page session,
without enabling focus emulation. It requests minimisation of the target's owned
window before freeze, restoration after resume, then tab activation. It records
all intervention commands and samples every 250ms for a preselected ten-second
foreground bound. Freeze/resume must be ordered with identical timer counts;
subsequent timer progress is required at foreground, rather than after the old
250ms hidden-page delay. A visible event after resume and visible/focused final
state are independently required. Default/headless checks retain their old bound.
One run on Chrome 141.0.7390.37 (revision as above), Playwright 1.56.1,
Node v24.20.0, linux x64, Xvfb/xserver-common `2:21.1.12-1ubuntu1.6`, display
`:99`, exited 1 at `Must observe visible transition after resume`:
- Baseline visible/focused at tick 5.
- Acknowledged minimise request: still visible/focused at tick 10, no event.
- Freeze/resume ordered at tick 10; post-resume hidden/unfocused at tick 10.
- Acknowledged restore and tab activation: hidden/focused throughout the bounded
observation; timers eventually reached tick 20, no visible event.
Thus the short timer bound is no longer the immediate failed assertion, but
headed launch alone did not establish visibility restoration. No window manager
was started; openbox and xdotool were unavailable. The unchanged minimise sample
is adverse evidence against this window mechanism, not proof that the browser
actually minimised. Do not repeat this bare-Xvfb arrangement for favourable samples.
A subsequent experiment needs verified native window/tab state transitions,
not merely a larger timeout or successful CDP acknowledgements. No Pulse fixture,
installed convergence, destination receipt or release qualification was exercised.
Complete output is retained in maintainer run `20260908T065015Z-web-product`,
`headed-window-control.log`. Earlier failed controls remain relevant.
@@ -6,10 +6,12 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createRequire } from 'node:module';
import { chromium } from '@playwright/test';
const checkForeground = process.argv.includes('--foreground');
const headedWindow = process.argv.includes('--headed-window');
const checkForeground = headedWindow || process.argv.includes('--foreground');
if (headedWindow && !process.env.DISPLAY) throw new Error('--headed-window requires an owned X display');
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
const profile = await mkdtemp(join(tmpdir(), 'pulse-lifecycle-'));
const child = spawn(chromium.executablePath(), ['--headless', '--no-sandbox',
const child = spawn(chromium.executablePath(), [...(headedWindow ? [] : ['--headless']), '--no-sandbox',
'--remote-debugging-port=0', `--user-data-dir=${profile}`, 'about:blank']);
let socket;
try {
@@ -47,14 +49,22 @@ try {
const require = createRequire(import.meta.url);
console.log(JSON.stringify({ browser: await send('Browser.getVersion'),
playwright: require('@playwright/test/package.json').version, node: process.version,
platform: process.platform, arch: process.arch, executable: chromium.executablePath() }));
platform: process.platform, arch: process.arch, executable: chromium.executablePath(),
headedWindow, display: process.env.DISPLAY, observationBoundMs: headedWindow ? 10000 : 250 }));
const results = [];
// Preselected negative (focus forced) and positive (no forced focus), fresh targets.
for (const focusEnabled of [true, false]) {
for (const focusEnabled of (headedWindow ? [false] : [true, false])) {
const { targetId } = await send('Target.createTarget', { url: 'about:blank' });
try {
const { sessionId } = await send('Target.attachToTarget', { targetId, flatten: true });
const commands = [];
const windowCommand = async (method, params) => {
const response = await send(method, params);
commands.push({ method, params, response });
return response;
};
const windowInfo = headedWindow
? await windowCommand('Browser.getWindowForTarget', { targetId }) : null;
const command = async (method, params) => {
const result = await send(method, params, sessionId);
commands.push({ method, params, response: result });
@@ -75,6 +85,12 @@ try {
const snapshot = `({...window.probe, visibility: document.visibilityState, focus: document.hasFocus()})`;
await wait(250);
const before = await evaluate(snapshot);
if (headedWindow) {
assert.ok(before.visibility === 'visible' && before.focus, 'Headed baseline must be visible and focused');
await windowCommand('Browser.setWindowBounds', { windowId: windowInfo.windowId, bounds: { windowState: 'minimized' } });
await wait(250);
console.log(JSON.stringify({ stage: 'minimized', snapshot: await evaluate(snapshot) }));
}
await command('Page.setWebLifecycleState', { state: 'frozen' });
await wait(2100); // Host wait: never evaluate while frozen.
await command('Page.setWebLifecycleState', { state: 'active' });
@@ -83,25 +99,40 @@ try {
const freeze = after.events.find(e => e.type === 'freeze');
const resume = after.events.find(e => e.type === 'resume');
const suspended = Boolean(freeze && resume && freeze.ticks === resume.ticks &&
after.events.indexOf(freeze) < after.events.indexOf(resume) && after.ticks > resume.ticks);
after.events.indexOf(freeze) < after.events.indexOf(resume) && (headedWindow || after.ticks > resume.ticks));
// Resume is not foreground activation. Observe the two transitions separately.
let foreground;
if (checkForeground) {
if (headedWindow) {
await windowCommand('Browser.setWindowBounds', { windowId: windowInfo.windowId, bounds: { windowState: 'normal' } });
}
await command('Page.bringToFront', {});
await wait(250);
foreground = await evaluate(snapshot);
const deadline = Date.now() + (headedWindow ? 10000 : 250);
do {
await wait(250);
foreground = await evaluate(snapshot);
if (headedWindow) console.log(JSON.stringify({ stage: 'foreground-observation', snapshot: foreground }));
if (foreground.visibility === 'visible' && foreground.focus && foreground.ticks > after.ticks) break;
} while (Date.now() < deadline);
}
const result = { focusEnabled, before, after, foreground, commands, suspended };
results.push(result); console.log(JSON.stringify(result));
} finally { await send('Target.closeTarget', { targetId }); }
}
assert.ok(!results[0].suspended && results[0].after.ticks - results[0].before.ticks >= 20,
if (!headedWindow) assert.ok(!results[0].suspended && results[0].after.ticks - results[0].before.ticks >= 20,
'Negative control must continue ticking');
assert.ok(results[1].suspended, 'Positive control must suspend and resume timers');
const positive = results.at(-1);
assert.ok(positive.suspended, 'Positive control must suspend and resume timers');
if (headedWindow) {
const events = positive.foreground.events;
const resumeIndex = events.findIndex(e => e.type === 'resume');
assert.ok(events.some((e, i) => i > resumeIndex && e.type === 'visibilitychange' && e.visibility === 'visible'),
'Must observe visible transition after resume');
}
if (checkForeground) {
assert.ok(results[1].foreground.visibility === 'visible' && results[1].foreground.focus,
assert.ok(positive.foreground.visibility === 'visible' && positive.foreground.focus,
'Positive control must return to visible and focused after tab activation');
assert.ok(results[1].foreground.ticks > results[1].after.ticks,
assert.ok(positive.foreground.ticks > positive.after.ticks,
'Foreground timer must continue advancing');
}
} finally {