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

2778 lines
131 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.equal(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_issue808_old/commands/cmd_issue808_old/result"), false);
const selectedOld = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
projectId: "prj_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, "running");
assert.equal(update.body.workspace.selectedConversation.messages.length, 0);
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 persisted final response fallback from terminal result", async () => {
const codeAgentChatResults = new Map();
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: "http://127.0.0.1:9",
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
},
codeAgentChatResults,
now: () => "2026-06-04T09:45:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue834-fallback", password: "alice-pass" }, adminLogin.cookie);
assert.equal(aliceCreate.status, 201);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue834-fallback", password: "alice-pass" });
const fallbackText = "Code Agent 仍在处理,可以继续 steer 或等待 trace 完成。";
const finalText = "**直接答:能,但只到一半。**\n\n功能已经过了,剩余风险另开 issue 跟踪。";
codeAgentChatResults.set("trc_issue834_final_response", {
status: "completed",
traceId: "trc_issue834_final_response",
conversationId: "cnv_issue834_fallback",
sessionId: "ses_issue834_fallback",
threadId: "thread-issue-834-fallback",
ownerUserId: aliceCreate.body.user.id,
reply: { role: "assistant", content: finalText, messageId: "msg_issue834_real_final" },
agentRun: { terminalStatus: "completed" },
session: {
sessionId: "ses_issue834_fallback",
conversationId: "cnv_issue834_fallback",
threadId: "thread-issue-834-fallback",
status: "idle"
}
});
const stored = await putJson(port, "/v1/agent/conversations/cnv_issue834_fallback", {
projectId: "prj_hwpod_workbench",
sessionId: "ses_issue834_fallback",
threadId: "thread-issue-834-fallback",
sessionStatus: "completed",
lastTraceId: "trc_issue834_final_response",
messages: [
{ id: "msg_issue834_question", role: "user", title: "用户", text: "功能是过了还是没过?", status: "sent", conversationId: "cnv_issue834_fallback", sessionId: "ses_issue834_fallback", threadId: "thread-issue-834-fallback" },
{ id: "msg_issue834_fallback", role: "agent", title: "Agent", text: fallbackText, status: "completed", traceId: "trc_issue834_final_response", conversationId: "cnv_issue834_fallback", sessionId: "ses_issue834_fallback", threadId: "thread-issue-834-fallback" }
]
}, aliceLogin.cookie);
assert.equal(stored.status, 200);
assert.equal(stored.body.conversation.messages[1].text, fallbackText);
assert.equal(stored.body.conversation.status, "completed");
const list = await getJson(port, "/v1/agent/conversations?projectId=prj_hwpod_workbench", aliceLogin.cookie);
assert.equal(list.status, 200);
const listed = list.body.conversations.find((conversation) => conversation.conversationId === "cnv_issue834_fallback");
assert.ok(listed, "expected fallback conversation to be listed");
assert.equal(listed.status, "idle");
assert.equal(listed.session.status, "idle");
assert.equal(listed.lastTraceId, "trc_issue834_final_response");
assert.equal(listed.messages[1].text, finalText);
assert.equal(listed.messages[1].status, "idle");
const direct = await getJson(port, "/v1/agent/conversations/cnv_issue834_fallback", aliceLogin.cookie);
assert.equal(direct.status, 200);
assert.equal(direct.body.conversation.status, "idle");
assert.equal(direct.body.conversation.lastTraceId, "trc_issue834_final_response");
assert.equal(direct.body.conversation.messages[1].text, finalText);
assert.equal(direct.body.conversation.messages[1].status, "idle");
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_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_issue834_fallback",
sessionId: "ses_issue834_fallback",
updatedByClient: "test-suite"
}, aliceLogin.cookie);
assert.equal(selected.status, 200);
assert.equal(selected.body.workspace.selectedConversation.messages[1].text, finalText);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api repairs conversation snapshot when persisted result advances last trace", async () => {
const codeAgentChatResults = new Map();
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: "http://127.0.0.1:9",
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
},
codeAgentChatResults,
now: () => "2026-06-04T10:05:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue842-snapshot", password: "alice-pass" }, adminLogin.cookie);
assert.equal(aliceCreate.status, 201);
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue842-snapshot", password: "alice-pass" });
const finalText = "持久化验收OK。";
codeAgentChatResults.set("trc_issue842_persisted_final", {
status: "completed",
traceId: "trc_issue842_persisted_final",
conversationId: "cnv_issue842_snapshot",
sessionId: "ses_issue842_snapshot",
threadId: "thread-issue-842-snapshot",
ownerUserId: aliceCreate.body.user.id,
reply: { role: "assistant", content: finalText, messageId: "msg_issue842_persisted_final" },
agentRun: { terminalStatus: "completed" },
session: {
sessionId: "ses_issue842_snapshot",
conversationId: "cnv_issue842_snapshot",
threadId: "thread-issue-842-snapshot",
status: "idle"
}
});
const stored = await putJson(port, "/v1/agent/conversations/cnv_issue842_snapshot", {
projectId: "prj_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 keeps final response separate from progress assistant text (#934)", async () => {
const projectId = "prj_hwpod_workbench";
const ownerUserId = "usr_issue934_owner_merge";
const conversationId = "cnv_issue934_owner_merge";
const sessionId = "ses_issue934_owner_merge";
const traceId = "trc_issue934_owner_merge";
const threadId = "thread-issue-934-owner-merge";
const nowValue = "2026-06-05T13:39:48.000Z";
const userText = "看看有几个hwpod可用?";
const progressText = "Let me check the available HWPODs using the `hwpod` CLI tool.";
const middleText = "当前工作区还没有 `.hwlab/hwpod-spec.yaml` 文件,所以 `hwpod inspect` 无法获取可用 HWPOD 信息。";
const finalText = "当前工作区没有 `.hwlab/hwpod-spec.yaml`,也没有 `.hwlab` 目录或现成的 hwpod-spec 文件。这意味着这个 AgentRun 会话还没有绑定具体的 HWPOD。\n\n目前可用的 HWPOD**0 个**(需要先创建 spec 并绑定)。";
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_issue934_user", role: "user", title: "用户", text: userText, status: "sent", traceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue934_agent_pending", role: "agent", title: "Code Agent 处理中", text: `${progressText}${middleText}`, 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: "completed",
conversationId,
threadId,
traceId,
session: {
source: "code-agent-result",
status: "completed",
sessionStatus: "completed",
lastTraceId: traceId,
finalResponse: {
text: finalText,
textChars: finalText.length,
role: "assistant",
status: "completed",
traceId,
valuesPrinted: false
},
messages: [
{ id: "msg_issue934_agent_result", role: "agent", title: "Code Agent 回复", text: finalText, status: "completed", traceId, conversationId, sessionId, threadId, createdAt: nowValue }
],
messageCount: 1,
firstUserMessagePreview: userText,
valuesRedacted: true,
secretMaterialStored: false
}
});
await accessController.recordAgentSessionOwner({
ownerUserId,
sessionId,
projectId,
agentId: "hwlab-code-agent",
status: "completed",
conversationId,
threadId,
traceId,
session: {
source: "cloud-web-react-account-sync",
sessionStatus: "completed",
lastTraceId: traceId,
messages: [
{ id: "msg_issue934_user", role: "user", title: "用户", text: userText, status: "sent", traceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue934_agent_pending", role: "agent", title: "Code Agent 回复", text: `${progressText}${middleText}${finalText}`, status: "completed", traceId, conversationId, sessionId, threadId, createdAt: nowValue }
],
messageCount: 2,
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.finalResponse.text, finalText);
assert.equal(session.session.messages[1].text, finalText);
assert.equal(conversation.messages[1].text, finalText);
assert.doesNotMatch(conversation.messages[1].text, /Let me check the available HWPODs/u);
assert.doesNotMatch(conversation.messages[1].text, /当前工作区还没有/u);
assert.equal(conversation.messageCount, 2);
assert.equal(conversation.firstUserMessagePreview, userText);
});
test("cloud api exposes final response for legacy progress-polluted snapshots (#934)", async () => {
const nowValue = "2026-06-05T13:39:48.000Z";
const projectId = "prj_hwpod_workbench";
const conversationId = "cnv_issue934_legacy_snapshot";
const sessionId = "ses_issue934_legacy_snapshot";
const traceId = "trc_issue934_legacy_final";
const staleLastTraceId = "trc_issue934_legacy_greeting";
const threadId = "thread-issue-934-legacy";
const userText = "看看有几个hwpod可用?";
const progressText = "Let me check the available HWPODs using the hwpod CLI tool.";
const middleText = "当前工作区还没有 .hwlab/hwpod-spec.yaml 文件,所以 hwpod inspect 无法获取可用 HWPOD 信息。";
const finalText = "当前工作区没有 .hwlab/hwpod-spec.yaml,也没有 .hwlab 目录或现成的 hwpod-spec 文件。\n\n目前可用的 HWPOD0 个。";
const accessController = createAccessController({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass" },
now: () => nowValue
});
const server = createCloudApiServer({ env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" }, accessController, now: () => nowValue });
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 ownerUserId = login.body.actor.id;
accessController.store.agentSessions.set(sessionId, {
id: sessionId,
projectId,
agentId: "hwlab-code-agent",
status: "completed",
startedAt: nowValue,
endedAt: null,
ownerUserId,
conversationId,
threadId,
lastTraceId: staleLastTraceId,
updatedAt: nowValue,
session: {
source: "legacy-progress-polluted-snapshot",
sessionStatus: "completed",
lastTraceId: staleLastTraceId,
finalResponse: {
text: finalText,
textChars: finalText.length,
role: "assistant",
status: "completed",
traceId,
valuesPrinted: false
},
messages: [
{ id: "msg_issue934_legacy_user", role: "user", title: "用户", text: userText, status: "sent", traceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue934_legacy_agent", role: "agent", title: "Code Agent 回复", text: `${progressText}${middleText}${finalText}`, status: "completed", traceId, conversationId, sessionId, threadId, createdAt: nowValue }
],
messageCount: 2,
firstUserMessagePreview: userText,
valuesRedacted: true,
secretMaterialStored: false
}
});
const list = await getJson(port, `/v1/agent/conversations?projectId=${projectId}`, login.cookie);
assert.equal(list.status, 200);
const listed = list.body.conversations.find((conversation) => conversation.conversationId === conversationId);
assert.ok(listed, "expected legacy conversation to be listed");
assert.equal(listed.messages[1].text, finalText);
assert.doesNotMatch(listed.messages[1].text, /Let me check|当前工作区还没有/u);
const direct = await getJson(port, `/v1/agent/conversations/${conversationId}?projectId=${projectId}`, login.cookie);
assert.equal(direct.status, 200);
assert.equal(direct.body.conversation.messages[1].text, finalText);
assert.doesNotMatch(direct.body.conversation.messages[1].text, /Let me check|当前工作区还没有/u);
const workspace = await getJson(port, `/v1/workbench/workspace?projectId=${projectId}`, login.cookie);
assert.equal(workspace.status, 200);
const selected = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
projectId,
conversationId,
sessionId,
updatedByClient: "test-suite"
}, login.cookie);
assert.equal(selected.status, 200);
assert.equal(selected.body.workspace.selectedConversation.messages[1].text, finalText);
assert.doesNotMatch(selected.body.workspace.selectedConversation.messages[1].text, /Let me check|当前工作区还没有/u);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api trims folded progress from historical multi-turn final response snapshots (#934)", async () => {
const nowValue = "2026-06-05T13:39:48.000Z";
const projectId = "prj_hwpod_workbench";
const conversationId = "cnv_issue934_multiturn_history";
const sessionId = "ses_issue934_multiturn_history";
const firstTraceId = "trc_issue934_first_turn";
const secondTraceId = "trc_issue934_second_turn";
const threadId = "thread-issue-934-multiturn";
const firstFinalText = "当前工作区没有 `.hwlab/hwpod-spec.yaml`,也没有 `.hwlab` 目录或现成的 hwpod-spec 文件。这意味着这个 AgentRun 会话还没有绑定具体的 HWPOD。\n\n目前可用的 HWPOD**0 个**(需要先创建 spec 并绑定)。\n\n如果你有已知的 HWPOD 信息(例如节点 ID、target 或 spec 模板),可以给我,我可以帮你生成 `.hwlab/hwpod-spec.yaml`。";
const secondUserText = "看看有几个hwpod可用?";
const progressText = "Let me check the available HWPODs using the `hwpod` CLI tool.当前工作区还没有 `.hwlab/hwpod-spec.yaml` 文件,所以 `hwpod inspect` 无法获取可用 HWPOD 信息。 让我看看仓库里有没有现成的 spec 示例或相关配置:";
const accessController = createAccessController({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass" },
now: () => nowValue
});
const server = createCloudApiServer({ env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" }, accessController, now: () => nowValue });
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 ownerUserId = login.body.actor.id;
accessController.store.agentSessions.set(sessionId, {
id: sessionId,
projectId,
agentId: "hwlab-code-agent",
status: "completed",
startedAt: nowValue,
endedAt: null,
ownerUserId,
conversationId,
threadId,
lastTraceId: firstTraceId,
updatedAt: nowValue,
session: {
source: "legacy-multiturn-progress-polluted-snapshot",
sessionStatus: "completed",
lastTraceId: firstTraceId,
finalResponse: {
text: firstFinalText,
textChars: firstFinalText.length,
role: "assistant",
status: "completed",
traceId: firstTraceId,
valuesPrinted: false
},
messages: [
{ id: "msg_issue934_first_user", role: "user", title: "用户", text: "你好", status: "sent", traceId: firstTraceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue934_first_agent", role: "agent", title: "Code Agent 回复", text: firstFinalText, status: "completed", traceId: firstTraceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue934_second_user", role: "user", title: "用户", text: secondUserText, status: "sent", traceId: secondTraceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue934_second_agent", role: "agent", title: "Code Agent 回复", text: `${progressText}${firstFinalText}`, status: "completed", traceId: secondTraceId, conversationId, sessionId, threadId, createdAt: nowValue }
],
messageCount: 4,
firstUserMessagePreview: "你好",
valuesRedacted: true,
secretMaterialStored: false
}
});
const direct = await getJson(port, `/v1/agent/conversations/${conversationId}?projectId=${projectId}`, login.cookie);
assert.equal(direct.status, 200);
assert.equal(direct.body.conversation.messages.length, 4);
assert.equal(direct.body.conversation.messages[1].text, firstFinalText);
assert.equal(direct.body.conversation.messages[3].text, firstFinalText);
assert.equal(direct.body.conversation.messages[3].traceId, secondTraceId);
assert.doesNotMatch(direct.body.conversation.messages[3].text, /Let me check|当前工作区还没有|让我看看/u);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api trims folded progress when only conversation messages preserve the clean final text (#934)", async () => {
const nowValue = "2026-06-05T15:08:00.000Z";
const projectId = "prj_hwpod_workbench";
const conversationId = "cnv_issue934_message_only_history";
const sessionId = "ses_issue934_message_only_history";
const firstTraceId = "trc_issue934_message_only_first";
const secondTraceId = "trc_issue934_message_only_second";
const threadId = "thread-issue-934-message-only";
const finalText = "当前工作区没有 `.hwlab/hwpod-spec.yaml`,也没有 `.hwlab` 目录或现成的 hwpod-spec 文件。这意味着这个 AgentRun 会话还没有绑定具体的 HWPOD。\n\n目前可用的 HWPOD**0 个**(需要先创建 spec 并绑定)。\n\n如果你有已知的 HWPOD 信息(例如节点 ID、target 或 spec 模板),可以给我,我可以帮你生成 `.hwlab/hwpod-spec.yaml`。";
const progressText = "Let me check the available HWPODs using the `hwpod` CLI tool.当前工作区还没有 `.hwlab/hwpod-spec.yaml` 文件,所以 `hwpod inspect` 无法获取可用 HWPOD 信息。 让我看看仓库里有没有现成的 spec 示例或相关配置:";
const whitespaceFoldedFinalText = finalText.replace(/\s+/gu, " ");
const accessController = createAccessController({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass" },
now: () => nowValue
});
const server = createCloudApiServer({ env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" }, accessController, now: () => nowValue });
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 ownerUserId = login.body.actor.id;
accessController.store.agentSessions.set(sessionId, {
id: sessionId,
projectId,
agentId: "hwlab-code-agent",
status: "completed",
startedAt: nowValue,
endedAt: null,
ownerUserId,
conversationId,
threadId,
lastTraceId: firstTraceId,
updatedAt: nowValue,
session: {
source: "legacy-message-only-progress-polluted-snapshot",
sessionStatus: "completed",
lastTraceId: firstTraceId,
messages: [
{ id: "msg_issue934_message_only_first_user", role: "user", title: "用户", text: "你好", status: "sent", traceId: firstTraceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue934_message_only_first_agent", role: "agent", title: "Code Agent 回复", text: finalText, status: "completed", traceId: firstTraceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue934_message_only_second_user", role: "user", title: "用户", text: "看看有几个hwpod可用?", status: "sent", traceId: secondTraceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue934_message_only_second_agent", role: "agent", title: "Code Agent 回复", text: `${progressText}${whitespaceFoldedFinalText}`, status: "completed", traceId: secondTraceId, conversationId, sessionId, threadId, createdAt: nowValue }
],
messageCount: 4,
firstUserMessagePreview: "你好",
valuesRedacted: true,
secretMaterialStored: false
}
});
const direct = await getJson(port, `/v1/agent/conversations/${conversationId}?projectId=${projectId}`, login.cookie);
assert.equal(direct.status, 200);
assert.equal(direct.body.conversation.messages.length, 4);
assert.equal(direct.body.conversation.messages[1].text, finalText);
assert.equal(direct.body.conversation.messages[3].text, finalText);
assert.equal(direct.body.conversation.messages[3].traceId, secondTraceId);
assert.doesNotMatch(direct.body.conversation.messages[3].text, /Let me check|当前工作区还没有|让我看看/u);
const list = await getJson(port, `/v1/agent/conversations?projectId=${projectId}`, login.cookie);
assert.equal(list.status, 200);
const listed = list.body.conversations.find((conversation) => conversation.conversationId === conversationId);
assert.ok(listed, "expected message-only legacy conversation to be listed");
assert.equal(listed.messages[3].text, finalText);
assert.doesNotMatch(listed.messages[3].text, /Let me check|当前工作区还没有|让我看看/u);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
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())));
}
});