import { test, expect } from "@playwright/test"; const FAKE_SKILLS = [ { endpoint_name: "mafft", display_name: "MAFFT", description: "Multiple sequence alignment.", category: "alignment", version: "7.5", author: "Hub", compute: { gpu: false, typical_runtime_seconds: 30 }, }, ]; const FAKE_DETAIL = { endpoint_name: "mafft", display_name: "MAFFT", description: "Multiple sequence alignment.", category: "alignment", license: "BSD", compute: { gpu: false, typical_runtime_seconds: 30 }, parameters: { type: "object", properties: { input: { type: "string", description: "Input FASTA." }, }, required: ["input"], }, }; const FAKE_ESTIMATE = { estimated_usd: 0.12, gpu_type: "CPU", minutes_used: 0.5, cost_alert: false, compute_time_raw: "30s", }; const FAKE_JOB_SUBMIT = { campaign_job_id: "job_123", hub_task_execution_id: "hub_456", provider: "runpod", hub_response: {}, }; const FAKE_JOB_POLLING = { campaign_job_id: "job_123", campaign_id: "debug-campaign", role: "run", submitted_by: "dev@test", hub_task_execution_id: "hub_456", hub: { status: "running" }, artifacts: [], }; const FAKE_JOB_COMPLETED = { ...FAKE_JOB_POLLING, hub: { status: "completed", output_data: {} }, artifacts: [ { id: "art_001", hub_output_key: "alignment.fasta", phylo_tags: [], scientist_note: null, }, ], }; test("Tool invocation: drawer Run Tool -> configure -> submit -> poll -> complete", async ({ page, }) => { let pollCount = 0; // Stub API routes await page.route("**/api/skills", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ skills: FAKE_SKILLS.map((t) => ({ id: t.endpoint_name, name: t.display_name, description: t.description, version: t.version, author: t.author, inputs: {}, outputs: {}, category: t.category, gpu: t.compute.gpu, })), }), }); }); await page.route("**/api/skills/mafft", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(FAKE_DETAIL), }); }); await page.route("**/api/tools/mafft/estimate", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(FAKE_ESTIMATE), }); }); await page.route("**/api/jobs/submit", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(FAKE_JOB_SUBMIT), }); }); await page.route("**/api/jobs/job_123", async (route) => { pollCount++; const body = pollCount >= 2 ? FAKE_JOB_COMPLETED : FAKE_JOB_POLLING; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(body), }); }); await page.goto("/debug/skills"); // Open detail drawer await expect(page.getByText("MAFFT")).toBeVisible(); await page .getByTestId("skill-row") .first() .getByRole("button", { name: /Inspect MAFFT/ }) .click(); const drawer = page.getByTestId("skill-detail-drawer"); await expect(drawer).toBeVisible(); // Click "Run Tool" button in the drawer await drawer.getByTestId("skill-run-tool-button").click(); // ToolRunPanel should appear const panel = page.getByTestId("tool-run-panel"); await expect(panel).toBeVisible(); // Cost estimate should load await expect(panel.getByTestId("estimate-cost")).toContainText("$0.12"); // Upload a fake file const fileInput = panel.getByTestId("tool-run-file-input"); await fileInput.setInputFiles({ name: "test.fasta", mimeType: "text/plain", buffer: Buffer.from(">seq1\nACGT"), }); // Click Run await panel.getByTestId("tool-run-button").click(); // Should show polling state await expect(panel.getByTestId("tool-run-polling")).toBeVisible(); // Wait for completion (poll returns "completed" on 2nd call) await expect(panel.getByTestId("tool-run-completed")).toBeVisible({ timeout: 15_000, }); // Artifact name should be visible await expect(panel.getByText("alignment.fasta")).toBeVisible(); }); test("Tool invocation: shows error on failed job", async ({ page }) => { await page.route("**/api/skills", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ skills: FAKE_SKILLS.map((t) => ({ id: t.endpoint_name, name: t.display_name, description: t.description, version: t.version, author: t.author, inputs: {}, outputs: {}, category: t.category, gpu: t.compute.gpu, })), }), }); }); await page.route("**/api/skills/mafft", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(FAKE_DETAIL), }); }); await page.route("**/api/tools/mafft/estimate", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(FAKE_ESTIMATE), }); }); await page.route("**/api/jobs/submit", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(FAKE_JOB_SUBMIT), }); }); await page.route("**/api/jobs/job_123", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ...FAKE_JOB_POLLING, hub: { status: "failed", error: "Out of GPU memory" }, }), }); }); await page.goto("/debug/skills"); // Open drawer, click Run Tool await page .getByTestId("skill-row") .first() .getByRole("button", { name: /Inspect MAFFT/ }) .click(); await page.getByTestId("skill-detail-drawer").getByTestId("skill-run-tool-button").click(); const panel = page.getByTestId("tool-run-panel"); await expect(panel).toBeVisible(); // Upload file and run await panel.getByTestId("tool-run-file-input").setInputFiles({ name: "test.fasta", mimeType: "text/plain", buffer: Buffer.from(">seq1\nACGT"), }); await panel.getByTestId("tool-run-button").click(); // Should show error await expect(panel.getByTestId("tool-run-error")).toBeVisible({ timeout: 15_000, }); await expect(panel.getByTestId("tool-run-error")).toContainText( "Out of GPU memory", ); });