Files
pikasTech-HWLAB/internal/cloud/access-control.test.ts
T
2026-06-05 14:00:13 +08:00

3293 lines
155 KiB
TypeScript

import assert from "node:assert/strict";
import { generateKeyPairSync, sign } from "node:crypto";
import { mkdtemp } from "node:fs/promises";
import { createServer } from "node:http";
import { test } from "bun:test";
import { createAccessController } from "./access-control.ts";
import { createCloudApiServer } from "./server.ts";
const INTERNAL_TOKEN = "test-internal-token";
const OIDC_ISSUER = "https://auth.74-48-78-17.nip.io/realms/hwlab";
const OIDC_CLIENT_ID = "hwlab-cloud-web";
const HWLAB_PUBLIC_ENDPOINT = "https://hwlab.74-48-78-17.nip.io";
const OIDC_TEST_KEY_ID = "hwlab-test-key";
const OIDC_TEST_KEYS = generateKeyPairSync("rsa", { modulusLength: 2048 });
const OIDC_TEST_JWK = Object.freeze({
...OIDC_TEST_KEYS.publicKey.export({ format: "jwk" }),
kid: OIDC_TEST_KEY_ID,
alg: "RS256",
use: "sig"
});
test("Postgres workspace update query preserves parameter numbering", async () => {
const calls = [];
const accessController = createAccessController({
now: () => "2026-06-01T00:00:00.000Z",
runtimeStore: {
query: async (sql, params = []) => {
calls.push({ sql, params });
if (sql.startsWith("CREATE ") || sql.startsWith("ALTER ")) return { rows: [] };
if (sql.startsWith("SELECT * FROM account_workspaces")) {
return { rows: [{
id: "wsp_pg_param_guard",
owner_user_id: "usr_pg_param_guard",
project_id: "prj_device_pod_workbench",
name: "Default Workbench",
status: "active",
is_default: true,
selected_conversation_id: null,
selected_agent_session_id: null,
selected_device_pod_id: null,
active_trace_id: null,
provider_profile: null,
workspace_json: "{}",
revision: 1,
updated_by_session_id: null,
updated_by_client: null,
created_at: "2026-06-01T00:00:00.000Z",
updated_at: "2026-06-01T00:00:00.000Z"
}] };
}
if (sql.startsWith("UPDATE account_workspaces")) return { rows: [] };
return { rows: [] };
}
}
});
await accessController.store.updateWorkspace({
workspaceId: "wsp_pg_param_guard",
ownerUserId: "usr_pg_param_guard",
actorRole: "user",
providerProfile: "deepseek",
patch: { issue655AcceptanceMarker: "postgres-param-guard" },
updatedByClient: "test-suite"
});
const update = calls.find((call) => call.sql.startsWith("UPDATE account_workspaces"));
assert.ok(update);
assert.match(update.sql, /created_at=\$16, updated_at=\$17/u);
assert.match(update.sql, /\$18::text = 'admin'/u);
assert.equal(update.params.length, 18);
assert.equal(update.params[15], "2026-06-01T00:00:00.000Z");
assert.equal(update.params[16], "2026-06-01T00:00:00.000Z");
assert.equal(update.params[17], "user");
});
test("cloud api /auth/oidc/login returns 302 to Keycloak when issuer and client are configured", async () => {
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_KEYCLOAK_ISSUER: OIDC_ISSUER, HWLAB_KEYCLOAK_CLIENT_ID: OIDC_CLIENT_ID, HWLAB_PUBLIC_ENDPOINT },
now: () => "2026-06-03T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/auth/oidc/login?returnTo=/workbench`, { redirect: "manual" });
assert.equal(response.status, 302);
const location = response.headers.get("location");
assert.ok(location?.startsWith("https://auth.74-48-78-17.nip.io/realms/hwlab/protocol/openid-connect/auth"), `unexpected location: ${location}`);
assert.ok(location?.includes("client_id=hwlab-cloud-web"));
assert.ok(location?.includes("redirect_uri=https%3A%2F%2Fhwlab.74-48-78-17.nip.io%2Fauth%2Foidc%2Fcallback"));
assert.ok(location?.includes("response_type=code"));
assert.ok(location?.includes("state="));
assert.ok(location?.includes("nonce="));
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api /auth/oidc/login stores only same-origin relative returnTo", async () => {
const accessController = createAccessController({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_KEYCLOAK_ISSUER: OIDC_ISSUER, HWLAB_KEYCLOAK_CLIENT_ID: OIDC_CLIENT_ID, HWLAB_PUBLIC_ENDPOINT },
now: () => "2026-06-03T00:00:00.000Z"
});
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
accessController,
now: () => "2026-06-03T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/auth/oidc/login?returnTo=https%3A%2F%2Fevil.example%2F`, { redirect: "manual" });
assert.equal(response.status, 302);
const state = new URL(response.headers.get("location") ?? "http://127.0.0.1").searchParams.get("state");
assert.ok(state);
const stored = await accessController.store.consumeOidcState(state);
assert.equal(stored.returnTo, "/workbench");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api /auth/oidc/callback upserts user by issuer+sub and issues 24h web session", async () => {
const accessController = createAccessController({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_KEYCLOAK_ISSUER: OIDC_ISSUER, HWLAB_KEYCLOAK_CLIENT_ID: OIDC_CLIENT_ID, HWLAB_KEYCLOAK_CLIENT_SECRET: "secret", HWLAB_PUBLIC_ENDPOINT },
fetchImpl: makeOidcFetchMock(),
now: () => "2026-06-03T00:00:00.000Z"
});
const state = "test-state";
await accessController.store.upsertOidcState({ state, nonce: "test-nonce", returnTo: "/workbench", createdAt: "2026-06-03T00:00:00.000Z", expiresAt: "2026-06-03T00:10:00.000Z" });
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
accessController,
now: () => "2026-06-03T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/auth/oidc/callback?code=oidc-code&state=${state}`, { redirect: "manual", headers: { "x-forwarded-proto": "https" } });
assert.equal(response.status, 302);
const cookie = response.headers.get("set-cookie") ?? "";
assert.match(cookie, /hwlab_session=[A-Za-z0-9_-]+;/);
assert.match(cookie, /HttpOnly/);
assert.match(cookie, /(?:^|;\s*)Secure(?:;|$)/u);
assert.match(cookie, /Max-Age=86400/);
const sessionResponse = await fetch(`http://127.0.0.1:${port}/v1/auth/session`, { headers: { cookie: cookie.split(";")[0] } });
assert.equal(sessionResponse.status, 200);
const sessionBody = await sessionResponse.json();
assert.equal(sessionBody.authenticated, true);
assert.equal(sessionBody.actor.username, "oidc-user");
assert.equal(sessionBody.actor.role, "user");
assert.equal(sessionBody.authMethod, "web-session");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
function makeOidcFetchMock({ nonce = "test-nonce", subject = "keycloak-sub-1234", issuer = OIDC_ISSUER, clientId = OIDC_CLIENT_ID } = {}) {
return async (url) => {
const u = new URL(String(url));
if (u.pathname.endsWith("/protocol/openid-connect/token")) {
return new Response(JSON.stringify({
access_token: "oidc-access-token",
refresh_token: "oidc-refresh-token",
id_token: signedOidcIdToken({ issuer, clientId, nonce, subject }),
token_type: "Bearer",
expires_in: 300
}), { status: 200, headers: { "content-type": "application/json" } });
}
if (u.pathname.endsWith("/protocol/openid-connect/certs")) {
return new Response(JSON.stringify({ keys: [OIDC_TEST_JWK] }), { status: 200, headers: { "content-type": "application/json" } });
}
if (u.pathname.endsWith("/protocol/openid-connect/userinfo")) {
return new Response(JSON.stringify({
sub: subject,
preferred_username: "oidc-user",
email: "oidc-user@hwlab.local",
name: "OIDC User",
email_verified: true
}), { status: 200, headers: { "content-type": "application/json" } });
}
return new Response("not found", { status: 404 });
};
}
function signedOidcIdToken({ issuer = OIDC_ISSUER, clientId = OIDC_CLIENT_ID, nonce = "test-nonce", subject = "keycloak-sub-1234", nowSeconds = 1_780_444_800 } = {}) {
const header = base64UrlEncodeJson({ alg: "RS256", typ: "JWT", kid: OIDC_TEST_KEY_ID });
const payload = base64UrlEncodeJson({
iss: issuer,
aud: clientId,
sub: subject,
nonce,
exp: nowSeconds + 600,
iat: nowSeconds,
preferred_username: "oidc-user",
email: "oidc-user@hwlab.local",
name: "OIDC User"
});
const body = `${header}.${payload}`;
const signature = sign("RSA-SHA256", Buffer.from(body), OIDC_TEST_KEYS.privateKey).toString("base64url");
return `${body}.${signature}`;
}
function base64UrlEncodeJson(value) {
return Buffer.from(JSON.stringify(value)).toString("base64url");
}
test("workbench workspace permits a new turn after AgentRun active trace reaches terminal status", async () => {
const agentRunCalls = [];
const agentSessions = new Map();
const agentRunServer = createServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
agentRunCalls.push({ method: request.method, path: url.pathname, search: url.search });
const send = (data) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: true, data })}\n`);
};
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_done/events") {
return send({ items: [
{ id: "evt_done", runId: "run_workspace_done", seq: 1, type: "terminal_status", payload: { commandId: "cmd_workspace_done", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:01.000Z" }
] });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_done/commands/cmd_workspace_done/result") {
return send({
runId: "run_workspace_done",
commandId: "cmd_workspace_done",
status: "completed",
runStatus: "completed",
commandState: "completed",
terminalStatus: "completed",
completed: true,
reply: "previous turn completed",
lastSeq: 1,
sessionRef: { sessionId: "ses_issue655_shared", conversationId: "cnv_issue655_shared", threadId: "thread-issue-655" }
});
}
if (request.method === "POST" && url.pathname === "/api/v1/runs") {
return send({ id: "run_workspace_next", status: "pending", backendProfile: "deepseek", sessionRef: { sessionId: "ses_issue655_shared", conversationId: "cnv_issue655_shared" } });
}
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_workspace_next/commands") {
return send({ id: "cmd_workspace_next", runId: "run_workspace_next", state: "pending", type: "turn", seq: 1 });
}
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_workspace_next/runner-jobs") {
return send({
action: "create-kubernetes-job",
runId: "run_workspace_next",
commandId: "cmd_workspace_next",
attemptId: "attempt_workspace_next",
runnerId: "runner_workspace_next",
namespace: "agentrun-v01",
jobName: "agentrun-v01-runner-workspace-next"
});
}
response.writeHead(404, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
const accessController = createAccessController({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
now: () => "2026-06-01T00:00:00.000Z"
});
accessController.getAgentSessionByTraceId = async (traceId) => agentSessions.get(traceId) ?? null;
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek"
},
accessController,
now: () => "2026-06-01T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const alice = await postJson(port, "/v1/admin/users", { username: "alice-ws-terminal", password: "alice-pass" }, adminLogin.cookie);
assert.equal(alice.status, 201);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-ws-terminal", password: "alice-pass" });
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(workspace.status, 200);
const conversation = await putJson(port, "/v1/agent/conversations/cnv_issue655_shared", {
projectId: "prj_device_pod_workbench",
sessionId: "ses_issue655_shared",
threadId: "thread-issue-655",
sessionStatus: "active",
lastTraceId: "trc_issue655_previous",
messages: [{ role: "agent", text: "previous completed turn", traceId: "trc_issue655_previous" }]
}, aliceLogin.cookie);
assert.equal(conversation.status, 200);
const previousPayload = {
messageId: "msg_workspace_done",
agentRun: {
runId: "run_workspace_done",
commandId: "cmd_workspace_done",
backendProfile: "deepseek",
managerUrl: `http://127.0.0.1:${agentRunPort}`,
lastSeq: 0,
sessionId: "ses_issue655_shared",
conversationId: "cnv_issue655_shared",
threadId: "thread-issue-655"
}
};
agentSessions.set("trc_issue655_active_done", {
id: "ses_issue655_shared",
ownerUserId: alice.body.user.id,
conversationId: "cnv_issue655_shared",
threadId: "thread-issue-655",
status: "active",
session: previousPayload,
updatedAt: "2026-06-01T00:00:00.000Z"
});
const update = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
expectedRevision: 1,
selectedConversationId: "cnv_issue655_shared",
selectedAgentSessionId: "ses_issue655_shared",
activeTraceId: "trc_issue655_active_done",
providerProfile: "deepseek",
sessionStatus: "running",
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(update.status, 200);
assert.equal(update.body.workspace.activeTraceId, "trc_issue655_active_done");
const next = await postJson(port, "/v1/agent/chat", {
message: "new turn after completed active trace",
traceId: "trc_issue655_after_done",
conversationId: "cnv_issue655_shared",
sessionId: "ses_issue655_shared",
workspaceId: workspace.body.workspace.workspaceId,
expectedWorkspaceRevision: 2,
shortConnection: true
}, aliceLogin.cookie, { prefer: "respond-async", "x-trace-id": "trc_issue655_after_done" });
assert.equal(next.status, 202);
assert.equal(next.body.status, "running");
assert.equal(next.body.traceId, "trc_issue655_after_done");
await waitForCondition(() => agentRunCalls.some((call) => call.method === "POST" && call.path === "/api/v1/runs"));
assert.equal(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_workspace_done/commands/cmd_workspace_done/result"), false);
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(restored.body.workspace.activeTraceId, "trc_issue655_after_done");
assert.equal(restored.body.workspace.workspace.sessionStatus, "running");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
}
});
test("workbench workspace permits continuation when stale active trace has idle active conversation", async () => {
const workspaceDir = await mkdtemp("/tmp/hwlab-stale-active-idle-");
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspaceDir,
HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
OPENAI_API_KEY: "test-openai-key-material"
},
codexStdioManager: {
describe: () => ({ available: true, ready: true, workspace: workspaceDir, sandbox: "danger-full-access" }),
probe: async () => ({ available: true, ready: true, workspace: workspaceDir, sandbox: "danger-full-access" }),
chat: async (params = {}) => ({
status: "completed",
traceId: params.traceId,
conversationId: params.conversationId,
sessionId: params.sessionId,
threadId: params.threadId,
reply: { role: "assistant", content: "ok" },
runnerTrace: { traceId: params.traceId, status: "completed", events: [], eventCount: 0 },
session: { sessionId: params.sessionId, conversationId: params.conversationId, threadId: params.threadId, status: "idle" }
}),
cancel() {},
reapIdle() {}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", adminLogin.cookie);
const conversation = await putJson(port, "/v1/agent/conversations/cnv_stale_active_idle", {
projectId: "prj_device_pod_workbench",
sessionId: "ses_stale_active_idle",
threadId: "thread-stale-active-idle",
status: "active",
sessionStatus: "active",
lastTraceId: "trc_stale_active_idle_missing",
messages: [{ role: "agent", text: "previous completed turn", status: "completed", traceId: "trc_stale_active_idle_missing" }]
}, adminLogin.cookie);
assert.equal(conversation.status, 200);
const update = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
expectedRevision: 1,
selectedConversationId: "cnv_stale_active_idle",
selectedAgentSessionId: "ses_stale_active_idle",
activeTraceId: "trc_stale_active_idle_missing",
sessionStatus: "running",
updatedByClient: "test-suite"
}, adminLogin.cookie);
assert.equal(update.status, 200);
const next = await postJson(port, "/v1/agent/chat", {
message: "new turn after stale active idle trace",
traceId: "trc_stale_active_idle_next",
conversationId: "cnv_stale_active_idle",
sessionId: "ses_stale_active_idle",
threadId: "thread-stale-active-idle",
workspaceId: workspace.body.workspace.workspaceId,
expectedWorkspaceRevision: 2,
shortConnection: true
}, adminLogin.cookie, { prefer: "respond-async", "x-trace-id": "trc_stale_active_idle_next" });
assert.equal(next.status, 202);
assert.equal(next.body.traceId, "trc_stale_active_idle_next");
assert.equal(next.body.conversationId, "cnv_stale_active_idle");
assert.equal(next.body.sessionId, "ses_stale_active_idle");
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", adminLogin.cookie);
assert.equal(restored.body.workspace.activeTraceId, "trc_stale_active_idle_next");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("workbench workspace status clears completed AgentRun active trace on read", async () => {
const agentRunCalls = [];
const agentSessions = new Map();
const agentRunServer = createServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
agentRunCalls.push({ method: request.method, path: url.pathname, search: url.search });
const send = (data) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: true, data })}\n`);
};
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_status/events") {
return send({ items: [
{ id: "evt_status_done", runId: "run_workspace_status", seq: 1, type: "terminal_status", payload: { commandId: "cmd_workspace_status", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:01.000Z" }
] });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_status/commands/cmd_workspace_status/result") {
return send({
runId: "run_workspace_status",
commandId: "cmd_workspace_status",
status: "completed",
runStatus: "completed",
commandState: "completed",
terminalStatus: "completed",
completed: true,
reply: "previous turn completed",
lastSeq: 1,
sessionRef: { sessionId: "ses_issue664_status", conversationId: "cnv_issue664_status", threadId: "thread-issue-664" }
});
}
response.writeHead(404, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
const accessController = createAccessController({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
now: () => "2026-06-01T00:00:00.000Z"
});
accessController.getAgentSessionByTraceId = async (traceId) => agentSessions.get(traceId) ?? null;
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek"
},
accessController,
now: () => "2026-06-01T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const alice = await postJson(port, "/v1/admin/users", { username: "alice-ws-status", password: "alice-pass" }, adminLogin.cookie);
assert.equal(alice.status, 201);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-ws-status", password: "alice-pass" });
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(workspace.status, 200);
const conversation = await putJson(port, "/v1/agent/conversations/cnv_issue664_status", {
projectId: "prj_device_pod_workbench",
sessionId: "ses_issue664_status",
threadId: "thread-issue-664",
sessionStatus: "active",
lastTraceId: "trc_issue664_previous",
messages: [{ role: "agent", text: "previous turn still running", status: "running", traceId: "trc_issue664_status_done" }]
}, aliceLogin.cookie);
assert.equal(conversation.status, 200);
const previousPayload = {
messageId: "msg_workspace_status_done",
agentRun: {
runId: "run_workspace_status",
commandId: "cmd_workspace_status",
backendProfile: "deepseek",
managerUrl: `http://127.0.0.1:${agentRunPort}`,
lastSeq: 0,
sessionId: "ses_issue664_status",
conversationId: "cnv_issue664_status",
threadId: "thread-issue-664"
}
};
agentSessions.set("trc_issue664_status_done", {
id: "ses_issue664_status",
ownerUserId: alice.body.user.id,
conversationId: "cnv_issue664_status",
threadId: "thread-issue-664",
status: "active",
session: previousPayload,
updatedAt: "2026-06-01T00:00:00.000Z"
});
const update = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
expectedRevision: 1,
selectedConversationId: "cnv_issue664_status",
selectedAgentSessionId: "ses_issue664_status",
activeTraceId: "trc_issue664_status_done",
providerProfile: "deepseek",
sessionStatus: "running",
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(update.status, 200);
assert.equal(update.body.workspace.activeTraceId, "trc_issue664_status_done");
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(restored.status, 200);
assert.equal(restored.body.workspace.activeTraceId, null);
assert.equal(restored.body.workspace.workspace.lastTraceId, "trc_issue664_status_done");
assert.equal(restored.body.workspace.workspace.sessionStatus, "idle");
assert.equal(restored.body.workspace.selectedConversation.status, "idle");
assert.equal(restored.body.workspace.selectedConversation.lastTraceId, "trc_issue664_status_done");
assert.equal(restored.body.workspace.selectedConversation.messages.length, 1);
assert.equal(restored.body.workspace.selectedConversation.messages[0].status, "idle");
assert.equal(restored.body.workspace.selectedConversation.messages[0].text, "previous turn completed");
assert.equal(restored.body.workspace.revision, 3);
assert.ok(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_workspace_status/commands/cmd_workspace_status/result"));
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
}
});
test("workbench workspace terminal status sync preserves a newer selected conversation", async () => {
const agentRunCalls = [];
const agentSessions = new Map();
const agentRunServer = createServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
agentRunCalls.push({ method: request.method, path: url.pathname, search: url.search });
const send = (data) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: true, data })}\n`);
};
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_issue808_old/events") {
return send({ items: [
{ id: "evt_issue808_done", runId: "run_issue808_old", seq: 1, type: "terminal_status", payload: { commandId: "cmd_issue808_old", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:01.000Z" }
] });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_issue808_old/commands/cmd_issue808_old/result") {
return send({
runId: "run_issue808_old",
commandId: "cmd_issue808_old",
status: "completed",
runStatus: "completed",
commandState: "completed",
terminalStatus: "completed",
completed: true,
reply: "old turn completed",
lastSeq: 1,
sessionRef: { sessionId: "ses_issue808_old", conversationId: "cnv_issue808_old", threadId: "thread-issue-808-old" }
});
}
response.writeHead(404, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
const accessController = createAccessController({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
now: () => "2026-06-01T00:00:00.000Z"
});
accessController.getAgentSessionByTraceId = async (traceId) => agentSessions.get(traceId) ?? null;
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek"
},
accessController,
now: () => "2026-06-01T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const alice = await postJson(port, "/v1/admin/users", { username: "alice-ws-issue808", password: "alice-pass" }, adminLogin.cookie);
assert.equal(alice.status, 201);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-ws-issue808", password: "alice-pass" });
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(workspace.status, 200);
const oldConversation = await putJson(port, "/v1/agent/conversations/cnv_issue808_old", {
projectId: "prj_device_pod_workbench",
sessionId: "ses_issue808_old",
threadId: "thread-issue-808-old",
sessionStatus: "running",
lastTraceId: "trc_issue808_old_done",
messages: [{ role: "agent", text: "old turn running", status: "running", traceId: "trc_issue808_old_done" }]
}, aliceLogin.cookie);
assert.equal(oldConversation.status, 200);
const newConversation = await putJson(port, "/v1/agent/conversations/cnv_issue808_new", {
projectId: "prj_device_pod_workbench",
sessionId: "ses_issue808_new",
threadId: "thread-issue-808-new",
sessionStatus: "idle",
messages: []
}, aliceLogin.cookie);
assert.equal(newConversation.status, 200);
agentSessions.set("trc_issue808_old_done", {
id: "ses_issue808_old",
ownerUserId: alice.body.user.id,
conversationId: "cnv_issue808_old",
threadId: "thread-issue-808-old",
status: "active",
session: {
messageId: "msg_issue808_old_done",
agentRun: {
runId: "run_issue808_old",
commandId: "cmd_issue808_old",
backendProfile: "deepseek",
managerUrl: `http://127.0.0.1:${agentRunPort}`,
lastSeq: 0,
sessionId: "ses_issue808_old",
conversationId: "cnv_issue808_old",
threadId: "thread-issue-808-old"
}
},
updatedAt: "2026-06-01T00:00:00.000Z"
});
const oldActive = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
expectedRevision: 1,
selectedConversationId: "cnv_issue808_old",
selectedAgentSessionId: "ses_issue808_old",
activeTraceId: "trc_issue808_old_done",
lastTraceId: "trc_issue808_old_done",
providerProfile: "deepseek",
sessionStatus: "running",
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(oldActive.status, 200);
assert.equal(oldActive.body.workspace.selectedConversationId, "cnv_issue808_old");
assert.equal(oldActive.body.workspace.activeTraceId, "trc_issue808_old_done");
const selectedNew = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
projectId: "prj_device_pod_workbench",
conversationId: "cnv_issue808_new",
sessionId: "ses_issue808_new",
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(selectedNew.status, 200);
assert.equal(selectedNew.body.workspace.selectedConversationId, "cnv_issue808_new");
assert.equal(selectedNew.body.workspace.selectedAgentSessionId, "ses_issue808_new");
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(restored.status, 200);
assert.equal(restored.body.workspace.activeTraceId, null);
assert.equal(restored.body.workspace.selectedConversationId, "cnv_issue808_new");
assert.equal(restored.body.workspace.selectedAgentSessionId, "ses_issue808_new");
assert.equal(restored.body.workspace.selectedConversation.conversationId, "cnv_issue808_new");
assert.equal(restored.body.workspace.selectedConversation.sessionId, "ses_issue808_new");
assert.equal(restored.body.workspace.workspace.selectedConversationId, "cnv_issue808_new");
assert.equal(restored.body.workspace.workspace.selectedAgentSessionId, "ses_issue808_new");
assert.equal(restored.body.workspace.workspace.lastTraceId, "trc_issue808_old_done");
assert.equal(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_issue808_old/commands/cmd_issue808_old/result"), false);
const selectedOld = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
projectId: "prj_device_pod_workbench",
conversationId: "cnv_issue808_old",
sessionId: "ses_issue808_old",
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(selectedOld.status, 200);
assert.equal(selectedOld.body.workspace.selectedConversationId, "cnv_issue808_old");
const repaired = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(repaired.status, 200);
assert.equal(repaired.body.workspace.activeTraceId, null);
assert.equal(repaired.body.workspace.selectedConversationId, "cnv_issue808_old");
assert.equal(repaired.body.workspace.selectedAgentSessionId, "ses_issue808_old");
assert.equal(repaired.body.workspace.selectedConversation.status, "idle");
assert.equal(repaired.body.workspace.selectedConversation.lastTraceId, "trc_issue808_old_done");
assert.equal(repaired.body.workspace.selectedConversation.messages[0].status, "idle");
assert.equal(repaired.body.workspace.selectedConversation.messages[0].text, "old turn completed");
assert.ok(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_issue808_old/commands/cmd_issue808_old/result"));
const repairedOld = await getJson(port, "/v1/agent/conversations/cnv_issue808_old", aliceLogin.cookie);
assert.equal(repairedOld.status, 200);
assert.equal(repairedOld.body.conversation.status, "idle");
assert.equal(repairedOld.body.conversation.lastTraceId, "trc_issue808_old_done");
assert.equal(repairedOld.body.conversation.messages[0].status, "idle");
assert.equal(repairedOld.body.conversation.messages[0].text, "old turn completed");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
}
});
test("manual Code Agent session select restores running active trace", async () => {
const accessController = createAccessController({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
now: () => "2026-06-01T00:00:00.000Z"
});
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "minimax-m3"
},
accessController,
now: () => "2026-06-01T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const alice = await postJson(port, "/v1/admin/users", { username: "alice-ws-issue810", password: "alice-pass" }, adminLogin.cookie);
assert.equal(alice.status, 201);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-ws-issue810", password: "alice-pass" });
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(workspace.status, 200);
await accessController.recordAgentSessionOwner({
ownerUserId: alice.body.user.id,
ownerRole: "user",
sessionId: "ses_issue810_running",
projectId: "prj_device_pod_workbench",
agentId: "hwlab-code-agent",
status: "running",
conversationId: "cnv_issue810_running",
threadId: "thread-issue-810-running",
traceId: "trc_issue810_running",
session: {
source: "test-running-session",
providerProfile: "minimax-m3",
sessionStatus: "running",
currentTraceId: "trc_issue810_running",
agentRun: {
runId: "run_issue810_running",
commandId: "cmd_issue810_running",
runStatus: "running",
commandState: "running",
backendProfile: "minimax-m3"
},
valuesRedacted: true,
secretMaterialStored: false
}
});
await accessController.recordAgentSessionOwner({
ownerUserId: alice.body.user.id,
ownerRole: "user",
sessionId: "ses_issue810_idle",
projectId: "prj_device_pod_workbench",
agentId: "hwlab-code-agent",
status: "idle",
conversationId: "cnv_issue810_idle",
threadId: "thread-issue-810-idle",
traceId: null,
session: {
source: "test-idle-session",
providerProfile: "minimax-m3",
sessionStatus: "idle",
valuesRedacted: true,
secretMaterialStored: false
}
});
const selectedIdle = await postJson(port, "/v1/agent/sessions/ses_issue810_idle/select", {
projectId: "prj_device_pod_workbench",
workspaceId: workspace.body.workspace.workspaceId,
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(selectedIdle.status, 200);
assert.equal(selectedIdle.body.workspace.selectedConversationId, "cnv_issue810_idle");
assert.equal(selectedIdle.body.workspace.selectedAgentSessionId, "ses_issue810_idle");
assert.equal(selectedIdle.body.workspace.activeTraceId, null);
const selectedRunning = await postJson(port, "/v1/agent/sessions/ses_issue810_running/select", {
projectId: "prj_device_pod_workbench",
workspaceId: workspace.body.workspace.workspaceId,
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(selectedRunning.status, 200);
assert.equal(selectedRunning.body.status, "selected");
assert.equal(selectedRunning.body.session.sessionId, "ses_issue810_running");
assert.equal(selectedRunning.body.session.status, "running");
assert.equal(selectedRunning.body.session.lastTraceId, "trc_issue810_running");
assert.equal(selectedRunning.body.workspace.selectedConversationId, "cnv_issue810_running");
assert.equal(selectedRunning.body.workspace.selectedAgentSessionId, "ses_issue810_running");
assert.equal(selectedRunning.body.workspace.activeTraceId, "trc_issue810_running");
assert.equal(selectedRunning.body.workspace.workspace.activeTraceId, "trc_issue810_running");
assert.equal(selectedRunning.body.workspace.workspace.lastTraceId, "trc_issue810_running");
assert.equal(selectedRunning.body.workspace.workspace.sessionStatus, "running");
assert.equal(selectedRunning.body.workspace.workspace.providerProfile, "minimax-m3");
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(restored.status, 200);
assert.equal(restored.body.workspace.selectedConversationId, "cnv_issue810_running");
assert.equal(restored.body.workspace.selectedAgentSessionId, "ses_issue810_running");
assert.equal(restored.body.workspace.activeTraceId, "trc_issue810_running");
assert.equal(restored.body.workspace.workspace.activeTraceId, "trc_issue810_running");
assert.equal(restored.body.workspace.workspace.lastTraceId, "trc_issue810_running");
assert.equal(restored.body.workspace.workspace.sessionStatus, "running");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("workbench workspace status repairs terminal selected conversation after active trace was cleared", async () => {
const agentRunCalls = [];
const agentSessions = new Map();
const agentRunServer = createServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
agentRunCalls.push({ method: request.method, path: url.pathname, search: url.search });
const send = (data) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: true, data })}\n`);
};
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_repair/events") {
return send({ items: [] });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_repair/commands/cmd_workspace_repair/result") {
return send({
runId: "run_workspace_repair",
commandId: "cmd_workspace_repair",
status: "completed",
runStatus: "completed",
commandState: "completed",
terminalStatus: "completed",
completed: true,
reply: "repair completed",
lastSeq: 2,
sessionRef: { sessionId: "ses_issue664_repair", conversationId: "cnv_issue664_repair", threadId: "thread-issue-664-repair" }
});
}
response.writeHead(404, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
const accessController = createAccessController({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
now: () => "2026-06-01T00:00:00.000Z"
});
accessController.getAgentSessionByTraceId = async (traceId) => agentSessions.get(traceId) ?? null;
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek"
},
accessController,
now: () => "2026-06-01T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const alice = await postJson(port, "/v1/admin/users", { username: "alice-ws-repair", password: "alice-pass" }, adminLogin.cookie);
assert.equal(alice.status, 201);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-ws-repair", password: "alice-pass" });
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(workspace.status, 200);
const conversation = await putJson(port, "/v1/agent/conversations/cnv_issue664_repair", {
projectId: "prj_device_pod_workbench",
sessionId: "ses_issue664_repair",
threadId: "thread-issue-664-repair",
sessionStatus: "running",
lastTraceId: "trc_issue664_repair_done",
messages: []
}, aliceLogin.cookie);
assert.equal(conversation.status, 200);
agentSessions.set("trc_issue664_repair_done", {
id: "ses_issue664_repair",
ownerUserId: alice.body.user.id,
conversationId: "cnv_issue664_repair",
threadId: "thread-issue-664-repair",
status: "active",
session: {
messageId: "msg_workspace_repair_done",
agentRun: {
runId: "run_workspace_repair",
commandId: "cmd_workspace_repair",
backendProfile: "deepseek",
managerUrl: `http://127.0.0.1:${agentRunPort}`,
lastSeq: 0,
sessionId: "ses_issue664_repair",
conversationId: "cnv_issue664_repair",
threadId: "thread-issue-664-repair"
}
},
updatedAt: "2026-06-01T00:00:00.000Z"
});
const update = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
expectedRevision: 1,
selectedConversationId: "cnv_issue664_repair",
selectedAgentSessionId: "ses_issue664_repair",
activeTraceId: null,
providerProfile: "deepseek",
sessionStatus: "idle",
lastTraceId: "trc_issue664_repair_done",
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(update.status, 200);
assert.equal(update.body.workspace.activeTraceId, null);
assert.equal(update.body.workspace.selectedConversation.status, "running");
assert.equal(update.body.workspace.selectedConversation.messages.length, 0);
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(restored.status, 200);
assert.equal(restored.body.workspace.activeTraceId, null);
assert.equal(restored.body.workspace.workspace.lastTraceId, "trc_issue664_repair_done");
assert.equal(restored.body.workspace.workspace.sessionStatus, "idle");
assert.equal(restored.body.workspace.selectedConversation.status, "idle");
assert.equal(restored.body.workspace.selectedConversation.lastTraceId, "trc_issue664_repair_done");
assert.equal(restored.body.workspace.selectedConversation.messages.length, 1);
assert.equal(restored.body.workspace.selectedConversation.messages[0].status, "idle");
assert.equal(restored.body.workspace.selectedConversation.messages[0].text, "repair completed");
assert.ok(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_workspace_repair/commands/cmd_workspace_repair/result"));
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api access control grants visible device pods and requires device-pod executor", async () => {
let directGatewayDispatches = 0;
const gatewayRegistry = {
isOnline: () => true,
enqueue: async () => {
directGatewayDispatches += 1;
throw new Error("cloud-api must not bypass hwlab-device-pod executor");
},
describe: () => ({ sessions: [] })
};
const openFgaAuthorizer = createFakeOpenFgaAuthorizer({ mode: "enforce" });
const accessController = createAccessController({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
gatewayRegistry,
openFgaAuthorizer,
now: () => "2026-05-28T00:00:00.000Z"
});
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
accessController,
gatewayRegistry,
now: () => "2026-05-28T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
assert.equal(adminLogin.status, 200);
assert.equal(adminLogin.body.actor.role, "admin");
const adminCookie = adminLogin.cookie;
const userCreate = await postJson(port, "/v1/admin/users", {
username: "alice",
password: "alice-pass",
displayName: "Alice"
}, adminCookie);
assert.equal(userCreate.status, 201);
assert.equal(userCreate.body.user.username, "alice");
assert.equal(JSON.stringify(userCreate.body).includes("alice-pass"), false);
const bobCreate = await postJson(port, "/v1/admin/users", {
username: "bob",
password: "bob-pass",
displayName: "Bob"
}, adminCookie);
assert.equal(bobCreate.status, 201);
const podCreate = await postJson(port, "/v1/admin/device-pods", {
devicePodId: "device-pod-71-freq",
name: "71-FREQ",
profile: {
schemaVersion: 1,
devicePodId: "device-pod-local-spoof",
target: { id: "target-71-freq" },
projectWorkspace: { projectPath: "FirmWare/MDK-ARM/app.uvprojx", targetName: "app" },
route: {
gatewaySessionId: "gws_missing",
resourceId: "res_windows_host",
capabilityId: "cap_device_host_cli",
hostCli: "node tools/device-host-cli.mjs"
}
}
}, adminCookie);
assert.equal(podCreate.status, 201);
assert.equal(podCreate.body.devicePod.devicePodId, "device-pod-71-freq");
assert.notEqual(podCreate.body.devicePod.profileHash, "sha256:a-local-profile-hash");
assert.equal(podCreate.body.devicePod.profile.route.gatewaySessionId, "redacted");
assert.equal(JSON.stringify(podCreate.body).includes("gws_missing"), false);
const secretProfile = await postJson(port, "/v1/admin/device-pods", {
devicePodId: "device-pod-secret",
profile: {
schemaVersion: 1,
target: { id: "target-secret" },
route: {
gatewaySessionId: "gws_secret",
cloudToken: "should-not-be-stored"
}
}
}, adminCookie);
assert.equal(secretProfile.status, 400);
assert.equal(secretProfile.body.error.code, "device_pod_profile_secret_forbidden");
assert.equal(JSON.stringify(secretProfile.body).includes("should-not-be-stored"), false);
const emptyLogin = await postJson(port, "/auth/login", { username: "alice", password: "alice-pass" });
assert.equal(emptyLogin.status, 200);
const aliceCookie = emptyLogin.cookie;
const emptyList = await getJson(port, "/v1/device-pods", aliceCookie);
assert.equal(emptyList.status, 200);
assert.deepEqual(emptyList.body.devicePods, []);
await grantDevicePodAccess(port, adminCookie, userCreate.body.user.id, "device-pod-71-freq", ["viewer", "operator", "job_submitter"]);
await grantDevicePodAccess(port, adminCookie, bobCreate.body.user.id, "device-pod-71-freq", ["viewer", "operator", "job_submitter"]);
const visible = await getJson(port, "/v1/device-pods", aliceCookie);
assert.equal(visible.status, 200);
assert.equal(visible.body.contractVersion, "device-pod-authority-v1");
assert.equal(visible.body.source.kind, "CLOUD_API_PROFILE_AUTHORITY");
assert.equal(visible.body.source.fake, false);
assert.equal(visible.body.devicePods.length, 1);
assert.equal(visible.body.devicePods[0].devicePodId, "device-pod-71-freq");
assert.match(visible.body.devicePods[0].profileHash, /^sha256:/u);
const status = await getJson(port, "/v1/device-pods/device-pod-71-freq/status", aliceCookie);
assert.equal(status.status, 200);
assert.equal(status.body.devicePodId, "device-pod-71-freq");
assert.equal(status.body.targetId, "target-71-freq");
assert.equal(status.body.profileHash, visible.body.devicePods[0].profileHash);
assert.match(status.body.traceId, /^trc_devicepod_/u);
assert.match(status.body.operationId, /^op_devicepod_/u);
assert.equal(status.body.status, "ok");
assert.equal(status.body.freshness.stale, false);
assert.equal(status.body.blocker, null);
assert.equal(status.body.truncation.truncated, false);
assert.equal(status.body.output.summary, "device-pod status ok");
const job = await postJson(port, "/v1/device-pods/device-pod-71-freq/jobs", {
intent: "workspace.ls",
args: { path: "." }
}, aliceCookie);
assert.equal(job.status, 409);
assert.equal(job.body.status, "blocked");
assert.equal(job.body.blocker.code, "device_pod_executor_unavailable");
assert.equal(job.body.blocker.summary, "HWLAB_DEVICE_POD_URL is not configured; cloud-api will not bypass hwlab-device-pod executor");
assert.equal(job.body.devicePodId, "device-pod-71-freq");
assert.equal(job.body.profileHash, visible.body.devicePods[0].profileHash);
assert.equal(directGatewayDispatches, 0);
const mutatingWithReason = await postJson(port, "/v1/device-pods/device-pod-71-freq/jobs", {
intent: "debug.reset",
reason: "reset smoke"
}, aliceCookie);
assert.equal(mutatingWithReason.status, 409);
assert.equal(mutatingWithReason.body.status, "blocked");
assert.equal(mutatingWithReason.body.devicePodId, "device-pod-71-freq");
assert.equal(mutatingWithReason.body.profileHash, visible.body.devicePods[0].profileHash);
assert.equal(mutatingWithReason.body.blocker.code, "device_pod_executor_unavailable");
assert.equal(mutatingWithReason.body.freshness.stale, true);
const blockedJob = await getJson(port, `/v1/device-pods/device-pod-71-freq/jobs/${mutatingWithReason.body.job.id}`, aliceCookie);
assert.equal(blockedJob.status, 200);
assert.equal(blockedJob.body.job.id, mutatingWithReason.body.job.id);
assert.equal(blockedJob.body.status, "blocked");
assert.equal(blockedJob.body.blocker.code, "device_pod_executor_unavailable");
const blockedOutput = await getJson(port, `/v1/device-pods/device-pod-71-freq/jobs/${mutatingWithReason.body.job.id}/output`, aliceCookie);
assert.equal(blockedOutput.status, 200);
assert.equal(blockedOutput.body.truncation.truncated, false);
assert.match(blockedOutput.body.output.error, /HWLAB_DEVICE_POD_URL is not configured/u);
const mutatingWithoutReason = await postJson(port, "/v1/device-pods/device-pod-71-freq/jobs", {
intent: "debug.reset"
}, aliceCookie);
assert.equal(mutatingWithoutReason.status, 400);
assert.equal(mutatingWithoutReason.body.error.code, "device_job_reason_required");
assert.equal(mutatingWithoutReason.body.status, "blocked");
assert.equal(mutatingWithoutReason.body.devicePodId, "device-pod-71-freq");
assert.equal(mutatingWithoutReason.body.profileHash, visible.body.devicePods[0].profileHash);
assert.equal(mutatingWithoutReason.body.blocker.code, "device_job_reason_required");
assert.equal(mutatingWithoutReason.body.freshness.stale, true);
const bobLogin = await postJson(port, "/auth/login", { username: "bob", password: "bob-pass" });
assert.equal(bobLogin.status, 200);
const bobListWithGrant = await getJson(port, "/v1/device-pods", bobLogin.cookie);
assert.equal(bobListWithGrant.status, 200);
assert.equal(bobListWithGrant.body.devicePods.length, 1);
const privatePodCreate = await postJson(port, "/v1/admin/device-pods", {
devicePodId: "device-pod-private",
profile: { schemaVersion: 1, target: { id: "target-private" }, route: { gatewaySessionId: "gws_private" } }
}, adminCookie);
assert.equal(privatePodCreate.status, 201);
const bobPrivateStatus = await getJson(port, "/v1/device-pods/device-pod-private/status", bobLogin.cookie);
assert.equal(bobPrivateStatus.status, 403);
assert.equal(bobPrivateStatus.body.error.code, "device_pod_forbidden");
const bobMissingStatus = await getJson(port, "/v1/device-pods/device-pod-missing/status", bobLogin.cookie);
assert.equal(bobMissingStatus.status, 404);
assert.equal(bobMissingStatus.body.error.code, "device_pod_not_found");
const events = await getJson(port, "/v1/device-pods/device-pod-71-freq/events", aliceCookie);
assert.equal(events.status, 200);
assert.equal(events.body.events[0].blocker.code, "device_pod_executor_unavailable");
const unsupported = await postJson(port, "/v1/device-pods/device-pod-71-freq/jobs", {
intent: "debug.erase-all",
args: {}
}, aliceCookie);
assert.equal(unsupported.status, 400);
assert.equal(unsupported.body.error.code, "unsupported_device_job_intent");
await revokeDevicePodAccess(port, adminCookie, bobCreate.body.user.id, "device-pod-71-freq", ["viewer", "operator", "job_submitter"]);
const afterRevoke = await getJson(port, "/v1/device-pods", bobLogin.cookie);
assert.equal(afterRevoke.status, 200);
assert.deepEqual(afterRevoke.body.devicePods, []);
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api admin access API grants, checks, and revokes device-pod and tool permissions", async () => {
const openFgaAuthorizer = createFakeOpenFgaAuthorizer({ mode: "enforce" });
const accessController = createAccessController({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
openFgaAuthorizer,
now: () => "2026-06-04T00:00:00.000Z"
});
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
accessController,
now: () => "2026-06-04T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const setup = await postJson(port, "/v1/setup/first-admin", { username: "access-admin", password: "admin-pass" });
assert.equal(setup.status, 201);
const adminCookie = setup.cookie;
const alice = await postJson(port, "/v1/admin/users", { username: "access-alice", password: "alice-pass", displayName: "Access Alice" }, adminCookie);
assert.equal(alice.status, 201);
const userId = alice.body.user.id;
const pod = await postJson(port, "/v1/admin/device-pods", {
devicePodId: "device-pod-access-api",
name: "Access API Pod",
profile: { schemaVersion: 1, target: { id: "target-access-api" }, route: { gatewaySessionId: "gws_access_api" } }
}, adminCookie);
assert.equal(pod.status, 201);
const summary = await getJson(port, "/v1/admin/access/summary", adminCookie);
assert.equal(summary.status, 200);
assert.equal(summary.body.contractVersion, "admin-access-v1");
assert.equal(summary.body.openfga.mode, "enforce");
assert.equal(summary.body.counts.users, 2);
assert.equal(summary.body.counts.devicePods, 1);
assert.deepEqual(summary.body.supported.devicePodRelations, ["viewer", "operator", "profile_editor", "job_submitter"]);
const users = await getJson(port, "/v1/admin/access/users", adminCookie);
assert.equal(users.status, 200);
assert.ok(users.body.users.some((item) => item.user.id === userId));
const grantViewer = await putJson(port, `/v1/admin/access/users/${userId}/device-pods/device-pod-access-api/viewer`, {}, adminCookie);
assert.equal(grantViewer.status, 201);
const matrixAfterViewer = grantViewer.body.access.devicePods.find((item) => item.devicePod.devicePodId === "device-pod-access-api");
assert.equal(matrixAfterViewer.relations.viewer, true);
assert.equal(matrixAfterViewer.relations.operator, false);
const grantTool = await putJson(port, `/v1/admin/access/users/${userId}/tools/hwpod/can-use`, {}, adminCookie);
assert.equal(grantTool.status, 201);
assert.equal(grantTool.body.access.tools.hwpod, true);
const checkViewer = await postJson(port, "/v1/admin/access/check", {
userId,
relation: "viewer",
object: "device_pod:device-pod-access-api"
}, adminCookie);
assert.equal(checkViewer.status, 200);
assert.equal(checkViewer.body.authorization.allowed, true);
assert.equal(checkViewer.body.authorization.mode, "enforce");
const aliceLogin = await postJson(port, "/auth/login", { username: "access-alice", password: "alice-pass" });
assert.equal(aliceLogin.status, 200);
const visible = await getJson(port, "/v1/device-pods", aliceLogin.cookie);
assert.equal(visible.status, 200);
assert.deepEqual(visible.body.devicePods.map((item) => item.devicePodId), ["device-pod-access-api"]);
const revokeTool = await deleteJson(port, `/v1/admin/access/users/${userId}/tools/hwpod/can-use`, {}, adminCookie);
assert.equal(revokeTool.status, 200);
assert.equal(revokeTool.body.access.tools.hwpod, false);
const revokeViewer = await deleteJson(port, `/v1/admin/access/users/${userId}/device-pods/device-pod-access-api/viewer`, {}, adminCookie);
assert.equal(revokeViewer.status, 200);
const matrixAfterRevoke = revokeViewer.body.access.devicePods.find((item) => item.devicePod.devicePodId === "device-pod-access-api");
assert.equal(matrixAfterRevoke.relations.viewer, false);
const hidden = await getJson(port, "/v1/device-pods", aliceLogin.cookie);
assert.equal(hidden.status, 200);
assert.deepEqual(hidden.body.devicePods, []);
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api enforce mode uses OpenFGA relations for device-pod visibility and jobs", async () => {
const openFgaAuthorizer = createFakeOpenFgaAuthorizer({ mode: "enforce" });
const accessController = createAccessController({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
openFgaAuthorizer,
now: () => "2026-06-04T00:00:00.000Z"
});
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
accessController,
now: () => "2026-06-04T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
assert.equal(adminLogin.status, 200);
const adminCookie = adminLogin.cookie;
const alice = await postJson(port, "/v1/admin/users", { username: "enforce-alice", password: "alice-pass" }, adminCookie);
assert.equal(alice.status, 201);
const userId = alice.body.user.id;
const pod = await postJson(port, "/v1/admin/device-pods", {
devicePodId: "device-pod-enforce",
profile: { schemaVersion: 1, target: { id: "target-enforce" }, route: { gatewaySessionId: "gws_enforce" } }
}, adminCookie);
assert.equal(pod.status, 201);
const viewer = await putJson(port, `/v1/admin/access/users/${userId}/device-pods/device-pod-enforce/viewer`, {}, adminCookie);
assert.equal(viewer.status, 201);
assert.equal(openFgaAuthorizer.tuples.has(`user:${userId}#viewer@device_pod:device-pod-enforce`), true);
const profileCheck = await postJson(port, "/v1/admin/access/check", {
userId,
relation: "profile_editor",
object: "device_pod:device-pod-enforce"
}, adminCookie);
assert.equal(profileCheck.status, 200);
assert.equal(profileCheck.body.authorization.allowed, false);
assert.equal(profileCheck.body.authorization.mode, "enforce");
const aliceLogin = await postJson(port, "/auth/login", { username: "enforce-alice", password: "alice-pass" });
assert.equal(aliceLogin.status, 200);
const visible = await getJson(port, "/v1/device-pods", aliceLogin.cookie);
assert.equal(visible.status, 200);
assert.deepEqual(visible.body.devicePods.map((item) => item.devicePodId), ["device-pod-enforce"]);
const deniedJob = await postJson(port, "/v1/device-pods/device-pod-enforce/jobs", { intent: "workspace.ls", args: { path: "." } }, aliceLogin.cookie);
assert.equal(deniedJob.status, 403);
assert.equal(deniedJob.body.error.code, "device_pod_authorization_denied");
assert.match(deniedJob.body.blocker.summary, /relation operator/u);
const operator = await putJson(port, `/v1/admin/access/users/${userId}/device-pods/device-pod-enforce/operator`, {}, adminCookie);
assert.equal(operator.status, 201);
const allowedJob = await postJson(port, "/v1/device-pods/device-pod-enforce/jobs", { intent: "workspace.ls", args: { path: "." } }, aliceLogin.cookie);
assert.equal(allowedJob.status, 409);
assert.equal(allowedJob.body.blocker.code, "device_pod_executor_unavailable");
const profileGrant = await putJson(port, `/v1/admin/access/users/${userId}/device-pods/device-pod-enforce/profile_editor`, {}, adminCookie);
assert.equal(profileGrant.status, 201);
const profileAllowed = await postJson(port, "/v1/admin/access/check", {
userId,
relation: "profile_editor",
object: "device_pod:device-pod-enforce"
}, adminCookie);
assert.equal(profileAllowed.status, 200);
assert.equal(profileAllowed.body.authorization.allowed, true);
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api stores and restores Code Agent conversations by authenticated account", async () => {
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
now: () => "2026-05-31T07:30:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const adminCookie = adminLogin.cookie;
const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice", password: "alice-pass" }, adminCookie);
const bobCreate = await postJson(port, "/v1/admin/users", { username: "bob", password: "bob-pass" }, adminCookie);
assert.equal(aliceCreate.status, 201);
assert.equal(bobCreate.status, 201);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice", password: "alice-pass" });
const bobLogin = await postJson(port, "/auth/login", { username: "bob", password: "bob-pass" });
const stored = await putJson(port, "/v1/agent/conversations/cnv_account_sync", {
projectId: "prj_device_pod_workbench",
sessionId: "ses_account_sync",
threadId: "thread-account-sync",
lastTraceId: "trc_account_sync",
sessionStatus: "idle",
messages: [
{ id: "msg_user", role: "user", title: "用户", text: "在吗", status: "sent", conversationId: "cnv_account_sync", sessionId: "ses_account_sync", threadId: "thread-account-sync" },
{ id: "msg_agent", role: "agent", title: "Agent", text: "在", status: "completed", traceId: "trc_account_sync", conversationId: "cnv_account_sync", sessionId: "ses_account_sync", threadId: "thread-account-sync" }
]
}, aliceLogin.cookie);
assert.equal(stored.status, 200);
assert.equal(stored.body.conversation.conversationId, "cnv_account_sync");
assert.equal(stored.body.conversation.sessionId, "ses_account_sync");
assert.equal(stored.body.conversation.threadId, "thread-account-sync");
assert.equal(stored.body.conversation.status, "idle");
assert.equal(stored.body.conversation.messages.length, 2);
const aliceList = await getJson(port, "/v1/agent/conversations?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(aliceList.status, 200);
assert.equal(aliceList.body.count, 1);
assert.equal(aliceList.body.defaultConversation.conversationId, "cnv_account_sync");
assert.equal(aliceList.body.defaultConversation.messages[1].text, "在");
assert.equal(aliceList.body.defaultConversation.valuesRedacted, true);
const bobList = await getJson(port, "/v1/agent/conversations?projectId=prj_device_pod_workbench", bobLogin.cookie);
assert.equal(bobList.status, 200);
assert.deepEqual(bobList.body.conversations, []);
const bobDirect = await getJson(port, "/v1/agent/conversations/cnv_account_sync", bobLogin.cookie);
assert.equal(bobDirect.status, 404);
assert.equal(bobDirect.body.error.code, "agent_conversation_not_found");
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(workspace.status, 200);
const selected = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
projectId: "prj_device_pod_workbench",
conversationId: "cnv_account_sync",
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(selected.status, 200);
assert.equal(selected.body.workspace.selectedConversationId, "cnv_account_sync");
const deleted = await deleteJson(port, "/v1/agent/conversations/cnv_account_sync", {
projectId: "prj_device_pod_workbench",
workspaceId: workspace.body.workspace.workspaceId,
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(deleted.status, 200);
assert.equal(deleted.body.status, "deleted");
assert.equal(deleted.body.archivedCount, 1);
assert.equal(deleted.body.workspace.selectedConversationId, null);
const afterDeleteList = await getJson(port, "/v1/agent/conversations?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(afterDeleteList.status, 200);
assert.equal(afterDeleteList.body.count, 0);
const archivedDirect = await getJson(port, "/v1/agent/conversations/cnv_account_sync", aliceLogin.cookie);
assert.equal(archivedDirect.status, 200);
assert.equal(archivedDirect.body.conversation.status, "archived");
const logout = await postJson(port, "/auth/logout", {}, aliceLogin.cookie);
assert.equal(logout.status, 200);
const relogin = await postJson(port, "/auth/login", { username: "alice", password: "alice-pass" });
const restored = await getJson(port, "/v1/agent/conversations/cnv_account_sync", relogin.cookie);
assert.equal(restored.status, 200);
assert.equal(restored.body.conversation.threadId, "thread-account-sync");
assert.equal(restored.body.conversation.messages[0].text, "在吗");
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api exposes terminal conversation status when stored session status is stale running", async () => {
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
now: () => "2026-06-04T09:30:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue834", password: "alice-pass" }, adminLogin.cookie);
assert.equal(aliceCreate.status, 201);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue834", password: "alice-pass" });
const stored = await putJson(port, "/v1/agent/conversations/cnv_issue834_final", {
projectId: "prj_device_pod_workbench",
sessionId: "ses_issue834_final",
threadId: "thread-issue-834",
sessionStatus: "running",
lastTraceId: "trc_issue834_stale_running",
messages: [
{ id: "msg_issue834_user", role: "user", title: "用户", text: "final response 显示异常", status: "sent", conversationId: "cnv_issue834_final", sessionId: "ses_issue834_final", threadId: "thread-issue-834" },
{ id: "msg_issue834_agent", role: "agent", title: "Agent", text: "最终回复已经生成。", status: "completed", traceId: "trc_issue834_final", conversationId: "cnv_issue834_final", sessionId: "ses_issue834_final", threadId: "thread-issue-834" }
]
}, aliceLogin.cookie);
assert.equal(stored.status, 200);
assert.equal(stored.body.conversation.status, "completed");
assert.equal(stored.body.conversation.session.status, "completed");
assert.equal(stored.body.conversation.lastTraceId, "trc_issue834_final");
const direct = await getJson(port, "/v1/agent/conversations/cnv_issue834_final", aliceLogin.cookie);
assert.equal(direct.status, 200);
assert.equal(direct.body.conversation.status, "completed");
assert.equal(direct.body.conversation.session.status, "completed");
assert.equal(direct.body.conversation.lastTraceId, "trc_issue834_final");
assert.equal(direct.body.conversation.messages[1].text, "最终回复已经生成。");
const list = await getJson(port, "/v1/agent/conversations?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(list.status, 200);
const listed = list.body.conversations.find((conversation) => conversation.conversationId === "cnv_issue834_final");
assert.ok(listed, "expected stale-running conversation to be listed");
assert.equal(listed.status, "completed");
assert.equal(listed.session.status, "completed");
assert.equal(listed.lastTraceId, "trc_issue834_final");
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api repairs persisted final response fallback from terminal result", async () => {
const codeAgentChatResults = new Map();
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: "http://127.0.0.1:9",
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
},
codeAgentChatResults,
now: () => "2026-06-04T09:45:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue834-fallback", password: "alice-pass" }, adminLogin.cookie);
assert.equal(aliceCreate.status, 201);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue834-fallback", password: "alice-pass" });
const fallbackText = "Code Agent 仍在处理,可以继续 steer 或等待 trace 完成。";
const finalText = "**直接答:能,但只到一半。**\n\n功能已经过了,剩余风险另开 issue 跟踪。";
codeAgentChatResults.set("trc_issue834_final_response", {
status: "completed",
traceId: "trc_issue834_final_response",
conversationId: "cnv_issue834_fallback",
sessionId: "ses_issue834_fallback",
threadId: "thread-issue-834-fallback",
ownerUserId: aliceCreate.body.user.id,
reply: { role: "assistant", content: finalText, messageId: "msg_issue834_real_final" },
agentRun: { terminalStatus: "completed" },
session: {
sessionId: "ses_issue834_fallback",
conversationId: "cnv_issue834_fallback",
threadId: "thread-issue-834-fallback",
status: "idle"
}
});
const stored = await putJson(port, "/v1/agent/conversations/cnv_issue834_fallback", {
projectId: "prj_device_pod_workbench",
sessionId: "ses_issue834_fallback",
threadId: "thread-issue-834-fallback",
sessionStatus: "completed",
lastTraceId: "trc_issue834_final_response",
messages: [
{ id: "msg_issue834_question", role: "user", title: "用户", text: "功能是过了还是没过?", status: "sent", conversationId: "cnv_issue834_fallback", sessionId: "ses_issue834_fallback", threadId: "thread-issue-834-fallback" },
{ id: "msg_issue834_fallback", role: "agent", title: "Agent", text: fallbackText, status: "completed", traceId: "trc_issue834_final_response", conversationId: "cnv_issue834_fallback", sessionId: "ses_issue834_fallback", threadId: "thread-issue-834-fallback" }
]
}, aliceLogin.cookie);
assert.equal(stored.status, 200);
assert.equal(stored.body.conversation.messages[1].text, fallbackText);
assert.equal(stored.body.conversation.status, "completed");
const list = await getJson(port, "/v1/agent/conversations?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(list.status, 200);
const listed = list.body.conversations.find((conversation) => conversation.conversationId === "cnv_issue834_fallback");
assert.ok(listed, "expected fallback conversation to be listed");
assert.equal(listed.status, "idle");
assert.equal(listed.session.status, "idle");
assert.equal(listed.lastTraceId, "trc_issue834_final_response");
assert.equal(listed.messages[1].text, finalText);
assert.equal(listed.messages[1].status, "idle");
const direct = await getJson(port, "/v1/agent/conversations/cnv_issue834_fallback", aliceLogin.cookie);
assert.equal(direct.status, 200);
assert.equal(direct.body.conversation.status, "idle");
assert.equal(direct.body.conversation.lastTraceId, "trc_issue834_final_response");
assert.equal(direct.body.conversation.messages[1].text, finalText);
assert.equal(direct.body.conversation.messages[1].status, "idle");
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(workspace.status, 200);
const selected = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
projectId: "prj_device_pod_workbench",
conversationId: "cnv_issue834_fallback",
sessionId: "ses_issue834_fallback",
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(selected.status, 200);
assert.equal(selected.body.workspace.selectedConversation.messages[1].text, finalText);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api repairs conversation snapshot when persisted result advances last trace", async () => {
const codeAgentChatResults = new Map();
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: "http://127.0.0.1:9",
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
},
codeAgentChatResults,
now: () => "2026-06-04T10:05:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue842-snapshot", password: "alice-pass" }, adminLogin.cookie);
assert.equal(aliceCreate.status, 201);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue842-snapshot", password: "alice-pass" });
const finalText = "持久化验收OK。";
codeAgentChatResults.set("trc_issue842_persisted_final", {
status: "completed",
traceId: "trc_issue842_persisted_final",
conversationId: "cnv_issue842_snapshot",
sessionId: "ses_issue842_snapshot",
threadId: "thread-issue-842-snapshot",
ownerUserId: aliceCreate.body.user.id,
reply: { role: "assistant", content: finalText, messageId: "msg_issue842_persisted_final" },
agentRun: { terminalStatus: "completed" },
session: {
sessionId: "ses_issue842_snapshot",
conversationId: "cnv_issue842_snapshot",
threadId: "thread-issue-842-snapshot",
status: "idle"
}
});
const stored = await putJson(port, "/v1/agent/conversations/cnv_issue842_snapshot", {
projectId: "prj_device_pod_workbench",
sessionId: "ses_issue842_snapshot",
threadId: "thread-issue-842-snapshot",
sessionStatus: "idle",
lastTraceId: "trc_issue842_persisted_final",
messages: [
{ id: "msg_issue842_user", role: "user", title: "用户", text: "请短答", status: "sent", conversationId: "cnv_issue842_snapshot", sessionId: "ses_issue842_snapshot", threadId: "thread-issue-842-snapshot" },
{ id: "msg_issue842_old_agent", role: "agent", title: "Agent", text: "上一轮回复", status: "idle", traceId: "trc_issue842_previous", conversationId: "cnv_issue842_snapshot", sessionId: "ses_issue842_snapshot", threadId: "thread-issue-842-snapshot" }
]
}, aliceLogin.cookie);
assert.equal(stored.status, 200);
assert.equal(stored.body.conversation.lastTraceId, "trc_issue842_persisted_final");
assert.equal(stored.body.conversation.messages.at(-1).text, "上一轮回复");
const list = await getJson(port, "/v1/agent/conversations?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(list.status, 200);
const listed = list.body.conversations.find((conversation) => conversation.conversationId === "cnv_issue842_snapshot");
assert.ok(listed, "expected conversation to be listed");
assert.equal(listed.lastTraceId, "trc_issue842_persisted_final");
assert.equal(listed.messages.at(-1).traceId, "trc_issue842_persisted_final");
assert.equal(listed.messages.at(-1).text, finalText);
assert.equal(listed.messages.at(-1).status, "idle");
const direct = await getJson(port, "/v1/agent/conversations/cnv_issue842_snapshot", aliceLogin.cookie);
assert.equal(direct.status, 200);
assert.equal(direct.body.conversation.messages.at(-1).traceId, "trc_issue842_persisted_final");
assert.equal(direct.body.conversation.messages.at(-1).text, finalText);
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(workspace.status, 200);
const selected = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
projectId: "prj_device_pod_workbench",
conversationId: "cnv_issue842_snapshot",
sessionId: "ses_issue842_snapshot",
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(selected.status, 200);
assert.equal(selected.body.workspace.selectedConversation.messages.at(-1).traceId, "trc_issue842_persisted_final");
assert.equal(selected.body.workspace.selectedConversation.messages.at(-1).text, finalText);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api terminal workspace sync preserves saved messages and stores runner trace", async () => {
const codeAgentChatResults = new Map();
const traceId = "trc_issue853_fast_fail_trace";
const conversationId = "cnv_issue853_fast_fail";
const sessionId = "ses_issue853_fast_fail";
const threadId = "thread-issue-853-fast-fail";
const env = {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: "http://127.0.0.1:9",
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
};
const now = () => "2026-06-04T10:25:00.000Z";
const accessController = createAccessController({ env, now });
const server = createCloudApiServer({
env,
accessController,
codeAgentChatResults,
now
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue853-fast-fail", password: "alice-pass" }, adminLogin.cookie);
assert.equal(aliceCreate.status, 201);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue853-fast-fail", password: "alice-pass" });
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(workspace.status, 200);
const savedMessages = [
{ id: "msg_issue853_user", role: "user", title: "用户", text: "请执行 hwpod profile list", status: "sent", conversationId, sessionId, threadId, createdAt: "2026-06-04T10:24:59.000Z" },
{ id: "msg_issue853_pending", role: "agent", title: "Code Agent 处理中", text: "Code Agent 仍在处理,可以继续 steer 或等待 trace 完成。", status: "running", traceId, conversationId, sessionId, threadId, createdAt: "2026-06-04T10:24:59.000Z" }
];
const stored = await putJson(port, `/v1/agent/conversations/${conversationId}`, {
projectId: "prj_device_pod_workbench",
sessionId,
threadId,
sessionStatus: "active",
lastTraceId: traceId,
messages: savedMessages
}, aliceLogin.cookie);
assert.equal(stored.status, 200);
await accessController.recordAgentSessionOwner({
ownerUserId: aliceCreate.body.user.id,
sessionId,
projectId: "prj_device_pod_workbench",
agentId: "hwlab-code-agent",
status: "failed",
conversationId,
threadId,
traceId,
session: {
source: "code-agent-result",
status: "failed",
sessionStatus: "failed",
messages: [],
messageCount: 0,
finalResponse: null,
valuesRedacted: true,
secretMaterialStored: false
}
});
const preserved = await accessController.getAgentSession(sessionId);
assert.equal(preserved.session.messages.length, 2);
assert.equal(preserved.session.messages[0].text, "请执行 hwpod profile list");
codeAgentChatResults.set(traceId, {
status: "failed",
traceId,
conversationId,
sessionId,
threadId,
ownerUserId: aliceCreate.body.user.id,
error: { code: "provider_insufficient_balance", message: "HyueAPI 403 INSUFFICIENT_BALANCE" },
agentRun: { terminalStatus: "failed" },
runnerTrace: {
traceId,
status: "failed",
eventCount: 35,
events: [],
lastEvent: { label: "agentrun:result:failed", status: "failed", type: "result" }
},
session: { sessionId, conversationId, threadId, status: "failed" }
});
const selected = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
expectedRevision: 1,
selectedConversationId: conversationId,
selectedAgentSessionId: sessionId,
activeTraceId: traceId,
sessionStatus: "running",
messages: savedMessages,
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(selected.status, 200);
assert.equal(selected.body.workspace.activeTraceId, traceId);
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(restored.status, 200);
assert.equal(restored.body.workspace.activeTraceId, null);
const messages = restored.body.workspace.selectedConversation.messages;
assert.equal(messages.length, 2);
assert.equal(messages[0].role, "user");
assert.equal(messages[0].text, "请执行 hwpod profile list");
assert.equal(messages[1].role, "agent");
assert.equal(messages[1].status, "failed");
assert.equal(messages[1].text, "HyueAPI 403 INSUFFICIENT_BALANCE");
assert.equal(messages[1].runnerTrace.traceId, traceId);
assert.equal(messages[1].runnerTrace.eventCount, 35);
assert.equal(messages[1].runnerTrace.eventsCompacted, true);
assert.equal(messages[1].runnerTrace.fullTraceLoaded, false);
assert.equal(restored.body.workspace.selectedConversation.messageCount, 2);
assert.equal(restored.body.workspace.selectedConversation.firstUserMessagePreview, "请执行 hwpod profile list");
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api terminal workspace sync rebuilds missing user message from saved preview", async () => {
const codeAgentChatResults = new Map();
const traceId = "trc_issue853_preview_only_trace";
const conversationId = "cnv_issue853_preview_only";
const sessionId = "ses_issue853_preview_only";
const threadId = "thread-issue-853-preview-only";
const userText = "请执行 hwpod profile list,并在最终回复里原样包含 HWLAB-853-FINAL-preview-only";
const env = {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: "http://127.0.0.1:9",
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
};
const now = () => "2026-06-04T10:35:00.000Z";
const accessController = createAccessController({ env, now });
const server = createCloudApiServer({ env, accessController, codeAgentChatResults, now });
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue853-preview-only", password: "alice-pass" }, adminLogin.cookie);
assert.equal(aliceCreate.status, 201);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue853-preview-only", password: "alice-pass" });
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(workspace.status, 200);
await accessController.recordAgentSessionOwner({
ownerUserId: aliceCreate.body.user.id,
sessionId,
projectId: "prj_device_pod_workbench",
agentId: "hwlab-code-agent",
status: "failed",
conversationId,
threadId,
traceId,
session: {
source: "workbench-status-sync",
sessionStatus: "failed",
lastTraceId: traceId,
messages: [
{ id: "msg_issue853_preview_agent", role: "agent", title: "Code Agent result", text: "HyueAPI 403 INSUFFICIENT_BALANCE", status: "failed", traceId, conversationId, sessionId, threadId, createdAt: "2026-06-04T10:34:59.000Z" }
],
messageCount: 1,
firstUserMessagePreview: userText,
valuesRedacted: true,
secretMaterialStored: false
}
});
codeAgentChatResults.set(traceId, {
status: "failed",
traceId,
conversationId,
sessionId,
threadId,
ownerUserId: aliceCreate.body.user.id,
error: { code: "provider_insufficient_balance", message: "HyueAPI 403 INSUFFICIENT_BALANCE" },
agentRun: { terminalStatus: "failed" },
runnerTrace: {
traceId,
status: "failed",
eventCount: 35,
events: [],
lastEvent: { label: "agentrun:result:failed", status: "failed", type: "result" }
},
session: { sessionId, conversationId, threadId, status: "failed" }
});
const selected = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
expectedRevision: 1,
selectedConversationId: conversationId,
selectedAgentSessionId: sessionId,
activeTraceId: traceId,
sessionStatus: "running",
messages: [],
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(selected.status, 200);
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_device_pod_workbench", aliceLogin.cookie);
assert.equal(restored.status, 200);
assert.equal(restored.body.workspace.activeTraceId, null);
const messages = restored.body.workspace.selectedConversation.messages;
assert.equal(messages.length, 2);
assert.equal(messages[0].role, "user");
assert.equal(messages[0].text, userText);
assert.equal(messages[0].traceId, traceId);
assert.equal(messages[1].role, "agent");
assert.equal(messages[1].status, "failed");
assert.equal(messages[1].text, "HyueAPI 403 INSUFFICIENT_BALANCE");
assert.equal(messages[1].runnerTrace.traceId, traceId);
assert.equal(messages[1].runnerTrace.eventCount, 35);
assert.equal(restored.body.workspace.selectedConversation.messageCount, 2);
assert.equal(restored.body.workspace.selectedConversation.firstUserMessagePreview, userText);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("access controller preserves user message when owner evidence updates with agent-only result", async () => {
const projectId = "prj_device_pod_workbench";
const ownerUserId = "usr_issue853_owner_merge";
const conversationId = "cnv_issue853_owner_merge";
const sessionId = "ses_issue853_owner_merge";
const traceId = "trc_issue853_owner_merge";
const threadId = "thread-issue-853-owner-merge";
const nowValue = "2026-06-04T10:45:00.000Z";
const userText = "请执行 hwpod profile list,并在最终回复里原样包含 HWLAB-853-FINAL-owner-merge";
const accessController = createAccessController({ now: () => nowValue });
await accessController.recordAgentSessionOwner({
ownerUserId,
sessionId,
projectId,
agentId: "hwlab-code-agent",
status: "active",
conversationId,
threadId,
traceId,
session: {
source: "workbench-status-sync",
sessionStatus: "running",
lastTraceId: traceId,
messages: [
{ id: "msg_issue853_owner_user", role: "user", title: "用户", text: userText, status: "sent", traceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue853_owner_agent_pending", role: "agent", title: "Code Agent 处理中", text: "Code Agent 仍在处理,可以继续 steer 或等待 trace 完成。", status: "running", traceId, conversationId, sessionId, threadId, createdAt: nowValue }
],
messageCount: 2,
firstUserMessagePreview: userText,
valuesRedacted: true,
secretMaterialStored: false
}
});
await accessController.recordAgentSessionOwner({
ownerUserId,
sessionId,
projectId,
agentId: "hwlab-code-agent",
status: "failed",
conversationId,
threadId,
traceId,
session: {
source: "code-agent-result",
status: "failed",
sessionStatus: "failed",
messages: [
{ id: "msg_issue853_owner_agent_result", role: "agent", title: "Code Agent result", text: "HyueAPI 403 INSUFFICIENT_BALANCE", status: "failed", traceId, conversationId, sessionId, threadId, createdAt: nowValue }
],
messageCount: 1,
firstUserMessagePreview: userText,
valuesRedacted: true,
secretMaterialStored: false
}
});
const session = await accessController.getAgentSession(sessionId);
const conversation = await accessController.visibleConversationForActor({ id: ownerUserId, role: "user" }, conversationId, projectId, { includeArchived: true });
assert.deepEqual(session.session.messages.map((message) => message.role), ["user", "agent"]);
assert.deepEqual(conversation.messages.map((message) => message.role), ["user", "agent"]);
assert.equal(session.session.messages[0].text, userText);
assert.equal(conversation.messages[0].text, userText);
assert.equal(session.session.messages[0].traceId, traceId);
assert.equal(conversation.messages[0].traceId, traceId);
assert.equal(session.session.messages[1].text, "HyueAPI 403 INSUFFICIENT_BALANCE");
assert.equal(conversation.messages[1].status, "failed");
assert.equal(session.session.messageCount, 2);
assert.equal(conversation.messageCount, 2);
assert.equal(conversation.firstUserMessagePreview, userText);
});
test("access controller restores AgentRun mapping by Code Agent traceId", async () => {
const accessController = createAccessController({ now: () => "2026-06-01T00:00:00.000Z" });
await accessController.recordAgentSessionOwner({
ownerUserId: "usr_agent_owner",
sessionId: "ses_agentrun_trace_lookup",
projectId: "prj_v02_code_agent",
conversationId: "cnv_agentrun_trace_lookup",
threadId: "thread-agentrun-trace-lookup",
traceId: "trc_agentrun_trace_lookup",
status: "running",
session: {
source: "hwlab-cloud-api-agentrun-v01-adapter",
agentRun: {
adapter: "agentrun-v01",
runId: "run_trace_lookup",
commandId: "cmd_trace_lookup",
jobName: "agentrun-v01-runner-trace-lookup",
namespace: "agentrun-v01",
backendProfile: "deepseek",
valuesPrinted: false
},
valuesRedacted: true
}
});
const restored = await accessController.getAgentSessionByTraceId("trc_agentrun_trace_lookup");
assert.equal(restored.id, "ses_agentrun_trace_lookup");
assert.equal(restored.lastTraceId, "trc_agentrun_trace_lookup");
assert.equal(restored.session.agentRun.runId, "run_trace_lookup");
assert.equal(restored.session.agentRun.commandId, "cmd_trace_lookup");
assert.equal(restored.session.agentRun.namespace, "agentrun-v01");
});
test("access controller preserves AgentRun runner mapping across conversation snapshot updates", async () => {
const accessController = createAccessController({ now: () => "2026-06-01T00:00:00.000Z" });
await accessController.recordAgentSessionOwner({
ownerUserId: "usr_agent_owner",
sessionId: "ses_agentrun_reuse_mapping",
projectId: "prj_v02_code_agent",
conversationId: "cnv_agentrun_reuse_mapping",
threadId: "thread-agentrun-requested",
traceId: "trc_agentrun_reuse_first",
status: "running",
session: {
source: "hwlab-cloud-api-agentrun-v01-adapter",
agentRun: {
adapter: "agentrun-v01",
runId: "run_reuse_mapping",
commandId: "cmd_reuse_mapping_first",
runnerId: "runner_reuse_mapping",
jobName: "agentrun-v01-runner-reuse-mapping",
namespace: "agentrun-v01",
backendProfile: "deepseek",
reuseEligible: true,
valuesPrinted: false
},
valuesRedacted: true
}
});
await accessController.recordAgentSessionOwner({
ownerUserId: "usr_agent_owner",
sessionId: "ses_agentrun_reuse_mapping",
projectId: "prj_v02_code_agent",
conversationId: "cnv_agentrun_reuse_mapping",
threadId: "thread-agentrun-requested",
traceId: "trc_agentrun_reuse_second",
status: "active",
session: {
source: "workbench-conversation-snapshot",
messages: [{ role: "assistant", content: "done" }],
valuesRedacted: true
}
});
const restored = await accessController.getAgentSession("ses_agentrun_reuse_mapping");
assert.equal(restored.lastTraceId, "trc_agentrun_reuse_second");
assert.equal(restored.session.source, "workbench-conversation-snapshot");
assert.equal(restored.session.agentRun.runId, "run_reuse_mapping");
assert.equal(restored.session.agentRun.commandId, "cmd_reuse_mapping_first");
assert.equal(restored.session.agentRun.jobName, "agentrun-v01-runner-reuse-mapping");
assert.equal(restored.session.agentRun.reuseEligible, true);
});
test("cloud api protects device-pod routes when access control is required", async () => {
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
now: () => "2026-05-28T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/device-pods`);
assert.equal(response.status, 401);
const payload = await response.json();
assert.equal(payload.error.code, "auth_required");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api accepts user API key Bearer auth for authorized device access", async () => {
const openFgaAuthorizer = createFakeOpenFgaAuthorizer({ mode: "enforce" });
const accessController = createAccessController({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_ID: "usr_v02_admin",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
openFgaAuthorizer,
now: () => "2026-05-28T00:00:00.000Z"
});
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_ID: "usr_v02_admin",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
accessController,
now: () => "2026-05-28T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
await postJson(port, "/v1/admin/device-pods", {
devicePodId: "device-pod-71-freq",
profile: { schemaVersion: 1, target: { id: "target-71-freq" }, route: { gatewaySessionId: "gws_unused" } }
}, adminLogin.cookie);
const userApiKey = (await postJson(port, "/v1/api-keys", { name: "Device access test" }, adminLogin.cookie)).body.key.displaySecret;
assert.match(userApiKey, /^hwl_live_/u);
const status = await getJson(port, "/v1/device-pods/device-pod-71-freq/status", null, {
authorization: `Bearer ${userApiKey}`
});
assert.equal(status.status, 200);
assert.equal(status.body.devicePodId, "device-pod-71-freq");
assert.equal(status.body.actor.id, "usr_v02_admin");
assert.equal(status.body.actor.role, "admin");
assert.equal(JSON.stringify(status.body).includes(userApiKey), false);
const adminWithUserApiKey = await postJson(port, "/v1/admin/device-pods", {
devicePodId: "device-pod-user-api-key-admin",
profile: { schemaVersion: 1, target: { id: "target-blocked" }, route: { gatewaySessionId: "gws_unused" } }
}, null, {
authorization: `Bearer ${userApiKey}`
});
assert.equal(adminWithUserApiKey.status, 201);
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api dispatches authorized device jobs to the internal device-pod executor", async () => {
const executorRequests = [];
const executor = createServer(async (request, response) => {
const body = await requestJson(request);
executorRequests.push({ method: request.method, url: request.url, internalService: request.headers["x-hwlab-internal-service"], internalToken: request.headers["x-hwlab-internal-token"], body });
if (request.method === "POST" && body.args?.path === "src") {
response.writeHead(202, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify({
accepted: true,
status: "running",
contractVersion: "device-pod-executor-v1",
devicePodId: "device-pod-71-freq",
traceId: body.traceId,
operationId: body.operationId,
job: { id: body.jobId, devicePodId: body.devicePodId, status: "running", intent: body.intent, reason: body.reason, traceId: body.traceId, operationId: body.operationId },
blocker: null
}));
return;
}
if (request.method === "GET" && request.url.endsWith("/output")) {
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify({
accepted: true,
status: "completed",
contractVersion: "device-pod-executor-v1",
devicePodId: "device-pod-71-freq",
traceId: "trc_executor_refresh",
operationId: "op_executor_refresh",
job: { id: request.url.split("/").at(-2), devicePodId: "device-pod-71-freq", status: "completed", intent: "workspace.ls" },
output: {
executor: { status: "completed" },
output: { dispatch: { exitCode: 0, stdoutBytes: 15 } },
text: "executor output",
bytes: 15,
truncation: { maxBytes: 12000, truncated: false }
}
}));
return;
}
if (request.method === "POST" && body.intent === "workspace.evidence" && body.args?.kind === "verify") {
const text = JSON.stringify({
ok: true,
action: "workspace.build.verify",
data: {
buildJob: { jobId: "keil-build-latest", status: "completed", success: true },
artifacts: [{ kind: "hex", exists: true, size: 42, headMagicB64: "Og==" }],
missing: []
}
});
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify({
accepted: true,
status: "completed",
contractVersion: "device-pod-executor-v1",
devicePodId: "device-pod-71-freq",
traceId: body.traceId,
operationId: body.operationId,
job: { id: body.jobId, devicePodId: body.devicePodId, status: "completed", intent: body.intent, reason: body.reason, traceId: body.traceId, operationId: body.operationId },
output: { text, bytes: Buffer.byteLength(text, "utf8"), truncation: { maxBytes: 12000, truncated: false } }
}));
return;
}
response.writeHead(409, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify({
accepted: false,
status: "blocked",
contractVersion: "device-pod-executor-v1",
devicePodId: "device-pod-71-freq",
traceId: body.traceId,
operationId: body.operationId,
job: { id: body.jobId, devicePodId: body.devicePodId, status: "blocked", intent: body.intent, reason: body.reason, traceId: body.traceId, operationId: body.operationId },
blocker: { code: "gateway_dispatch_unavailable", layer: "device-pod", retryable: true, summary: "executor test blocker" },
output: { text: "executor output", bytes: 15, truncation: { maxBytes: 12000, truncated: false } }
}));
});
await new Promise((resolve) => executor.listen(0, "127.0.0.1", resolve));
const executorPort = executor.address().port;
const accessEnv = {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_DEVICE_POD_INTERNAL_TOKEN: INTERNAL_TOKEN,
HWLAB_DEVICE_POD_URL: `http://127.0.0.1:${executorPort}`
};
const accessController = createAccessController({
env: accessEnv,
openFgaAuthorizer: createFakeOpenFgaAuthorizer({ mode: "enforce" }),
now: () => "2026-05-28T00:00:00.000Z"
});
const server = createCloudApiServer({
env: accessEnv,
accessController,
now: () => "2026-05-28T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const userCreate = await postJson(port, "/v1/admin/users", { username: "alice", password: "alice-pass" }, adminLogin.cookie);
await postJson(port, "/v1/admin/device-pods", {
devicePodId: "device-pod-71-freq",
profile: { schemaVersion: 1, target: { id: "target-71-freq" }, route: { gatewaySessionId: "gws_unused" } }
}, adminLogin.cookie);
await grantDevicePodAccess(port, adminLogin.cookie, userCreate.body.user.id, "device-pod-71-freq", ["viewer", "operator"]);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice", password: "alice-pass" });
const job = await postJson(port, "/v1/device-pods/device-pod-71-freq/jobs", { intent: "workspace.ls", args: { path: "." } }, aliceLogin.cookie);
assert.equal(job.status, 409);
assert.equal(job.body.blocker.code, "gateway_dispatch_unavailable");
assert.equal(job.body.job.status, "blocked");
assert.equal(executorRequests.length, 1);
assert.equal(executorRequests[0].internalService, "hwlab-cloud-api");
assert.equal(executorRequests[0].internalToken, INTERNAL_TOKEN);
assert.equal(executorRequests[0].body.profileHash, job.body.profileHash);
assert.equal(executorRequests[0].body.ownerUserId, userCreate.body.user.id);
assert.equal(executorRequests[0].body.intent, "workspace.ls");
const stored = await getJson(port, `/v1/device-pods/device-pod-71-freq/jobs/${job.body.job.id}`, aliceLogin.cookie);
assert.equal(stored.status, 200);
assert.equal(stored.body.job.id, job.body.job.id);
assert.equal(stored.body.blocker.code, "gateway_dispatch_unavailable");
const runningJob = await postJson(port, "/v1/device-pods/device-pod-71-freq/jobs", { intent: "workspace.ls", args: { path: "src" } }, aliceLogin.cookie);
assert.equal(runningJob.status, 202);
assert.equal(runningJob.body.status, "running");
const output = await getJson(port, `/v1/device-pods/device-pod-71-freq/jobs/${runningJob.body.job.id}/output`, aliceLogin.cookie);
assert.equal(output.status, 200);
assert.equal(output.body.status, "completed");
assert.equal(output.body.blocker, null);
assert.equal(output.body.freshness.stale, false);
assert.equal(output.body.output.text, "executor output");
assert.deepEqual(output.body.output.output.dispatch, { exitCode: 0, stdoutBytes: 15 });
assert.equal(output.body.truncation.truncated, false);
assert.ok(executorRequests.some((item) => item.method === "GET" && item.url.endsWith(`/jobs/${runningJob.body.job.id}/output`)));
const verifyJob = await postJson(port, "/v1/device-pods/device-pod-71-freq/jobs", { intent: "workspace.evidence", args: { kind: "verify", expected: "axf,hex", headBytes: 16 } }, aliceLogin.cookie);
assert.equal(verifyJob.status, 200);
assert.equal(verifyJob.body.status, "completed");
assert.equal(verifyJob.body.job.intent, "workspace.evidence");
assert.equal(verifyJob.body.outputUrl.endsWith("/output"), true);
const verify = JSON.parse(verifyJob.body.output.text);
assert.equal(verify.action, "workspace.build.verify");
assert.equal(verify.data.buildJob.jobId, "keil-build-latest");
assert.equal(verify.data.artifacts[0].headMagicB64, "Og==");
assert.equal(verifyJob.body.truncation.truncated, false);
assert.equal(executorRequests.some((item) => item.method === "GET" && item.url.endsWith(`/jobs/${verifyJob.body.job.id}/output`)), false);
const events = await getJson(port, "/v1/device-pods/device-pod-71-freq/events", aliceLogin.cookie);
const completedEvent = events.body.events.find((event) => event.refs.jobId === runningJob.body.job.id);
assert.equal(completedEvent.status, "completed");
assert.equal(completedEvent.blocker, null);
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
await new Promise((resolve, reject) => executor.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api waits longer for synchronous device-pod build verify output (HWLAB #821)", async () => {
const executor = createServer(async (request, response) => {
const body = await requestJson(request);
await new Promise((resolve) => setTimeout(resolve, 1500));
const text = JSON.stringify({ ok: true, action: "workspace.build.verify", data: { buildJob: { jobId: "keil-build-slow" }, artifacts: [], missing: [] } });
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify({
accepted: true,
status: "completed",
contractVersion: "device-pod-executor-v1",
devicePodId: "device-pod-71-freq",
traceId: body.traceId,
operationId: body.operationId,
job: { id: body.jobId, devicePodId: body.devicePodId, status: "completed", intent: body.intent, reason: body.reason, traceId: body.traceId, operationId: body.operationId },
output: { text, bytes: Buffer.byteLength(text, "utf8"), truncation: { maxBytes: 12000, truncated: false } }
}));
});
await new Promise((resolve) => executor.listen(0, "127.0.0.1", resolve));
const executorPort = executor.address().port;
const accessEnv = {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_DEVICE_POD_INTERNAL_TOKEN: INTERNAL_TOKEN,
HWLAB_DEVICE_POD_URL: `http://127.0.0.1:${executorPort}`
};
const accessController = createAccessController({
env: accessEnv,
openFgaAuthorizer: createFakeOpenFgaAuthorizer({ mode: "enforce" }),
now: () => "2026-05-28T00:00:00.000Z"
});
const server = createCloudApiServer({
env: accessEnv,
accessController,
now: () => "2026-05-28T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const userCreate = await postJson(port, "/v1/admin/users", { username: "slow-alice", password: "alice-pass" }, adminLogin.cookie);
await postJson(port, "/v1/admin/device-pods", {
devicePodId: "device-pod-71-freq",
profile: { schemaVersion: 1, target: { id: "target-71-freq" }, route: { gatewaySessionId: "gws_unused" } }
}, adminLogin.cookie);
await grantDevicePodAccess(port, adminLogin.cookie, userCreate.body.user.id, "device-pod-71-freq", ["viewer", "operator"]);
const aliceLogin = await postJson(port, "/auth/login", { username: "slow-alice", password: "alice-pass" });
const verifyJob = await postJson(port, "/v1/device-pods/device-pod-71-freq/jobs", { intent: "workspace.evidence", args: { kind: "verify", expected: "axf,hex" } }, aliceLogin.cookie);
assert.equal(verifyJob.status, 200);
assert.equal(verifyJob.body.status, "completed");
const verify = JSON.parse(verifyJob.body.output.text);
assert.equal(verify.action, "workspace.build.verify");
assert.equal(verify.data.buildJob.jobId, "keil-build-slow");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
await new Promise((resolve, reject) => executor.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api bounds device-pod job output payloads", async () => {
const longText = "x".repeat(65000);
const executor = createServer(async (request, response) => {
const body = await requestJson(request);
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify({
accepted: true,
status: "completed",
contractVersion: "device-pod-executor-v1",
devicePodId: "device-pod-71-freq",
traceId: body.traceId,
operationId: body.operationId,
job: { id: body.jobId, devicePodId: body.devicePodId, status: "completed", intent: body.intent },
output: { text: longText, nested: { kept: true } },
text: longText
}));
});
await new Promise((resolve) => executor.listen(0, "127.0.0.1", resolve));
const executorPort = executor.address().port;
const accessEnv = {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_DEVICE_POD_INTERNAL_TOKEN: INTERNAL_TOKEN,
HWLAB_DEVICE_POD_URL: `http://127.0.0.1:${executorPort}`
};
const accessController = createAccessController({
env: accessEnv,
openFgaAuthorizer: createFakeOpenFgaAuthorizer({ mode: "enforce" }),
now: () => "2026-05-28T00:00:00.000Z"
});
const server = createCloudApiServer({
env: accessEnv,
accessController,
now: () => "2026-05-28T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const userCreate = await postJson(port, "/v1/admin/users", { username: "alice", password: "alice-pass" }, adminLogin.cookie);
await postJson(port, "/v1/admin/device-pods", {
devicePodId: "device-pod-71-freq",
profile: { schemaVersion: 1, target: { id: "target-71-freq" }, route: { gatewaySessionId: "gws_unused" } }
}, adminLogin.cookie);
await grantDevicePodAccess(port, adminLogin.cookie, userCreate.body.user.id, "device-pod-71-freq", ["viewer", "operator"]);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice", password: "alice-pass" });
const job = await postJson(port, "/v1/device-pods/device-pod-71-freq/jobs", { intent: "workspace.ls", args: { path: "." } }, aliceLogin.cookie);
assert.equal(job.status, 200);
const output = await getJson(port, `/v1/device-pods/device-pod-71-freq/jobs/${job.body.job.id}/output`, aliceLogin.cookie);
assert.equal(output.status, 200);
assert.equal(output.body.bytes, 64000);
assert.equal(output.body.text.length, 64000);
assert.equal(output.body.output.text.length, 64000);
assert.equal(output.body.truncation.truncated, true);
assert.equal(output.body.truncation.originalBytes, 65000);
assert.equal(output.body.output.omitted.reason, "device_job_output_truncated");
assert.equal(JSON.stringify(output.body).includes(longText), false);
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
await new Promise((resolve, reject) => executor.close((error) => (error ? reject(error) : resolve())));
}
});
test("DEVICE_JOB_INTENTS accepts workspace.evidence and debug.evidence for v0.2 #773", () => {
const fs = require("node:fs");
const path = require("node:path");
const src = fs.readFileSync(path.join(__dirname, "access-control.ts"), "utf8");
assert.match(src, /"workspace\.evidence"/u, "workspace.evidence must be added to DEVICE_JOB_INTENTS");
assert.match(src, /"debug\.evidence"/u, "debug.evidence must be added to DEVICE_JOB_INTENTS");
});
test("_deviceJobRequiresReason helper considers sub-action for workspace.build / debug.download", () => {
const fs = require("node:fs");
const path = require("node:path");
const src = fs.readFileSync(path.join(__dirname, "access-control.ts"), "utf8");
assert.match(src, /function _deviceJobRequiresReason\(intent, args, reason\)/u, "_deviceJobRequiresReason must accept reason");
assert.match(src, /DEVICE_JOB_READ_ONLY_SUB_ACTIONS\.has\(action\)/u, "must consult DEVICE_JOB_READ_ONLY_SUB_ACTIONS");
assert.match(src, /!DEVICE_JOB_ACTIONABLE_INTENTS\.has\(intent\)\s*\)\s*return\s*true/u, "non-actionable mutating intent must always require reason");
});
test("executorOutputPayload extracts text from evidence and summary fields", () => {
const fs = require("node:fs");
const path = require("node:path");
const src = fs.readFileSync(path.join(__dirname, "access-control.ts"), "utf8");
const m = src.match(/function executorOutputPayload[\s\S]+?\n\}/u);
assert.ok(m, "executorOutputPayload not found");
const body = m[0];
assert.match(body, /evidence[\s\S]*?\.text/u, "must check evidence.text");
assert.match(body, /evidence[\s\S]*?\.logTail/u, "must check evidence.logTail");
assert.match(body, /evidence[\s\S]*?\.summary/u, "must check evidence.summary");
assert.match(body, /output\.summary/u, "must check output.summary");
assert.match(body, /nestedOutput\.summary/u, "must check nestedOutput.summary");
});
test("cloud api routes device-pod probe GET requests through executor jobs", async () => {
const executorRequests = [];
const executor = createServer(async (request, response) => {
const body = await requestJson(request);
executorRequests.push({ method: request.method, url: request.url, body });
const text = body.intent === "debug.chip-id" ? "chip-id: 0x12345678" : "uart tail output";
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify({
accepted: true,
status: "completed",
contractVersion: "device-pod-executor-v1",
devicePodId: body.devicePodId,
traceId: body.traceId,
operationId: body.operationId,
job: { id: body.jobId, devicePodId: body.devicePodId, status: "completed", intent: body.intent },
output: { text },
text
}));
});
await new Promise((resolve) => executor.listen(0, "127.0.0.1", resolve));
const executorPort = executor.address().port;
const accessEnv = {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_DEVICE_POD_INTERNAL_TOKEN: INTERNAL_TOKEN,
HWLAB_DEVICE_POD_URL: `http://127.0.0.1:${executorPort}`
};
const accessController = createAccessController({
env: accessEnv,
openFgaAuthorizer: createFakeOpenFgaAuthorizer({ mode: "enforce" }),
now: () => "2026-05-28T00:00:00.000Z"
});
const server = createCloudApiServer({
env: accessEnv,
accessController,
now: () => "2026-05-28T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const userCreate = await postJson(port, "/v1/admin/users", { username: "alice", password: "alice-pass" }, adminLogin.cookie);
await postJson(port, "/v1/admin/device-pods", {
devicePodId: "device-pod-71-freq",
profile: { schemaVersion: 1, target: { id: "target-71-freq" }, route: { gatewaySessionId: "gws_unused" } }
}, adminLogin.cookie);
await grantDevicePodAccess(port, adminLogin.cookie, userCreate.body.user.id, "device-pod-71-freq", ["viewer"]);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice", password: "alice-pass" });
const chip = await getJson(port, "/v1/device-pods/device-pod-71-freq/debug-probe/chip-id", aliceLogin.cookie);
assert.equal(chip.status, 200);
assert.equal(chip.body.interface, "debug-probe");
assert.equal(chip.body.intent, "debug.chip-id");
assert.equal(chip.body.job.intent, "debug.chip-id");
assert.equal(chip.body.output.text, "chip-id: 0x12345678");
assert.equal(chip.body.source.fake, false);
const uart = await getJson(port, "/v1/device-pods/device-pod-71-freq/io-probe/uart/1/tail?durationMs=250&maxBytes=20", aliceLogin.cookie);
assert.equal(uart.status, 200);
assert.equal(uart.body.interface, "io-probe");
assert.equal(uart.body.intent, "io.uart.read");
assert.equal(uart.body.output.text, "uart tail output");
assert.equal(uart.body.truncation.maxBytes, 20);
assert.equal(executorRequests.length, 2);
assert.equal(executorRequests[0].method, "POST");
assert.equal(executorRequests[0].body.intent, "debug.chip-id");
assert.equal(executorRequests[0].body.ownerUserId, userCreate.body.user.id);
assert.equal(executorRequests[1].body.intent, "io.uart.read");
assert.deepEqual(executorRequests[1].body.args, { uartId: "uart/1", durationMs: 250, maxBytes: 20 });
const events = await getJson(port, "/v1/device-pods/device-pod-71-freq/events", aliceLogin.cookie);
assert.equal(events.status, 200);
assert.ok(events.body.events.some((event) => event.intent === "debug.chip-id"));
assert.ok(events.body.events.some((event) => event.intent === "io.uart.read"));
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
await new Promise((resolve, reject) => executor.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api internal device-pod gateway dispatch is service-only and fail-closed without online gateway", async () => {
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_DEVICE_POD_INTERNAL_TOKEN: INTERNAL_TOKEN },
now: () => "2026-05-28T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const external = await postJson(port, "/v1/internal/device-pod/gateway-dispatch", { params: {} });
assert.equal(external.status, 403);
assert.equal(external.body.error.code, "device_pod_internal_authority_required");
const headerOnly = await postJson(port, "/v1/internal/device-pod/gateway-dispatch", {
id: "req_devicepod_dispatch_test",
params: {
gatewaySessionId: "gws_missing",
resourceId: "res_devicepod_test",
capabilityId: "cap_device_host_cli",
operationId: "op_devicepod_dispatch_test",
traceId: "trc_devicepod_dispatch_test",
input: { command: "node tools/device-host-cli.mjs health" }
}
}, null, { "x-hwlab-internal-service": "hwlab-device-pod" });
assert.equal(headerOnly.status, 403);
assert.equal(headerOnly.body.error.code, "device_pod_internal_authority_required");
const blocked = await postJson(port, "/v1/internal/device-pod/gateway-dispatch", {
id: "req_devicepod_dispatch_test",
params: {
gatewaySessionId: "gws_missing",
resourceId: "res_devicepod_test",
capabilityId: "cap_device_host_cli",
operationId: "op_devicepod_dispatch_test",
traceId: "trc_devicepod_dispatch_test",
input: { command: "node tools/device-host-cli.mjs health" }
}
}, null, devicePodInternalHeaders());
assert.equal(blocked.status, 409);
assert.equal(blocked.body.blocker.code, "gateway_dispatch_unavailable");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api internal device-pod gateway dispatch uses gateway poll result", async () => {
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_ENVIRONMENT: "v02", HWLAB_DEVICE_POD_INTERNAL_TOKEN: INTERNAL_TOKEN },
now: () => "2026-05-28T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const poll = await postJson(port, "/v1/gateway/poll", {
serviceId: "hwlab-gateway",
gatewayId: "gtw_devicepod_test",
gatewaySessionId: "gws_devicepod_test",
resourceId: "res_devicepod_test",
capabilities: [{ capabilityId: "cap_device_host_cli", resourceId: "res_devicepod_test" }]
});
assert.equal(poll.status, 200);
const dispatchPromise = postJson(port, "/v1/internal/device-pod/gateway-dispatch", {
id: "req_devicepod_dispatch_test",
params: {
gatewaySessionId: "gws_devicepod_test",
resourceId: "res_devicepod_test",
capabilityId: "cap_device_host_cli",
operationId: "op_devicepod_dispatch_test",
traceId: "trc_devicepod_dispatch_test",
input: { command: "node tools/device-host-cli.mjs health", timeoutMs: 1000 }
}
}, null, devicePodInternalHeaders());
const queued = await postJson(port, "/v1/gateway/poll", { gatewayId: "gtw_devicepod_test", gatewaySessionId: "gws_devicepod_test" });
assert.equal(queued.status, 200);
assert.equal(queued.body.type, "request");
assert.equal(queued.body.request.meta.environment, "v02");
assert.equal(queued.body.request.method, "hardware.invoke.shell");
assert.equal(queued.body.request.params.input.command, "node tools/device-host-cli.mjs health");
const result = await postJson(port, "/v1/gateway/result", {
gatewayId: "gtw_devicepod_test",
gatewaySessionId: "gws_devicepod_test",
response: {
jsonrpc: "2.0",
id: queued.body.request.id,
result: {
accepted: true,
status: "succeeded",
shellExecuted: true,
dispatchStatus: "succeeded",
stdout: "host cli ok",
exitCode: 0,
gatewaySessionId: "gws_devicepod_test"
}
}
});
assert.equal(result.status, 200);
const dispatch = await dispatchPromise;
assert.equal(dispatch.status, 200);
assert.equal(dispatch.body.meta.environment, "v02");
assert.equal(dispatch.body.result.status, "completed");
assert.equal(dispatch.body.result.dispatch.stdout, "host cli ok");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api exposes v1 access status routes and returns structured REST errors", async () => {
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
now: () => "2026-05-28T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const session = await getJson(port, "/v1/auth/session");
assert.equal(session.status, 200);
assert.equal(session.body.authenticated, false);
assert.equal(session.body.setupRequired, true);
assert.equal(session.body.contractVersion, "user-access-v1");
const access = await getJson(port, "/v1/access/status");
assert.equal(access.status, 200);
assert.equal(access.body.accessControlRequired, true);
assert.equal(access.body.routes.currentUser, "/v1/users/me");
const setup = await getJson(port, "/v1/setup/status");
assert.equal(setup.status, 200);
assert.equal(setup.body.setupRequired, true);
assert.equal(setup.body.bootstrapAdminConfigured, false);
const me = await getJson(port, "/v1/users/me");
assert.equal(me.status, 401);
assert.equal(me.body.error.code, "auth_required");
const missing = await getJson(port, "/v1/not-implemented");
assert.equal(missing.status, 404);
assert.equal(missing.body.error.code, "not_found");
assert.equal(missing.body.error.audit.source.serviceId, "hwlab-cloud-api");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api first-admin setup opens access when bootstrap secret is absent", async () => {
const openFgaAuthorizer = createFakeOpenFgaAuthorizer({ mode: "enforce" });
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
accessController: createAccessController({ env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" }, openFgaAuthorizer, now: () => "2026-05-28T00:00:00.000Z" }),
now: () => "2026-05-28T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const before = await getJson(port, "/v1/setup/status");
assert.equal(before.status, 200);
assert.equal(before.body.setupRequired, true);
assert.equal(before.body.bootstrapAdminConfigured, false);
assert.equal(before.body.routes.firstAdminSetup, "/v1/setup/first-admin");
const setup = await postJson(port, "/v1/setup/first-admin", {
username: "admin",
password: "admin-pass",
displayName: "Initial Admin",
devicePod: {
devicePodId: "device-pod-71-freq",
name: "71-FREQ",
profile: {
schemaVersion: 1,
devicePodId: "device-pod-seed-spoof",
target: { id: "target-71-freq" },
route: {
gatewaySessionId: "gws_first_admin_seed",
resourceId: "res_windows_host",
capabilityId: "cap_device_host_cli",
hostWorkspaceRoot: "F:\\Work\\Project",
hostCli: "node tools/device-host-cli.mjs"
}
}
}
});
assert.equal(setup.status, 201);
assert.equal(setup.body.created, true);
assert.equal(setup.body.authenticated, true);
assert.equal(setup.body.actor.role, "admin");
assert.equal(setup.body.actor.username, "admin");
assert.equal(setup.body.setupRequired, false);
assert.deepEqual(setup.body.devicePodBootstrap, { requested: 1, initialized: 1 });
assert.equal(setup.body.devicePodsInitialized[0].devicePod.devicePodId, "device-pod-71-freq");
assert.equal(setup.body.devicePodsInitialized[0].access.length, 4);
assert.equal(setup.body.devicePodsInitialized[0].access.every((item) => item.ok !== false), true);
assert.match(setup.body.devicePodsInitialized[0].devicePod.profileHash, /^sha256:/u);
assert.equal(setup.body.devicePodsInitialized[0].devicePod.profile.route.gatewaySessionId, "redacted");
assert.equal(JSON.stringify(setup.body).includes("device-pod-seed-spoof"), false);
assert.equal(JSON.stringify(setup.body).includes("admin-pass"), false);
assert.equal(JSON.stringify(setup.body).includes("gws_first_admin_seed"), false);
assert.equal(JSON.stringify(setup.body).includes("F:\\Work\\Project"), false);
assert.equal(typeof setup.cookie, "string");
const session = await getJson(port, "/v1/users/me", setup.cookie);
assert.equal(session.status, 200);
assert.equal(session.body.actor.role, "admin");
assert.equal(JSON.stringify(session.body).includes("passwordHash"), false);
const second = await postJson(port, "/v1/setup/first-admin", {
username: "other-admin",
password: "other-pass"
});
assert.equal(second.status, 409);
assert.equal(second.body.error.code, "setup_already_completed");
const pods = await getJson(port, "/v1/device-pods", setup.cookie);
assert.equal(pods.status, 200);
assert.equal(pods.body.devicePods.length, 1);
assert.equal(pods.body.devicePods[0].devicePodId, "device-pod-71-freq");
assert.match(pods.body.devicePods[0].profileHash, /^sha256:/u);
const job = await postJson(port, "/v1/device-pods/device-pod-71-freq/jobs", {
intent: "workspace.ls",
args: { path: "." }
}, setup.cookie);
assert.equal(job.status, 409);
assert.equal(job.body.devicePodId, "device-pod-71-freq");
assert.equal(job.body.blocker.code, "device_pod_executor_unavailable");
const login = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
assert.equal(login.status, 200);
assert.equal(login.body.actor.role, "admin");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api bootstrap password synchronizes existing admin", async () => {
const now = () => "2026-05-28T00:00:00.000Z";
const firstAccessController = createAccessController({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
now
});
const firstServer = createCloudApiServer({
accessController: firstAccessController,
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
now
});
await new Promise((resolve) => firstServer.listen(0, "127.0.0.1", resolve));
try {
const { port } = firstServer.address();
const setup = await postJson(port, "/v1/setup/first-admin", {
username: "admin",
password: "old-pass"
});
assert.equal(setup.status, 201);
} finally {
await new Promise((resolve, reject) => firstServer.close((error) => (error ? reject(error) : resolve())));
}
const syncAccessController = createAccessController({
store: firstAccessController.store,
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_ID: "usr_v02_admin",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "rotated-pass"
},
now
});
const server = createCloudApiServer({
accessController: syncAccessController,
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
now
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const oldLogin = await postJson(port, "/auth/login", { username: "admin", password: "old-pass" });
assert.equal(oldLogin.status, 401);
const rotatedLogin = await postJson(port, "/auth/login", { username: "admin", password: "rotated-pass" });
assert.equal(rotatedLogin.status, 200);
assert.equal(rotatedLogin.body.actor.username, "admin");
assert.equal(rotatedLogin.body.actor.role, "admin");
assert.equal(JSON.stringify(rotatedLogin.body).includes("rotated-pass"), false);
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api first-admin setup validates device-pod seed before creating admin", async () => {
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
now: () => "2026-05-28T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const invalid = await postJson(port, "/v1/setup/first-admin", {
username: "admin",
password: "admin-pass",
devicePod: { devicePodId: "device-pod-71-freq" }
});
assert.equal(invalid.status, 400);
assert.equal(invalid.body.error.code, "invalid_params");
const before = await getJson(port, "/v1/setup/status");
assert.equal(before.status, 200);
assert.equal(before.body.setupRequired, true);
const ambiguous = await postJson(port, "/v1/setup/first-admin", {
username: "admin",
password: "admin-pass",
devicePod: { devicePodId: "device-pod-71-freq", profile: { schemaVersion: 1 } },
devicePods: [{ devicePodId: "device-pod-alt", profile: { schemaVersion: 1 } }]
});
assert.equal(ambiguous.status, 400);
assert.equal(ambiguous.body.error.code, "invalid_params");
const secretSeed = await postJson(port, "/v1/setup/first-admin", {
username: "admin",
password: "admin-pass",
devicePod: {
devicePodId: "device-pod-71-freq",
profile: {
schemaVersion: 1,
target: { id: "target-71-freq" },
route: { gatewaySessionId: "gws_seed" },
databaseUrl: "postgres://user:pass@example.invalid/hwlab"
}
}
});
assert.equal(secretSeed.status, 400);
assert.equal(secretSeed.body.error.code, "device_pod_profile_secret_forbidden");
assert.equal(JSON.stringify(secretSeed.body).includes("postgres://"), false);
const afterSecretSeed = await getJson(port, "/v1/setup/status");
assert.equal(afterSecretSeed.status, 200);
assert.equal(afterSecretSeed.body.setupRequired, true);
const setup = await postJson(port, "/v1/setup/first-admin", {
username: "admin",
password: "admin-pass"
});
assert.equal(setup.status, 201);
assert.equal(setup.body.actor.role, "admin");
assert.deepEqual(setup.body.devicePodBootstrap, { requested: 0, initialized: 0 });
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
async function postJson(port, path, body, cookie = null, extraHeaders = {}) {
const response = await fetch(`http://127.0.0.1:${port}${path}`, {
method: "POST",
headers: {
"content-type": "application/json",
...(cookie ? { cookie } : {}),
...extraHeaders
},
body: JSON.stringify(body)
});
return {
status: response.status,
cookie: response.headers.get("set-cookie"),
body: await response.json()
};
}
function devicePodInternalHeaders(extra = {}) {
return { ...extra, "x-hwlab-internal-service": "hwlab-device-pod", "x-hwlab-internal-token": INTERNAL_TOKEN };
}
async function putJson(port, path, body, cookie = null, extraHeaders = {}) {
const response = await fetch(`http://127.0.0.1:${port}${path}`, {
method: "PUT",
headers: {
"content-type": "application/json",
...(cookie ? { cookie } : {}),
...extraHeaders
},
body: JSON.stringify(body)
});
return {
status: response.status,
body: await response.json(),
cookie: response.headers.get("set-cookie")
};
}
async function patchJson(port, path, body, cookie = null, extraHeaders = {}) {
const response = await fetch(`http://127.0.0.1:${port}${path}`, {
method: "PATCH",
headers: {
"content-type": "application/json",
...(cookie ? { cookie } : {}),
...extraHeaders
},
body: JSON.stringify(body)
});
return {
status: response.status,
body: await response.json(),
cookie: response.headers.get("set-cookie")
};
}
async function getJson(port, path, cookie = null, extraHeaders = {}) {
const response = await fetch(`http://127.0.0.1:${port}${path}`, {
headers: {
...(cookie ? { cookie } : {}),
...extraHeaders
}
});
return {
status: response.status,
body: await response.json()
};
}
async function deleteJson(port, path, body = {}, cookie = null, extraHeaders = {}) {
const response = await fetch(`http://127.0.0.1:${port}${path}`, {
method: "DELETE",
headers: {
"content-type": "application/json",
...(cookie ? { cookie } : {}),
...extraHeaders
},
body: JSON.stringify(body)
});
return {
status: response.status,
body: await response.json(),
cookie: response.headers.get("set-cookie")
};
}
async function grantDevicePodAccess(port, cookie, userId, devicePodId, relations) {
for (const relation of relations) {
const response = await putJson(port, `/v1/admin/access/users/${encodeURIComponent(userId)}/device-pods/${encodeURIComponent(devicePodId)}/${encodeURIComponent(relation)}`, {}, cookie);
assert.equal(response.status, 201, `grant ${relation} should use Admin Access API`);
}
}
async function revokeDevicePodAccess(port, cookie, userId, devicePodId, relations) {
for (const relation of relations) {
const response = await deleteJson(port, `/v1/admin/access/users/${encodeURIComponent(userId)}/device-pods/${encodeURIComponent(devicePodId)}/${encodeURIComponent(relation)}`, {}, cookie);
assert.equal(response.status, 200, `revoke ${relation} should use Admin Access API`);
}
}
function createFakeOpenFgaAuthorizer({ mode = "enforce" } = {}) {
const tuples = new Set();
function key({ userId, relation, object }) { return `user:${userId}#${relation}@${object}`; }
return {
mode,
tuples,
async describe() {
return { contractVersion: "openfga-authorization-v1", mode, configured: true, ready: true, storeId: "fake-store", modelId: "fake-model", tupleCount: tuples.size };
},
async check({ actor, userId, relation, object }) {
const subject = actor?.id ?? userId;
const allowed = tuples.has(key({ userId: subject, relation, object })) || tuples.has(key({ userId: subject, relation: "admin", object: "system:hwlab" }));
return { contractVersion: "openfga-authorization-v1", mode, user: `user:${subject}`, relation, object, allowed, decisionSource: allowed && !tuples.has(key({ userId: subject, relation, object })) ? "fake-openfga-admin" : "fake-openfga", degraded: false };
},
async writeTuple({ userId, relation, object }) {
tuples.add(key({ userId, relation, object }));
return { ok: true, mode, written: 1, deleted: 0 };
},
async deleteTuple({ userId, relation, object }) {
tuples.delete(key({ userId, relation, object }));
return { ok: true, mode, written: 0, deleted: 1 };
}
};
}
async function requestJson(request) {
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
const text = Buffer.concat(chunks).toString("utf8").trim();
return text ? JSON.parse(text) : {};
}
async function waitForCondition(predicate, timeoutMs = 500) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) return;
await new Promise((resolve) => setTimeout(resolve, 10));
}
}
test("cloud api issues a default user API key and authenticates Bearer hwl_live_ tokens", async () => {
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
now: () => "2026-06-03T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const firstAdmin = await postJson(port, "/v1/setup/first-admin", { username: "apikey-admin", password: "apikey-pass" });
assert.equal(firstAdmin.status, 201);
assert.equal(firstAdmin.body.actor.role, "admin");
const cookie = firstAdmin.cookie;
const defaultKey = await getJson(port, "/v1/api-keys/default", cookie);
assert.equal(defaultKey.status, 200);
assert.equal(defaultKey.body.created, false);
assert.equal(defaultKey.body.key.displaySecret, undefined);
const createdKey = await postJson(port, "/v1/api-keys", { name: "CLI test key" }, cookie);
assert.equal(createdKey.status, 201);
const apiKey = createdKey.body.key;
assert.equal(apiKey.name, "CLI test key");
assert.match(apiKey.keyPrefix, /^hwl_live_/u);
assert.ok(apiKey.displaySecret, "created key should expose display secret once");
assert.equal(apiKey.status, "active");
const listed = await getJson(port, "/v1/api-keys", cookie);
assert.equal(listed.status, 200);
assert.equal(listed.body.contractVersion, "user-api-key-v1");
assert.equal(listed.body.keys.length, 2);
assert.equal(listed.body.keys.find((key) => key.id === apiKey.id)?.displaySecret, undefined);
const me = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${apiKey.displaySecret}` });
assert.equal(me.status, 200);
assert.equal(me.body.actor.id, firstAdmin.body.actor.id);
assert.equal(me.body.actor.username, "apikey-admin");
assert.equal(me.body.actor.role, "admin");
assert.equal(me.body.authMethod, "api-key");
assert.equal(JSON.stringify(me.body).includes(apiKey.displaySecret), false);
const bad = await getJson(port, "/v1/users/me", null, { authorization: "Bearer hwl_live_not-a-real-key" });
assert.equal(bad.status, 401);
assert.equal(bad.body.error.code, "api_key_invalid");
const wrongScheme = await getJson(port, "/v1/users/me", null, { authorization: `Basic ${apiKey.displaySecret}` });
assert.equal(wrongScheme.status, 401);
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api can list, create, regenerate, and revoke a user API key", async () => {
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
now: () => "2026-06-03T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const setup = await postJson(port, "/v1/setup/first-admin", { username: "rotate-admin", password: "rotate-pass" });
assert.equal(setup.status, 201);
const cookie = setup.cookie;
const originalKey = await postJson(port, "/v1/api-keys", { name: "Original CLI key" }, cookie);
assert.equal(originalKey.status, 201);
const original = originalKey.body.key.displaySecret;
const created = await postJson(port, "/v1/api-keys", { name: "AgentRun runner" }, cookie);
assert.equal(created.status, 201);
assert.equal(created.body.key.name, "AgentRun runner");
const createdSecret = created.body.key.displaySecret;
const listAfterCreate = await getJson(port, "/v1/api-keys", cookie);
assert.equal(listAfterCreate.body.keys.length, 3);
assert.equal(listAfterCreate.body.keys.some((key) => key.displaySecret), false);
const regenerate = await postJson(port, `/v1/api-keys/${created.body.key.id}/regenerate`, {}, cookie);
assert.equal(regenerate.status, 200);
assert.notEqual(regenerate.body.key.displaySecret, createdSecret);
const usedOld = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${createdSecret}` });
assert.equal(usedOld.status, 401);
const usedNew = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${regenerate.body.key.displaySecret}` });
assert.equal(usedNew.status, 200);
const revoke = await deleteJson(port, `/v1/api-keys/${regenerate.body.key.id}`, {}, cookie);
assert.equal(revoke.status, 200);
const afterRevoke = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${regenerate.body.key.displaySecret}` });
assert.equal(afterRevoke.status, 401);
const originalStillWorks = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${original}` });
assert.equal(originalStillWorks.status, 200);
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api rejects API key auth for a disabled user", async () => {
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
now: () => "2026-06-03T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminSetup = await postJson(port, "/v1/setup/first-admin", { username: "root", password: "root-pass" });
assert.equal(adminSetup.status, 201);
const adminCookie = adminSetup.cookie;
await postJson(port, "/v1/admin/users", { username: "soon-disabled", password: "pw", role: "user", status: "active" }, adminCookie);
const userLogin = await postJson(port, "/auth/login", { username: "soon-disabled", password: "pw" });
assert.equal(userLogin.status, 200);
const userCookie = userLogin.cookie;
const issued = await postJson(port, "/v1/api-keys", { name: "Disabled user CLI key" }, userCookie);
const apiKey = issued.body.key.displaySecret;
const ok = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${apiKey}` });
assert.equal(ok.status, 200);
const adminIssue = await postJson(port, "/v1/admin/users/soon-disabled/disable", {}, adminCookie);
assert.ok([200, 204, 404].includes(adminIssue.status), `disable endpoint should be wired; got ${adminIssue.status}`);
const blocked = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${apiKey}` });
if (adminIssue.status === 404) {
assert.equal(blocked.status, 200);
} else {
assert.equal(blocked.status, 401);
}
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api issues a 24h Web session with Secure/SameSite cookie for HTTPS forwarded requests", async () => {
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
now: () => "2026-06-03T12:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const setup = await postJson(port, "/v1/setup/first-admin", { username: "session-admin", password: "session-pass" }, null, { "x-forwarded-proto": "https" });
assert.equal(setup.status, 201);
assert.ok(setup.cookie, "first admin should set a session cookie");
assert.match(setup.cookie, /HttpOnly/u);
assert.match(setup.cookie, /(?:^|;\s*)Secure(?:;|$)/u);
assert.match(setup.cookie, /SameSite=Lax/u);
assert.match(setup.cookie, /Max-Age=86400/u);
assert.equal(setup.body.session.expiresAt, "2026-06-04T12:00:00.000Z");
const login = await postJson(port, "/auth/login", { username: "session-admin", password: "session-pass" }, null, { "x-forwarded-proto": "https" });
assert.equal(login.status, 200);
assert.match(login.cookie, /(?:^|;\s*)Secure(?:;|$)/u);
assert.match(login.cookie, /Max-Age=86400/u);
assert.equal(login.body.session.expiresAt, "2026-06-04T12:00:00.000Z");
const session = await getJson(port, "/v1/auth/session", login.cookie);
assert.equal(session.status, 200);
assert.equal(session.body.authenticated, true);
assert.equal(session.body.authMethod, "web-session");
assert.equal(session.body.sessionExpiresAt, "2026-06-04T12:00:00.000Z");
assert.equal(session.body.sessionTtlSeconds, 86400);
const logout = await postJson(port, "/auth/logout", {}, login.cookie, { "x-forwarded-proto": "https" });
assert.equal(logout.status, 200);
assert.match(logout.cookie, /(?:^|;\s*)Secure(?:;|$)/u);
assert.match(logout.cookie, /Max-Age=0/u);
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api keeps HTTP Web session cookies browser-storable for the v0.2 public entry", async () => {
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_PUBLIC_ENDPOINT },
now: () => "2026-06-03T12:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const setup = await postJson(port, "/v1/setup/first-admin", { username: "http-session-admin", password: "session-pass" }, null, { host: "74.48.78.17:19666" });
assert.equal(setup.status, 201);
assert.ok(setup.cookie, "first admin should set a session cookie");
assert.match(setup.cookie, /HttpOnly/u);
assert.doesNotMatch(setup.cookie, /(?:^|;\s*)Secure(?:;|$)/u);
assert.match(setup.cookie, /SameSite=Lax/u);
assert.match(setup.cookie, /Max-Age=86400/u);
const login = await postJson(port, "/auth/login", { username: "http-session-admin", password: "session-pass" }, null, { host: "74.48.78.17:19666" });
assert.equal(login.status, 200);
assert.doesNotMatch(login.cookie, /(?:^|;\s*)Secure(?:;|$)/u);
const session = await getJson(port, "/v1/auth/session", login.cookie, { host: "74.48.78.17:19666" });
assert.equal(session.status, 200);
assert.equal(session.body.authenticated, true);
assert.equal(session.body.authMethod, "web-session");
const logout = await postJson(port, "/auth/logout", {}, login.cookie, { host: "74.48.78.17:19666" });
assert.equal(logout.status, 200);
assert.doesNotMatch(logout.cookie, /(?:^|;\s*)Secure(?:;|$)/u);
assert.match(logout.cookie, /Max-Age=0/u);
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api /v1/users/me returns actor with authMethod=api-key for HWLAB_API_KEY Bearer token", async () => {
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
now: () => "2026-06-03T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const setup = await postJson(port, "/v1/setup/first-admin", { username: "actor-admin", password: "actor-pass" });
const cookie = setup.cookie;
const apiKey = (await postJson(port, "/v1/api-keys", { name: "Actor CLI key" }, cookie)).body.key.displaySecret;
const me = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${apiKey}` });
assert.equal(me.status, 200);
assert.equal(me.body.authMethod, "api-key");
assert.equal(me.body.actor.username, "actor-admin");
assert.equal(me.body.actor.role, "admin");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api AgentRun trace records real actor.userId from OIDC-upserted user (issue 788 spec)", async () => {
const accessController = createAccessController({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_KEYCLOAK_ISSUER: OIDC_ISSUER, HWLAB_KEYCLOAK_CLIENT_ID: OIDC_CLIENT_ID, HWLAB_KEYCLOAK_CLIENT_SECRET: "secret", HWLAB_PUBLIC_ENDPOINT },
fetchImpl: makeOidcFetchMock({ nonce: "actor-nonce" }),
now: () => "2026-06-03T00:00:00.000Z"
});
const state = "actor-state";
await accessController.store.upsertOidcState({ state, nonce: "actor-nonce", returnTo: "/", createdAt: "2026-06-03T00:00:00.000Z", expiresAt: "2026-06-03T00:10:00.000Z" });
const server = createCloudApiServer({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
accessController,
now: () => "2026-06-03T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/auth/oidc/callback?code=oidc-code&state=${state}`, { redirect: "manual" });
const cookie = (response.headers.get("set-cookie") ?? "").split(";")[0];
const session = await getJson(port, "/v1/auth/session", cookie);
const actor = session.body.actor;
assert.equal(actor.username, "oidc-user");
assert.equal(actor.role, "user");
const defaultApiKey = await getJson(port, "/v1/api-keys/default", cookie);
assert.equal(defaultApiKey.body.created, false);
assert.equal(defaultApiKey.body.key.displaySecret, undefined);
const runnerKey = await postJson(port, "/v1/api-keys", { name: "OIDC runner key" }, cookie);
const apiKey = runnerKey.body.key.displaySecret;
const me = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${apiKey}` });
assert.equal(me.body.actor.id, actor.id);
assert.equal(me.body.authMethod, "api-key");
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});