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_hwpod_workbench", name: "Default Workbench", status: "active", is_default: true, selected_conversation_id: null, selected_agent_session_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=\$15, updated_at=\$16/u); assert.match(update.sql, /\$17::text = 'admin'/u); assert.equal(update.params.length, 17); assert.equal(update.params[14], "2026-06-01T00:00:00.000Z"); assert.equal(update.params[15], "2026-06-01T00:00:00.000Z"); assert.equal(update.params[16], "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_hwpod_workbench", aliceLogin.cookie); assert.equal(workspace.status, 200); const conversation = await putJson(port, "/v1/agent/conversations/cnv_issue655_shared", { projectId: "prj_hwpod_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_hwpod_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_hwpod_workbench", adminLogin.cookie); const conversation = await putJson(port, "/v1/agent/conversations/cnv_stale_active_idle", { projectId: "prj_hwpod_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_hwpod_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") { return send({ items: [ { id: "cmd_workspace_status", runId: "run_workspace_status", state: "completed", type: "turn", seq: 1, idempotencyKey: "trc_issue664_status_done", payload: { traceId: "trc_issue664_status_done", conversationId: "cnv_issue664_status", hwlabSessionId: "ses_issue664_status", threadId: "thread-issue-664", providerProfile: "deepseek" } } ] }); } 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_hwpod_workbench", aliceLogin.cookie); assert.equal(workspace.status, 200); const conversation = await putJson(port, "/v1/agent/conversations/cnv_issue664_status", { projectId: "prj_hwpod_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_hwpod_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") { return send({ items: [ { id: "cmd_issue808_old", runId: "run_issue808_old", state: "completed", type: "turn", seq: 1, idempotencyKey: "trc_issue808_old_done", payload: { traceId: "trc_issue808_old_done", conversationId: "cnv_issue808_old", hwlabSessionId: "ses_issue808_old", threadId: "thread-issue-808-old", providerProfile: "deepseek" } } ] }); } 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_hwpod_workbench", aliceLogin.cookie); assert.equal(workspace.status, 200); const oldConversation = await putJson(port, "/v1/agent/conversations/cnv_issue808_old", { projectId: "prj_hwpod_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_hwpod_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_hwpod_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_hwpod_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.ok(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_issue808_old/commands/cmd_issue808_old/result")); const selectedOld = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, { projectId: "prj_hwpod_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_hwpod_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_hwpod_workbench", aliceLogin.cookie); assert.equal(workspace.status, 200); await accessController.recordAgentSessionOwner({ ownerUserId: alice.body.user.id, ownerRole: "user", sessionId: "ses_issue810_running", projectId: "prj_hwpod_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_hwpod_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_hwpod_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_hwpod_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_hwpod_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") { return send({ items: [ { id: "cmd_workspace_repair", runId: "run_workspace_repair", state: "completed", type: "turn", seq: 1, idempotencyKey: "trc_issue664_repair_done", payload: { traceId: "trc_issue664_repair_done", conversationId: "cnv_issue664_repair", hwlabSessionId: "ses_issue664_repair", threadId: "thread-issue-664-repair", providerProfile: "deepseek" } } ] }); } 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_hwpod_workbench", aliceLogin.cookie); assert.equal(workspace.status, 200); const conversation = await putJson(port, "/v1/agent/conversations/cnv_issue664_repair", { projectId: "prj_hwpod_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, "idle"); assert.equal(update.body.workspace.selectedConversation.messages.length, 1); assert.equal(update.body.workspace.selectedConversation.messages[0].status, "idle"); assert.equal(update.body.workspace.selectedConversation.messages[0].text, "repair completed"); const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_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 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_hwpod_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_hwpod_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_hwpod_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_hwpod_workbench", aliceLogin.cookie); assert.equal(workspace.status, 200); const selected = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, { projectId: "prj_hwpod_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_hwpod_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_hwpod_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_hwpod_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_hwpod_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 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_hwpod_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_hwpod_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_hwpod_workbench", aliceLogin.cookie); assert.equal(workspace.status, 200); const selected = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, { projectId: "prj_hwpod_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_hwpod_workbench", aliceLogin.cookie); assert.equal(workspace.status, 200); const savedMessages = [ { id: "msg_issue853_user", role: "user", title: "用户", text: "请执行 hwpod inspect", 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_hwpod_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_hwpod_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 inspect"); 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_hwpod_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 inspect"); 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 inspect"); } 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 inspect,并在最终回复里原样包含 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_hwpod_workbench", aliceLogin.cookie); assert.equal(workspace.status, 200); await accessController.recordAgentSessionOwner({ ownerUserId: aliceCreate.body.user.id, sessionId, projectId: "prj_hwpod_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_hwpod_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_hwpod_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 inspect,并在最终回复里原样包含 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 dedupes duplicate refreshed assistant messages by id and trace", async () => { const projectId = "prj_hwpod_workbench"; const ownerUserId = "usr_issue932_dedupe"; const conversationId = "cnv_issue932_dedupe"; const sessionId = "ses_issue932_dedupe"; const traceId = "trc_issue932_dedupe"; const threadId = "thread-issue-932-dedupe"; const nowValue = "2026-06-05T09:30:00.000Z"; const accessController = createAccessController({ now: () => nowValue }); await accessController.recordAgentSessionOwner({ ownerUserId, sessionId, projectId, agentId: "hwlab-code-agent", status: "idle", conversationId, threadId, traceId, session: { source: "cloud-web-react-account-sync", sessionStatus: "completed", lastTraceId: traceId, messages: [ { id: "msg_issue932_user", role: "user", title: "用户", text: "你好", status: "sent", traceId, conversationId, sessionId, threadId, createdAt: nowValue }, { id: "msg_issue932_agent", role: "agent", title: "Code Agent 回复", text: "最终回复", status: "completed", traceId, conversationId, sessionId, threadId, createdAt: nowValue }, { id: "msg_issue932_agent", role: "agent", title: "Code Agent 回复", text: "最终回复", status: "completed", traceId, conversationId, sessionId, threadId, createdAt: nowValue } ], messageCount: 3, firstUserMessagePreview: "你好", 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.messageCount, 2); assert.equal(conversation.messageCount, 2); assert.equal(conversation.messages[1].text, "最终回复"); }); 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 accepts user API keys as Bearer auth for the same account", async () => { 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" }, 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" }); assert.equal(adminLogin.status, 200); const createdKey = await postJson(port, "/v1/api-keys", { name: "Code Agent runner" }, adminLogin.cookie); assert.equal(createdKey.status, 201); const apiKey = createdKey.body.key.displaySecret; assert.ok(apiKey.startsWith("hwl_live_")); 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.id, "usr_v02_admin"); assert.equal(me.body.actor.role, "admin"); assert.equal(JSON.stringify(me.body).includes(apiKey), false); } finally { await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); } }); test("cloud api bootstraps the master server admin API key from env", async () => { const apiKey = "hwl_live_master_server_admin_test_key_930"; const openFgaAuthorizer = createFakeOpenFgaAuthorizer(); 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", HWLAB_BOOTSTRAP_ADMIN_API_KEY: apiKey, HWLAB_BOOTSTRAP_ADMIN_API_KEY_ID: "key_master_server_admin" }, openFgaAuthorizer, now: () => "2026-06-05T00:00:00.000Z" }); const server = createCloudApiServer({ env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" }, accessController, now: () => "2026-06-05T00:00:00.000Z" }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const { port } = server.address(); 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.id, "usr_v02_admin"); assert.equal(me.body.actor.username, "admin"); assert.equal(me.body.actor.role, "admin"); assert.equal(JSON.stringify(me.body).includes(apiKey), false); const summary = await getJson(port, "/v1/admin/access/summary", null, { authorization: `Bearer ${apiKey}` }); assert.equal(summary.status, 200); assert.equal(summary.body.actor.id, "usr_v02_admin"); assert.equal(summary.body.supported.toolIds.includes("hwpod"), true); for (const toolId of summary.body.supported.toolIds) { const check = await postJson(port, "/v1/admin/access/check", { userId: "usr_v02_admin", relation: "can_use", object: `tool:${toolId}` }, null, { authorization: `Bearer ${apiKey}` }); assert.equal(check.status, 200); assert.equal(check.body.authorization.allowed, true, `expected admin key to access ${toolId}`); } const keys = await accessController.store.listApiKeysForUser("usr_v02_admin"); const masterKey = keys.find((key) => key.id === "key_master_server_admin"); assert.ok(masterKey); assert.equal(masterKey.name, "Master server admin API key"); assert.equal(masterKey.displaySecret, null); assert.deepEqual(masterKey.scopes, ["admin", "system:hwlab", "tool:*"]); } 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 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 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" }); 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.equal(JSON.stringify(setup.body).includes("admin-pass"), 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 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); const downgraded = await patchJson(port, "/v1/admin/access/users/usr_bootstrap_admin", { role: "user", status: "active" }, setup.cookie); assert.equal(downgraded.status, 200); assert.equal(downgraded.body.user.role, "user"); } 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); const summary = await getJson(port, "/v1/admin/access/summary", rotatedLogin.cookie); assert.equal(summary.status, 200); assert.equal(summary.body.actor.username, "admin"); assert.equal(summary.body.actor.role, "admin"); } finally { await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); } }); test("cloud api access matrix shows admin inherited tool authority", async () => { const env = { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_BOOTSTRAP_ADMIN_ID: "usr_v02_admin", HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin", HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass", HWLAB_OPENFGA_MODE: "enforce", HWLAB_OPENFGA_API_URL: "http://openfga.test" }; const tuples = new Set(); const accessController = createAccessController({ env, fetchImpl: async (url, init = {}) => { const pathname = new URL(String(url)).pathname; if (pathname === "/healthz") return new Response("ok", { status: 200 }); if (pathname === "/stores") return new Response(JSON.stringify({ id: "store_admin_tools" }), { status: 200 }); if (pathname.endsWith("/authorization-models")) return new Response(JSON.stringify({ authorization_model_id: "model_admin_tools" }), { status: 200 }); if (pathname.endsWith("/write")) { const body = JSON.parse(String(init.body ?? "{}")); for (const tuple of body.writes?.tuple_keys ?? []) tuples.add(`${tuple.user}|${tuple.relation}|${tuple.object}`); for (const tuple of body.deletes?.tuple_keys ?? []) tuples.delete(`${tuple.user}|${tuple.relation}|${tuple.object}`); return new Response(JSON.stringify({ ok: true }), { status: 200 }); } if (pathname.endsWith("/check")) { const body = JSON.parse(String(init.body ?? "{}")); const tuple = body.tuple_key ?? {}; return new Response(JSON.stringify({ allowed: tuples.has(`${tuple.user}|${tuple.relation}|${tuple.object}`) }), { status: 200 }); } return new Response(JSON.stringify({ error: "unexpected route" }), { status: 404 }); }, now: () => "2026-05-28T00:00:00.000Z" }); const server = createCloudApiServer({ env, 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 login = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" }); assert.equal(login.status, 200); const users = await getJson(port, "/v1/admin/access/users", login.cookie); assert.equal(users.status, 200); const admin = users.body.users.find((item) => item.user.id === "usr_v02_admin"); assert.equal(admin.user.role, "admin"); assert.deepEqual(admin.tools, { hwpod: true, unidesk_ssh: true, trans_cmd: true, github_pr: true }); const matrix = await getJson(port, "/v1/admin/access/users/usr_v02_admin", login.cookie); assert.equal(matrix.status, 200); assert.deepEqual(matrix.body.tools, { hwpod: true, unidesk_ssh: true, trans_cmd: true, github_pr: true }); assert.deepEqual(matrix.body.tuples.map((tuple) => `${tuple.relation}:${tuple.object}`), ["admin:system:hwlab"]); } 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() }; } 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") }; } 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: "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()))); } });