From 8fa0baf41cbc641789170070e1de53a33e3cc38d Mon Sep 17 00:00:00 2001 From: pulk17 Date: Mon, 14 Sep 2026 12:59:34 +0530 Subject: [PATCH] Web-Tests The regression suite: browse and filter tests, see per-platform history and baselines, and define a new test against a sample. --- web/src/pages/TestBuilder.tsx | 411 ++++++++++++++++++++++ web/src/pages/Workspace.tsx | 643 ++++++++++++++++++++++++++++++++++ 2 files changed, 1054 insertions(+) create mode 100644 web/src/pages/TestBuilder.tsx create mode 100644 web/src/pages/Workspace.tsx diff --git a/web/src/pages/TestBuilder.tsx b/web/src/pages/TestBuilder.tsx new file mode 100644 index 000000000..72b210947 --- /dev/null +++ b/web/src/pages/TestBuilder.tsx @@ -0,0 +1,411 @@ +import { Link, useNavigate } from "@tanstack/react-router"; +import { + ArrowLeft, + ArrowRight, + Check, + CheckCircle2, + ChevronLeft, + FileVideo, + FlaskConical, + Loader2, + Play, + Sparkles, +} from "lucide-react"; +import { motion } from "motion/react"; +import { useState } from "react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input, Textarea } from "@/components/ui/input"; +import { useQueryClient } from "@tanstack/react-query"; + +import { + commandPresets, + createRegressionTest, + deriveCategories, + useRegressionTests, + useSamples, +} from "@/lib/api"; +import { COMMAND_MAX } from "@/lib/validate"; +import type { Sample } from "@/lib/types"; +import { cn } from "@/lib/utils"; + +const STEPS = ["Sample", "Command", "Describe"] as const; + +/** + * Guided test creation. The test is stored as inactive ("draft") — the + * follow-up is a real verification run on CI (offered on the success + * screen), and activation happens from the test detail once a baseline + * exists. No step here pretends to run anything. + */ +export function TestBuilder() { + const [step, setStep] = useState(0); + const [sample, setSample] = useState(null); + const [command, setCommand] = useState(""); + const [expectedRc, setExpectedRc] = useState(0); + const [description, setDescription] = useState(""); + const [selectedCats, setSelectedCats] = useState([]); + const [saved, setSaved] = useState(null); + const [saving, setSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + const navigate = useNavigate(); + const qc = useQueryClient(); + + const create = async () => { + if (!sample) return; + setSaving(true); + setSaveError(null); + try { + const res = await createRegressionTest({ + sample_id: sample.id, + command, + expected_rc: expectedRc, + description, + categories: selectedCats, + }); + await qc.invalidateQueries({ queryKey: ["regression-tests"] }); + setSaved(res.regression_test_id); + } catch (e) { + setSaveError(e instanceof Error ? e.message : "Create failed"); + } finally { + setSaving(false); + } + }; + + const canNext = + (step === 0 && !!sample) || + (step === 1 && command.trim().length > 0) || + step === 2; + + if (saved !== null) { + return ( +
+ + + +

+ Test #{saved} created as a draft +

+

+ It stays inactive until its output is verified and a baseline is + recorded. Queue a run scoped to this test to see what it produces on + both platforms. +

+
+ + + +
+
+ ); + } + + return ( +
+
+ + + +
+

+ New regression test +

+

+ Define the test, verify its output on a run, then activate it. +

+
+
+ + {/* Stepper */} +
    + {STEPS.map((label, i) => ( +
  1. + + {i < STEPS.length - 1 &&
    } +
  2. + ))} +
+ + + {step === 0 && } + {step === 1 && sample && ( + + )} + {step === 2 && ( + + )} + + +
+ + {step < STEPS.length - 1 ? ( + + ) : ( +
+ {saveError && {saveError}} + +
+ )} +
+
+ ); +} + +/* Step 1 — sample picker with context card */ +function StepSample({ selected, onSelect }: Readonly<{ selected: Sample | null; onSelect: (s: Sample) => void }>) { + const [q, setQ] = useState(""); + const { data: allSamples = [] } = useSamples(); + const { data: tests = [] } = useRegressionTests(); + const filtered = allSamples + .filter((s) => + `${s.original_name} ${s.tags.join(" ")} ${s.extension}`.toLowerCase().includes(q.toLowerCase()), + ) + .slice(0, 40); + const existing = selected ? tests.filter((t) => t.sample_id === selected.id) : []; + + return ( +
+
+ setQ(e.target.value)} + className="mb-2" + /> +
+ {filtered.map((s) => ( + + ))} +
+
+
+ {selected ? ( +
+
{selected.original_name}
+
+
Extension
+
{selected.extension}
+
SHA
+
{selected.sha.slice(0, 18)}…
+
Tags
+
{selected.tags.length ? selected.tags.join(", ") : "—"}
+
+ {existing.length > 0 && ( +
+
+ Existing tests on this sample +
+ {existing.slice(0, 4).map((t) => ( +
+ #{t.id} {t.command} +
+ ))} +
+ )} +
+ ) : ( +
+ Select a sample to see its context and existing tests +
+ )} +
+
+ ); +} + +/* Step 2 — command composer, presets mined from the real suite */ +function StepCommand({ + sample, command, setCommand, expectedRc, setExpectedRc, +}: Readonly<{ + sample: Sample; + command: string; + setCommand: (c: string) => void; + expectedRc: number; + setExpectedRc: (n: number) => void; +}>) { + const { data: tests = [] } = useRegressionTests(); + const presets = commandPresets(tests, 6); + return ( +
+
+
+ Common commands in the suite +
+
+ {presets.map((p) => ( + + ))} +
+
+
+
+ Command +
+
+
+ ccextractor {sample.original_name}{sample.extension} … +
+