refactor: migrate v0.2 runtime services to TypeScript

This commit is contained in:
Codex
2026-05-29 00:46:32 +08:00
parent 8f149b3513
commit 03c09f5c94
38 changed files with 644 additions and 128 deletions
+121
View File
@@ -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;
}
@@ -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(),
+83
View File
@@ -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);
}
@@ -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",
@@ -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,
@@ -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,
+100
View File
@@ -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));
}
+122
View File
@@ -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));
}
+4 -4
View File
@@ -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
},
{
+3 -2
View File
@@ -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": [
{
@@ -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` 等路径。
+1 -1
View File
@@ -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。
+2 -2
View File
@@ -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`。 |
## 测试规格
@@ -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`,只能由受控调度实例化。 |
## 测试规格
@@ -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`
+1 -1
View File
@@ -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 请求代理窗口。
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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/",
@@ -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", () => {
+9
View File
@@ -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", () => {
+2 -1
View File
@@ -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);
+1 -1
View File
@@ -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" },
+1 -1
View File
@@ -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。"
]
+11 -9
View File
@@ -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);
+5 -5
View File
@@ -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"), "<html></html>\n");
await git(repo, ["init"]);
await git(repo, ["add", "."]);
+2 -4
View File
@@ -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 },
+1 -1
View File
@@ -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),
+22 -8
View File
@@ -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,
+4 -10
View File
@@ -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"] },
+9 -11
View File
@@ -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`;
+85 -35
View File
@@ -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")) &&
+13 -3
View File
@@ -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";
+7 -7
View File
@@ -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`
+12 -3
View File
@@ -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