@@ -4162,6 +4162,48 @@ test('cloud run controller uses the visible tab and persists terminal status', a
41624162 assert.equal(session.webbrainCloudRunSnapshots[0].status, 'completed');
41634163});
41644164
4165+ test('cloud run controller fails immediately if an interactive plan review leaks through', async () => {
4166+ const session = {};
4167+ const tab = { id: 19, url: 'https://webbrain.one/', active: true, windowId: 3 };
4168+ let abortedTabId = null;
4169+ const agent = {
4170+ isRunning: () => false,
4171+ abort: tabId => { abortedTabId = tabId; },
4172+ processMessage: async (_tabId, _task, onUpdate) => {
4173+ onUpdate('plan_review', { planId: 'plan_unexpected' });
4174+ return '[Stopped by user]';
4175+ },
4176+ };
4177+ const controller = createCloudRunController({
4178+ chromeApi: {
4179+ tabs: {
4180+ query: async () => [tab],
4181+ get: async () => tab,
4182+ update: async () => tab,
4183+ },
4184+ windows: { update: async () => ({}) },
4185+ storage: {
4186+ local: { get: async () => ({ webbrainCloudBridgeEnabled: false }) },
4187+ session: {
4188+ get: async key => ({ [key]: session[key] || [] }),
4189+ set: async value => Object.assign(session, value),
4190+ },
4191+ },
4192+ runtime: { sendMessage: async () => ({ connected: false }) },
4193+ },
4194+ agent,
4195+ ensureOffscreen: async () => {},
4196+ makeRunId: () => 'run_plan_review',
4197+ });
4198+
4199+ await controller.startRun({ task: 'Complex unattended task' });
4200+ await new Promise(resolve => setTimeout(resolve, 0));
4201+ const completed = await controller.status({ run_id: 'run_plan_review' });
4202+ assert.equal(completed.status, 'failed');
4203+ assert.equal(completed.error, 'Managed cloud runs cannot wait for interactive plan review.');
4204+ assert.equal(abortedTabId, 19);
4205+ });
4206+
41654207test('cloud run controller fails interrupted runs after service-worker restart', async () => {
41664208 const row = { runId: 'run_old', status: 'running', tabId: 2, task: 'Old task', updates: [], createdAt: '2020-01-01T00:00:00.000Z' };
41674209 const session = { webbrainCloudRunSnapshots: [row] };
@@ -21279,6 +21321,45 @@ test('planner gate: scheduled runs auto-approve plan review', async () => {
2127921321 });
2128021322});
2128121323
21324+ test('planner gate: managed cloud runs bypass planning in Chrome and Firefox', async () => {
21325+ await withPlannerBrowserGlobals(async () => {
21326+ for (const [label, AgentClass] of [['chrome', AgentCh], ['firefox', AgentFx]]) {
21327+ const tabId = label === 'chrome' ? 9223 : 9224;
21328+ const agent = new AgentClass({ getActive: () => ({}) });
21329+ agent.setPlanBeforeActMode('strict');
21330+ agent.setPlanReviewSettings({ mode: 'always', confidenceThreshold: 0.99 });
21331+ agent.conversations.set(tabId, [{ role: 'system', content: 'system' }]);
21332+ let plannerCalls = 0;
21333+ agent._chatWithCostAllowance = async () => {
21334+ plannerCalls += 1;
21335+ return { content: plannerFixtureJson() };
21336+ };
21337+ agent._waitForPlanReview = async () => {
21338+ throw new Error('managed cloud run should never request interactive review');
21339+ };
21340+
21341+ const messages = agent.conversations.get(tabId);
21342+ const updates = [];
21343+ const outcome = await agent._maybeRunPlannerGate(
21344+ tabId,
21345+ messages,
21346+ { role: 'user', content: 'complex cloud task' },
21347+ type => updates.push(type),
21348+ 'act',
21349+ null,
21350+ null,
21351+ null,
21352+ { cloudRun: true },
21353+ );
21354+
21355+ assert.equal(outcome.proceed, true, `${label} cloud run should proceed without planning`);
21356+ assert.equal(plannerCalls, 0, `${label} cloud run should not call the planner model`);
21357+ assert.equal(updates.includes('plan_review'), false, `${label} cloud run should not emit plan_review`);
21358+ assert.equal(messages.at(-1)?.content, 'complex cloud task', `${label} should retain the user turn`);
21359+ }
21360+ });
21361+ });
21362+
2128221363test('sidepanel: restored plan review cards rebind approve and cancel actions', () => {
2128321364 for (const file of [
2128421365 'src/chrome/src/ui/sidepanel.js',
0 commit comments