From 03c09f5c947c1edf6ad616f1b69d8894a47a9a10 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 29 May 2026 00:46:32 +0800 Subject: [PATCH] refactor: migrate v0.2 runtime services to TypeScript --- cmd/hwlab-agent-mgr/main.test.ts | 121 +++++++++++++++++ cmd/hwlab-agent-mgr/{main.mjs => main.ts} | 14 +- cmd/hwlab-agent-worker/main.test.ts | 83 ++++++++++++ cmd/hwlab-agent-worker/{main.mjs => main.ts} | 4 +- .../{main.test.mjs => main.test.ts} | 4 +- .../{main.mjs => main.ts} | 0 .../{main.test.mjs => main.test.ts} | 4 +- .../{main.mjs => main.ts} | 0 cmd/hwlab-device-pod/main.test.ts | 100 ++++++++++++++ cmd/hwlab-device-pod/{main.mjs => main.ts} | 0 cmd/hwlab-edge-proxy/main.test.ts | 122 ++++++++++++++++++ cmd/hwlab-edge-proxy/{main.mjs => main.ts} | 0 deploy/artifact-catalog.dev.json | 8 +- deploy/k8s/base/workloads.yaml | 5 +- .../reference/spec-v02-codex-api-forwarder.md | 2 +- docs/reference/spec-v02-deepseek-proxy.md | 2 +- docs/reference/spec-v02-hwlab-agent-mgr.md | 4 +- docs/reference/spec-v02-hwlab-agent-worker.md | 6 +- .../spec-v02-hwlab-device-pod-service.md | 2 +- docs/reference/spec-v02-hwlab-edge-proxy.md | 2 +- internal/cloud/code-agent-contract.ts | 2 +- package.json | 2 +- .../artifact-runtime-readiness-guard.test.mjs | 2 +- scripts/check-runner.test.mjs | 9 ++ scripts/code-agent-chat-smoke.mjs | 3 +- scripts/deploy-desired-state-plan.test.mjs | 2 +- scripts/dev-cloud-workbench-layout-smoke.mjs | 2 +- scripts/dev-cloud-workbench-smoke.test.mjs | 20 +-- scripts/g14-ci-plan.test.mjs | 10 +- scripts/g14-gitops-render.mjs | 6 +- scripts/gateway-outbound-demo-smoke.mjs | 2 +- scripts/m1-contract-smoke.mjs | 30 +++-- scripts/src/check-plan.mjs | 14 +- scripts/src/dev-artifact-services.mjs | 20 ++- scripts/src/dev-cloud-workbench-smoke-lib.mjs | 120 ++++++++++++----- scripts/src/g14-ci-plan-lib.mjs | 16 ++- skills/hwlab-agent-runtime/SKILL.md | 14 +- web/hwlab-cloud-web/scripts/check.ts | 15 ++- 38 files changed, 644 insertions(+), 128 deletions(-) create mode 100644 cmd/hwlab-agent-mgr/main.test.ts rename cmd/hwlab-agent-mgr/{main.mjs => main.ts} (90%) create mode 100644 cmd/hwlab-agent-worker/main.test.ts rename cmd/hwlab-agent-worker/{main.mjs => main.ts} (92%) rename cmd/hwlab-codex-api-responses-forwarder/{main.test.mjs => main.test.ts} (95%) rename cmd/hwlab-codex-api-responses-forwarder/{main.mjs => main.ts} (100%) rename cmd/hwlab-deepseek-responses-bridge/{main.test.mjs => main.test.ts} (96%) rename cmd/hwlab-deepseek-responses-bridge/{main.mjs => main.ts} (100%) create mode 100644 cmd/hwlab-device-pod/main.test.ts rename cmd/hwlab-device-pod/{main.mjs => main.ts} (100%) create mode 100644 cmd/hwlab-edge-proxy/main.test.ts rename cmd/hwlab-edge-proxy/{main.mjs => main.ts} (100%) diff --git a/cmd/hwlab-agent-mgr/main.test.ts b/cmd/hwlab-agent-mgr/main.test.ts new file mode 100644 index 00000000..cc7581b9 --- /dev/null +++ b/cmd/hwlab-agent-mgr/main.test.ts @@ -0,0 +1,121 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { createServer } from "node:http"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); +const bunCommand = process.env.HWLAB_TEST_BUN_COMMAND || "/root/.bun/bin/bun"; + +test("agent manager CLI reports degraded skills and manages local session lifecycle", async () => { + const stateDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-mgr-")); + try { + const health = await runManagerJson(["health", "--state-dir", stateDir]); + assert.equal(health.serviceId, "hwlab-agent-mgr"); + assert.equal(health.ok, true); + assert.equal(health.status, "degraded"); + assert.deepEqual(health.health.skills.manager.missing, ["skillCommitId", "skillVersion"]); + + const created = await runManagerJson([ + "create", + "--agent-session-id", "ags_test", + "--project-id", "prj_test", + "--goal", "keep behavior", + "--prompt", "run dry", + "--skill-commit-id", "commit-a", + "--skill-version", "1.0.0", + "--state-dir", stateDir + ]); + assert.equal(created.agentSessionId, "ags_test"); + assert.equal(created.status, "ok"); + assert.equal(created.task.status, "queued"); + assert.equal(created.traceCount, 1); + + const status = await runManagerJson(["status", "--agent-session-id", "ags_test", "--state-dir", stateDir]); + assert.equal(status.agentSession.metadata.skills.commitId, "commit-a"); + assert.equal(status.workerSession.metadata.expectedSkills.version, "1.0.0"); + + const trace = await runManagerJson(["trace", "--agent-session-id", "ags_test", "--state-dir", stateDir]); + assert.equal(trace.traceEvents.length, 1); + assert.equal(trace.traceEvents[0].metadata.state, "create"); + + const evidence = await runManagerJson(["evidence", "--agent-session-id", "ags_test", "--state-dir", stateDir]); + assert.equal(evidence.evidenceRecords.length, 0); + + const cleanup = await runManagerJson(["cleanup", "--agent-session-id", "ags_test", "--state-dir", stateDir]); + assert.equal(cleanup.serviceId, "hwlab-agent-mgr"); + assert.equal(cleanup.cleanup.completed, true); + assert.equal(cleanup.workspace.cleaned, true); + } finally { + await rm(stateDir, { recursive: true, force: true }); + } +}); + +test("agent manager server returns health JSON", async () => { + const port = await freePort(); + const server = await startManagerServer(port); + try { + const response = await fetch(`http://127.0.0.1:${port}/health/live`); + assert.equal(response.status, 200); + const health = await response.json(); + assert.equal(health.serviceId, "hwlab-agent-mgr"); + assert.equal(health.ok, true); + assert.equal(health.status, "degraded"); + } finally { + await server.stop(); + } +}); + +async function runManagerJson(args) { + const { stdout } = await execFileAsync(bunCommand, ["run", "cmd/hwlab-agent-mgr/main.ts", ...args], { + cwd: process.cwd(), + maxBuffer: 1024 * 1024 + }); + return JSON.parse(stdout); +} + +async function startManagerServer(port) { + const child = execFile(bunCommand, ["run", "cmd/hwlab-agent-mgr/main.ts", "server"], { + cwd: process.cwd(), + env: { + ...process.env, + PORT: String(port) + }, + maxBuffer: 1024 * 1024 + }); + await new Promise((resolve, reject) => { + let stderr = ""; + const timer = setTimeout(() => reject(new Error(`manager server did not start: ${stderr}`)), 5000); + child.stderr?.on("data", (chunk) => { + stderr += chunk; + }); + child.stdout?.on("data", (chunk) => { + if (chunk.toString().includes("listening")) { + clearTimeout(timer); + resolve(); + } + }); + child.on("exit", (code) => { + clearTimeout(timer); + reject(new Error(`manager server exited early code=${code}: ${stderr}`)); + }); + }); + return { + stop: async () => { + if (child.exitCode !== null) return; + child.kill("SIGTERM"); + await new Promise((resolve) => child.once("exit", resolve)); + } + }; +} + +async function freePort() { + const server = createServer(); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const port = server.address().port; + await new Promise((resolve) => server.close(resolve)); + return port; +} diff --git a/cmd/hwlab-agent-mgr/main.mjs b/cmd/hwlab-agent-mgr/main.ts similarity index 90% rename from cmd/hwlab-agent-mgr/main.mjs rename to cmd/hwlab-agent-mgr/main.ts index 25f1ab6d..3d34ff49 100644 --- a/cmd/hwlab-agent-mgr/main.mjs +++ b/cmd/hwlab-agent-mgr/main.ts @@ -211,13 +211,13 @@ function help() { serviceId: AGENT_MGR_SERVICE_ID, ok: true, usage: [ - "node cmd/hwlab-agent-mgr/main.mjs server [--skill-commit-id COMMIT --skill-version VERSION] [--state-dir DIR]", - "node cmd/hwlab-agent-mgr/main.mjs health [--skill-commit-id COMMIT --skill-version VERSION] [--state-dir DIR]", - "node cmd/hwlab-agent-mgr/main.mjs create --agent-session-id ID --project-id ID [--goal TEXT --prompt TEXT] [--skill-commit-id COMMIT --skill-version VERSION] [--state-dir DIR]", - "node cmd/hwlab-agent-mgr/main.mjs status --agent-session-id ID [--state-dir DIR]", - "node cmd/hwlab-agent-mgr/main.mjs trace --agent-session-id ID [--state-dir DIR]", - "node cmd/hwlab-agent-mgr/main.mjs evidence --agent-session-id ID [--state-dir DIR]", - "node cmd/hwlab-agent-mgr/main.mjs cleanup --agent-session-id ID [--state-dir DIR]" + "bun run cmd/hwlab-agent-mgr/main.ts server [--skill-commit-id COMMIT --skill-version VERSION] [--state-dir DIR]", + "bun run cmd/hwlab-agent-mgr/main.ts health [--skill-commit-id COMMIT --skill-version VERSION] [--state-dir DIR]", + "bun run cmd/hwlab-agent-mgr/main.ts create --agent-session-id ID --project-id ID [--goal TEXT --prompt TEXT] [--skill-commit-id COMMIT --skill-version VERSION] [--state-dir DIR]", + "bun run cmd/hwlab-agent-mgr/main.ts status --agent-session-id ID [--state-dir DIR]", + "bun run cmd/hwlab-agent-mgr/main.ts trace --agent-session-id ID [--state-dir DIR]", + "bun run cmd/hwlab-agent-mgr/main.ts evidence --agent-session-id ID [--state-dir DIR]", + "bun run cmd/hwlab-agent-mgr/main.ts cleanup --agent-session-id ID [--state-dir DIR]" ], contract: { stateDir: resolveAgentRuntimeStateDir(), diff --git a/cmd/hwlab-agent-worker/main.test.ts b/cmd/hwlab-agent-worker/main.test.ts new file mode 100644 index 00000000..1d934665 --- /dev/null +++ b/cmd/hwlab-agent-worker/main.test.ts @@ -0,0 +1,83 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { access, mkdtemp, rm } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); +const bunCommand = process.env.HWLAB_TEST_BUN_COMMAND || "/root/.bun/bin/bun"; + +test("agent worker health and dry-run preserve session evidence and cleanup behavior", async () => { + const stateDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-worker-")); + try { + const health = await runWorkerJson(["health", "--state-dir", stateDir]); + assert.equal(health.serviceId, "hwlab-agent-worker"); + assert.equal(health.ok, true); + assert.equal(health.status, "ok"); + assert.equal(health.skills.status, "degraded"); + + await runManagerJson([ + "create", + "--agent-session-id", "ags_worker", + "--project-id", "prj_worker", + "--skill-commit-id", "commit-worker", + "--skill-version", "2.0.0", + "--state-dir", stateDir + ]); + + const dryRun = await runWorkerJson([ + "dry-run", + "--agent-session-id", "ags_worker", + "--skill-commit-id", "commit-worker", + "--skill-version", "2.0.0", + "--state-dir", stateDir + ]); + assert.equal(dryRun.serviceId, "hwlab-agent-worker"); + assert.equal(dryRun.dryRun, true); + assert.equal(dryRun.task.status, "completed"); + assert.equal(dryRun.health.status, "ok"); + assert.equal(dryRun.evidenceRecord.evidenceId, "evi_ags_worker"); + assert.equal(dryRun.cleanup.completed, true); + assert.equal(dryRun.workspace.cleaned, true); + assert.equal(dryRun.workspace.existsAfterCleanup, false); + await assert.rejects(access(dryRun.workspace.path)); + + const evidence = await runManagerJson(["evidence", "--agent-session-id", "ags_worker", "--state-dir", stateDir]); + assert.equal(evidence.evidenceRecords.length, 1); + assert.equal(evidence.evidenceRecords[0].serviceId, "hwlab-agent-worker"); + } finally { + await rm(stateDir, { recursive: true, force: true }); + } +}); + +test("agent worker dry-run requires agent session id", async () => { + const result = await execFileAsync(bunCommand, ["run", "cmd/hwlab-agent-worker/main.ts", "dry-run"], { + cwd: process.cwd(), + maxBuffer: 1024 * 1024 + }).then( + () => ({ code: 0, stderr: "" }), + (error) => ({ code: error.code, stderr: error.stderr }) + ); + assert.equal(result.code, 1); + const payload = JSON.parse(result.stderr); + assert.equal(payload.serviceId, "hwlab-agent-worker"); + assert.match(payload.error.message, /--agent-session-id is required/u); +}); + +async function runManagerJson(args) { + const { stdout } = await execFileAsync(bunCommand, ["run", "cmd/hwlab-agent-mgr/main.ts", ...args], { + cwd: process.cwd(), + maxBuffer: 1024 * 1024 + }); + return JSON.parse(stdout); +} + +async function runWorkerJson(args) { + const { stdout } = await execFileAsync(bunCommand, ["run", "cmd/hwlab-agent-worker/main.ts", ...args], { + cwd: process.cwd(), + maxBuffer: 1024 * 1024 + }); + return JSON.parse(stdout); +} diff --git a/cmd/hwlab-agent-worker/main.mjs b/cmd/hwlab-agent-worker/main.ts similarity index 92% rename from cmd/hwlab-agent-worker/main.mjs rename to cmd/hwlab-agent-worker/main.ts index fa052f51..9fe1629b 100644 --- a/cmd/hwlab-agent-worker/main.mjs +++ b/cmd/hwlab-agent-worker/main.ts @@ -100,8 +100,8 @@ function help() { serviceId: AGENT_WORKER_SERVICE_ID, ok: true, usage: [ - "node cmd/hwlab-agent-worker/main.mjs health [--skill-commit-id COMMIT --skill-version VERSION]", - "node cmd/hwlab-agent-worker/main.mjs dry-run --agent-session-id ID [--skill-commit-id COMMIT --skill-version VERSION] [--state-dir DIR]" + "bun run cmd/hwlab-agent-worker/main.ts health [--skill-commit-id COMMIT --skill-version VERSION]", + "bun run cmd/hwlab-agent-worker/main.ts dry-run --agent-session-id ID [--skill-commit-id COMMIT --skill-version VERSION] [--state-dir DIR]" ], contract: { output: "json", diff --git a/cmd/hwlab-codex-api-responses-forwarder/main.test.mjs b/cmd/hwlab-codex-api-responses-forwarder/main.test.ts similarity index 95% rename from cmd/hwlab-codex-api-responses-forwarder/main.test.mjs rename to cmd/hwlab-codex-api-responses-forwarder/main.test.ts index 158c3281..d48ac5b8 100644 --- a/cmd/hwlab-codex-api-responses-forwarder/main.test.mjs +++ b/cmd/hwlab-codex-api-responses-forwarder/main.test.ts @@ -3,6 +3,8 @@ import { spawn } from "node:child_process"; import { createServer } from "node:http"; import test from "node:test"; +const bunCommand = process.env.HWLAB_TEST_BUN_COMMAND || "/root/.bun/bin/bun"; + test("Codex API Responses forwarder maps loopback Codex paths to configurable upstream and preserves stream body", async () => { const upstream = await startUpstream(); const forwarder = await startForwarder(upstream.port, { upstreamPath: "/" }); @@ -74,7 +76,7 @@ async function startUpstream() { async function startForwarder(upstreamPort, { upstreamPath }) { const port = await freePort(); - const child = spawn(process.execPath, ["cmd/hwlab-codex-api-responses-forwarder/main.mjs"], { + const child = spawn(bunCommand, ["run", "cmd/hwlab-codex-api-responses-forwarder/main.ts"], { cwd: process.cwd(), env: { ...process.env, diff --git a/cmd/hwlab-codex-api-responses-forwarder/main.mjs b/cmd/hwlab-codex-api-responses-forwarder/main.ts similarity index 100% rename from cmd/hwlab-codex-api-responses-forwarder/main.mjs rename to cmd/hwlab-codex-api-responses-forwarder/main.ts diff --git a/cmd/hwlab-deepseek-responses-bridge/main.test.mjs b/cmd/hwlab-deepseek-responses-bridge/main.test.ts similarity index 96% rename from cmd/hwlab-deepseek-responses-bridge/main.test.mjs rename to cmd/hwlab-deepseek-responses-bridge/main.test.ts index 648821e4..93b48b07 100644 --- a/cmd/hwlab-deepseek-responses-bridge/main.test.mjs +++ b/cmd/hwlab-deepseek-responses-bridge/main.test.ts @@ -3,6 +3,8 @@ import { spawn } from "node:child_process"; import { createServer } from "node:http"; import test from "node:test"; +const bunCommand = process.env.HWLAB_TEST_BUN_COMMAND || "/root/.bun/bin/bun"; + const zstdPayload = Buffer.from([ 0x28, 0xb5, 0x2f, 0xfd, 0x04, 0x58, 0x75, 0x03, 0x00, 0x32, 0x47, 0x16, 0x18, 0xa0, 0xa7, 0x39, 0x64, 0x06, 0x2d, 0xdf, 0x85, 0x36, 0x31, 0xd8, @@ -90,7 +92,7 @@ async function startUpstream() { async function startBridge(upstreamPort) { const port = await freePort(); - const child = spawn(process.execPath, ["cmd/hwlab-deepseek-responses-bridge/main.mjs"], { + const child = spawn(bunCommand, ["run", "cmd/hwlab-deepseek-responses-bridge/main.ts"], { cwd: process.cwd(), env: { ...process.env, diff --git a/cmd/hwlab-deepseek-responses-bridge/main.mjs b/cmd/hwlab-deepseek-responses-bridge/main.ts similarity index 100% rename from cmd/hwlab-deepseek-responses-bridge/main.mjs rename to cmd/hwlab-deepseek-responses-bridge/main.ts diff --git a/cmd/hwlab-device-pod/main.test.ts b/cmd/hwlab-device-pod/main.test.ts new file mode 100644 index 00000000..ad072ad5 --- /dev/null +++ b/cmd/hwlab-device-pod/main.test.ts @@ -0,0 +1,100 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { createServer } from "node:http"; +import test from "node:test"; + +const bunCommand = process.env.HWLAB_TEST_BUN_COMMAND || "/root/.bun/bin/bun"; + +test("device pod fake service exposes health, list, status, events, and method guards", async () => { + const service = await startDevicePod("device-pod-test"); + try { + const health = await fetchJson(`http://127.0.0.1:${service.port}/health/live`); + assert.equal(health.serviceId, "hwlab-device-pod"); + assert.equal(health.status, "live"); + assert.equal(health.devicePodId, "device-pod-test"); + assert.equal(health.role, "fake-device-pod-server"); + + const list = await fetchJson(`http://127.0.0.1:${service.port}/v1/device-pods`); + assert.equal(list.serviceId, "hwlab-device-pod"); + assert.equal(list.selectedDevicePodId, "device-pod-test"); + assert.equal(list.devicePods.length, 1); + + const status = await fetchJson(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-test/status`); + assert.equal(status.devicePod.devicePodId, "device-pod-test"); + assert.equal(status.summary.targetId, "stm32f103-minsys-01"); + + const events = await fetchJson(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-test/events?limit=1`); + assert.equal(events.events.length, 1); + assert.equal(events.truncation.limit, 1); + assert.equal(events.truncation.truncated, true); + + const missing = await fetch(`http://127.0.0.1:${service.port}/missing`); + assert.equal(missing.status, 404); + assert.deepEqual(await missing.json(), { error: "not_found", path: "/missing" }); + + const methodGuard = await fetch(`http://127.0.0.1:${service.port}/health`, { method: "POST" }); + assert.equal(methodGuard.status, 405); + assert.deepEqual(await methodGuard.json(), { error: "method_not_allowed", method: "POST" }); + } finally { + await service.stop(); + } +}); + +async function startDevicePod(devicePodId) { + const port = await freePort(); + const child = spawn(bunCommand, ["run", "cmd/hwlab-device-pod/main.ts"], { + cwd: process.cwd(), + env: { + ...process.env, + HWLAB_DEVICE_POD_PORT: String(port), + HWLAB_DEVICE_POD_ID: devicePodId, + HWLAB_ENVIRONMENT: "v02" + }, + stdio: ["ignore", "pipe", "pipe"] + }); + await waitForListening(child, "device pod"); + return { + port, + stop: () => stopChild(child) + }; +} + +async function fetchJson(url) { + const response = await fetch(url); + assert.equal(response.ok, true, `${url} returned ${response.status}`); + return response.json(); +} + +async function freePort() { + const server = createServer(); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const port = server.address().port; + await new Promise((resolve) => server.close(resolve)); + return port; +} + +async function waitForListening(child, label) { + let stderr = ""; + child.stderr.on("data", (chunk) => { + stderr += chunk; + }); + await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(`${label} did not start: ${stderr}`)), 5000); + child.stdout.on("data", (chunk) => { + if (chunk.toString().includes("listening")) { + clearTimeout(timer); + resolve(); + } + }); + child.on("exit", (code) => { + clearTimeout(timer); + reject(new Error(`${label} exited early code=${code}: ${stderr}`)); + }); + }); +} + +async function stopChild(child) { + if (child.exitCode !== null) return; + child.kill("SIGTERM"); + await new Promise((resolve) => child.once("exit", resolve)); +} diff --git a/cmd/hwlab-device-pod/main.mjs b/cmd/hwlab-device-pod/main.ts similarity index 100% rename from cmd/hwlab-device-pod/main.mjs rename to cmd/hwlab-device-pod/main.ts diff --git a/cmd/hwlab-edge-proxy/main.test.ts b/cmd/hwlab-edge-proxy/main.test.ts new file mode 100644 index 00000000..6edf9e9d --- /dev/null +++ b/cmd/hwlab-edge-proxy/main.test.ts @@ -0,0 +1,122 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { createServer } from "node:http"; +import test from "node:test"; + +const bunCommand = process.env.HWLAB_TEST_BUN_COMMAND || "/root/.bun/bin/bun"; + +test("edge proxy reports local health and proxies live health to upstream", async () => { + const upstream = await startUpstream(); + const proxy = await startEdgeProxy(upstream.port); + try { + const health = await fetchJson(`http://127.0.0.1:${proxy.port}/health`); + assert.equal(health.serviceId, "hwlab-edge-proxy"); + assert.equal(health.status, "ok"); + assert.equal(health.service.role, "public-dev-ingress"); + assert.equal(health.details.upstream, `http://127.0.0.1:${upstream.port}`); + assert.equal(health.details.mode, "dev-edge-proxy"); + + const routes = await fetchJson(`http://127.0.0.1:${proxy.port}/routes`); + assert.equal(routes.serviceId, "hwlab-edge-proxy"); + assert.deepEqual(routes.routes, [{ + pathPrefix: "/", + upstream: `http://127.0.0.1:${upstream.port}`, + mode: "http-proxy" + }]); + + const live = await fetchJson(`http://127.0.0.1:${proxy.port}/health/live`); + assert.equal(live.serviceId, "hwlab-cloud-api"); + assert.equal(live.environment, "v02"); + assert.equal(upstream.captured.method, "GET"); + assert.equal(upstream.captured.url, "/health/live"); + } finally { + await proxy.stop(); + await upstream.stop(); + } +}); + +async function startUpstream() { + let captured = null; + const server = createServer(async (request, response) => { + const chunks = []; + for await (const chunk of request) chunks.push(chunk); + captured = { + method: request.method, + url: request.url, + body: Buffer.concat(chunks).toString("utf8") + }; + response.writeHead(200, { "content-type": "application/json" }); + response.end(JSON.stringify({ + serviceId: "hwlab-cloud-api", + environment: "v02", + status: "live" + })); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + return { + port: server.address().port, + get captured() { + return captured; + }, + stop: () => new Promise((resolve) => server.close(resolve)) + }; +} + +async function startEdgeProxy(upstreamPort) { + const port = await freePort(); + const child = spawn(bunCommand, ["run", "cmd/hwlab-edge-proxy/main.ts"], { + cwd: process.cwd(), + env: { + ...process.env, + HWLAB_EDGE_HOST: "127.0.0.1", + HWLAB_EDGE_PORT: String(port), + HWLAB_EDGE_UPSTREAM: `http://127.0.0.1:${upstreamPort}` + }, + stdio: ["ignore", "pipe", "pipe"] + }); + await waitForListening(child, "edge proxy"); + return { + port, + stop: () => stopChild(child) + }; +} + +async function fetchJson(url, options = {}) { + const response = await fetch(url, options); + assert.equal(response.ok, true, `${url} returned ${response.status}`); + return response.json(); +} + +async function freePort() { + const server = createServer(); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const port = server.address().port; + await new Promise((resolve) => server.close(resolve)); + return port; +} + +async function waitForListening(child, label) { + let stderr = ""; + child.stderr.on("data", (chunk) => { + stderr += chunk; + }); + await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(`${label} did not start: ${stderr}`)), 5000); + child.stdout.on("data", (chunk) => { + if (chunk.toString().includes("listening")) { + clearTimeout(timer); + resolve(); + } + }); + child.on("exit", (code) => { + clearTimeout(timer); + reject(new Error(`${label} exited early code=${code}: ${stderr}`)); + }); + }); +} + +async function stopChild(child) { + if (child.exitCode !== null) return; + child.kill("SIGTERM"); + await new Promise((resolve) => child.once("exit", resolve)); +} diff --git a/cmd/hwlab-edge-proxy/main.mjs b/cmd/hwlab-edge-proxy/main.ts similarity index 100% rename from cmd/hwlab-edge-proxy/main.mjs rename to cmd/hwlab-edge-proxy/main.ts diff --git a/deploy/artifact-catalog.dev.json b/deploy/artifact-catalog.dev.json index c1ae39c3..9c176a2b 100644 --- a/deploy/artifact-catalog.dev.json +++ b/deploy/artifact-catalog.dev.json @@ -605,7 +605,7 @@ "runtimeKind": "node-command", "implementationState": "repo-entrypoint", "sourceState": "source-present", - "entrypoint": "cmd/hwlab-agent-mgr/main.mjs", + "entrypoint": "cmd/hwlab-agent-mgr/main.ts", "disabledReason": null }, { @@ -616,7 +616,7 @@ "runtimeKind": "node-command", "implementationState": "repo-entrypoint", "sourceState": "source-present", - "entrypoint": "cmd/hwlab-agent-worker/main.mjs", + "entrypoint": "cmd/hwlab-agent-worker/main.ts", "disabledReason": null }, { @@ -627,7 +627,7 @@ "runtimeKind": "node-command", "implementationState": "repo-entrypoint", "sourceState": "source-present", - "entrypoint": "cmd/hwlab-device-pod/main.mjs", + "entrypoint": "cmd/hwlab-device-pod/main.ts", "disabledReason": null }, { @@ -704,7 +704,7 @@ "runtimeKind": "node-command", "implementationState": "repo-entrypoint", "sourceState": "source-present", - "entrypoint": "cmd/hwlab-edge-proxy/main.mjs", + "entrypoint": "cmd/hwlab-edge-proxy/main.ts", "disabledReason": null }, { diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index c59597c6..e8037c3c 100644 --- a/deploy/k8s/base/workloads.yaml +++ b/deploy/k8s/base/workloads.yaml @@ -300,8 +300,9 @@ "name": "hwlab-codex-api-forwarder", "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:af46386", "command": [ - "node", - "/app/cmd/hwlab-codex-api-responses-forwarder/main.mjs" + "/usr/local/bin/bun", + "run", + "/app/cmd/hwlab-codex-api-responses-forwarder/main.ts" ], "ports": [ { diff --git a/docs/reference/spec-v02-codex-api-forwarder.md b/docs/reference/spec-v02-codex-api-forwarder.md index 406d195f..c76d89ae 100644 --- a/docs/reference/spec-v02-codex-api-forwarder.md +++ b/docs/reference/spec-v02-codex-api-forwarder.md @@ -11,7 +11,7 @@ ## 内部架构 -- `cmd/hwlab-codex-api-responses-forwarder/main.mjs` 启动 HTTP proxy。 +- `cmd/hwlab-codex-api-responses-forwarder/main.ts` 启动 HTTP proxy。 - listen host 默认 `127.0.0.1`,端口默认 `49280`。 - upstream 默认 `https://hyueapi.com`,可由 `HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL` 覆盖。 - forwarder 会过滤 hop-by-hop headers、限制请求体大小,并按 upstream base path 规范化 `/v1/responses`、`/v1/models` 等路径。 diff --git a/docs/reference/spec-v02-deepseek-proxy.md b/docs/reference/spec-v02-deepseek-proxy.md index d5f677ce..7ee446f3 100644 --- a/docs/reference/spec-v02-deepseek-proxy.md +++ b/docs/reference/spec-v02-deepseek-proxy.md @@ -11,7 +11,7 @@ ## 内部架构 - `ConfigMap/hwlab-deepseek-proxy-config` 提供 `render-config.sh`,initContainer 把 `DEEPSEEK_API_KEY` 写入 `/config/config.yml`。 -- `responses-bridge` 容器运行 `cmd/hwlab-deepseek-responses-bridge/main.mjs`,监听 `4000`,处理 `/health`、`/health/readiness`、`/v1/models` 和 `/v1/responses`。 +- `responses-bridge` 容器运行 `cmd/hwlab-deepseek-responses-bridge/main.ts`,监听 `4000`,处理 `/health`、`/health/readiness`、`/v1/models` 和 `/v1/responses`。 - `moonbridge` 容器监听 `4001`,读取渲染后的 config,负责 DeepSeek provider 调用和模型列表。 - bridge 会解压 zstd body,并过滤 Responses tools 中 Moon Bridge 不支持的 tool type。 diff --git a/docs/reference/spec-v02-hwlab-agent-mgr.md b/docs/reference/spec-v02-hwlab-agent-mgr.md index 1356d910..4b046384 100644 --- a/docs/reference/spec-v02-hwlab-agent-mgr.md +++ b/docs/reference/spec-v02-hwlab-agent-mgr.md @@ -11,7 +11,7 @@ ## 内部架构 -- `cmd/hwlab-agent-mgr/main.mjs` 同时提供 CLI 和健康 HTTP server。 +- `cmd/hwlab-agent-mgr/main.ts` 同时提供 CLI 和健康 HTTP server。 - `internal/agent/runtime.mjs` 负责文件系统状态、session lifecycle、trace/evidence 和 cleanup。 - `internal/agent/index.mjs` 定义 serviceId、session status 和 skills manifest 结构。 - `HWLAB_AGENT_RUNTIME_STATE_DIR` 可隔离状态目录;默认 `.state/agent-runtime` 只用于本地/测试状态。 @@ -22,7 +22,7 @@ | --- | --- | | `GET /health`、`GET /health/live` | HTTP server 模式下返回 manager health、skills 注入状态和 build metadata。 | | `GET /`、`GET /help` | 返回 CLI/服务用法。 | -| `node cmd/hwlab-agent-mgr/main.mjs health` | 本地 JSON health。 | +| `bun run cmd/hwlab-agent-mgr/main.ts health` | 本地 JSON health。 | | `create/status/trace/evidence/cleanup` | 本地 session lifecycle CLI;必须传 `--agent-session-id`,create 还需 `--project-id`。 | ## 测试规格 diff --git a/docs/reference/spec-v02-hwlab-agent-worker.md b/docs/reference/spec-v02-hwlab-agent-worker.md index 28df2765..16565896 100644 --- a/docs/reference/spec-v02-hwlab-agent-worker.md +++ b/docs/reference/spec-v02-hwlab-agent-worker.md @@ -10,7 +10,7 @@ ## 内部架构 -- `cmd/hwlab-agent-worker/main.mjs` 提供 CLI 命令 `health` 和 `dry-run/smoke`。 +- `cmd/hwlab-agent-worker/main.ts` 提供 CLI 命令 `health` 和 `dry-run/smoke`。 - `internal/agent/runtime.mjs` 提供 `runLocalWorkerDryRun`,读取 manager 创建的本地 session 状态并写入 evidence。 - Job template 通过 env 注入 `HWLAB_AGENT_SESSION_ID`、`HWLAB_SKILL_COMMIT_ID` 和 `HWLAB_SKILL_VERSION`。 @@ -18,8 +18,8 @@ | 接口 | 说明 | | --- | --- | -| `node cmd/hwlab-agent-worker/main.mjs health` | 返回 worker health、runtime mode 和 skills 注入状态。 | -| `node cmd/hwlab-agent-worker/main.mjs dry-run --agent-session-id ID` | 执行本地 dry-run worker 流程,写入 trace/evidence。 | +| `bun run cmd/hwlab-agent-worker/main.ts health` | 返回 worker health、runtime mode 和 skills 注入状态。 | +| `bun run cmd/hwlab-agent-worker/main.ts dry-run --agent-session-id ID` | 执行本地 dry-run worker 流程,写入 trace/evidence。 | | Kubernetes Job template | `hwlab-agent-worker-template` 默认 `suspend: true`,只能由受控调度实例化。 | ## 测试规格 diff --git a/docs/reference/spec-v02-hwlab-device-pod-service.md b/docs/reference/spec-v02-hwlab-device-pod-service.md index 956abbe9..df6fe597 100644 --- a/docs/reference/spec-v02-hwlab-device-pod-service.md +++ b/docs/reference/spec-v02-hwlab-device-pod-service.md @@ -10,7 +10,7 @@ ## 内部架构 -- `cmd/hwlab-device-pod/main.mjs` 提供 HTTP server,端口默认 `7601`。 +- `cmd/hwlab-device-pod/main.ts` 提供 HTTP server,端口默认 `7601`。 - `internal/device-pod/fake-data.mjs` 构造 list、status、events、chip-id、UART status/tail 等 fake payload。 - `HWLAB_DEVICE_POD_ID` 可指定默认 devicePodId;当前不读取正式 `device_pods.profile_json`。 diff --git a/docs/reference/spec-v02-hwlab-edge-proxy.md b/docs/reference/spec-v02-hwlab-edge-proxy.md index 5d0e26f3..ef0b85c3 100644 --- a/docs/reference/spec-v02-hwlab-edge-proxy.md +++ b/docs/reference/spec-v02-hwlab-edge-proxy.md @@ -10,7 +10,7 @@ ## 内部架构 -- `cmd/hwlab-edge-proxy/main.mjs` 启动 HTTP server。 +- `cmd/hwlab-edge-proxy/main.ts` 启动 HTTP server。 - `internal/dev-entrypoint/http.mjs` 的 `proxyHttpRequest` 负责 streaming proxy、timeout 和错误 payload。 - `HWLAB_EDGE_UPSTREAM` 指向 v02 `hwlab-cloud-api` ClusterIP;`HWLAB_EDGE_PROXY_TIMEOUT_MS` 控制长 Code Agent 请求代理窗口。 diff --git a/internal/cloud/code-agent-contract.ts b/internal/cloud/code-agent-contract.ts index 6a6a5f08..179b838e 100644 --- a/internal/cloud/code-agent-contract.ts +++ b/internal/cloud/code-agent-contract.ts @@ -49,7 +49,7 @@ export const DEV_CODE_AGENT_PROVIDER_CONTRACT = Object.freeze({ }), forwarder: Object.freeze({ containerName: "hwlab-codex-api-forwarder", - command: Object.freeze(["node", "/app/cmd/hwlab-codex-api-responses-forwarder/main.mjs"]), + command: Object.freeze(["/usr/local/bin/bun", "run", "/app/cmd/hwlab-codex-api-responses-forwarder/main.ts"]), port: 49280, portName: "codex-api", listenHostEnv: "HWLAB_CODE_AGENT_CODEX_API_FORWARDER_HOST", diff --git a/package.json b/package.json index a9dd0b7b..ecfe7e12 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "report:lifecycle:test": "node scripts/report-lifecycle.test.mjs", "dev-runtime:provisioning": "node scripts/run-bun.mjs scripts/dev-runtime-provisioning.mjs", "dev-runtime:migration": "node scripts/run-bun.mjs scripts/dev-runtime-migration.mjs", - "dev-runtime:postflight": "node scripts/dev-runtime-postflight.mjs", + "dev-runtime:postflight": "node scripts/run-bun.mjs scripts/dev-runtime-postflight.mjs", "dev-runtime:hotfix-audit": "node scripts/dev-runtime-hotfix-audit.mjs", "rpt004:mvp:e2e": "node scripts/run-bun.mjs scripts/rpt004-mvp-e2e-harness.mjs", "rpt004:mvp:e2e:live": "node scripts/run-bun.mjs scripts/rpt004-mvp-e2e-harness.mjs --live --url http://74.48.78.17:16666/ --api-url http://74.48.78.17:16667/", diff --git a/scripts/artifact-runtime-readiness-guard.test.mjs b/scripts/artifact-runtime-readiness-guard.test.mjs index 5639abbc..f9c9614b 100644 --- a/scripts/artifact-runtime-readiness-guard.test.mjs +++ b/scripts/artifact-runtime-readiness-guard.test.mjs @@ -312,7 +312,7 @@ test("compact summary blocks publish on stale Cloud Web source dist", () => { assert.equal(summary.canApply, false); assert.equal(summary.cloudWebBuildFreshness.status, "blocked"); assert.ok(summary.blockedReasons.some((item) => item.id === "cloud-web-build-freshness")); - assert.ok(summary.nextCommands.includes("node web/hwlab-cloud-web/scripts/build.mjs")); + assert.ok(summary.nextCommands.includes("bun run --cwd web/hwlab-cloud-web build")); }); test("compact summary separates Code Agent timeout and durable runtime blockers from apply readiness", () => { diff --git a/scripts/check-runner.test.mjs b/scripts/check-runner.test.mjs index 1502fe88..647b96ba 100644 --- a/scripts/check-runner.test.mjs +++ b/scripts/check-runner.test.mjs @@ -18,6 +18,15 @@ test("check plan exposes grouped tasks", () => { assert.ok(checkProfiles.validate.length > 5); assert.ok(checkProfiles.check.some((task) => task.group === "cloud-api")); assert.ok(checkProfiles.check.some((task) => task.group === "cloud-web")); + assert.ok(checkProfiles.check.some((task) => task.group === "runtime-services")); +}); + +test("check plan runs migrated runtime service TypeScript tests", () => { + const commands = checkProfiles.check.map((task) => task.command.join(" ")); + assert.ok(commands.some((command) => command.includes("cmd/hwlab-edge-proxy/main.test.ts"))); + assert.ok(commands.some((command) => command.includes("cmd/hwlab-deepseek-responses-bridge/main.test.ts"))); + assert.equal(commands.some((command) => command.includes("cmd/hwlab-edge-proxy/main.mjs")), false); + assert.equal(commands.some((command) => command.includes("cmd/hwlab-deepseek-responses-bridge/main.test.mjs")), false); }); test("runner filters profile tasks by group", () => { diff --git a/scripts/code-agent-chat-smoke.mjs b/scripts/code-agent-chat-smoke.mjs index 3c5b1f4c..680841cb 100644 --- a/scripts/code-agent-chat-smoke.mjs +++ b/scripts/code-agent-chat-smoke.mjs @@ -155,6 +155,7 @@ async function runLocalContractSmoke() { HWLAB_CODE_AGENT_CODEX_WORKSPACE: stdioWorkspace, HWLAB_CODE_AGENT_CODEX_SANDBOX: "workspace-write", HWLAB_CODE_AGENT_SKILLS_DIRS: stdioSkillsDir, + HWLAB_CODE_AGENT_SKILLS_STRICT: "1", HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses" }; const stdioPwd = await handleCodeAgentChat( @@ -213,7 +214,7 @@ async function runLocalContractSmoke() { assert.ok(runnerTraceLabels(stdioPwd).includes("tool:codex-app-server.thread/start+turn/start:started")); assert.ok(runnerTraceLabels(stdioPwd).includes("tool:codex-app-server.thread/start+turn/start:output_chunk")); assert.ok(runnerTraceLabels(stdioPwd).includes("tool:codex-app-server.thread/start+turn/start:completed")); - assert.ok(runnerTraceLabels(stdioPwd).includes("assistant:chunk")); + assert.ok(stdioPwd.runnerTrace.assistantStreams.some((stream) => stream.label === "assistant:stream" && stream.chunkCount >= 1)); assert.ok(runnerTraceLabels(stdioPwd).includes("assistant:completed")); assert.equal(classifyCodexRunnerCapability(stdioPwd, { httpStatus: 200 }).capabilityPass, true); diff --git a/scripts/deploy-desired-state-plan.test.mjs b/scripts/deploy-desired-state-plan.test.mjs index 5e100ab4..be85e2c1 100644 --- a/scripts/deploy-desired-state-plan.test.mjs +++ b/scripts/deploy-desired-state-plan.test.mjs @@ -281,7 +281,7 @@ async function makeFixture({ { name: "hwlab-codex-api-forwarder", image: workloadImage, - command: ["node", "/app/cmd/hwlab-codex-api-responses-forwarder/main.mjs"], + command: ["/usr/local/bin/bun", "run", "/app/cmd/hwlab-codex-api-responses-forwarder/main.ts"], ports: [{ name: "codex-api", containerPort: 49280 }], env: [ { name: "HWLAB_CODE_AGENT_CODEX_API_FORWARDER_HOST", value: "127.0.0.1" }, diff --git a/scripts/dev-cloud-workbench-layout-smoke.mjs b/scripts/dev-cloud-workbench-layout-smoke.mjs index 4817eb9e..d2bd20b1 100644 --- a/scripts/dev-cloud-workbench-layout-smoke.mjs +++ b/scripts/dev-cloud-workbench-layout-smoke.mjs @@ -102,7 +102,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { command: `node scripts/dev-cloud-workbench-layout-smoke.mjs [--static|--build|--live --url http://74.48.78.17:16666/] [--report ${tempReportPath("dev-cloud-workbench-layout.json")}]`, layoutNotes: [ "默认 --static 使用 source/static 本地静态服务。", - "--build 会先运行 web/hwlab-cloud-web/scripts/build.mjs,再检查本地 dist 构建产物。", + "--build 会先运行 cd web/hwlab-cloud-web && bun run build,再检查本地 dist 构建产物。", "--live --url http://74.48.78.17:16666/ 只做 DEV live UI 布局/命中/溢出检查,不发送 Code Agent 消息,不调用 Device Pod 或硬件写操作,不等同于硬件验收。", "结构化失败会包含 status、viewport、selector、failureType、artifact screenshot/report path。" ] diff --git a/scripts/dev-cloud-workbench-smoke.test.mjs b/scripts/dev-cloud-workbench-smoke.test.mjs index af6abe6c..14de83be 100644 --- a/scripts/dev-cloud-workbench-smoke.test.mjs +++ b/scripts/dev-cloud-workbench-smoke.test.mjs @@ -26,6 +26,7 @@ import { compactLayoutSmokeCliOutput, parseLayoutSmokeArgs } from "./dev-cloud-workbench-layout-smoke.mjs"; +import { checkProfiles } from "./src/check-plan.mjs"; const sourceIdentity = Object.freeze({ status: "observed", @@ -46,7 +47,7 @@ const expectedRuntimeIdentity = Object.freeze({ }); const rootPackage = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); const cloudWebPackage = JSON.parse(readFileSync(new URL("../web/hwlab-cloud-web/package.json", import.meta.url), "utf8")); -const cloudWebCheckSource = readFileSync(new URL("../web/hwlab-cloud-web/scripts/check.mjs", import.meta.url), "utf8"); +const cloudWebCheckSource = readFileSync(new URL("../web/hwlab-cloud-web/scripts/check.ts", import.meta.url), "utf8"); test("workbench smoke defaults to SOURCE mode and requires live confirmation before DEV-LIVE provider calls", () => { const defaultArgs = parseSmokeArgs([]); @@ -314,17 +315,17 @@ test("live web asset identity blocks stale deployed primary assets", () => { const pass = classifyLiveWebAssetIdentity([ { path: "index.html", status: "match" }, { path: "styles.css", status: "match" }, - { path: "app.mjs", status: "match" } + { path: "app.js", status: "match" } ]); assert.equal(pass.status, "pass"); const stale = classifyLiveWebAssetIdentity([ { path: "index.html", status: "match" }, { path: "styles.css", status: "mismatch" }, - { path: "app.mjs", status: "mismatch" } + { path: "app.js", status: "mismatch" } ]); assert.equal(stale.status, "blocked"); - assert.deepEqual(stale.mismatches, ["styles.css", "app.mjs"]); + assert.deepEqual(stale.mismatches, ["styles.css", "app.js"]); assert.match(stale.summary, /Deployment drift/u); }); @@ -731,8 +732,9 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou }); test("repo-owned web checks expose source build and DEV live layout smoke gates", () => { - assert.match(rootPackage.scripts["web:check"], /^node web\/hwlab-cloud-web\/scripts\/check\.mjs/u); - assert.match(rootPackage.scripts["web:check"], /node --test web\/hwlab-cloud-web\/message-markdown\.test\.mjs web\/hwlab-cloud-web\/scripts\/trace-scroll\.test\.mjs/u); + const checkCommands = checkProfiles.check.map((task) => task.command.join(" ")); + assert.match(rootPackage.scripts["web:check"], /cd web\/hwlab-cloud-web && bun run check/u); + assert.match(rootPackage.scripts["web:check"], /bun test web\/hwlab-cloud-web\/message-markdown\.test\.ts/u); assert.equal( rootPackage.scripts["web:layout"], "node scripts/dev-cloud-workbench-layout-smoke.mjs --static --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout.json" @@ -745,9 +747,9 @@ test("repo-owned web checks expose source build and DEV live layout smoke gates" rootPackage.scripts["web:layout:live"], "node scripts/dev-cloud-workbench-layout-smoke.mjs --live --url http://74.48.78.17:16666/ --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout-live.json" ); - assert.match(rootPackage.scripts.check, /node --check scripts\/dev-cloud-workbench-layout-smoke\.mjs/u); - assert.match(rootPackage.scripts.check, /node web\/hwlab-cloud-web\/scripts\/check\.mjs/u); - assert.doesNotMatch(rootPackage.scripts.check, /gate-summary\.mjs|export-web-gate-summary/u); + assert.ok(checkCommands.some((command) => command === "node --check scripts/dev-cloud-workbench-layout-smoke.mjs")); + assert.ok(checkProfiles.check.some((task) => task.cwd === "web/hwlab-cloud-web" && task.command.join(" ") === "bun run check")); + assert.doesNotMatch(checkCommands.join("\n"), /gate-summary\.mjs|export-web-gate-summary/u); assert.equal(cloudWebPackage.scripts.layout.includes("--static"), true); assert.equal(cloudWebPackage.scripts["layout:build"].includes("--build"), true); assert.equal(cloudWebPackage.scripts["layout:live"].includes("--live --url http://74.48.78.17:16666/"), true); diff --git a/scripts/g14-ci-plan.test.mjs b/scripts/g14-ci-plan.test.mjs index 4e4b621e..56e699a4 100644 --- a/scripts/g14-ci-plan.test.mjs +++ b/scripts/g14-ci-plan.test.mjs @@ -17,7 +17,7 @@ const execFileAsync = promisify(execFile); test("G14 CI planner maps bridge changes to cloud-api only", async () => { const repo = await createFixtureRepo(); - await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.mjs"), "console.log('bridge v2');\n"); + await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.ts"), "console.log('bridge v2');\n"); await git(repo, ["add", "."]); await git(repo, ["commit", "-m", "change bridge"]); @@ -72,7 +72,7 @@ test("component model uses built-in service paths", () => { test("planner remains compatible with deploy.k3s.serviceMappings shape", async () => { const repo = await createFixtureRepo({ deployServices: false, k3sServiceMappings: true }); - await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.mjs"), "console.log('bridge v2');\n"); + await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.ts"), "console.log('bridge v2');\n"); await git(repo, ["add", "."]); await git(repo, ["commit", "-m", "change bridge"]); @@ -85,7 +85,7 @@ test("planner remains compatible with deploy.k3s.serviceMappings shape", async ( test("global change classifier distinguishes gitops-only and test-only", () => { assert.equal(classifyGlobalChange(["deploy/gitops/g14/runtime-dev/workloads.yaml"]).gitopsOnly, true); - assert.equal(classifyGlobalChange(["cmd/hwlab-cloud-api/main.test.mjs"]).testOnly, true); + assert.equal(classifyGlobalChange(["cmd/hwlab-cloud-api/runtime-options.test.ts"]).testOnly, true); }); async function createFixtureRepo(options = {}) { @@ -104,8 +104,8 @@ async function createFixtureRepo(options = {}) { await writeFile(path.join(repo, "package.json"), JSON.stringify({ type: "module" }, null, 2)); await writeFile(path.join(repo, "package-lock.json"), JSON.stringify({ lockfileVersion: 3 }, null, 2)); await writeFile(path.join(repo, "cmd/hwlab-cloud-api/main.ts"), "console.log('api');\n"); - await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.mjs"), "console.log('bridge');\n"); - await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.test.mjs"), "console.log('test');\n"); + await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.ts"), "console.log('bridge');\n"); + await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.test.ts"), "console.log('test');\n"); await writeFile(path.join(repo, "web/hwlab-cloud-web/index.html"), "\n"); await git(repo, ["init"]); await git(repo, ["add", "."]); diff --git a/scripts/g14-gitops-render.mjs b/scripts/g14-gitops-render.mjs index 52129abb..639706a6 100644 --- a/scripts/g14-gitops-render.mjs +++ b/scripts/g14-gitops-render.mjs @@ -132,9 +132,7 @@ NODE`, { name: "codex-api-forwarder-check", commands: [ - "node --check cmd/hwlab-codex-api-responses-forwarder/main.mjs", - "node --check cmd/hwlab-codex-api-responses-forwarder/main.test.mjs", - "node --test cmd/hwlab-codex-api-responses-forwarder/main.test.mjs" + "/root/.bun/bin/bun test cmd/hwlab-codex-api-responses-forwarder/main.test.ts" ] } ]); @@ -2602,7 +2600,7 @@ function deepSeekProxyManifest({ profile = "dev", source, registryPrefix = defau name: "responses-bridge", image: bridgeImage, imagePullPolicy: "IfNotPresent", - command: ["node", "/app/cmd/hwlab-deepseek-responses-bridge/main.mjs"], + command: ["/usr/local/bin/bun", "run", "/app/cmd/hwlab-deepseek-responses-bridge/main.ts"], env: [ { name: "PORT", value: "4000" }, { name: "HWLAB_COMMIT_ID", value: bridgeCommit }, diff --git a/scripts/gateway-outbound-demo-smoke.mjs b/scripts/gateway-outbound-demo-smoke.mjs index df26ebd0..7bd6d825 100644 --- a/scripts/gateway-outbound-demo-smoke.mjs +++ b/scripts/gateway-outbound-demo-smoke.mjs @@ -39,7 +39,7 @@ try { await waitForJson(`http://127.0.0.1:${cloudPort}/health/live`); if (useEdgeProxy) { - const edge = startNode("cmd/hwlab-edge-proxy/main.mjs", { + const edge = startNode("cmd/hwlab-edge-proxy/main.ts", { ...commonEnv, HWLAB_EDGE_HOST: "127.0.0.1", HWLAB_EDGE_PORT: String(edgePort), diff --git a/scripts/m1-contract-smoke.mjs b/scripts/m1-contract-smoke.mjs index c2e8cd62..077dce28 100644 --- a/scripts/m1-contract-smoke.mjs +++ b/scripts/m1-contract-smoke.mjs @@ -48,8 +48,8 @@ function freePort() { } function startNode(relativeScript, env = {}) { - const command = relativeScript.endsWith(".ts") ? bunCommand : process.execPath; - const child = spawn(command, [relativeScript], { + const command = commandForScript(relativeScript); + const child = spawn(command.bin, command.args, { cwd: repoRoot, env: { ...process.env, @@ -87,7 +87,8 @@ function resolveBunCommand() { function runNodeJson(relativeScript, args = [], env = {}) { return new Promise((resolve, reject) => { - const child = spawn(process.execPath, [relativeScript, ...args], { + const command = commandForScript(relativeScript, args); + const child = spawn(command.bin, command.args, { cwd: repoRoot, env: { ...process.env, @@ -121,6 +122,19 @@ function runNodeJson(relativeScript, args = [], env = {}) { }); } +function commandForScript(relativeScript, args = []) { + if (relativeScript.endsWith(".ts")) { + return { + bin: bunCommand, + args: ["run", relativeScript, ...args] + }; + } + return { + bin: process.execPath, + args: [relativeScript, ...args] + }; +} + async function stopNode(record) { const { child } = record; if (child.exitCode !== null || child.signalCode !== null) { @@ -547,7 +561,7 @@ async function smokeAgentMgrLocalContract() { let agentMgr; try { - agentMgr = startNode("cmd/hwlab-agent-mgr/main.mjs", { + agentMgr = startNode("cmd/hwlab-agent-mgr/main.ts", { PORT: String(agentMgrPort), HWLAB_AGENT_RUNTIME_STATE_DIR: stateDir, HWLAB_ENVIRONMENT: ENVIRONMENT_DEV @@ -567,7 +581,7 @@ async function smokeAgentMgrLocalContract() { assert.ok(healthLive.body.health.skills.missing.includes("manager.skillVersion")); assert.equal(healthLive.body.skillsManifest, null); - const health = await runNodeJson("cmd/hwlab-agent-mgr/main.mjs", [ + const health = await runNodeJson("cmd/hwlab-agent-mgr/main.ts", [ "health", "--state-dir", stateDir, @@ -587,7 +601,7 @@ async function smokeAgentMgrLocalContract() { assert.equal(health.skillsManifest.services[0].serviceId, "hwlab-agent-skills"); assert.equal(health.skillsManifest.environment, ENVIRONMENT_DEV); - const created = await runNodeJson("cmd/hwlab-agent-mgr/main.mjs", [ + const created = await runNodeJson("cmd/hwlab-agent-mgr/main.ts", [ "create", "--state-dir", stateDir, @@ -616,7 +630,7 @@ async function smokeAgentMgrLocalContract() { assert.equal(created.traceCount, 1); assert.equal(created.evidenceCount, 0); - const status = await runNodeJson("cmd/hwlab-agent-mgr/main.mjs", [ + const status = await runNodeJson("cmd/hwlab-agent-mgr/main.ts", [ "status", "--state-dir", stateDir, @@ -629,7 +643,7 @@ async function smokeAgentMgrLocalContract() { assert.equal(status.status, "ok"); assert.equal(status.task.taskId, `tsk_${agentSessionId}`); - const cleanup = await runNodeJson("cmd/hwlab-agent-mgr/main.mjs", [ + const cleanup = await runNodeJson("cmd/hwlab-agent-mgr/main.ts", [ "cleanup", "--state-dir", stateDir, diff --git a/scripts/src/check-plan.mjs b/scripts/src/check-plan.mjs index 9334157f..5b8b90a5 100644 --- a/scripts/src/check-plan.mjs +++ b/scripts/src/check-plan.mjs @@ -11,7 +11,7 @@ export const checkProfiles = Object.freeze({ { id: "validate-004-deploy-run-bun", group: "deploy", command: ["node","scripts/run-bun.mjs","scripts/deploy-desired-state-plan.mjs","--check"] }, { id: "validate-005-dev-runtime-run-bun", group: "dev-runtime", command: ["node","scripts/run-bun.mjs","scripts/dev-runtime-provisioning.mjs","--check"] }, { id: "validate-006-dev-runtime-run-bun", group: "dev-runtime", command: ["node","scripts/run-bun.mjs","scripts/dev-runtime-migration.mjs","--check"] }, - { id: "validate-007-dev-runtime-dev-runtime-postflight", group: "dev-runtime", command: ["node","scripts/dev-runtime-postflight.mjs","--check"] }, + { id: "validate-007-dev-runtime-run-bun", group: "dev-runtime", command: ["node","scripts/run-bun.mjs","scripts/dev-runtime-postflight.mjs","--check"] }, { id: "validate-008-dev-runtime-dev-runtime-hotfix-audit", group: "dev-runtime", command: ["node","scripts/dev-runtime-hotfix-audit.mjs"] }, { id: "validate-009-smoke-run-bun", group: "smoke", command: ["node","scripts/run-bun.mjs","scripts/rpt004-mvp-e2e-harness.mjs","--check","--no-write"] }, { id: "validate-010-artifact-artifact-runtime-readiness-guard", group: "artifact", command: ["node","--check","scripts/artifact-runtime-readiness-guard.mjs"] }, @@ -68,13 +68,7 @@ export const checkProfiles = Object.freeze({ { id: "check-045-cloud-api-run-bun", group: "cloud-api", command: ["node","scripts/run-bun.mjs","build","cmd/hwlab-cloud-api/main.ts","--target=bun","--packages=external","--outdir=/tmp/hwlab-ts-check"] }, { id: "check-046-cloud-api-run-bun", group: "cloud-api", command: ["node","scripts/run-bun.mjs","build","cmd/hwlab-cloud-api/provision.ts","--target=bun","--packages=external","--outdir=/tmp/hwlab-ts-check"] }, { id: "check-047-cloud-api-run-bun", group: "cloud-api", command: ["node","scripts/run-bun.mjs","build","cmd/hwlab-cloud-api/migrate.ts","--target=bun","--packages=external","--outdir=/tmp/hwlab-ts-check"] }, - { id: "check-048-repo-main", group: "repo", command: ["node","--check","cmd/hwlab-deepseek-responses-bridge/main.mjs"] }, - { id: "check-049-repo-main-test", group: "repo", command: ["node","--check","cmd/hwlab-deepseek-responses-bridge/main.test.mjs"] }, - { id: "check-050-repo-main-test", group: "repo", command: ["node","--test","cmd/hwlab-deepseek-responses-bridge/main.test.mjs"] }, - { id: "check-051-repo-main", group: "repo", command: ["node","--check","cmd/hwlab-edge-proxy/main.mjs"] }, - { id: "check-052-repo-main", group: "repo", command: ["node","--check","cmd/hwlab-agent-mgr/main.mjs"] }, - { id: "check-053-repo-main", group: "repo", command: ["node","--check","cmd/hwlab-agent-worker/main.mjs"] }, - { id: "check-054-repo-main", group: "repo", command: ["node","--check","cmd/hwlab-device-pod/main.mjs"] }, + { id: "check-048-runtime-services-run-bun", group: "runtime-services", command: ["node","scripts/run-bun.mjs","test","cmd/hwlab-edge-proxy/main.test.ts","cmd/hwlab-device-pod/main.test.ts","cmd/hwlab-agent-mgr/main.test.ts","cmd/hwlab-agent-worker/main.test.ts","cmd/hwlab-codex-api-responses-forwarder/main.test.ts","cmd/hwlab-deepseek-responses-bridge/main.test.ts"] }, { id: "check-055-repo-main", group: "repo", command: ["node","--check","cmd/hwlab-box-simu/main.mjs"] }, { id: "check-056-repo-main", group: "repo", command: ["node","--check","cmd/hwlab-gateway/main.mjs"] }, { id: "check-057-repo-main", group: "repo", command: ["node","--check","cmd/hwlab-gateway-simu/main.mjs"] }, @@ -157,8 +151,8 @@ export const checkProfiles = Object.freeze({ { id: "check-134-dev-runtime-run-bun", group: "dev-runtime", command: ["node","scripts/run-bun.mjs","scripts/dev-runtime-migration.mjs","--check"] }, { id: "check-135-cloud-api-run-bun", group: "cloud-api", command: ["node","scripts/run-bun.mjs","cmd/hwlab-cloud-api/provision.ts","--check"] }, { id: "check-136-cloud-api-run-bun", group: "cloud-api", command: ["node","scripts/run-bun.mjs","cmd/hwlab-cloud-api/migrate.ts","--check"] }, - { id: "check-137-dev-runtime-dev-runtime-postflight", group: "dev-runtime", command: ["node","scripts/dev-runtime-postflight.mjs","--check"] }, - { id: "check-138-smoke-rpt004-mvp-e2e-harness", group: "smoke", command: ["node","scripts/rpt004-mvp-e2e-harness.mjs","--check","--no-write"] }, + { id: "check-137-dev-runtime-run-bun", group: "dev-runtime", command: ["node","scripts/run-bun.mjs","scripts/dev-runtime-postflight.mjs","--check"] }, + { id: "check-138-smoke-run-bun", group: "smoke", command: ["node","scripts/run-bun.mjs","scripts/rpt004-mvp-e2e-harness.mjs","--check","--no-write"] }, { id: "check-139-smoke-dev-evidence-blocker-aggregator", group: "smoke", command: ["node","scripts/dev-evidence-blocker-aggregator.mjs","--check"] }, { id: "check-140-smoke-l2-runtime-contract-smoke", group: "smoke", command: ["node","scripts/l2-runtime-contract-smoke.mjs"] }, { id: "check-141-smoke-l6-cli-web-smoke", group: "smoke", command: ["node","scripts/l6-cli-web-smoke.mjs"] }, diff --git a/scripts/src/dev-artifact-services.mjs b/scripts/src/dev-artifact-services.mjs index aa91d10c..0f2a7eca 100644 --- a/scripts/src/dev-artifact-services.mjs +++ b/scripts/src/dev-artifact-services.mjs @@ -63,17 +63,15 @@ function serviceSourceState(catalog, serviceId) { export async function resolveDevArtifactService(repoRoot, serviceId, catalog = null) { const sourceState = serviceSourceState(catalog, serviceId); - if (serviceId === "hwlab-cloud-api") { - const tsCmdPath = `cmd/${serviceId}/main.ts`; - if (await pathExists(repoRoot, tsCmdPath)) { - return withServicePublishPolicy({ - serviceId, - runtimeKind: "bun-command", - entrypoint: tsCmdPath, - implementationState: "repo-entrypoint", - sourceState - }); - } + const tsCmdPath = `cmd/${serviceId}/main.ts`; + if (await pathExists(repoRoot, tsCmdPath)) { + return withServicePublishPolicy({ + serviceId, + runtimeKind: "bun-command", + entrypoint: tsCmdPath, + implementationState: "repo-entrypoint", + sourceState + }); } const cmdPath = `cmd/${serviceId}/main.mjs`; diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index 29336370..100b6c5a 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -6,7 +6,6 @@ import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { buildDevicePodRestPayload } from "../../internal/device-pod/fake-data.mjs"; -import { runtime } from "../../web/hwlab-cloud-web/runtime.mjs"; import { classifyCodeAgentBrowserJourney, classifyCodeAgentBrowserFailure, @@ -20,6 +19,7 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../ const domOnlyReportPath = tempReportPath("dev-cloud-workbench-dom-only.json"); const liveReportPath = tempReportPath("dev-cloud-workbench-live.json"); const webRoot = path.join(repoRoot, "web/hwlab-cloud-web"); +const runtime = readCloudWebRuntime(); const defaultLiveUrl = "http://74.48.78.17:16666/"; const helpOwner = "codex_1779444232735_1"; const legacyFailureWindowMs = 4500; @@ -76,23 +76,6 @@ const readOnlyRpcMethods = Object.freeze([ "cloud.adapter.describe" ]); -const requiredWebAssets = Object.freeze([ - "index.html", - "styles.css", - "auth.mjs", - "app.mjs", - "code-agent-facts.mjs", - "code-agent-status.mjs", - "message-markdown.mjs", - "live-status.mjs", - "runtime.mjs", - "help.md", - "third_party/marked/marked.esm.js", - "third_party/marked/LICENSE" -]); - -const liveIdentityWebAssets = Object.freeze(["index.html", "styles.css", "auth.mjs", "app.mjs", "code-agent-facts.mjs", "code-agent-status.mjs", "message-markdown.mjs", "live-status.mjs"]); - const chineseWorkbenchLabels = Object.freeze([ "云工作台登录", "HWLAB 云工作台", @@ -296,6 +279,37 @@ const markdownRendererPackages = Object.freeze([ "commonmark" ]); +const cloudWebAppSourceFiles = Object.freeze([ + "app.ts", + "app-device-pod.ts", + "app-conversation.ts", + "app-trace.ts", + "app-helpers.ts" +]); + +const cloudWebModuleSourceFiles = Object.freeze([ + "auth.ts", + "code-agent-facts.ts", + "code-agent-status.ts", + "message-markdown.ts", + "live-status.ts", + "runtime.ts" +]); + +const requiredWebAssets = Object.freeze([ + "index.html", + "styles.css", + ...cloudWebAppSourceFiles, + ...cloudWebModuleSourceFiles, + "favicon.svg", + "favicon.ico", + "help.md", + "third_party/marked/marked.esm.js", + "third_party/marked/LICENSE" +]); + +const liveIdentityWebAssets = Object.freeze(["index.html", "styles.css", "app.js"]); + const playwrightDependencyCommand = "npm install"; const playwrightDependencySummary = "Install repository dependencies before running browser smokes: npm install provides the declared playwright package; the Code Queue image may also provide the same package globally."; @@ -418,8 +432,8 @@ function runStaticSmoke() { const files = readStaticFiles(); const source = Object.values(files).join("\n"); const artifactPublisher = readText("scripts/artifact-publish.mjs"); - const buildScript = readText("web/hwlab-cloud-web/scripts/build.mjs"); - const distContractScript = readText("web/hwlab-cloud-web/scripts/dist-contract.mjs"); + const buildScript = readText("web/hwlab-cloud-web/scripts/build.ts"); + const distContractScript = readText("web/hwlab-cloud-web/scripts/dist-contract.ts"); addCheck(checks, blockers, "static-web-assets", assetsExist(), "Cloud Web source assets are present.", { evidence: requiredWebAssets.map((file) => `web/hwlab-cloud-web/${file}`) @@ -427,7 +441,7 @@ function runStaticSmoke() { addCheck(checks, blockers, "default-login-entry", hasDefaultLoginEntry(files, artifactPublisher), "Default 16666 route shows the Chinese login page before the Cloud Workbench is exposed.", { blocker: "runtime_blocker", - evidence: ["云工作台登录", "账号或密码不正确,请重新输入。", "data-app-shell hidden", "auth.mjs", "HWLAB_CLOUD_WEB_AUTH_USERNAME", "HWLAB_CLOUD_WEB_AUTH_PASSWORD"] + evidence: ["云工作台登录", "账号或密码不正确,请重新输入。", "data-app-shell hidden", "auth.ts", "HWLAB_CLOUD_WEB_AUTH_USERNAME", "HWLAB_CLOUD_WEB_AUTH_PASSWORD"] }); addCheck(checks, blockers, "default-workbench-shell", hasDefaultWorkbench(files), "Authenticated route is the VS Code-style Cloud Workbench.", { @@ -955,7 +969,7 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) { } if (!useLiveUrl && args.build === true) { - execFileSync("node", ["web/hwlab-cloud-web/scripts/build.mjs"], { + execFileSync(resolveBunCommand(), ["run", "web/hwlab-cloud-web/scripts/build.ts"], { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], @@ -1631,18 +1645,18 @@ function readStaticFiles() { return { html: readText("web/hwlab-cloud-web/index.html"), styles: readText("web/hwlab-cloud-web/styles.css"), - auth: readText("web/hwlab-cloud-web/auth.mjs"), - app: readText("web/hwlab-cloud-web/app.mjs"), - codeAgentFacts: readText("web/hwlab-cloud-web/code-agent-facts.mjs"), - codeAgentStatus: readText("web/hwlab-cloud-web/code-agent-status.mjs"), - messageMarkdown: readText("web/hwlab-cloud-web/message-markdown.mjs"), - liveStatus: readText("web/hwlab-cloud-web/live-status.mjs"), - runtime: readText("web/hwlab-cloud-web/runtime.mjs"), + auth: readText("web/hwlab-cloud-web/auth.ts"), + app: readCloudWebAppSource(), + codeAgentFacts: readText("web/hwlab-cloud-web/code-agent-facts.ts"), + codeAgentStatus: readText("web/hwlab-cloud-web/code-agent-status.ts"), + messageMarkdown: readText("web/hwlab-cloud-web/message-markdown.ts"), + liveStatus: readText("web/hwlab-cloud-web/live-status.ts"), + runtime: readText("web/hwlab-cloud-web/runtime.ts"), help: readText("web/hwlab-cloud-web/help.md"), artifactPublisher: readText("scripts/artifact-publish.mjs"), cloudApiServer: readText("internal/cloud/server.ts"), devicePodData: readText("internal/device-pod/fake-data.mjs"), - devicePodService: readText("cmd/hwlab-device-pod/main.mjs"), + devicePodService: readText("cmd/hwlab-device-pod/main.ts"), protocol: readText("internal/protocol/index.mjs"), deployJson: readText("deploy/deploy.json"), artifactCatalog: readText("deploy/artifact-catalog.dev.json") @@ -1653,6 +1667,24 @@ function readText(relativePath) { return fs.readFileSync(path.join(repoRoot, relativePath), "utf8"); } +function readCloudWebAppSource() { + return cloudWebAppSourceFiles.map((file) => readText(`web/hwlab-cloud-web/${file}`)).join("\n"); +} + +function readCloudWebRuntime() { + const source = fs.readFileSync(path.join(webRoot, "runtime.ts"), "utf8"); + const match = source.match(/export const runtime\s*=\s*([\s\S]*?);\s*$/u); + if (!match) throw new Error("Unable to parse web/hwlab-cloud-web/runtime.ts"); + return Function(`return (${match[1]});`)(); +} + +function resolveBunCommand() { + for (const candidate of [process.env.HWLAB_BUN_BINARY, process.env.BUN_BINARY, "/root/.bun/bin/bun", "bun"]) { + if (typeof candidate === "string" && candidate.trim()) return candidate; + } + return "bun"; +} + function observeSourceIdentity() { const head = runGit(["rev-parse", "--verify", "HEAD"]); const shortHead = head ? runGit(["rev-parse", "--short=12", "HEAD"]) : null; @@ -1835,7 +1867,7 @@ function identityVariantMatches(accepted, observed) { async function inspectLiveWebAssetIdentity(baseUrl, fetchedIndexHtml) { const assets = []; for (const assetPath of liveIdentityWebAssets) { - const sourceText = readText(`web/hwlab-cloud-web/${assetPath}`); + const sourceText = assetPath === "app.js" ? await buildCloudWebAppBundleText() : readText(`web/hwlab-cloud-web/${assetPath}`); const live = assetPath === "index.html" ? { ok: true, status: 200, contentType: "text/html; charset=utf-8", body: fetchedIndexHtml } : await fetchText(new URL(assetPath, baseUrl).toString()); @@ -1874,6 +1906,24 @@ function compareLiveAsset(assetPath, live, sourceText) { }; } +async function buildCloudWebAppBundleText() { + const tmpDir = fs.mkdtempSync(path.join("/tmp", "hwlab-cloud-web-live-asset-")); + const entryPath = path.join(webRoot, ".app-entry.generated.ts"); + try { + fs.writeFileSync(entryPath, `${readCloudWebAppSource()}\n`); + execFileSync(resolveBunCommand(), ["build", entryPath, "--target", "browser", "--format", "esm", "--outfile", path.join(tmpDir, "app.js")], { + cwd: repoRoot, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + timeout: 30000 + }); + return fs.readFileSync(path.join(tmpDir, "app.js"), "utf8"); + } finally { + fs.rmSync(entryPath, { force: true }); + fs.rmSync(tmpDir, { recursive: true, force: true }); + } +} + function hashText(value) { return createHash("sha256").update(value).digest("hex").slice(0, 16); } @@ -2197,10 +2247,10 @@ function hasScrollLockContract(styles) { return ( /html,\s*\nbody\s*\{[^\}]*height:\s*100%;[^\}]*overflow:\s*hidden;/su.test(styles) && /body\s*>\s*\[data-app-shell\]\s*\{[^\}]*min-height:\s*0;/su.test(styles) && - /\.workbench-shell\s*\{[^\}]*height:\s*100(?:d)?vh;[^\}]*overflow:\s*hidden;/su.test(styles) && + /\.workbench-shell\s*\{[^\}]*height:\s*100vh;[^\}]*height:\s*100dvh;[^\}]*overflow:\s*hidden;/su.test(styles) && /\.view\s*\{[^\}]*min-height:\s*0;[^\}]*overflow:\s*auto;/su.test(styles) && - /\.conversation-list[^\{]*\{[^\}]*min-height:\s*0;[^\}]*overflow:\s*auto;/su.test(styles) && - /\.device-event-scroll\s*\{[^\}]*min-height:\s*0;[^\}]*overflow:\s*auto;/su.test(styles) + /[^\{]*\.conversation-list[^\{]*\{[^\}]*min-height:\s*0;[^\}]*overflow:\s*auto;/su.test(styles) && + /\.device-event-scroll\s*\{[^\}]*min-height:\s*[^;]+;[^\}]*overflow:\s*auto;/su.test(styles) ); } @@ -2714,7 +2764,7 @@ function findMarkdownRenderers() { const json = JSON.parse(readText(file)); return Object.keys({ ...(json.dependencies ?? {}), ...(json.devDependencies ?? {}) }); }); - const source = `${readText("web/hwlab-cloud-web/app.mjs")}\n${readText("web/hwlab-cloud-web/index.html")}`; + const source = `${readCloudWebAppSource()}\n${readText("web/hwlab-cloud-web/index.html")}`; const imported = markdownRendererPackages.filter((name) => new RegExp(`from\\s+["']${escapeRegExp(name)}["']|import\\(["']${escapeRegExp(name)}["']\\)`, "u").test(source)); const vendoredMarked = /from\s+["']\.\/third_party\/marked\/marked\.esm\.js["']/u.test(source) && fs.existsSync(path.join(repoRoot, "web/hwlab-cloud-web/third_party/marked/marked.esm.js")) && diff --git a/scripts/src/g14-ci-plan-lib.mjs b/scripts/src/g14-ci-plan-lib.mjs index 850c3625..3f0d47c4 100644 --- a/scripts/src/g14-ci-plan-lib.mjs +++ b/scripts/src/g14-ci-plan-lib.mjs @@ -64,6 +64,16 @@ const serviceSpecificPaths = Object.freeze({ "hwlab-agent-skills": ["skills/"] }); +const bunCommandServices = new Set([ + "hwlab-agent-mgr", + "hwlab-agent-worker", + "hwlab-cloud-api", + "hwlab-codex-api-responses-forwarder", + "hwlab-deepseek-responses-bridge", + "hwlab-device-pod", + "hwlab-edge-proxy" +]); + export async function createG14CiPlan(options = {}) { const repoRoot = options.repoRoot ?? process.cwd(); const deployJsonPath = options.deployJsonPath ?? "deploy/deploy.json"; @@ -243,7 +253,7 @@ export function pathMatches(pattern, filePath) { export function isTestOnlyPath(filePath) { const normalized = normalizeRepoPath(filePath); - return normalized.endsWith(".test.mjs") || normalized.includes("/__tests__/") || normalized.includes("/fixtures/"); + return /\.test\.(mjs|ts)$/u.test(normalized) || normalized.includes("/__tests__/") || normalized.includes("/fixtures/"); } export function isDocsOnlyPath(filePath) { @@ -353,7 +363,7 @@ function reuseCandidate(catalogRecord, affected) { } function runtimeKindForService(serviceId) { - if (serviceId === "hwlab-cloud-api") return "bun-command"; + if (bunCommandServices.has(serviceId)) return "bun-command"; if (serviceId === "hwlab-cloud-web") return "cloud-web"; if (serviceId === "hwlab-cli") return "cli"; if (serviceId === "hwlab-agent-skills") return "skills-bundle"; @@ -361,7 +371,7 @@ function runtimeKindForService(serviceId) { } function entrypointForService(serviceId) { - if (serviceId === "hwlab-cloud-api") return "cmd/hwlab-cloud-api/main.ts"; + if (bunCommandServices.has(serviceId)) return `cmd/${serviceId}/main.ts`; if (serviceId === "hwlab-cloud-web") return "web/hwlab-cloud-web/index.html"; if (serviceId === "hwlab-cli") return "tools/hwlab-cli/bin/hwlab-cli.mjs"; if (serviceId === "hwlab-agent-skills") return "skills/hwlab-agent-runtime/SKILL.md"; diff --git a/skills/hwlab-agent-runtime/SKILL.md b/skills/hwlab-agent-runtime/SKILL.md index 3e891c62..17a13dde 100644 --- a/skills/hwlab-agent-runtime/SKILL.md +++ b/skills/hwlab-agent-runtime/SKILL.md @@ -35,13 +35,13 @@ Use this skill when working on HWLAB agent runtime skeletons. All commands emit JSON and stay local: -- `node cmd/hwlab-agent-mgr/main.mjs health` -- `node cmd/hwlab-agent-mgr/main.mjs create --agent-session-id ID --project-id ID` -- `node cmd/hwlab-agent-mgr/main.mjs status --agent-session-id ID` -- `node cmd/hwlab-agent-mgr/main.mjs trace --agent-session-id ID` -- `node cmd/hwlab-agent-mgr/main.mjs evidence --agent-session-id ID` -- `node cmd/hwlab-agent-mgr/main.mjs cleanup --agent-session-id ID` -- `node cmd/hwlab-agent-worker/main.mjs dry-run --agent-session-id ID` +- `bun run cmd/hwlab-agent-mgr/main.ts health` +- `bun run cmd/hwlab-agent-mgr/main.ts create --agent-session-id ID --project-id ID` +- `bun run cmd/hwlab-agent-mgr/main.ts status --agent-session-id ID` +- `bun run cmd/hwlab-agent-mgr/main.ts trace --agent-session-id ID` +- `bun run cmd/hwlab-agent-mgr/main.ts evidence --agent-session-id ID` +- `bun run cmd/hwlab-agent-mgr/main.ts cleanup --agent-session-id ID` +- `bun run cmd/hwlab-agent-worker/main.ts dry-run --agent-session-id ID` - `node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs m3 status --api-base-url URL` - `node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs m3 io --action do.write --value true --approved --api-base-url URL` - `node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs m3 io --action do.write --value false --approved --api-base-url URL` diff --git a/web/hwlab-cloud-web/scripts/check.ts b/web/hwlab-cloud-web/scripts/check.ts index 8fd0d9d6..bd3e8eb8 100644 --- a/web/hwlab-cloud-web/scripts/check.ts +++ b/web/hwlab-cloud-web/scripts/check.ts @@ -7,6 +7,7 @@ import { assertCloudWebDistFresh, buildCloudWebDist, readCloudWebAppSource } fro const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const repoRoot = path.resolve(rootDir, "../.."); +const bunCommand = process.env.HWLAB_TEST_BUN_COMMAND || "/root/.bun/bin/bun"; const requiredFiles = Object.freeze([ "index.html", @@ -47,7 +48,7 @@ const faviconIco = readWeb("favicon.ico"); const distContractScript = readWeb("scripts/dist-contract.ts"); const cloudApiServer = readRepo("internal/cloud/server.ts"); const devicePodData = readRepo("internal/device-pod/fake-data.mjs"); -const devicePodService = readRepo("cmd/hwlab-device-pod/main.mjs"); +const devicePodService = readRepo("cmd/hwlab-device-pod/main.ts"); const protocol = readRepo("internal/protocol/index.mjs"); const deployJson = readRepo("deploy/deploy.json"); const artifactCatalog = readRepo("deploy/artifact-catalog.dev.json"); @@ -178,11 +179,19 @@ function runJavaScriptSyntaxCheck() { ...collectJavaScriptFiles(rootDir), path.resolve(repoRoot, "internal/cloud/server.ts"), path.resolve(repoRoot, "internal/device-pod/fake-data.mjs"), - path.resolve(repoRoot, "cmd/hwlab-device-pod/main.mjs") + path.resolve(repoRoot, "cmd/hwlab-device-pod/main.ts") ]); for (const filePath of [...files].sort()) { - const result = spawnSync(process.execPath, ["--check", filePath], { + const isTypeScript = filePath.endsWith(".ts"); + const result = isTypeScript + ? spawnSync(bunCommand, ["build", filePath, "--target", "bun", "--outfile", "/tmp/hwlab-cloud-web-check-ts.js"], { + cwd: repoRoot, + encoding: "utf8", + windowsHide: true, + timeout: 5000 + }) + : spawnSync(process.execPath, ["--check", filePath], { cwd: repoRoot, encoding: "utf8", windowsHide: true