| 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; |
|
|
| |
| 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"); |
|
|
| |
| 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(); |
|
|
| |
| await drawer.getByTestId("skill-run-tool-button").click(); |
|
|
| |
| const panel = page.getByTestId("tool-run-panel"); |
| await expect(panel).toBeVisible(); |
|
|
| |
| await expect(panel.getByTestId("estimate-cost")).toContainText("$0.12"); |
|
|
| |
| const fileInput = panel.getByTestId("tool-run-file-input"); |
| await fileInput.setInputFiles({ |
| name: "test.fasta", |
| mimeType: "text/plain", |
| buffer: Buffer.from(">seq1\nACGT"), |
| }); |
|
|
| |
| await panel.getByTestId("tool-run-button").click(); |
|
|
| |
| await expect(panel.getByTestId("tool-run-polling")).toBeVisible(); |
|
|
| |
| await expect(panel.getByTestId("tool-run-completed")).toBeVisible({ |
| timeout: 15_000, |
| }); |
|
|
| |
| 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"); |
|
|
| |
| 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(); |
|
|
| |
| 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(); |
|
|
| |
| await expect(panel.getByTestId("tool-run-error")).toBeVisible({ |
| timeout: 15_000, |
| }); |
| await expect(panel.getByTestId("tool-run-error")).toContainText( |
| "Out of GPU memory", |
| ); |
| }); |
|
|