refactor: migrate v0.2 runtime services to TypeScript
This commit is contained in:
@@ -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(),
|
||||
@@ -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
-1
@@ -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
-1
@@ -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,
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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` 等路径。
|
||||
|
||||
@@ -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。
|
||||
|
||||
|
||||
@@ -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`。
|
||||
|
||||
|
||||
@@ -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 请求代理窗口。
|
||||
|
||||
|
||||
@@ -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
@@ -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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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。"
|
||||
]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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", "."]);
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"] },
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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")) &&
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user