Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions deployed/lib/browser-liveview.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Server-rendered controls can be visible before LiveView installs its event
// handlers. Wait for the connection before filling or submitting those forms;
// Playwright's visibility/actionability checks do not establish hydration.
export async function waitForLiveView(page) {
await page.locator('[data-phx-main].phx-connected').waitFor({ state: 'attached' });
Comment thread
managoat-review-loop[bot] marked this conversation as resolved.
}
36 changes: 31 additions & 5 deletions deployed/profiles/browser-console.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
import { reserveBrowserFixture, adoptBrowserFixture } from '../lib/browser-fixtures.mjs';
import { ensure } from '../lib/execution.mjs';
import { browserCredentials } from './browser-credentials.mjs';
import { waitForLiveView } from '../lib/browser-liveview.mjs';

// Picks the pinned provider in the new-agent form. Returns whether an explicit
// selection was made; a hidden selector is only acceptable when the catalog
// enables exactly the pinned provider.
export async function selectBrowserProvider(provider, catalog, pinned) {
const enabledProviders = catalog.data.sandbox_providers.enabled;
ensure(enabledProviders.includes(pinned), 'Pinned browser provider is not enabled');
// Runtime/model validation can temporarily replace the form. A zero count
// during that patch is not evidence of a single-provider deployment.
const selected = enabledProviders.length > 1 || (await provider.count()) > 0;
if (selected) await provider.selectOption(pinned);
else ensure(JSON.stringify(enabledProviders) === JSON.stringify([pinned]), 'Hidden provider selector does not imply the pinned sole provider');
Comment thread
managoat-review-loop[bot] marked this conversation as resolved.
return selected;
}

// An explicit selection must persist as the stored value. Only a hidden
// sole-provider selector may rely on the instance default.
export function verifyBrowserProvider(agent, catalog, pinned, selected) {
const savedProvider = selected
? agent.sandbox_provider
: (agent.sandbox_provider ?? catalog.data.sandbox_providers.default);
ensure(savedProvider === pinned, 'UI agent sandbox provider did not persist');
}

// Called with a fresh browser context by the browser driver. All writes go
// through visible console forms. Public API reads independently establish
Expand Down Expand Up @@ -29,26 +53,26 @@ export async function browserConsole(ctx, page, evidence, { handoff } = {}) {
await evidence.step('console/create-agent', async () => {
const intent = reserveBrowserFixture(fixtures, 'agent');
await page.goto(`${config.base_url}/agents/new`);
await waitForLiveView(page);
await page.getByLabel('Name', { exact: true }).fill(intent.name);
await page.getByLabel('Description', { exact: true }).fill('Dedicated deployed browser fixture');
await page.getByLabel('System prompt', { exact: true }).fill('Perform only the requested small file task. Use a shell tool. Do not access the network or start background work.');
await page.locator('select[name="agent[runtime]"]').selectOption(settings.agent.runtime);
await page.getByLabel('Model', { exact: true }).fill(settings.agent.model);
const provider = page.locator('select[name="agent[sandbox_provider]"]');
if (await provider.count()) await provider.selectOption(settings.agent.sandbox_provider);
else {
const { body } = await client.request('GET', '/api/catalog', { expected: 200 });
ensure(JSON.stringify(body.data.sandbox_providers.enabled) === JSON.stringify([settings.agent.sandbox_provider]), 'Hidden provider selector does not imply the pinned sole provider');
}
const { body: catalog } = await client.request('GET', '/api/catalog', { expected: 200 });
const selectedProvider = await selectBrowserProvider(provider, catalog, settings.agent.sandbox_provider);
Comment thread
managoat-review-loop[bot] marked this conversation as resolved.
await page.getByRole('button', { name: 'Save', exact: true }).click();
await page.waitForURL(`${config.base_url}/agents`);
agent = await adoptBrowserFixture(fixtures, intent, ctx.signal);
ensure(agent.runtime === settings.agent.runtime && agent.model === settings.agent.model, 'UI agent runtime/model did not persist');
verifyBrowserProvider(agent, catalog, settings.agent.sandbox_provider, selectedProvider);
report.browser.console.agent_id = agent.id;
});

await evidence.step('console/edit-agent', async () => {
await page.goto(`${config.base_url}/agents/${agent.id}/edit`);
await waitForLiveView(page);
const description = `Browser edit verified for ${report.run_id}`;
await page.getByLabel('Description', { exact: true }).fill(description);
await page.getByRole('button', { name: 'Save', exact: true }).click();
Expand All @@ -61,6 +85,7 @@ export async function browserConsole(ctx, page, evidence, { handoff } = {}) {
await evidence.step('console/api-key-lifecycle', async () => {
const intent = reserveBrowserFixture(fixtures, 'api_key');
await page.goto(`${config.base_url}/api-keys`);
await waitForLiveView(page);
await page.getByLabel('Key label', { exact: true }).fill(intent.name);
await page.getByRole('button', { name: 'Create key', exact: true }).click();
await page.getByText('New API key created', { exact: true }).waitFor({ state: 'visible' });
Expand All @@ -73,6 +98,7 @@ export async function browserConsole(ctx, page, evidence, { handoff } = {}) {
ensure(me.body.id === report.owner_id, 'UI key authenticated as another account');
await page.getByRole('button', { name: "I've copied it, dismiss", exact: true }).click();
if (settings.conversations) await handoff(agent, key);
await waitForLiveView(page);
Comment thread
managoat-review-loop[bot] marked this conversation as resolved.
const row = page.getByRole('row').filter({ hasText: intent.name });
page.once('dialog', dialog => dialog.accept());
await row.getByRole('button', { name: 'Revoke', exact: true }).click();
Expand Down
3 changes: 3 additions & 0 deletions deployed/profiles/browser-credentials.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { credentialPath, credentialStatus, reserveCredential } from '../lib/browser-credentials.mjs';
import { ensure } from '../lib/execution.mjs';
import { waitForLiveView } from '../lib/browser-liveview.mjs';

export async function browserCredentials(ctx, page, evidence) {
const { config, fixtures, client, report, env, redactor, signal } = ctx;
Expand All @@ -10,6 +11,7 @@ export async function browserCredentials(ctx, page, evidence) {
await evidence.step('console/credential-validation', async () => {
const before = await client.request('GET', credentialPath, { expected: 200, recordBody: false });
await page.goto(`${config.base_url}/account/inference-credentials`);
await waitForLiveView(page);
ensure(await form().locator('input[name="value"]').getAttribute('type') === 'password', 'Credential field must hide its value');
await form().getByRole('button', { name: 'Save', exact: true }).click();
await page.getByText('Paste a value before saving.', { exact: true }).waitFor({ state: 'visible' });
Expand Down Expand Up @@ -37,6 +39,7 @@ export async function browserCredentials(ctx, page, evidence) {

return async () => evidence.step('console/credential-clear', async () => {
await page.goto(`${config.base_url}/account/inference-credentials`);
await waitForLiveView(page);
await form().getByRole('button', { name: 'Clear', exact: true }).click();
await page.getByText('Credential cleared.', { exact: true }).waitFor({ state: 'visible' });
ensure(!await credentialStatus(client, provider, signal), 'Cleared provider credential remains set');
Expand Down
38 changes: 38 additions & 0 deletions deployed/test/browser-console.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { selectBrowserProvider, verifyBrowserProvider } from '../profiles/browser-console.mjs';

const pinned = 'sprites';

// Drives the console create-agent provider steps against a fake selector:
// `visible` is whether the form currently renders the provider select,
// `stored` is what the public API reads back for the saved agent.
async function createAgent({ enabled, visible, stored, defaultProvider = pinned }) {
const selections = [];
const provider = { async count() { return visible ? 1 : 0; }, async selectOption(value) { selections.push(value); } };
const catalog = { data: { sandbox_providers: { enabled, default: defaultProvider } } };
const selected = await selectBrowserProvider(provider, catalog, pinned);
const agent = { sandbox_provider: stored };
verifyBrowserProvider(agent, catalog, pinned, selected);
return { selections, agent };
}

test('multiple providers require the explicit selection to persist even when it equals the default', async () => {
const scenario = { enabled: ['sprites', 'runner'], visible: true };
await assert.rejects(createAgent({ ...scenario, stored: null }), /UI agent sandbox provider did not persist/);
const result = await createAgent({ ...scenario, stored: 'sprites' });
assert.deepEqual(result.selections, ['sprites']);
assert.equal(result.agent.sandbox_provider, 'sprites');
});

test('a visible sole-provider selector also requires explicit persistence', async () => {
await assert.rejects(createAgent({ enabled: ['sprites'], visible: true, stored: null }), /UI agent sandbox provider did not persist/);
});

test('a hidden sole-provider selector permits the effective instance default', async () => {
const scenario = { enabled: ['sprites'], visible: false, stored: null };
const result = await createAgent(scenario);
assert.deepEqual(result.selections, []);
assert.equal(result.agent.sandbox_provider, null);
await assert.rejects(createAgent({ ...scenario, defaultProvider: 'runner' }), /UI agent sandbox provider did not persist/);
});
Loading