Files
paseo/packages/app/e2e/browser/provider-subagents.real.spec.ts
T
Mohamed BoudraandGitHub f0eb7cea4b feat(providers): add native options and exact MCP grants (#3025)
* feat(providers): add native options and exact MCP grants

Keep provider policy in provider-owned schemas so unattended Hub runs can grant only requested tools from injected MCP servers. Preserve the native resolved sandbox policy when a workflow override is present, including configured writable roots.

* test(hub): verify real provider tool policy

* test(app): avoid pinning transient restart status
2026-08-08 16:49:29 +02:00

146 lines
5.9 KiB
TypeScript

import { mkdtempSync, realpathSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { test, expect } from "../support/fixtures";
import {
cleanupRewindFlow,
launchAgent,
sendMessage,
type AgentHandle,
type RewindFlowProvider,
} from "../support/helpers/rewind-flow";
import { openSubagentsTrack } from "../support/helpers/subagents";
interface ProviderSubagentCase {
provider: RewindFlowProvider;
sentinel: string;
expectedName: string;
expectedSubtitle?: RegExp;
expectsUserMessage?: boolean;
prompt: string;
providerConfig?: Parameters<typeof launchAgent>[0]["providerConfig"];
}
const cases: ProviderSubagentCase[] = [
{
provider: "claude",
sentinel: "CLAUDE_CHILD_SENTINEL",
expectedName: "sentinel_child",
providerConfig: { model: "opus" },
prompt:
'Use Claude Code\'s native Task tool exactly once. Set its subagent_type input to "Explore" and its name input to "sentinel_child". Ask it to reply with exactly CLAUDE_CHILD_SENTINEL and do nothing else. Wait for it, then reply ROOT_DONE. Do not use Paseo tools.',
},
{
provider: "codex",
sentinel: "CODEX_CHILD_SENTINEL",
expectedName: "Sentinel child",
providerConfig: { providerOptions: { features: { multi_agent_v2: true } } },
prompt:
'Use the native collaboration.spawn_agent tool exactly once with task_name "sentinel_child" and fork_turns "none". Ask it to reply with exactly CODEX_CHILD_SENTINEL and do nothing else. Wait for it with collaboration.wait_agent, then reply ROOT_DONE. Do not use Paseo tools.',
},
{
provider: "opencode",
sentinel: "OPENCODE_CHILD_SENTINEL",
expectedName: "Verify OpenCode descriptor",
expectedSubtitle: /explore · gpt-5\.4(?: · [^\n·]+)? · \d+(?:\.\d+)?k? tokens/i,
expectsUserMessage: false,
providerConfig: { model: "openai/gpt-5.4" },
prompt:
'Use the task tool exactly once with description "Verify OpenCode descriptor" and the explore subagent. Ask it to reply with exactly OPENCODE_CHILD_SENTINEL and do nothing else. Wait for it, then reply ROOT_DONE.',
},
];
test.describe("real provider subagent timelines", () => {
test.setTimeout(600_000);
for (const scenario of cases) {
test(`${scenario.provider} exposes native child output from the subagent track`, async ({
page,
}, testInfo) => {
const cwd = realpathSync(
mkdtempSync(path.join(tmpdir(), `paseo-provider-subagent-${scenario.provider}-`)),
);
let handle: AgentHandle | undefined;
try {
handle = await launchAgent({
page,
provider: scenario.provider,
cwd,
mode: "full-access",
providerConfig: scenario.providerConfig,
});
await sendMessage(handle, scenario.prompt);
await openSubagentsTrack(page);
const rows = page.locator('[data-testid^="subagents-track-row-"]');
await expect(rows).toHaveCount(1, { timeout: 60_000 });
await expect(rows.first()).toContainText(scenario.expectedName);
if (scenario.expectedSubtitle) {
await expect(rows.first()).toContainText(scenario.expectedSubtitle);
const desktopTrackScreenshot = testInfo.outputPath(
`${scenario.provider}-subagent-track-desktop.png`,
);
await page.screenshot({ path: desktopTrackScreenshot });
await testInfo.attach(`${scenario.provider} subagent track desktop`, {
path: desktopTrackScreenshot,
contentType: "image/png",
});
}
await rows.first().click();
const panel = page.getByTestId("provider-subagent-panel");
await expect(panel).toBeVisible({ timeout: 30_000 });
if (scenario.expectedSubtitle) {
await expect(page.getByTestId("provider-subagent-pane-subtitle")).toHaveText(
scenario.expectedSubtitle,
);
const desktopScreenshot = testInfo.outputPath(
`${scenario.provider}-subagent-desktop.png`,
);
await page.screenshot({ path: desktopScreenshot });
await testInfo.attach(`${scenario.provider} subagent desktop`, {
path: desktopScreenshot,
contentType: "image/png",
});
await page.setViewportSize({ width: 494, height: 862 });
await expect(page.getByTestId("provider-subagent-pane-subtitle")).toHaveText(
scenario.expectedSubtitle,
);
const compactScreenshot = testInfo.outputPath(
`${scenario.provider}-subagent-compact.png`,
);
await page.screenshot({ path: compactScreenshot });
await testInfo.attach(`${scenario.provider} subagent compact`, {
path: compactScreenshot,
contentType: "image/png",
});
await page.setViewportSize({ width: 1280, height: 720 });
}
if (scenario.expectsUserMessage !== false) {
await expect(
panel.getByTestId("user-message").filter({ hasText: scenario.sentinel }),
).toBeVisible({ timeout: 30_000 });
}
await expect(
panel.getByTestId("assistant-message").filter({ hasText: scenario.sentinel }),
).toBeVisible({ timeout: 30_000 });
await expect(
panel.getByText("Start chatting with this agent...", { exact: true }),
).toHaveCount(0);
await page.getByTestId(`workspace-tab-agent_${handle.agentId}`).first().click();
await expect(
page.getByTestId("assistant-message").filter({ hasText: "ROOT_DONE" }).last(),
).toBeVisible({ timeout: 60_000 });
const archiveFinished = page.getByTestId("subagents-track-archive-finished");
await expect(archiveFinished).toBeVisible({ timeout: 30_000 });
await archiveFinished.click();
await expect(rows).toHaveCount(0, { timeout: 30_000 });
} finally {
await cleanupRewindFlow({ handle, cwd });
}
});
}
});