blob: f42e91794b6c449892d663a834897e4f0fd500ba [file]
/**
* The `screen_locked` producer.
*
* `screen_locked` existed as an outcome code, a session status, a block reason
* and a `screenUnlocked` release long before anything could ever produce it, so
* locking the machine mid-run did not stop Computer Use from driving it. These
* tests are written against the tool surface rather than against a lock monitor:
* a monitor that answers correctly and is never asked is exactly the defect.
*/
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import {
buildComputerUseTools,
type CuDispatchBackend,
type CuObservation,
} from '../computer-use-tools.js';
import type { MakaToolContext } from '../tool-runtime.js';
function ctx(sessionId = 's1'): MakaToolContext {
return {
sessionId,
turnId: 't1',
cwd: '/tmp',
toolCallId: 'call1',
abortSignal: new AbortController().signal,
emitOutput: () => {},
};
}
function observation(): CuObservation {
return {
observationId: 'backend-obs-1',
appId: 'Fixture',
pid: 42,
windowId: 7,
contentFingerprint: 'ax-structure-1',
elements: [
{
elementId: '5',
role: 'AXButton',
label: 'Continue',
identity: { token: 'button-token', role: 'AXButton', label: 'Continue' },
},
],
} as CuObservation;
}
/** Records everything the backend was ever asked to do. */
function recordingBackend(): CuDispatchBackend & { calls: string[] } {
const calls: string[] = [];
return {
calls,
async preflight() {
calls.push('preflight');
return { accessibility: true, screenRecording: true };
},
async observeApp() {
calls.push('observeApp');
return observation();
},
async run(action) {
calls.push(`run:${action.type}`);
return { outcome: { ok: true, tier: 'ax', verified: true } };
},
};
}
describe('screen-lock gate', () => {
test('refuses every call while the machine is locked, and asks before touching the backend', async () => {
const backend = recordingBackend();
const [tool] = buildComputerUseTools({
backend,
screenLocked: () => true,
});
for (const action of ['observe', 'list_apps', 'screenshot'] as const) {
const result = (await tool.impl(
{ action, ...(action === 'list_apps' ? {} : { app: 'Fixture' }) } as never,
ctx(),
)) as { text: string; error?: string };
assert.match(result.text, /screen_locked/, `${action} must refuse while locked`);
assert.equal(result.error, 'screen_locked');
}
// Not "the backend refused" — the backend was never reached. A guard that
// lets the driver look at a locked machine first has already lost.
assert.deepEqual(backend.calls, []);
});
test('is consulted per call, so locking mid-run stops the run', async () => {
const backend = recordingBackend();
let locked = false;
const [tool] = buildComputerUseTools({
backend,
screenLocked: () => locked,
});
const observed = (await tool.impl({ action: 'observe', app: 'Fixture' } as never, ctx())) as {
text: string;
};
assert.doesNotMatch(observed.text, /screen_locked/);
assert.ok(backend.calls.includes('observeApp'));
locked = true;
const refused = (await tool.impl({ action: 'observe', app: 'Fixture' } as never, ctx())) as {
text: string;
};
assert.match(refused.text, /screen_locked/);
});
test('latches the session state, so the refusal survives the machine unlocking itself', async () => {
const backend = recordingBackend();
let locked = true;
const tools = buildComputerUseTools({
backend,
screenLocked: () => locked,
});
const [tool] = tools;
await tool.impl({ action: 'observe', app: 'Fixture' } as never, ctx());
assert.equal(tools.sessionEvents.snapshot('s1').status, 'screen_locked');
// The lock is gone but the state is not: whatever the agent last saw is
// from before the machine was locked, and only an explicit release moves it.
locked = false;
const stillRefused = (await tool.impl(
{ action: 'observe', app: 'Fixture' } as never,
ctx(),
)) as { text: string };
assert.match(stillRefused.text, /screen_locked/);
tools.sessionEvents.screenUnlocked('s1');
assert.equal(tools.sessionEvents.snapshot('s1').status, 'reobserve_required');
const allowed = (await tool.impl({ action: 'observe', app: 'Fixture' } as never, ctx())) as {
text: string;
};
assert.doesNotMatch(allowed.text, /screen_locked/);
});
test('tells the host which session it refused, so the host can release it later', async () => {
const backend = recordingBackend();
const asked: string[] = [];
const [tool] = buildComputerUseTools({
backend,
screenLocked: ({ sessionId }) => {
asked.push(sessionId);
return true;
},
});
await tool.impl({ action: 'observe', app: 'Fixture' } as never, ctx('session-A'));
// Without the session, a run whose FIRST call landed on a locked screen
// would latch `screen_locked` while being unknown to the guard that owns
// `screenUnlocked` — and would stay latched for the rest of its life.
assert.deepEqual(asked, ['session-A']);
});
test('a refusal never reaches the overlay hook, so the probe is the only witness', async () => {
// The desktop's screen-lock wrapper is documented as covering the refused
// attempt too, "since onActionBegin runs before the backend sees the
// action". It does not. `onActionBegin` has one call site, inside
// `runWithPresentation`, and the refusal returns hundreds of lines above
// it — so the probe callback is the only thing that sees a refused
// session, and the wrapper's job is the other producer entirely: a
// dispatch the probe let through that the executor then refuses.
//
// Pinned here rather than left to the comment, because the two are
// indistinguishable from the desktop side: both end with the session in
// the guard's set, and only the order says which one put it there.
const backend = recordingBackend();
const began: string[] = [];
const ended: string[] = [];
const asked: string[] = [];
const [tool] = buildComputerUseTools({
backend,
overlay: {
onActionBegin: (_action, context) => {
began.push(context.sessionId);
},
onActionEnd: (_action, _result, context) => {
ended.push(context.sessionId);
},
},
screenLocked: ({ sessionId }) => {
asked.push(sessionId);
return true;
},
});
for (const args of [
{ action: 'observe', app: 'Fixture' },
{ action: 'left_click', coordinate: [10, 10], observation_id: 'obs-1' },
{ action: 'click_element', element_id: '5', observation_id: 'obs-1' },
]) {
const result = (await tool.impl(args as never, ctx('session-A'))) as { text: string };
assert.match(result.text, /screen_locked/, `${args.action} must be refused`);
}
assert.deepEqual(asked, ['session-A', 'session-A', 'session-A'], 'the probe saw every call');
assert.deepEqual(began, [], 'and the overlay hook saw none of them');
assert.deepEqual(ended, []);
assert.deepEqual(backend.calls, [], 'nor did the backend');
});
test('without a probe there is no guard, rather than a silent refusal', async () => {
const backend = recordingBackend();
const [tool] = buildComputerUseTools({ backend });
const result = (await tool.impl({ action: 'observe', app: 'Fixture' } as never, ctx())) as {
text: string;
};
assert.doesNotMatch(result.text, /screen_locked/);
});
});