diff --git a/web/hwlab-cloud-web/package.json b/web/hwlab-cloud-web/package.json index 85c6b041..75fded24 100644 --- a/web/hwlab-cloud-web/package.json +++ b/web/hwlab-cloud-web/package.json @@ -6,6 +6,12 @@ "scripts": { "deps": "node ../../scripts/worktree-deps.mjs --web", "dev": "bun run deps --quiet && vite", + "dev:native-caserun": "CHOKIDAR_USEPOLLING=1 HWLAB_CASERUN_NATIVE_TEST=1 bun run dev", + "caserun:native": "bun scripts/caserun-native-supervisor.ts", + "caserun:native:start": "bun run scripts/caserun-native-service.ts start", + "caserun:native:stop": "bun run scripts/caserun-native-service.ts stop", + "caserun:native:status": "bun run scripts/caserun-native-service.ts status", + "caserun:native:logs": "bun run scripts/caserun-native-service.ts logs", "check": "bun run deps --quiet && bun run scripts/tsc-check.ts && bun test scripts", "check:tsc": "bun run deps --quiet && bun run scripts/tsc-check.ts", "check:tsc-strict": "bun run deps --quiet && bun run scripts/tsc-check.ts --strict", diff --git a/web/hwlab-cloud-web/scripts/caserun-native-fixtures.test.ts b/web/hwlab-cloud-web/scripts/caserun-native-fixtures.test.ts new file mode 100644 index 00000000..0872c743 --- /dev/null +++ b/web/hwlab-cloud-web/scripts/caserun-native-fixtures.test.ts @@ -0,0 +1,40 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { listNativeCases, readNativeEvents, readNativeRun, resetNativeFixtures, startNativeRun } from "./caserun-native-fixtures"; + +test("native CaseRun fixtures expose six deterministic API-owned states", () => { + resetNativeFixtures(); + const cases = listNativeCases(); + assert.equal(cases.mode, "native-test"); + assert.deepEqual(cases.cases.map((item) => item.caseId), ["native-queued", "native-running", "native-completed", "native-failed", "native-blocked", "native-canceled"]); + + const started = startNativeRun("native-completed"); + assert.equal(started?.status, "queued"); + assert.equal(started?.terminal, false); + const running = readNativeRun(started!.runId); + assert.equal(running?.status, "running"); + assert.equal(running?.terminal, false); + const completed = readNativeRun(started!.runId); + assert.equal(completed?.status, "completed"); + assert.equal(completed?.terminal, true); + assert.match(completed?.references.aggregate.sha256 ?? "", /^[a-f0-9]{64}$/); + + const events = readNativeEvents(started!.runId); + assert.deepEqual(events?.events.map((event) => event.status), ["queued", "running", "completed"]); + assert.equal(events?.mode, "native-test"); +}); + +test("native blockers and cancellation come from the API fixture", () => { + resetNativeFixtures(); + const blocked = startNativeRun("native-blocked")!; + const blockedTerminal = readNativeRun(blocked.runId)!; + assert.equal(blockedTerminal.terminal, true); + assert.equal(blockedTerminal.blocker?.code, "native-capability-blocked"); + + const canceled = startNativeRun("native-canceled")!; + readNativeRun(canceled.runId); + const canceledTerminal = readNativeRun(canceled.runId)!; + assert.equal(canceledTerminal.status, "canceled"); + assert.equal(canceledTerminal.terminal, true); +}); diff --git a/web/hwlab-cloud-web/scripts/caserun-native-fixtures.ts b/web/hwlab-cloud-web/scripts/caserun-native-fixtures.ts new file mode 100644 index 00000000..617708e6 --- /dev/null +++ b/web/hwlab-cloud-web/scripts/caserun-native-fixtures.ts @@ -0,0 +1,122 @@ +import { createHash } from "node:crypto"; + +type NativeStatus = "queued" | "running" | "completed" | "failed" | "blocked" | "canceled"; + +interface NativeStep { + status: NativeStatus; + stage: string; + terminal: boolean; + blocker?: { code: string; summary: string; layer: string }; +} + +interface NativeRunState { + runId: string; + caseId: string; + cursor: number; + steps: NativeStep[]; +} + +const MODE = "native-test"; +const CONTRACT_VERSION = "caserun.v1"; +const BASE_TIME = Date.parse("2026-07-17T00:00:00.000Z"); + +const scenarios: Record = { + "native-queued": { title: "Native queued", steps: [{ status: "queued", stage: "queued", terminal: false }] }, + "native-running": { title: "Native running", steps: [{ status: "queued", stage: "queued", terminal: false }, { status: "running", stage: "agent-running", terminal: false }] }, + "native-completed": { title: "Native completed", steps: [{ status: "queued", stage: "queued", terminal: false }, { status: "running", stage: "agent-running", terminal: false }, { status: "completed", stage: "completed", terminal: true }] }, + "native-failed": { title: "Native failed", steps: [{ status: "queued", stage: "queued", terminal: false }, { status: "running", stage: "building", terminal: false }, { status: "failed", stage: "failed", terminal: true, blocker: { code: "native-build-failed", summary: "Deterministic native build failure", layer: "build" } }] }, + "native-blocked": { title: "Native blocked", steps: [{ status: "queued", stage: "queued", terminal: false }, { status: "blocked", stage: "preparing", terminal: true, blocker: { code: "native-capability-blocked", summary: "Deterministic native capability blocker", layer: "hwpod" } }] }, + "native-canceled": { title: "Native canceled", steps: [{ status: "queued", stage: "queued", terminal: false }, { status: "running", stage: "agent-running", terminal: false }, { status: "canceled", stage: "canceled", terminal: true }] } +}; + +let runSequence = 0; +const runs = new Map(); + +function timestamp(index: number): string { + return new Date(BASE_TIME + index * 1000).toISOString(); +} + +function aggregateSha(runId: string, status: NativeStatus): string { + return createHash("sha256").update(`${MODE}:${runId}:${status}`).digest("hex"); +} + +function runResponse(run: NativeRunState) { + const step = run.steps[run.cursor] ?? run.steps.at(-1)!; + return { + ok: true, + mode: MODE, + contractVersion: CONTRACT_VERSION, + runId: run.runId, + caseId: run.caseId, + status: step.status, + stage: step.stage, + terminal: step.terminal, + blocker: step.blocker ?? null, + createdAt: timestamp(0), + updatedAt: timestamp(run.cursor), + facts: { + jobId: `job-${run.runId}`, + artifactCount: step.status === "completed" ? 2 : 0, + evidenceStatus: step.status + }, + references: { + status: { href: `/v1/caserun/runs/${run.runId}` }, + events: { href: `/v1/caserun/runs/${run.runId}/events`, count: run.cursor + 1 }, + aggregate: { href: `/v1/caserun/runs/${run.runId}/aggregate`, sha256: aggregateSha(run.runId, step.status) }, + manifest: step.status === "completed" ? { href: `/v1/caserun/runs/${run.runId}/manifest`, count: 2 } : null, + replay: null, + trace: { traceId: `trace-${run.runId}` }, + hwpod: { hwpodId: "native-hwpod", nodeId: "native-node" } + }, + sourceAuthority: { mode: MODE, sequence: "request-driven" }, + error: null, + statusUrl: `/v1/caserun/runs/${run.runId}` + }; +} + +export function listNativeCases() { + const cases = Object.entries(scenarios).map(([caseId, scenario]) => ({ + caseId, + title: scenario.title, + mode: MODE, + available: true, + hwpodSpec: "native-test", + runtime: { sequence: scenario.steps.map((step) => step.status) } + })); + return { ok: true, mode: MODE, contractVersion: CONTRACT_VERSION, count: cases.length, cases, caseAuthority: { mode: MODE } }; +} + +export function startNativeRun(caseId: string) { + const scenario = scenarios[caseId]; + if (!scenario) return null; + runSequence += 1; + const runId = `${caseId}-${runSequence}`; + const run: NativeRunState = { runId, caseId, cursor: 0, steps: scenario.steps }; + runs.set(runId, run); + return runResponse(run); +} + +export function readNativeRun(runId: string, advance = true) { + const run = runs.get(runId); + if (!run) return null; + if (advance && run.cursor < run.steps.length - 1) run.cursor += 1; + return runResponse(run); +} + +export function readNativeEvents(runId: string) { + const run = runs.get(runId); + if (!run) return null; + const current = runResponse(run); + const events = run.steps.slice(0, run.cursor + 1).map((step, index) => ({ + at: timestamp(index), + status: step.status, + stage: step.stage, + payload: { mode: MODE, terminal: step.terminal, blocker: step.blocker ?? null } + })); + return { ok: true, mode: MODE, contractVersion: CONTRACT_VERSION, runId, caseId: run.caseId, status: current.status, stage: current.stage, terminal: current.terminal, sourceAuthority: current.sourceAuthority, events }; +} + +export function resetNativeFixtures(): void { + runSequence = 0; + runs.clear(); +} diff --git a/web/hwlab-cloud-web/scripts/caserun-native-server.ts b/web/hwlab-cloud-web/scripts/caserun-native-server.ts new file mode 100644 index 00000000..1f5b5e65 --- /dev/null +++ b/web/hwlab-cloud-web/scripts/caserun-native-server.ts @@ -0,0 +1,61 @@ +import { listNativeCases, readNativeEvents, readNativeRun, startNativeRun } from "./caserun-native-fixtures"; + +const port = Number(process.env.HWLAB_CASERUN_NATIVE_PORT || 4316); +const hostname = process.env.HWLAB_CASERUN_NATIVE_HOST || "127.0.0.1"; + +function json(data: unknown, status = 200): Response { + return Response.json(data, { status, headers: { "cache-control": "no-store" } }); +} + +function notFound(kind: string, id?: string): Response { + return json({ ok: false, mode: "native-test", error: { code: `${kind}-not-found`, message: id ? `${kind} ${id} not found` : `${kind} not found` } }, 404); +} + +function authSession(): Record { + return { + ok: true, + authenticated: true, + mode: "native-test", + sessionKind: "native-test", + user: { id: "native-admin", username: "native-admin", displayName: "Native Admin", role: "admin", status: "active" }, + capabilities: { admin: "available" }, + access: { nav: { profileId: "native-test", allowedIds: ["*"], valuesRedacted: true } }, + valuesRedacted: true + }; +} + +const server = Bun.serve({ + hostname, + port, + development: true, + async fetch(request) { + const url = new URL(request.url); + if (request.method === "GET" && url.pathname === "/health") return json({ ok: true, mode: "native-test" }); + if (request.method === "POST" && url.pathname === "/auth/login") { + return Response.json(authSession(), { headers: { "cache-control": "no-store", "set-cookie": "hwlab_session=native-test; Path=/; HttpOnly; SameSite=Lax" } }); + } + if (request.method === "GET" && url.pathname === "/auth/session") return json(authSession()); + if (request.method === "GET" && url.pathname === "/v1/caserun/cases") return json(listNativeCases()); + if (request.method === "POST" && url.pathname === "/v1/caserun/runs") { + const body = await request.json().catch(() => null) as { caseId?: string } | null; + if (!body?.caseId) return json({ ok: false, mode: "native-test", error: { code: "case-id-required", message: "caseId is required" } }, 400); + const run = startNativeRun(body.caseId); + return run ? json(run, 202) : notFound("case", body.caseId); + } + const eventsMatch = url.pathname.match(/^\/v1\/caserun\/runs\/([^/]+)\/events$/); + if (request.method === "GET" && eventsMatch) { + const runId = decodeURIComponent(eventsMatch[1]); + const events = readNativeEvents(runId); + return events ? json(events) : notFound("run", runId); + } + const runMatch = url.pathname.match(/^\/v1\/caserun\/runs\/([^/]+)$/); + if (request.method === "GET" && runMatch) { + const runId = decodeURIComponent(runMatch[1]); + const run = readNativeRun(runId); + return run ? json(run) : notFound("run", runId); + } + return notFound("route", url.pathname); + } +}); + +console.log(JSON.stringify({ event: "caserun-native-listening", mode: "native-test", hostname: server.hostname, port: server.port })); diff --git a/web/hwlab-cloud-web/scripts/caserun-native-service.ts b/web/hwlab-cloud-web/scripts/caserun-native-service.ts new file mode 100644 index 00000000..e017fbbb --- /dev/null +++ b/web/hwlab-cloud-web/scripts/caserun-native-service.ts @@ -0,0 +1,62 @@ +import { mkdirSync, openSync, readFileSync, rmSync } from "node:fs"; +import { resolve } from "node:path"; + +const action = process.argv[2]; +const root = process.cwd(); +const stateDir = resolve(root, ".state/caserun-native"); +const pidPath = resolve(stateDir, "server.pid"); +const logPath = resolve(stateDir, "server.log"); + +function readPid(): number | null { + try { + const pid = Number(readFileSync(pidPath, "utf8").trim()); + return Number.isInteger(pid) && pid > 0 ? pid : null; + } catch { + return null; + } +} + +function isRunning(pid: number | null): boolean { + if (!pid) return false; + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +mkdirSync(stateDir, { recursive: true }); +const pid = readPid(); + +if (action === "start") { + if (isRunning(pid)) { + console.log(JSON.stringify({ ok: true, action, status: "already-running", pid, logPath })); + process.exit(0); + } + rmSync(pidPath, { force: true }); + const logFd = openSync(logPath, "a"); + const child = Bun.spawn([process.execPath, "scripts/caserun-native-supervisor.ts"], { cwd: root, stdin: "ignore", stdout: logFd, stderr: logFd }); + child.unref(); + await Bun.write(pidPath, `${child.pid}\n`); + console.log(JSON.stringify({ ok: true, action, status: "started", pid: child.pid, logPath })); +} else if (action === "stop") { + if (!isRunning(pid)) { + rmSync(pidPath, { force: true }); + console.log(JSON.stringify({ ok: true, action, status: "stopped" })); + } else { + process.kill(pid!, "SIGTERM"); + rmSync(pidPath, { force: true }); + console.log(JSON.stringify({ ok: true, action, status: "stopping", pid })); + } +} else if (action === "status") { + const running = isRunning(pid); + console.log(JSON.stringify({ ok: running, action, status: running ? "running" : "stopped", pid: running ? pid : null, logPath })); + process.exit(running ? 0 : 1); +} else if (action === "logs") { + const lines = readFileSync(logPath, "utf8").trimEnd().split("\n").slice(-80); + console.log(lines.join("\n")); +} else { + console.error("usage: bun run scripts/caserun-native-service.ts "); + process.exit(2); +} diff --git a/web/hwlab-cloud-web/scripts/caserun-native-supervisor.ts b/web/hwlab-cloud-web/scripts/caserun-native-supervisor.ts new file mode 100644 index 00000000..e470c17a --- /dev/null +++ b/web/hwlab-cloud-web/scripts/caserun-native-supervisor.ts @@ -0,0 +1,49 @@ +import { statSync } from "node:fs"; +import { resolve } from "node:path"; + +const root = process.cwd(); +const watchedFiles = [ + resolve(root, "scripts/caserun-native-server.ts"), + resolve(root, "scripts/caserun-native-fixtures.ts") +]; + +let child: ReturnType | null = null; +let signature = sourceSignature(); +let stopping = false; + +function sourceSignature(): string { + return watchedFiles.map((path) => `${path}:${statSync(path).mtimeMs}:${statSync(path).size}`).join("|"); +} + +function startChild(reason: string): void { + child = Bun.spawn([process.execPath, "scripts/caserun-native-server.ts"], { cwd: root, stdin: "ignore", stdout: "inherit", stderr: "inherit" }); + console.log(JSON.stringify({ event: "caserun-native-reload", mode: "native-test", reason, pid: child.pid })); + void child.exited.then((exitCode) => { + if (!stopping && child?.exitCode === exitCode) console.log(JSON.stringify({ event: "caserun-native-exit", exitCode })); + }); +} + +function stopChild(): void { + if (child && child.exitCode === null) child.kill("SIGTERM"); + child = null; +} + +startChild("initial"); + +const timer = setInterval(() => { + const nextSignature = sourceSignature(); + if (nextSignature === signature) return; + signature = nextSignature; + stopChild(); + startChild("source-change"); +}, 500); + +function shutdown(): void { + stopping = true; + clearInterval(timer); + stopChild(); + process.exit(0); +} + +process.on("SIGINT", shutdown); +process.on("SIGTERM", shutdown); diff --git a/web/hwlab-cloud-web/scripts/caserun-native-web-probe.mjs b/web/hwlab-cloud-web/scripts/caserun-native-web-probe.mjs new file mode 100644 index 00000000..b799d977 --- /dev/null +++ b/web/hwlab-cloud-web/scripts/caserun-native-web-probe.mjs @@ -0,0 +1,32 @@ +export default async function caserunNativeSmoke({ page, goto, wait, screenshot, recordStep }) { + await goto("/workbench"); + const panel = page.locator("#caserun-panel"); + await panel.waitFor({ state: "visible", timeout: 20000 }); + await page.locator("#caserun-case-select").selectOption("native-completed"); + await page.locator("#caserun-start").click(); + await page.locator("#caserun-refresh").click(); + await wait(300); + + const state = await page.locator("#caserun-status").evaluate((element) => element.textContent || ""); + const events = await page.locator("#caserun-event-text").textContent(); + const aggregate = await page.locator("#caserun-aggregate").textContent(); + const conditions = { + mode: state.includes("native-test"), + terminal: state.includes("true"), + completed: aggregate?.includes("completed") === true, + queuedEvent: events?.includes("queued") === true, + runningEvent: events?.includes("running") === true, + completedEvent: events?.includes("completed") === true, + aggregate: aggregate?.includes("aggregate pending") === false + }; + const image = await screenshot("caserun-native-completed.png"); + recordStep("caserun-native-completed", { ok: Object.values(conditions).every(Boolean), conditions }); + return { + ok: Object.values(conditions).every(Boolean), + mode: "native-test", + conditions, + screenshot: image, + finalUrl: page.url(), + valuesRedacted: true + }; +} diff --git a/web/hwlab-cloud-web/src/components/caserun/CaseRunPanel.vue b/web/hwlab-cloud-web/src/components/caserun/CaseRunPanel.vue index 7d76fbb5..85c2b87f 100644 --- a/web/hwlab-cloud-web/src/components/caserun/CaseRunPanel.vue +++ b/web/hwlab-cloud-web/src/components/caserun/CaseRunPanel.vue @@ -20,12 +20,13 @@ const artifactCount = computed(() => Number(caserun.currentRun?.facts?.artifactC const jobId = computed(() => String(caserun.currentRun?.facts?.jobId ?? "")); const aggregateHash = computed(() => caserun.currentRun?.references?.aggregate?.sha256 ?? ""); const blockerText = computed(() => caserun.currentRun?.blocker ? `${caserun.currentRun.blocker.code}: ${caserun.currentRun.blocker.summary}` : ""); +const fixtureMode = computed(() => String(caserun.currentRun?.mode ?? caserun.cases[0]?.mode ?? "live")); onMounted(async () => { await caserun.refreshCases(); selectedCaseId.value = caserun.selectedCaseId; pollTimer = setInterval(() => { - if (["queued", "running"].includes(runStatus.value)) void caserun.refreshRun(); + if (caserun.shouldPoll) void caserun.refreshRun(); }, 5000); }); @@ -64,6 +65,8 @@ async function startRun(): Promise {
run
{{ caserun.currentRun?.runId || '-' }}
stage
{{ caserun.currentRun?.stage || '-' }}
+
mode
{{ fixtureMode }}
+
terminal
{{ caserun.currentRun?.terminal ?? '-' }}
job
{{ jobId || '-' }}
artifacts
{{ artifactCount }}
diff --git a/web/hwlab-cloud-web/src/stores/caserun.test.ts b/web/hwlab-cloud-web/src/stores/caserun.test.ts new file mode 100644 index 00000000..4d8f2675 --- /dev/null +++ b/web/hwlab-cloud-web/src/stores/caserun.test.ts @@ -0,0 +1,49 @@ +import { createPinia, setActivePinia } from "pinia"; +import { beforeEach, expect, test, vi } from "vitest"; + +const { casesRequest, startRequest, runRequest, eventsRequest } = vi.hoisted(() => ({ + casesRequest: vi.fn(), + startRequest: vi.fn(), + runRequest: vi.fn(), + eventsRequest: vi.fn() +})); + +vi.mock("@/api", () => ({ + caserunAPI: { cases: casesRequest, startRun: startRequest, run: runRequest, events: eventsRequest } +})); + +import { useCaseRunStore } from "./caserun"; + +beforeEach(() => { + setActivePinia(createPinia()); + casesRequest.mockReset(); + startRequest.mockReset(); + runRequest.mockReset(); + eventsRequest.mockReset(); +}); + +test("polling follows API terminal instead of status names", async () => { + runRequest.mockResolvedValue({ ok: true, data: { runId: "run-1", caseId: "native-completed", status: "custom-progress", stage: "agent-running", terminal: false } }); + eventsRequest.mockResolvedValue({ ok: true, data: { runId: "run-1", events: [] } }); + const store = useCaseRunStore(); + + await store.refreshRun("run-1"); + expect(store.shouldPoll).toBe(true); + + runRequest.mockResolvedValue({ ok: true, data: { runId: "run-1", caseId: "native-completed", status: "running", stage: "completed", terminal: true } }); + await store.refreshRun("run-1"); + expect(store.shouldPoll).toBe(false); +}); + +test("start keeps blocker and aggregate from API read model", async () => { + startRequest.mockResolvedValue({ ok: true, data: { runId: "run-2", caseId: "native-blocked", status: "queued", stage: "queued", terminal: false } }); + runRequest.mockResolvedValue({ ok: true, data: { runId: "run-2", caseId: "native-blocked", status: "blocked", stage: "preparing", terminal: true, blocker: { code: "native-capability-blocked", summary: "blocked" }, references: { aggregate: { sha256: "abc123" } } } }); + eventsRequest.mockResolvedValue({ ok: true, data: { runId: "run-2", events: [{ status: "blocked", stage: "preparing" }] } }); + const store = useCaseRunStore(); + + await store.startRun("native-blocked"); + + expect(store.currentRun?.blocker?.code).toBe("native-capability-blocked"); + expect(store.currentRun?.references?.aggregate?.sha256).toBe("abc123"); + expect(store.shouldPoll).toBe(false); +}); diff --git a/web/hwlab-cloud-web/src/stores/caserun.ts b/web/hwlab-cloud-web/src/stores/caserun.ts index 95f27515..3943925b 100644 --- a/web/hwlab-cloud-web/src/stores/caserun.ts +++ b/web/hwlab-cloud-web/src/stores/caserun.ts @@ -11,6 +11,7 @@ export const useCaseRunStore = defineStore("caserun", () => { const starting = ref(false); const error = ref(null); const selectedCaseId = computed(() => currentRun.value?.caseId ?? cases.value[0]?.caseId ?? ""); + const shouldPoll = computed(() => Boolean(currentRun.value?.runId && currentRun.value.terminal === false)); async function refreshCases(): Promise { loading.value = true; @@ -53,5 +54,5 @@ export const useCaseRunStore = defineStore("caserun", () => { if (result.ok && result.data?.runId === runId) events.value = result.data.events ?? []; } - return { cases, currentRun, events, loading, starting, error, selectedCaseId, refreshCases, startRun, refreshRun, refreshEvents }; + return { cases, currentRun, events, loading, starting, error, selectedCaseId, shouldPoll, refreshCases, startRun, refreshRun, refreshEvents }; }); diff --git a/web/hwlab-cloud-web/src/types/index.ts b/web/hwlab-cloud-web/src/types/index.ts index 36f43521..38eae6a5 100644 --- a/web/hwlab-cloud-web/src/types/index.ts +++ b/web/hwlab-cloud-web/src/types/index.ts @@ -516,12 +516,12 @@ export interface HwlabNodeUpdateMetadata { observedAt: string; } export interface CaseRunCaseSummary { caseId: string; title?: string; mode?: string; available?: boolean; hwpodSpec?: string; subject?: Record | null; expected?: Record | null; runtime?: Record | null; [key: string]: unknown } -export interface CaseRunCasesResponse { ok?: boolean; contractVersion?: string; count?: number; cases?: CaseRunCaseSummary[]; caseAuthority?: Record; [key: string]: unknown } +export interface CaseRunCasesResponse { ok?: boolean; mode?: string; contractVersion?: string; count?: number; cases?: CaseRunCaseSummary[]; caseAuthority?: Record; [key: string]: unknown } export interface CaseRunEvent { at?: string; status?: string; stage?: string; payload?: Record; [key: string]: unknown } export interface CaseRunBlocker { code: string; summary: string; layer?: string | null; details?: Record | null } export interface CaseRunReference { href?: string; ref?: string | null; sha256?: string | null; count?: number; relationship?: Record | null; [key: string]: unknown } -export interface CaseRunRunResponse { ok?: boolean; contractVersion?: string; runId?: string; caseId?: string; status?: string; stage?: string; terminal?: boolean; blocker?: CaseRunBlocker | null; createdAt?: string; updatedAt?: string; facts?: { jobId?: string | null; artifactCount?: number; evidenceStatus?: string | null }; references?: { status?: CaseRunReference; events?: CaseRunReference; aggregate?: CaseRunReference; manifest?: CaseRunReference | null; replay?: CaseRunReference | null; trace?: Record | null; hwpod?: Record | null }; sourceAuthority?: Record; error?: { code?: string; message?: string } | null; statusUrl?: string; [key: string]: unknown } -export interface CaseRunEventsResponse { ok?: boolean; contractVersion?: string; runId?: string; caseId?: string; status?: string; stage?: string; terminal?: boolean; sourceAuthority?: Record; events?: CaseRunEvent[]; [key: string]: unknown } +export interface CaseRunRunResponse { ok?: boolean; mode?: string; contractVersion?: string; runId?: string; caseId?: string; status?: string; stage?: string; terminal?: boolean; blocker?: CaseRunBlocker | null; createdAt?: string; updatedAt?: string; facts?: { jobId?: string | null; artifactCount?: number; evidenceStatus?: string | null }; references?: { status?: CaseRunReference; events?: CaseRunReference; aggregate?: CaseRunReference; manifest?: CaseRunReference | null; replay?: CaseRunReference | null; trace?: Record | null; hwpod?: Record | null }; sourceAuthority?: Record; error?: { code?: string; message?: string } | null; statusUrl?: string; [key: string]: unknown } +export interface CaseRunEventsResponse { ok?: boolean; mode?: string; contractVersion?: string; runId?: string; caseId?: string; status?: string; stage?: string; terminal?: boolean; sourceAuthority?: Record; events?: CaseRunEvent[]; [key: string]: unknown } export interface ProviderProfilesResponse { ok?: boolean; status?: string; profiles?: Array>; [key: string]: unknown } export interface ProviderProfileCatalogItem { profile: string; diff --git a/web/hwlab-cloud-web/vite.config.ts b/web/hwlab-cloud-web/vite.config.ts index 1f404f7a..06055ca4 100644 --- a/web/hwlab-cloud-web/vite.config.ts +++ b/web/hwlab-cloud-web/vite.config.ts @@ -1,25 +1,51 @@ +import { Agent } from "node:http"; import { fileURLToPath, URL } from "node:url"; import vue from "@vitejs/plugin-vue"; -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from "vite"; -export default defineConfig({ - plugins: [vue()], - resolve: { - alias: { - "@": fileURLToPath(new URL("./src", import.meta.url)) - } - }, - build: { - outDir: "dist", - emptyOutDir: true, - sourcemap: false, - rollupOptions: { - output: { - entryFileNames: "app.js", - chunkFileNames: "assets/[name].js", - assetFileNames: "assets/[name][extname]" +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ""); + const nativeCaseRunEnabled = (process.env.HWLAB_CASERUN_NATIVE_TEST ?? env.HWLAB_CASERUN_NATIVE_TEST) === "1"; + const nativeCaseRunTarget = process.env.HWLAB_CASERUN_NATIVE_URL ?? env.HWLAB_CASERUN_NATIVE_URL ?? "http://127.0.0.1:4316"; + const nativeCaseRunAgent = new Agent({ keepAlive: true }); + + return { + plugins: [vue()], + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)) + } + }, + server: nativeCaseRunEnabled ? { + watch: { + usePolling: true, + interval: 300 + }, + proxy: { + "/auth": { + target: nativeCaseRunTarget, + changeOrigin: false, + agent: nativeCaseRunAgent + }, + "/v1/caserun": { + target: nativeCaseRunTarget, + changeOrigin: false, + agent: nativeCaseRunAgent + } + } + } : undefined, + build: { + outDir: "dist", + emptyOutDir: true, + sourcemap: false, + rollupOptions: { + output: { + entryFileNames: "app.js", + chunkFileNames: "assets/[name].js", + assetFileNames: "assets/[name][extname]" + } } } - } + }; });