import assert from "node:assert/strict";
import { generateKeyPairSync, sign } from "node:crypto";
import { mkdtemp } from "node:fs/promises";
import { createServer } from "node:http";
import { test } from "bun:test";

import { createAccessController } from "./access-control.ts";
import { createCloudApiServer } from "./server.ts";

const INTERNAL_TOKEN = "test-internal-token";
const OIDC_ISSUER = "https://auth.74-48-78-17.nip.io/realms/hwlab";
const OIDC_CLIENT_ID = "hwlab-cloud-web";
const HWLAB_PUBLIC_ENDPOINT = "https://hwlab.74-48-78-17.nip.io";
const OIDC_TEST_KEY_ID = "hwlab-test-key";
const OIDC_TEST_KEYS = generateKeyPairSync("rsa", { modulusLength: 2048 });
const OIDC_TEST_JWK = Object.freeze({
  ...OIDC_TEST_KEYS.publicKey.export({ format: "jwk" }),
  kid: OIDC_TEST_KEY_ID,
  alg: "RS256",
  use: "sig"
});

test("Postgres workspace update query preserves parameter numbering", async () => {
  const calls = [];
  const accessController = createAccessController({
    now: () => "2026-06-01T00:00:00.000Z",
    runtimeStore: {
      query: async (sql, params = []) => {
        calls.push({ sql, params });
        if (sql.startsWith("CREATE ") || sql.startsWith("ALTER ")) return { rows: [] };
        if (sql.startsWith("SELECT * FROM account_workspaces")) {
          return { rows: [{
            id: "wsp_pg_param_guard",
            owner_user_id: "usr_pg_param_guard",
            project_id: "prj_hwpod_workbench",
            name: "Default Workbench",
            status: "active",
            is_default: true,
            selected_conversation_id: null,
            selected_agent_session_id: null,
            active_trace_id: null,
            provider_profile: null,
            workspace_json: "{}",
            revision: 1,
            updated_by_session_id: null,
            updated_by_client: null,
            created_at: "2026-06-01T00:00:00.000Z",
            updated_at: "2026-06-01T00:00:00.000Z"
          }] };
        }
        if (sql.startsWith("UPDATE account_workspaces")) return { rows: [] };
        return { rows: [] };
      }
    }
  });
  await accessController.store.updateWorkspace({
    workspaceId: "wsp_pg_param_guard",
    ownerUserId: "usr_pg_param_guard",
    actorRole: "user",
    providerProfile: "deepseek",
    patch: { issue655AcceptanceMarker: "postgres-param-guard" },
    updatedByClient: "test-suite"
  });

  const update = calls.find((call) => call.sql.startsWith("UPDATE account_workspaces"));
  assert.ok(update);
  assert.match(update.sql, /created_at=\$15, updated_at=\$16/u);
  assert.match(update.sql, /\$17::text = 'admin'/u);
  assert.equal(update.params.length, 17);
  assert.equal(update.params[14], "2026-06-01T00:00:00.000Z");
  assert.equal(update.params[15], "2026-06-01T00:00:00.000Z");
  assert.equal(update.params[16], "user");
});

test("cloud api /auth/oidc/login returns 302 to Keycloak when issuer and client are configured", async () => {
  const server = createCloudApiServer({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_KEYCLOAK_ISSUER: OIDC_ISSUER, HWLAB_KEYCLOAK_CLIENT_ID: OIDC_CLIENT_ID, HWLAB_PUBLIC_ENDPOINT },
    now: () => "2026-06-03T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
  try {
    const { port } = server.address();
    const response = await fetch(`http://127.0.0.1:${port}/auth/oidc/login?returnTo=/workbench`, { redirect: "manual" });
    assert.equal(response.status, 302);
    const location = response.headers.get("location");
    assert.ok(location?.startsWith("https://auth.74-48-78-17.nip.io/realms/hwlab/protocol/openid-connect/auth"), `unexpected location: ${location}`);
    assert.ok(location?.includes("client_id=hwlab-cloud-web"));
    assert.ok(location?.includes("redirect_uri=https%3A%2F%2Fhwlab.74-48-78-17.nip.io%2Fauth%2Foidc%2Fcallback"));
    assert.ok(location?.includes("response_type=code"));
    assert.ok(location?.includes("state="));
    assert.ok(location?.includes("nonce="));
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

test("cloud api /auth/oidc/login stores only same-origin relative returnTo", async () => {
  const accessController = createAccessController({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_KEYCLOAK_ISSUER: OIDC_ISSUER, HWLAB_KEYCLOAK_CLIENT_ID: OIDC_CLIENT_ID, HWLAB_PUBLIC_ENDPOINT },
    now: () => "2026-06-03T00:00:00.000Z"
  });
  const server = createCloudApiServer({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
    accessController,
    now: () => "2026-06-03T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
  try {
    const { port } = server.address();
    const response = await fetch(`http://127.0.0.1:${port}/auth/oidc/login?returnTo=https%3A%2F%2Fevil.example%2F`, { redirect: "manual" });
    assert.equal(response.status, 302);
    const state = new URL(response.headers.get("location") ?? "http://127.0.0.1").searchParams.get("state");
    assert.ok(state);
    const stored = await accessController.store.consumeOidcState(state);
    assert.equal(stored.returnTo, "/workbench");
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

test("cloud api /auth/oidc/callback upserts user by issuer+sub and issues 24h web session", async () => {
  const accessController = createAccessController({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_KEYCLOAK_ISSUER: OIDC_ISSUER, HWLAB_KEYCLOAK_CLIENT_ID: OIDC_CLIENT_ID, HWLAB_KEYCLOAK_CLIENT_SECRET: "secret", HWLAB_PUBLIC_ENDPOINT },
    fetchImpl: makeOidcFetchMock(),
    now: () => "2026-06-03T00:00:00.000Z"
  });
  const state = "test-state";
  await accessController.store.upsertOidcState({ state, nonce: "test-nonce", returnTo: "/workbench", createdAt: "2026-06-03T00:00:00.000Z", expiresAt: "2026-06-03T00:10:00.000Z" });
  const server = createCloudApiServer({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
    accessController,
    now: () => "2026-06-03T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
  try {
    const { port } = server.address();
    const response = await fetch(`http://127.0.0.1:${port}/auth/oidc/callback?code=oidc-code&state=${state}`, { redirect: "manual", headers: { "x-forwarded-proto": "https" } });
    assert.equal(response.status, 302);
    const cookie = response.headers.get("set-cookie") ?? "";
    assert.match(cookie, /hwlab_session=[A-Za-z0-9_-]+;/);
    assert.match(cookie, /HttpOnly/);
    assert.match(cookie, /(?:^|;\s*)Secure(?:;|$)/u);
    assert.match(cookie, /Max-Age=86400/);
    const sessionResponse = await fetch(`http://127.0.0.1:${port}/v1/auth/session`, { headers: { cookie: cookie.split(";")[0] } });
    assert.equal(sessionResponse.status, 200);
    const sessionBody = await sessionResponse.json();
    assert.equal(sessionBody.authenticated, true);
    assert.equal(sessionBody.actor.username, "oidc-user");
    assert.equal(sessionBody.actor.role, "user");
    assert.equal(sessionBody.authMethod, "web-session");
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

function makeOidcFetchMock({ nonce = "test-nonce", subject = "keycloak-sub-1234", issuer = OIDC_ISSUER, clientId = OIDC_CLIENT_ID } = {}) {
  return async (url) => {
    const u = new URL(String(url));
    if (u.pathname.endsWith("/protocol/openid-connect/token")) {
      return new Response(JSON.stringify({
        access_token: "oidc-access-token",
        refresh_token: "oidc-refresh-token",
        id_token: signedOidcIdToken({ issuer, clientId, nonce, subject }),
        token_type: "Bearer",
        expires_in: 300
      }), { status: 200, headers: { "content-type": "application/json" } });
    }
    if (u.pathname.endsWith("/protocol/openid-connect/certs")) {
      return new Response(JSON.stringify({ keys: [OIDC_TEST_JWK] }), { status: 200, headers: { "content-type": "application/json" } });
    }
    if (u.pathname.endsWith("/protocol/openid-connect/userinfo")) {
      return new Response(JSON.stringify({
        sub: subject,
        preferred_username: "oidc-user",
        email: "oidc-user@hwlab.local",
        name: "OIDC User",
        email_verified: true
      }), { status: 200, headers: { "content-type": "application/json" } });
    }
    return new Response("not found", { status: 404 });
  };
}

function signedOidcIdToken({ issuer = OIDC_ISSUER, clientId = OIDC_CLIENT_ID, nonce = "test-nonce", subject = "keycloak-sub-1234", nowSeconds = 1_780_444_800 } = {}) {
  const header = base64UrlEncodeJson({ alg: "RS256", typ: "JWT", kid: OIDC_TEST_KEY_ID });
  const payload = base64UrlEncodeJson({
    iss: issuer,
    aud: clientId,
    sub: subject,
    nonce,
    exp: nowSeconds + 600,
    iat: nowSeconds,
    preferred_username: "oidc-user",
    email: "oidc-user@hwlab.local",
    name: "OIDC User"
  });
  const body = `${header}.${payload}`;
  const signature = sign("RSA-SHA256", Buffer.from(body), OIDC_TEST_KEYS.privateKey).toString("base64url");
  return `${body}.${signature}`;
}

function base64UrlEncodeJson(value) {
  return Buffer.from(JSON.stringify(value)).toString("base64url");
}

test("workbench workspace permits a new turn after AgentRun active trace reaches terminal status", async () => {
  const agentRunCalls = [];
  const agentSessions = new Map();
  const agentRunServer = createServer(async (request, response) => {
    const url = new URL(request.url || "/", "http://127.0.0.1");
    agentRunCalls.push({ method: request.method, path: url.pathname, search: url.search });
    const send = (data) => {
      response.writeHead(200, { "content-type": "application/json" });
      response.end(`${JSON.stringify({ ok: true, data })}\n`);
    };
    if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_done/events") {
      return send({ items: [
        { id: "evt_done", runId: "run_workspace_done", seq: 1, type: "terminal_status", payload: { commandId: "cmd_workspace_done", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:01.000Z" }
      ] });
    }
    if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_done/commands/cmd_workspace_done/result") {
      return send({
        runId: "run_workspace_done",
        commandId: "cmd_workspace_done",
        status: "completed",
        runStatus: "completed",
        commandState: "completed",
        terminalStatus: "completed",
        completed: true,
        reply: "previous turn completed",
        lastSeq: 1,
        sessionRef: { sessionId: "ses_issue655_shared", conversationId: "cnv_issue655_shared", threadId: "thread-issue-655" }
      });
    }
    if (request.method === "POST" && url.pathname === "/api/v1/runs") {
      return send({ id: "run_workspace_next", status: "pending", backendProfile: "deepseek", sessionRef: { sessionId: "ses_issue655_shared", conversationId: "cnv_issue655_shared" } });
    }
    if (request.method === "POST" && url.pathname === "/api/v1/runs/run_workspace_next/commands") {
      return send({ id: "cmd_workspace_next", runId: "run_workspace_next", state: "pending", type: "turn", seq: 1 });
    }
    if (request.method === "POST" && url.pathname === "/api/v1/runs/run_workspace_next/runner-jobs") {
      return send({
        action: "create-kubernetes-job",
        runId: "run_workspace_next",
        commandId: "cmd_workspace_next",
        attemptId: "attempt_workspace_next",
        runnerId: "runner_workspace_next",
        namespace: "agentrun-v01",
        jobName: "agentrun-v01-runner-workspace-next"
      });
    }
    response.writeHead(404, { "content-type": "application/json" });
    response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
  });
  await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));

  const agentRunPort = agentRunServer.address().port;
  const accessController = createAccessController({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
    },
    now: () => "2026-06-01T00:00:00.000Z"
  });
  accessController.getAgentSessionByTraceId = async (traceId) => agentSessions.get(traceId) ?? null;
  const server = createCloudApiServer({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
      HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
      AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
      HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
      HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
      HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek"
    },
    accessController,
    now: () => "2026-06-01T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
    const alice = await postJson(port, "/v1/admin/users", { username: "alice-ws-terminal", password: "alice-pass" }, adminLogin.cookie);
    assert.equal(alice.status, 201);
    const aliceLogin = await postJson(port, "/auth/login", { username: "alice-ws-terminal", password: "alice-pass" });
    const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(workspace.status, 200);
    const conversation = await putJson(port, "/v1/agent/conversations/cnv_issue655_shared", {
      projectId: "prj_hwpod_workbench",
      sessionId: "ses_issue655_shared",
      threadId: "thread-issue-655",
      sessionStatus: "active",
      lastTraceId: "trc_issue655_previous",
      messages: [{ role: "agent", text: "previous completed turn", traceId: "trc_issue655_previous" }]
    }, aliceLogin.cookie);
    assert.equal(conversation.status, 200);

    const previousPayload = {
      messageId: "msg_workspace_done",
      agentRun: {
        runId: "run_workspace_done",
        commandId: "cmd_workspace_done",
        backendProfile: "deepseek",
        managerUrl: `http://127.0.0.1:${agentRunPort}`,
        lastSeq: 0,
        sessionId: "ses_issue655_shared",
        conversationId: "cnv_issue655_shared",
        threadId: "thread-issue-655"
      }
    };
    agentSessions.set("trc_issue655_active_done", {
      id: "ses_issue655_shared",
      ownerUserId: alice.body.user.id,
      conversationId: "cnv_issue655_shared",
      threadId: "thread-issue-655",
      status: "active",
      session: previousPayload,
      updatedAt: "2026-06-01T00:00:00.000Z"
    });

    const update = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
      expectedRevision: 1,
      selectedConversationId: "cnv_issue655_shared",
      selectedAgentSessionId: "ses_issue655_shared",
      activeTraceId: "trc_issue655_active_done",
      providerProfile: "deepseek",
      sessionStatus: "running",
      updatedByClient: "test-suite"
    }, aliceLogin.cookie);
    assert.equal(update.status, 200);
    assert.equal(update.body.workspace.activeTraceId, "trc_issue655_active_done");

    const next = await postJson(port, "/v1/agent/chat", {
      message: "new turn after completed active trace",
      traceId: "trc_issue655_after_done",
      conversationId: "cnv_issue655_shared",
      sessionId: "ses_issue655_shared",
      workspaceId: workspace.body.workspace.workspaceId,
      expectedWorkspaceRevision: 2,
      shortConnection: true
    }, aliceLogin.cookie, { prefer: "respond-async", "x-trace-id": "trc_issue655_after_done" });
    assert.equal(next.status, 202);
    assert.equal(next.body.status, "running");
    assert.equal(next.body.traceId, "trc_issue655_after_done");
    await waitForCondition(() => agentRunCalls.some((call) => call.method === "POST" && call.path === "/api/v1/runs"));
    assert.equal(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_workspace_done/commands/cmd_workspace_done/result"), false);

    const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(restored.body.workspace.activeTraceId, "trc_issue655_after_done");
    assert.equal(restored.body.workspace.workspace.sessionStatus, "running");
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
    await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
  }
});

test("workbench workspace permits continuation when stale active trace has idle active conversation", async () => {
  const workspaceDir = await mkdtemp("/tmp/hwlab-stale-active-idle-");
  const server = createCloudApiServer({
    env: {
      PATH: process.env.PATH,
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
      HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
      HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspaceDir,
      HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
      OPENAI_API_KEY: "test-openai-key-material"
    },
    codexStdioManager: {
      describe: () => ({ available: true, ready: true, workspace: workspaceDir, sandbox: "danger-full-access" }),
      probe: async () => ({ available: true, ready: true, workspace: workspaceDir, sandbox: "danger-full-access" }),
      chat: async (params = {}) => ({
        status: "completed",
        traceId: params.traceId,
        conversationId: params.conversationId,
        sessionId: params.sessionId,
        threadId: params.threadId,
        reply: { role: "assistant", content: "ok" },
        runnerTrace: { traceId: params.traceId, status: "completed", events: [], eventCount: 0 },
        session: { sessionId: params.sessionId, conversationId: params.conversationId, threadId: params.threadId, status: "idle" }
      }),
      cancel() {},
      reapIdle() {}
    }
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
    const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", adminLogin.cookie);
    const conversation = await putJson(port, "/v1/agent/conversations/cnv_stale_active_idle", {
      projectId: "prj_hwpod_workbench",
      sessionId: "ses_stale_active_idle",
      threadId: "thread-stale-active-idle",
      status: "active",
      sessionStatus: "active",
      lastTraceId: "trc_stale_active_idle_missing",
      messages: [{ role: "agent", text: "previous completed turn", status: "completed", traceId: "trc_stale_active_idle_missing" }]
    }, adminLogin.cookie);
    assert.equal(conversation.status, 200);

    const update = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
      expectedRevision: 1,
      selectedConversationId: "cnv_stale_active_idle",
      selectedAgentSessionId: "ses_stale_active_idle",
      activeTraceId: "trc_stale_active_idle_missing",
      sessionStatus: "running",
      updatedByClient: "test-suite"
    }, adminLogin.cookie);
    assert.equal(update.status, 200);

    const next = await postJson(port, "/v1/agent/chat", {
      message: "new turn after stale active idle trace",
      traceId: "trc_stale_active_idle_next",
      conversationId: "cnv_stale_active_idle",
      sessionId: "ses_stale_active_idle",
      threadId: "thread-stale-active-idle",
      workspaceId: workspace.body.workspace.workspaceId,
      expectedWorkspaceRevision: 2,
      shortConnection: true
    }, adminLogin.cookie, { prefer: "respond-async", "x-trace-id": "trc_stale_active_idle_next" });

    assert.equal(next.status, 202);
    assert.equal(next.body.traceId, "trc_stale_active_idle_next");
    assert.equal(next.body.conversationId, "cnv_stale_active_idle");
    assert.equal(next.body.sessionId, "ses_stale_active_idle");

    const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", adminLogin.cookie);
    assert.equal(restored.body.workspace.activeTraceId, "trc_stale_active_idle_next");
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

test("workbench workspace status clears completed AgentRun active trace on read", async () => {
  const agentRunCalls = [];
  const agentSessions = new Map();
  const agentRunServer = createServer(async (request, response) => {
    const url = new URL(request.url || "/", "http://127.0.0.1");
    agentRunCalls.push({ method: request.method, path: url.pathname, search: url.search });
    const send = (data) => {
      response.writeHead(200, { "content-type": "application/json" });
      response.end(`${JSON.stringify({ ok: true, data })}\n`);
    };
    if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_status/events") {
      return send({ items: [
        { id: "evt_status_done", runId: "run_workspace_status", seq: 1, type: "terminal_status", payload: { commandId: "cmd_workspace_status", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:01.000Z" }
      ] });
    }
    if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_status/commands") {
      return send({ items: [
        { id: "cmd_workspace_status", runId: "run_workspace_status", state: "completed", type: "turn", seq: 1, idempotencyKey: "trc_issue664_status_done", payload: { traceId: "trc_issue664_status_done", conversationId: "cnv_issue664_status", hwlabSessionId: "ses_issue664_status", threadId: "thread-issue-664", providerProfile: "deepseek" } }
      ] });
    }
    if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_status/commands/cmd_workspace_status/result") {
      return send({
        runId: "run_workspace_status",
        commandId: "cmd_workspace_status",
        status: "completed",
        runStatus: "completed",
        commandState: "completed",
        terminalStatus: "completed",
        completed: true,
        reply: "previous turn completed",
        lastSeq: 1,
        sessionRef: { sessionId: "ses_issue664_status", conversationId: "cnv_issue664_status", threadId: "thread-issue-664" }
      });
    }
    response.writeHead(404, { "content-type": "application/json" });
    response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
  });
  await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));

  const agentRunPort = agentRunServer.address().port;
  const accessController = createAccessController({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
    },
    now: () => "2026-06-01T00:00:00.000Z"
  });
  accessController.getAgentSessionByTraceId = async (traceId) => agentSessions.get(traceId) ?? null;
  const server = createCloudApiServer({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
      HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
      AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
      HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
      HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek"
    },
    accessController,
    now: () => "2026-06-01T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
    const alice = await postJson(port, "/v1/admin/users", { username: "alice-ws-status", password: "alice-pass" }, adminLogin.cookie);
    assert.equal(alice.status, 201);
    const aliceLogin = await postJson(port, "/auth/login", { username: "alice-ws-status", password: "alice-pass" });
    const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(workspace.status, 200);
    const conversation = await putJson(port, "/v1/agent/conversations/cnv_issue664_status", {
      projectId: "prj_hwpod_workbench",
      sessionId: "ses_issue664_status",
      threadId: "thread-issue-664",
      sessionStatus: "active",
      lastTraceId: "trc_issue664_previous",
      messages: [{ role: "agent", text: "previous turn still running", status: "running", traceId: "trc_issue664_status_done" }]
    }, aliceLogin.cookie);
    assert.equal(conversation.status, 200);

    const previousPayload = {
      messageId: "msg_workspace_status_done",
      agentRun: {
        runId: "run_workspace_status",
        commandId: "cmd_workspace_status",
        backendProfile: "deepseek",
        managerUrl: `http://127.0.0.1:${agentRunPort}`,
        lastSeq: 0,
        sessionId: "ses_issue664_status",
        conversationId: "cnv_issue664_status",
        threadId: "thread-issue-664"
      }
    };
    agentSessions.set("trc_issue664_status_done", {
      id: "ses_issue664_status",
      ownerUserId: alice.body.user.id,
      conversationId: "cnv_issue664_status",
      threadId: "thread-issue-664",
      status: "active",
      session: previousPayload,
      updatedAt: "2026-06-01T00:00:00.000Z"
    });

    const update = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
      expectedRevision: 1,
      selectedConversationId: "cnv_issue664_status",
      selectedAgentSessionId: "ses_issue664_status",
      activeTraceId: "trc_issue664_status_done",
      providerProfile: "deepseek",
      sessionStatus: "running",
      updatedByClient: "test-suite"
    }, aliceLogin.cookie);
    assert.equal(update.status, 200);
    assert.equal(update.body.workspace.activeTraceId, "trc_issue664_status_done");

    const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(restored.status, 200);
    assert.equal(restored.body.workspace.activeTraceId, null);
    assert.equal(restored.body.workspace.workspace.lastTraceId, "trc_issue664_status_done");
    assert.equal(restored.body.workspace.workspace.sessionStatus, "idle");
    assert.equal(restored.body.workspace.selectedConversation.status, "idle");
    assert.equal(restored.body.workspace.selectedConversation.lastTraceId, "trc_issue664_status_done");
    assert.equal(restored.body.workspace.selectedConversation.messages.length, 1);
    assert.equal(restored.body.workspace.selectedConversation.messages[0].status, "idle");
    assert.equal(restored.body.workspace.selectedConversation.messages[0].text, "previous turn completed");
    assert.equal(restored.body.workspace.revision, 3);
    assert.ok(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_workspace_status/commands/cmd_workspace_status/result"));
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
    await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
  }
});

test("workbench workspace terminal status sync preserves a newer selected conversation", async () => {
  const agentRunCalls = [];
  const agentSessions = new Map();
  const agentRunServer = createServer(async (request, response) => {
    const url = new URL(request.url || "/", "http://127.0.0.1");
    agentRunCalls.push({ method: request.method, path: url.pathname, search: url.search });
    const send = (data) => {
      response.writeHead(200, { "content-type": "application/json" });
      response.end(`${JSON.stringify({ ok: true, data })}\n`);
    };
    if (request.method === "GET" && url.pathname === "/api/v1/runs/run_issue808_old/events") {
      return send({ items: [
        { id: "evt_issue808_done", runId: "run_issue808_old", seq: 1, type: "terminal_status", payload: { commandId: "cmd_issue808_old", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:01.000Z" }
      ] });
    }
    if (request.method === "GET" && url.pathname === "/api/v1/runs/run_issue808_old/commands") {
      return send({ items: [
        { id: "cmd_issue808_old", runId: "run_issue808_old", state: "completed", type: "turn", seq: 1, idempotencyKey: "trc_issue808_old_done", payload: { traceId: "trc_issue808_old_done", conversationId: "cnv_issue808_old", hwlabSessionId: "ses_issue808_old", threadId: "thread-issue-808-old", providerProfile: "deepseek" } }
      ] });
    }
    if (request.method === "GET" && url.pathname === "/api/v1/runs/run_issue808_old/commands/cmd_issue808_old/result") {
      return send({
        runId: "run_issue808_old",
        commandId: "cmd_issue808_old",
        status: "completed",
        runStatus: "completed",
        commandState: "completed",
        terminalStatus: "completed",
        completed: true,
        reply: "old turn completed",
        lastSeq: 1,
        sessionRef: { sessionId: "ses_issue808_old", conversationId: "cnv_issue808_old", threadId: "thread-issue-808-old" }
      });
    }
    response.writeHead(404, { "content-type": "application/json" });
    response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
  });
  await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));

  const agentRunPort = agentRunServer.address().port;
  const accessController = createAccessController({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
    },
    now: () => "2026-06-01T00:00:00.000Z"
  });
  accessController.getAgentSessionByTraceId = async (traceId) => agentSessions.get(traceId) ?? null;
  const server = createCloudApiServer({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
      HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
      AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
      HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
      HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek"
    },
    accessController,
    now: () => "2026-06-01T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
    const alice = await postJson(port, "/v1/admin/users", { username: "alice-ws-issue808", password: "alice-pass" }, adminLogin.cookie);
    assert.equal(alice.status, 201);
    const aliceLogin = await postJson(port, "/auth/login", { username: "alice-ws-issue808", password: "alice-pass" });
    const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(workspace.status, 200);

    const oldConversation = await putJson(port, "/v1/agent/conversations/cnv_issue808_old", {
      projectId: "prj_hwpod_workbench",
      sessionId: "ses_issue808_old",
      threadId: "thread-issue-808-old",
      sessionStatus: "running",
      lastTraceId: "trc_issue808_old_done",
      messages: [{ role: "agent", text: "old turn running", status: "running", traceId: "trc_issue808_old_done" }]
    }, aliceLogin.cookie);
    assert.equal(oldConversation.status, 200);
    const newConversation = await putJson(port, "/v1/agent/conversations/cnv_issue808_new", {
      projectId: "prj_hwpod_workbench",
      sessionId: "ses_issue808_new",
      threadId: "thread-issue-808-new",
      sessionStatus: "idle",
      messages: []
    }, aliceLogin.cookie);
    assert.equal(newConversation.status, 200);

    agentSessions.set("trc_issue808_old_done", {
      id: "ses_issue808_old",
      ownerUserId: alice.body.user.id,
      conversationId: "cnv_issue808_old",
      threadId: "thread-issue-808-old",
      status: "active",
      session: {
        messageId: "msg_issue808_old_done",
        agentRun: {
          runId: "run_issue808_old",
          commandId: "cmd_issue808_old",
          backendProfile: "deepseek",
          managerUrl: `http://127.0.0.1:${agentRunPort}`,
          lastSeq: 0,
          sessionId: "ses_issue808_old",
          conversationId: "cnv_issue808_old",
          threadId: "thread-issue-808-old"
        }
      },
      updatedAt: "2026-06-01T00:00:00.000Z"
    });

    const oldActive = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
      expectedRevision: 1,
      selectedConversationId: "cnv_issue808_old",
      selectedAgentSessionId: "ses_issue808_old",
      activeTraceId: "trc_issue808_old_done",
      lastTraceId: "trc_issue808_old_done",
      providerProfile: "deepseek",
      sessionStatus: "running",
      updatedByClient: "test-suite"
    }, aliceLogin.cookie);
    assert.equal(oldActive.status, 200);
    assert.equal(oldActive.body.workspace.selectedConversationId, "cnv_issue808_old");
    assert.equal(oldActive.body.workspace.activeTraceId, "trc_issue808_old_done");

    const selectedNew = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
      projectId: "prj_hwpod_workbench",
      conversationId: "cnv_issue808_new",
      sessionId: "ses_issue808_new",
      updatedByClient: "test-suite"
    }, aliceLogin.cookie);
    assert.equal(selectedNew.status, 200);
    assert.equal(selectedNew.body.workspace.selectedConversationId, "cnv_issue808_new");
    assert.equal(selectedNew.body.workspace.selectedAgentSessionId, "ses_issue808_new");

    const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(restored.status, 200);
    assert.equal(restored.body.workspace.activeTraceId, null);
    assert.equal(restored.body.workspace.selectedConversationId, "cnv_issue808_new");
    assert.equal(restored.body.workspace.selectedAgentSessionId, "ses_issue808_new");
    assert.equal(restored.body.workspace.selectedConversation.conversationId, "cnv_issue808_new");
    assert.equal(restored.body.workspace.selectedConversation.sessionId, "ses_issue808_new");
    assert.equal(restored.body.workspace.workspace.selectedConversationId, "cnv_issue808_new");
    assert.equal(restored.body.workspace.workspace.selectedAgentSessionId, "ses_issue808_new");
    assert.equal(restored.body.workspace.workspace.lastTraceId, "trc_issue808_old_done");
    assert.ok(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_issue808_old/commands/cmd_issue808_old/result"));

    const selectedOld = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
      projectId: "prj_hwpod_workbench",
      conversationId: "cnv_issue808_old",
      sessionId: "ses_issue808_old",
      updatedByClient: "test-suite"
    }, aliceLogin.cookie);
    assert.equal(selectedOld.status, 200);
    assert.equal(selectedOld.body.workspace.selectedConversationId, "cnv_issue808_old");

    const repaired = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(repaired.status, 200);
    assert.equal(repaired.body.workspace.activeTraceId, null);
    assert.equal(repaired.body.workspace.selectedConversationId, "cnv_issue808_old");
    assert.equal(repaired.body.workspace.selectedAgentSessionId, "ses_issue808_old");
    assert.equal(repaired.body.workspace.selectedConversation.status, "idle");
    assert.equal(repaired.body.workspace.selectedConversation.lastTraceId, "trc_issue808_old_done");
    assert.equal(repaired.body.workspace.selectedConversation.messages[0].status, "idle");
    assert.equal(repaired.body.workspace.selectedConversation.messages[0].text, "old turn completed");
    assert.ok(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_issue808_old/commands/cmd_issue808_old/result"));

    const repairedOld = await getJson(port, "/v1/agent/conversations/cnv_issue808_old", aliceLogin.cookie);
    assert.equal(repairedOld.status, 200);
    assert.equal(repairedOld.body.conversation.status, "idle");
    assert.equal(repairedOld.body.conversation.lastTraceId, "trc_issue808_old_done");
    assert.equal(repairedOld.body.conversation.messages[0].status, "idle");
    assert.equal(repairedOld.body.conversation.messages[0].text, "old turn completed");
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
    await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
  }
});

test("manual Code Agent session select restores running active trace", async () => {
  const accessController = createAccessController({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
    },
    now: () => "2026-06-01T00:00:00.000Z"
  });
  const server = createCloudApiServer({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
      HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "minimax-m3"
    },
    accessController,
    now: () => "2026-06-01T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
    const alice = await postJson(port, "/v1/admin/users", { username: "alice-ws-issue810", password: "alice-pass" }, adminLogin.cookie);
    assert.equal(alice.status, 201);
    const aliceLogin = await postJson(port, "/auth/login", { username: "alice-ws-issue810", password: "alice-pass" });
    const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(workspace.status, 200);

    await accessController.recordAgentSessionOwner({
      ownerUserId: alice.body.user.id,
      ownerRole: "user",
      sessionId: "ses_issue810_running",
      projectId: "prj_hwpod_workbench",
      agentId: "hwlab-code-agent",
      status: "running",
      conversationId: "cnv_issue810_running",
      threadId: "thread-issue-810-running",
      traceId: "trc_issue810_running",
      session: {
        source: "test-running-session",
        providerProfile: "minimax-m3",
        sessionStatus: "running",
        currentTraceId: "trc_issue810_running",
        agentRun: {
          runId: "run_issue810_running",
          commandId: "cmd_issue810_running",
          runStatus: "running",
          commandState: "running",
          backendProfile: "minimax-m3"
        },
        valuesRedacted: true,
        secretMaterialStored: false
      }
    });
    await accessController.recordAgentSessionOwner({
      ownerUserId: alice.body.user.id,
      ownerRole: "user",
      sessionId: "ses_issue810_idle",
      projectId: "prj_hwpod_workbench",
      agentId: "hwlab-code-agent",
      status: "idle",
      conversationId: "cnv_issue810_idle",
      threadId: "thread-issue-810-idle",
      traceId: null,
      session: {
        source: "test-idle-session",
        providerProfile: "minimax-m3",
        sessionStatus: "idle",
        valuesRedacted: true,
        secretMaterialStored: false
      }
    });

    const selectedIdle = await postJson(port, "/v1/agent/sessions/ses_issue810_idle/select", {
      projectId: "prj_hwpod_workbench",
      workspaceId: workspace.body.workspace.workspaceId,
      updatedByClient: "test-suite"
    }, aliceLogin.cookie);
    assert.equal(selectedIdle.status, 200);
    assert.equal(selectedIdle.body.workspace.selectedConversationId, "cnv_issue810_idle");
    assert.equal(selectedIdle.body.workspace.selectedAgentSessionId, "ses_issue810_idle");
    assert.equal(selectedIdle.body.workspace.activeTraceId, null);

    const selectedRunning = await postJson(port, "/v1/agent/sessions/ses_issue810_running/select", {
      projectId: "prj_hwpod_workbench",
      workspaceId: workspace.body.workspace.workspaceId,
      updatedByClient: "test-suite"
    }, aliceLogin.cookie);
    assert.equal(selectedRunning.status, 200);
    assert.equal(selectedRunning.body.status, "selected");
    assert.equal(selectedRunning.body.session.sessionId, "ses_issue810_running");
    assert.equal(selectedRunning.body.session.status, "running");
    assert.equal(selectedRunning.body.session.lastTraceId, "trc_issue810_running");
    assert.equal(selectedRunning.body.workspace.selectedConversationId, "cnv_issue810_running");
    assert.equal(selectedRunning.body.workspace.selectedAgentSessionId, "ses_issue810_running");
    assert.equal(selectedRunning.body.workspace.activeTraceId, "trc_issue810_running");
    assert.equal(selectedRunning.body.workspace.workspace.activeTraceId, "trc_issue810_running");
    assert.equal(selectedRunning.body.workspace.workspace.lastTraceId, "trc_issue810_running");
    assert.equal(selectedRunning.body.workspace.workspace.sessionStatus, "running");
    assert.equal(selectedRunning.body.workspace.workspace.providerProfile, "minimax-m3");

    const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(restored.status, 200);
    assert.equal(restored.body.workspace.selectedConversationId, "cnv_issue810_running");
    assert.equal(restored.body.workspace.selectedAgentSessionId, "ses_issue810_running");
    assert.equal(restored.body.workspace.activeTraceId, "trc_issue810_running");
    assert.equal(restored.body.workspace.workspace.activeTraceId, "trc_issue810_running");
    assert.equal(restored.body.workspace.workspace.lastTraceId, "trc_issue810_running");
    assert.equal(restored.body.workspace.workspace.sessionStatus, "running");
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

test("workbench workspace status repairs terminal selected conversation after active trace was cleared", async () => {
  const agentRunCalls = [];
  const agentSessions = new Map();
  const agentRunServer = createServer(async (request, response) => {
    const url = new URL(request.url || "/", "http://127.0.0.1");
    agentRunCalls.push({ method: request.method, path: url.pathname, search: url.search });
    const send = (data) => {
      response.writeHead(200, { "content-type": "application/json" });
      response.end(`${JSON.stringify({ ok: true, data })}\n`);
    };
    if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_repair/events") {
      return send({ items: [] });
    }
    if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_repair/commands") {
      return send({ items: [
        { id: "cmd_workspace_repair", runId: "run_workspace_repair", state: "completed", type: "turn", seq: 1, idempotencyKey: "trc_issue664_repair_done", payload: { traceId: "trc_issue664_repair_done", conversationId: "cnv_issue664_repair", hwlabSessionId: "ses_issue664_repair", threadId: "thread-issue-664-repair", providerProfile: "deepseek" } }
      ] });
    }
    if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_repair/commands/cmd_workspace_repair/result") {
      return send({
        runId: "run_workspace_repair",
        commandId: "cmd_workspace_repair",
        status: "completed",
        runStatus: "completed",
        commandState: "completed",
        terminalStatus: "completed",
        completed: true,
        reply: "repair completed",
        lastSeq: 2,
        sessionRef: { sessionId: "ses_issue664_repair", conversationId: "cnv_issue664_repair", threadId: "thread-issue-664-repair" }
      });
    }
    response.writeHead(404, { "content-type": "application/json" });
    response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
  });
  await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));

  const agentRunPort = agentRunServer.address().port;
  const accessController = createAccessController({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
    },
    now: () => "2026-06-01T00:00:00.000Z"
  });
  accessController.getAgentSessionByTraceId = async (traceId) => agentSessions.get(traceId) ?? null;
  const server = createCloudApiServer({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
      HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
      AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
      HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
      HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek"
    },
    accessController,
    now: () => "2026-06-01T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
    const alice = await postJson(port, "/v1/admin/users", { username: "alice-ws-repair", password: "alice-pass" }, adminLogin.cookie);
    assert.equal(alice.status, 201);
    const aliceLogin = await postJson(port, "/auth/login", { username: "alice-ws-repair", password: "alice-pass" });
    const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(workspace.status, 200);
    const conversation = await putJson(port, "/v1/agent/conversations/cnv_issue664_repair", {
      projectId: "prj_hwpod_workbench",
      sessionId: "ses_issue664_repair",
      threadId: "thread-issue-664-repair",
      sessionStatus: "running",
      lastTraceId: "trc_issue664_repair_done",
      messages: []
    }, aliceLogin.cookie);
    assert.equal(conversation.status, 200);

    agentSessions.set("trc_issue664_repair_done", {
      id: "ses_issue664_repair",
      ownerUserId: alice.body.user.id,
      conversationId: "cnv_issue664_repair",
      threadId: "thread-issue-664-repair",
      status: "active",
      session: {
        messageId: "msg_workspace_repair_done",
        agentRun: {
          runId: "run_workspace_repair",
          commandId: "cmd_workspace_repair",
          backendProfile: "deepseek",
          managerUrl: `http://127.0.0.1:${agentRunPort}`,
          lastSeq: 0,
          sessionId: "ses_issue664_repair",
          conversationId: "cnv_issue664_repair",
          threadId: "thread-issue-664-repair"
        }
      },
      updatedAt: "2026-06-01T00:00:00.000Z"
    });

    const update = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
      expectedRevision: 1,
      selectedConversationId: "cnv_issue664_repair",
      selectedAgentSessionId: "ses_issue664_repair",
      activeTraceId: null,
      providerProfile: "deepseek",
      sessionStatus: "idle",
      lastTraceId: "trc_issue664_repair_done",
      updatedByClient: "test-suite"
    }, aliceLogin.cookie);
    assert.equal(update.status, 200);
    assert.equal(update.body.workspace.activeTraceId, null);
    assert.equal(update.body.workspace.selectedConversation.status, "idle");
    assert.equal(update.body.workspace.selectedConversation.messages.length, 1);
    assert.equal(update.body.workspace.selectedConversation.messages[0].status, "idle");
    assert.equal(update.body.workspace.selectedConversation.messages[0].text, "repair completed");

    const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(restored.status, 200);
    assert.equal(restored.body.workspace.activeTraceId, null);
    assert.equal(restored.body.workspace.workspace.lastTraceId, "trc_issue664_repair_done");
    assert.equal(restored.body.workspace.workspace.sessionStatus, "idle");
    assert.equal(restored.body.workspace.selectedConversation.status, "idle");
    assert.equal(restored.body.workspace.selectedConversation.lastTraceId, "trc_issue664_repair_done");
    assert.equal(restored.body.workspace.selectedConversation.messages.length, 1);
    assert.equal(restored.body.workspace.selectedConversation.messages[0].status, "idle");
    assert.equal(restored.body.workspace.selectedConversation.messages[0].text, "repair completed");
    assert.ok(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_workspace_repair/commands/cmd_workspace_repair/result"));
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
    await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
  }
});

test("cloud api stores and restores Code Agent conversations by authenticated account", async () => {
  const server = createCloudApiServer({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
    },
    now: () => "2026-05-31T07:30:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
    const adminCookie = adminLogin.cookie;
    const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice", password: "alice-pass" }, adminCookie);
    const bobCreate = await postJson(port, "/v1/admin/users", { username: "bob", password: "bob-pass" }, adminCookie);
    assert.equal(aliceCreate.status, 201);
    assert.equal(bobCreate.status, 201);

    const aliceLogin = await postJson(port, "/auth/login", { username: "alice", password: "alice-pass" });
    const bobLogin = await postJson(port, "/auth/login", { username: "bob", password: "bob-pass" });

    const stored = await putJson(port, "/v1/agent/conversations/cnv_account_sync", {
      projectId: "prj_hwpod_workbench",
      sessionId: "ses_account_sync",
      threadId: "thread-account-sync",
      lastTraceId: "trc_account_sync",
      sessionStatus: "idle",
      messages: [
        { id: "msg_user", role: "user", title: "用户", text: "在吗", status: "sent", conversationId: "cnv_account_sync", sessionId: "ses_account_sync", threadId: "thread-account-sync" },
        { id: "msg_agent", role: "agent", title: "Agent", text: "在", status: "completed", traceId: "trc_account_sync", conversationId: "cnv_account_sync", sessionId: "ses_account_sync", threadId: "thread-account-sync" }
      ]
    }, aliceLogin.cookie);
    assert.equal(stored.status, 200);
    assert.equal(stored.body.conversation.conversationId, "cnv_account_sync");
    assert.equal(stored.body.conversation.sessionId, "ses_account_sync");
    assert.equal(stored.body.conversation.threadId, "thread-account-sync");
    assert.equal(stored.body.conversation.status, "idle");
    assert.equal(stored.body.conversation.messages.length, 2);

    const aliceList = await getJson(port, "/v1/agent/conversations?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(aliceList.status, 200);
    assert.equal(aliceList.body.count, 1);
    assert.equal(aliceList.body.defaultConversation.conversationId, "cnv_account_sync");
    assert.equal(aliceList.body.defaultConversation.messages[1].text, "在");
    assert.equal(aliceList.body.defaultConversation.valuesRedacted, true);

    const bobList = await getJson(port, "/v1/agent/conversations?projectId=prj_hwpod_workbench", bobLogin.cookie);
    assert.equal(bobList.status, 200);
    assert.deepEqual(bobList.body.conversations, []);

    const bobDirect = await getJson(port, "/v1/agent/conversations/cnv_account_sync", bobLogin.cookie);
    assert.equal(bobDirect.status, 404);
    assert.equal(bobDirect.body.error.code, "agent_conversation_not_found");

    const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(workspace.status, 200);
    const selected = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
      projectId: "prj_hwpod_workbench",
      conversationId: "cnv_account_sync",
      updatedByClient: "test-suite"
    }, aliceLogin.cookie);
    assert.equal(selected.status, 200);
    assert.equal(selected.body.workspace.selectedConversationId, "cnv_account_sync");

    const deleted = await deleteJson(port, "/v1/agent/conversations/cnv_account_sync", {
      projectId: "prj_hwpod_workbench",
      workspaceId: workspace.body.workspace.workspaceId,
      updatedByClient: "test-suite"
    }, aliceLogin.cookie);
    assert.equal(deleted.status, 200);
    assert.equal(deleted.body.status, "deleted");
    assert.equal(deleted.body.archivedCount, 1);
    assert.equal(deleted.body.workspace.selectedConversationId, null);

    const afterDeleteList = await getJson(port, "/v1/agent/conversations?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(afterDeleteList.status, 200);
    assert.equal(afterDeleteList.body.count, 0);

    const archivedDirect = await getJson(port, "/v1/agent/conversations/cnv_account_sync", aliceLogin.cookie);
    assert.equal(archivedDirect.status, 200);
    assert.equal(archivedDirect.body.conversation.status, "archived");

    const logout = await postJson(port, "/auth/logout", {}, aliceLogin.cookie);
    assert.equal(logout.status, 200);
    const relogin = await postJson(port, "/auth/login", { username: "alice", password: "alice-pass" });
    const restored = await getJson(port, "/v1/agent/conversations/cnv_account_sync", relogin.cookie);
    assert.equal(restored.status, 200);
    assert.equal(restored.body.conversation.threadId, "thread-account-sync");
    assert.equal(restored.body.conversation.messages[0].text, "在吗");
  } finally {
    await new Promise((resolve, reject) => {
      server.close((error) => (error ? reject(error) : resolve()));
    });
  }
});

test("cloud api exposes terminal conversation status when stored session status is stale running", async () => {
  const server = createCloudApiServer({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
    },
    now: () => "2026-06-04T09:30:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
    const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue834", password: "alice-pass" }, adminLogin.cookie);
    assert.equal(aliceCreate.status, 201);
    const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue834", password: "alice-pass" });

    const stored = await putJson(port, "/v1/agent/conversations/cnv_issue834_final", {
      projectId: "prj_hwpod_workbench",
      sessionId: "ses_issue834_final",
      threadId: "thread-issue-834",
      sessionStatus: "running",
      lastTraceId: "trc_issue834_stale_running",
      messages: [
        { id: "msg_issue834_user", role: "user", title: "用户", text: "final response 显示异常", status: "sent", conversationId: "cnv_issue834_final", sessionId: "ses_issue834_final", threadId: "thread-issue-834" },
        { id: "msg_issue834_agent", role: "agent", title: "Agent", text: "最终回复已经生成。", status: "completed", traceId: "trc_issue834_final", conversationId: "cnv_issue834_final", sessionId: "ses_issue834_final", threadId: "thread-issue-834" }
      ]
    }, aliceLogin.cookie);
    assert.equal(stored.status, 200);
    assert.equal(stored.body.conversation.status, "completed");
    assert.equal(stored.body.conversation.session.status, "completed");
    assert.equal(stored.body.conversation.lastTraceId, "trc_issue834_final");

    const direct = await getJson(port, "/v1/agent/conversations/cnv_issue834_final", aliceLogin.cookie);
    assert.equal(direct.status, 200);
    assert.equal(direct.body.conversation.status, "completed");
    assert.equal(direct.body.conversation.session.status, "completed");
    assert.equal(direct.body.conversation.lastTraceId, "trc_issue834_final");
    assert.equal(direct.body.conversation.messages[1].text, "最终回复已经生成。");

    const list = await getJson(port, "/v1/agent/conversations?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(list.status, 200);
    const listed = list.body.conversations.find((conversation) => conversation.conversationId === "cnv_issue834_final");
    assert.ok(listed, "expected stale-running conversation to be listed");
    assert.equal(listed.status, "completed");
    assert.equal(listed.session.status, "completed");
    assert.equal(listed.lastTraceId, "trc_issue834_final");
  } finally {
    await new Promise((resolve, reject) => {
      server.close((error) => (error ? reject(error) : resolve()));
    });
  }
});

test("cloud api repairs conversation snapshot when persisted result advances last trace", async () => {
  const codeAgentChatResults = new Map();
  const server = createCloudApiServer({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
      HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
      AGENTRUN_MGR_URL: "http://127.0.0.1:9",
      HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
    },
    codeAgentChatResults,
    now: () => "2026-06-04T10:05:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
    const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue842-snapshot", password: "alice-pass" }, adminLogin.cookie);
    assert.equal(aliceCreate.status, 201);
    const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue842-snapshot", password: "alice-pass" });
    const finalText = "持久化验收OK。";
    codeAgentChatResults.set("trc_issue842_persisted_final", {
      status: "completed",
      traceId: "trc_issue842_persisted_final",
      conversationId: "cnv_issue842_snapshot",
      sessionId: "ses_issue842_snapshot",
      threadId: "thread-issue-842-snapshot",
      ownerUserId: aliceCreate.body.user.id,
      reply: { role: "assistant", content: finalText, messageId: "msg_issue842_persisted_final" },
      agentRun: { terminalStatus: "completed" },
      session: {
        sessionId: "ses_issue842_snapshot",
        conversationId: "cnv_issue842_snapshot",
        threadId: "thread-issue-842-snapshot",
        status: "idle"
      }
    });

    const stored = await putJson(port, "/v1/agent/conversations/cnv_issue842_snapshot", {
      projectId: "prj_hwpod_workbench",
      sessionId: "ses_issue842_snapshot",
      threadId: "thread-issue-842-snapshot",
      sessionStatus: "idle",
      lastTraceId: "trc_issue842_persisted_final",
      messages: [
        { id: "msg_issue842_user", role: "user", title: "用户", text: "请短答", status: "sent", conversationId: "cnv_issue842_snapshot", sessionId: "ses_issue842_snapshot", threadId: "thread-issue-842-snapshot" },
        { id: "msg_issue842_old_agent", role: "agent", title: "Agent", text: "上一轮回复", status: "idle", traceId: "trc_issue842_previous", conversationId: "cnv_issue842_snapshot", sessionId: "ses_issue842_snapshot", threadId: "thread-issue-842-snapshot" }
      ]
    }, aliceLogin.cookie);
    assert.equal(stored.status, 200);
    assert.equal(stored.body.conversation.lastTraceId, "trc_issue842_persisted_final");
    assert.equal(stored.body.conversation.messages.at(-1).text, "上一轮回复");

    const list = await getJson(port, "/v1/agent/conversations?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(list.status, 200);
    const listed = list.body.conversations.find((conversation) => conversation.conversationId === "cnv_issue842_snapshot");
    assert.ok(listed, "expected conversation to be listed");
    assert.equal(listed.lastTraceId, "trc_issue842_persisted_final");
    assert.equal(listed.messages.at(-1).traceId, "trc_issue842_persisted_final");
    assert.equal(listed.messages.at(-1).text, finalText);
    assert.equal(listed.messages.at(-1).status, "idle");

    const direct = await getJson(port, "/v1/agent/conversations/cnv_issue842_snapshot", aliceLogin.cookie);
    assert.equal(direct.status, 200);
    assert.equal(direct.body.conversation.messages.at(-1).traceId, "trc_issue842_persisted_final");
    assert.equal(direct.body.conversation.messages.at(-1).text, finalText);

    const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(workspace.status, 200);
    const selected = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
      projectId: "prj_hwpod_workbench",
      conversationId: "cnv_issue842_snapshot",
      sessionId: "ses_issue842_snapshot",
      updatedByClient: "test-suite"
    }, aliceLogin.cookie);
    assert.equal(selected.status, 200);
    assert.equal(selected.body.workspace.selectedConversation.messages.at(-1).traceId, "trc_issue842_persisted_final");
    assert.equal(selected.body.workspace.selectedConversation.messages.at(-1).text, finalText);
  } finally {
    await new Promise((resolve, reject) => {
      server.close((error) => (error ? reject(error) : resolve()));
    });
  }
});

test("cloud api terminal workspace sync preserves saved messages and stores runner trace", async () => {
  const codeAgentChatResults = new Map();
  const traceId = "trc_issue853_fast_fail_trace";
  const conversationId = "cnv_issue853_fast_fail";
  const sessionId = "ses_issue853_fast_fail";
  const threadId = "thread-issue-853-fast-fail";
  const env = {
    HWLAB_ACCESS_CONTROL_REQUIRED: "1",
    HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
    HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
    HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
    AGENTRUN_MGR_URL: "http://127.0.0.1:9",
    HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
  };
  const now = () => "2026-06-04T10:25:00.000Z";
  const accessController = createAccessController({ env, now });
  const server = createCloudApiServer({
    env,
    accessController,
    codeAgentChatResults,
    now
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
    const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue853-fast-fail", password: "alice-pass" }, adminLogin.cookie);
    assert.equal(aliceCreate.status, 201);
    const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue853-fast-fail", password: "alice-pass" });
    const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(workspace.status, 200);

    const savedMessages = [
      { id: "msg_issue853_user", role: "user", title: "用户", text: "请执行 hwpod inspect", status: "sent", conversationId, sessionId, threadId, createdAt: "2026-06-04T10:24:59.000Z" },
      { id: "msg_issue853_pending", role: "agent", title: "Code Agent 处理中", text: "Code Agent 仍在处理，可以继续 steer 或等待 trace 完成。", status: "running", traceId, conversationId, sessionId, threadId, createdAt: "2026-06-04T10:24:59.000Z" }
    ];
    const stored = await putJson(port, `/v1/agent/conversations/${conversationId}`, {
      projectId: "prj_hwpod_workbench",
      sessionId,
      threadId,
      sessionStatus: "active",
      lastTraceId: traceId,
      messages: savedMessages
    }, aliceLogin.cookie);
    assert.equal(stored.status, 200);

    await accessController.recordAgentSessionOwner({
      ownerUserId: aliceCreate.body.user.id,
      sessionId,
      projectId: "prj_hwpod_workbench",
      agentId: "hwlab-code-agent",
      status: "failed",
      conversationId,
      threadId,
      traceId,
      session: {
        source: "code-agent-result",
        status: "failed",
        sessionStatus: "failed",
        messages: [],
        messageCount: 0,
        finalResponse: null,
        valuesRedacted: true,
        secretMaterialStored: false
      }
    });
    const preserved = await accessController.getAgentSession(sessionId);
    assert.equal(preserved.session.messages.length, 2);
    assert.equal(preserved.session.messages[0].text, "请执行 hwpod inspect");

    codeAgentChatResults.set(traceId, {
      status: "failed",
      traceId,
      conversationId,
      sessionId,
      threadId,
      ownerUserId: aliceCreate.body.user.id,
      error: { code: "provider_insufficient_balance", message: "HyueAPI 403 INSUFFICIENT_BALANCE" },
      agentRun: { terminalStatus: "failed" },
      runnerTrace: {
        traceId,
        status: "failed",
        eventCount: 35,
        events: [],
        lastEvent: { label: "agentrun:result:failed", status: "failed", type: "result" }
      },
      session: { sessionId, conversationId, threadId, status: "failed" }
    });
    const selected = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
      expectedRevision: 1,
      selectedConversationId: conversationId,
      selectedAgentSessionId: sessionId,
      activeTraceId: traceId,
      sessionStatus: "running",
      messages: savedMessages,
      updatedByClient: "test-suite"
    }, aliceLogin.cookie);
    assert.equal(selected.status, 200);
    assert.equal(selected.body.workspace.activeTraceId, traceId);

    const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(restored.status, 200);
    assert.equal(restored.body.workspace.activeTraceId, null);
    const messages = restored.body.workspace.selectedConversation.messages;
    assert.equal(messages.length, 2);
    assert.equal(messages[0].role, "user");
    assert.equal(messages[0].text, "请执行 hwpod inspect");
    assert.equal(messages[1].role, "agent");
    assert.equal(messages[1].status, "failed");
    assert.equal(messages[1].text, "HyueAPI 403 INSUFFICIENT_BALANCE");
    assert.equal(messages[1].runnerTrace.traceId, traceId);
    assert.equal(messages[1].runnerTrace.eventCount, 35);
    assert.equal(messages[1].runnerTrace.eventsCompacted, true);
    assert.equal(messages[1].runnerTrace.fullTraceLoaded, false);
    assert.equal(restored.body.workspace.selectedConversation.messageCount, 2);
    assert.equal(restored.body.workspace.selectedConversation.firstUserMessagePreview, "请执行 hwpod inspect");
  } finally {
    await new Promise((resolve, reject) => {
      server.close((error) => (error ? reject(error) : resolve()));
    });
  }
});

test("cloud api terminal workspace sync rebuilds missing user message from saved preview", async () => {
  const codeAgentChatResults = new Map();
  const traceId = "trc_issue853_preview_only_trace";
  const conversationId = "cnv_issue853_preview_only";
  const sessionId = "ses_issue853_preview_only";
  const threadId = "thread-issue-853-preview-only";
  const userText = "请执行 hwpod inspect，并在最终回复里原样包含 HWLAB-853-FINAL-preview-only";
  const env = {
    HWLAB_ACCESS_CONTROL_REQUIRED: "1",
    HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
    HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
    HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
    AGENTRUN_MGR_URL: "http://127.0.0.1:9",
    HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
  };
  const now = () => "2026-06-04T10:35:00.000Z";
  const accessController = createAccessController({ env, now });
  const server = createCloudApiServer({ env, accessController, codeAgentChatResults, now });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
    const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue853-preview-only", password: "alice-pass" }, adminLogin.cookie);
    assert.equal(aliceCreate.status, 201);
    const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue853-preview-only", password: "alice-pass" });
    const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(workspace.status, 200);

    await accessController.recordAgentSessionOwner({
      ownerUserId: aliceCreate.body.user.id,
      sessionId,
      projectId: "prj_hwpod_workbench",
      agentId: "hwlab-code-agent",
      status: "failed",
      conversationId,
      threadId,
      traceId,
      session: {
        source: "workbench-status-sync",
        sessionStatus: "failed",
        lastTraceId: traceId,
        messages: [
          { id: "msg_issue853_preview_agent", role: "agent", title: "Code Agent result", text: "HyueAPI 403 INSUFFICIENT_BALANCE", status: "failed", traceId, conversationId, sessionId, threadId, createdAt: "2026-06-04T10:34:59.000Z" }
        ],
        messageCount: 1,
        firstUserMessagePreview: userText,
        valuesRedacted: true,
        secretMaterialStored: false
      }
    });

    codeAgentChatResults.set(traceId, {
      status: "failed",
      traceId,
      conversationId,
      sessionId,
      threadId,
      ownerUserId: aliceCreate.body.user.id,
      error: { code: "provider_insufficient_balance", message: "HyueAPI 403 INSUFFICIENT_BALANCE" },
      agentRun: { terminalStatus: "failed" },
      runnerTrace: {
        traceId,
        status: "failed",
        eventCount: 35,
        events: [],
        lastEvent: { label: "agentrun:result:failed", status: "failed", type: "result" }
      },
      session: { sessionId, conversationId, threadId, status: "failed" }
    });

    const selected = await patchJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}`, {
      expectedRevision: 1,
      selectedConversationId: conversationId,
      selectedAgentSessionId: sessionId,
      activeTraceId: traceId,
      sessionStatus: "running",
      messages: [],
      updatedByClient: "test-suite"
    }, aliceLogin.cookie);
    assert.equal(selected.status, 200);

    const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
    assert.equal(restored.status, 200);
    assert.equal(restored.body.workspace.activeTraceId, null);
    const messages = restored.body.workspace.selectedConversation.messages;
    assert.equal(messages.length, 2);
    assert.equal(messages[0].role, "user");
    assert.equal(messages[0].text, userText);
    assert.equal(messages[0].traceId, traceId);
    assert.equal(messages[1].role, "agent");
    assert.equal(messages[1].status, "failed");
    assert.equal(messages[1].text, "HyueAPI 403 INSUFFICIENT_BALANCE");
    assert.equal(messages[1].runnerTrace.traceId, traceId);
    assert.equal(messages[1].runnerTrace.eventCount, 35);
    assert.equal(restored.body.workspace.selectedConversation.messageCount, 2);
    assert.equal(restored.body.workspace.selectedConversation.firstUserMessagePreview, userText);
  } finally {
    await new Promise((resolve, reject) => {
      server.close((error) => (error ? reject(error) : resolve()));
    });
  }
});

test("access controller preserves user message when owner evidence updates with agent-only result", async () => {
  const projectId = "prj_hwpod_workbench";
  const ownerUserId = "usr_issue853_owner_merge";
  const conversationId = "cnv_issue853_owner_merge";
  const sessionId = "ses_issue853_owner_merge";
  const traceId = "trc_issue853_owner_merge";
  const threadId = "thread-issue-853-owner-merge";
  const nowValue = "2026-06-04T10:45:00.000Z";
  const userText = "请执行 hwpod inspect，并在最终回复里原样包含 HWLAB-853-FINAL-owner-merge";
  const accessController = createAccessController({ now: () => nowValue });

  await accessController.recordAgentSessionOwner({
    ownerUserId,
    sessionId,
    projectId,
    agentId: "hwlab-code-agent",
    status: "active",
    conversationId,
    threadId,
    traceId,
    session: {
      source: "workbench-status-sync",
      sessionStatus: "running",
      lastTraceId: traceId,
      messages: [
        { id: "msg_issue853_owner_user", role: "user", title: "用户", text: userText, status: "sent", traceId, conversationId, sessionId, threadId, createdAt: nowValue },
        { id: "msg_issue853_owner_agent_pending", role: "agent", title: "Code Agent 处理中", text: "Code Agent 仍在处理，可以继续 steer 或等待 trace 完成。", status: "running", traceId, conversationId, sessionId, threadId, createdAt: nowValue }
      ],
      messageCount: 2,
      firstUserMessagePreview: userText,
      valuesRedacted: true,
      secretMaterialStored: false
    }
  });

  await accessController.recordAgentSessionOwner({
    ownerUserId,
    sessionId,
    projectId,
    agentId: "hwlab-code-agent",
    status: "failed",
    conversationId,
    threadId,
    traceId,
    session: {
      source: "code-agent-result",
      status: "failed",
      sessionStatus: "failed",
      messages: [
        { id: "msg_issue853_owner_agent_result", role: "agent", title: "Code Agent result", text: "HyueAPI 403 INSUFFICIENT_BALANCE", status: "failed", traceId, conversationId, sessionId, threadId, createdAt: nowValue }
      ],
      messageCount: 1,
      firstUserMessagePreview: userText,
      valuesRedacted: true,
      secretMaterialStored: false
    }
  });

  const session = await accessController.getAgentSession(sessionId);
  const conversation = await accessController.visibleConversationForActor({ id: ownerUserId, role: "user" }, conversationId, projectId, { includeArchived: true });
  assert.deepEqual(session.session.messages.map((message) => message.role), ["user", "agent"]);
  assert.deepEqual(conversation.messages.map((message) => message.role), ["user", "agent"]);
  assert.equal(session.session.messages[0].text, userText);
  assert.equal(conversation.messages[0].text, userText);
  assert.equal(session.session.messages[0].traceId, traceId);
  assert.equal(conversation.messages[0].traceId, traceId);
  assert.equal(session.session.messages[1].text, "HyueAPI 403 INSUFFICIENT_BALANCE");
  assert.equal(conversation.messages[1].status, "failed");
  assert.equal(session.session.messageCount, 2);
  assert.equal(conversation.messageCount, 2);
  assert.equal(conversation.firstUserMessagePreview, userText);
});

test("access controller dedupes duplicate refreshed assistant messages by id and trace", async () => {
  const projectId = "prj_hwpod_workbench";
  const ownerUserId = "usr_issue932_dedupe";
  const conversationId = "cnv_issue932_dedupe";
  const sessionId = "ses_issue932_dedupe";
  const traceId = "trc_issue932_dedupe";
  const threadId = "thread-issue-932-dedupe";
  const nowValue = "2026-06-05T09:30:00.000Z";
  const accessController = createAccessController({ now: () => nowValue });

  await accessController.recordAgentSessionOwner({
    ownerUserId,
    sessionId,
    projectId,
    agentId: "hwlab-code-agent",
    status: "idle",
    conversationId,
    threadId,
    traceId,
    session: {
      source: "cloud-web-react-account-sync",
      sessionStatus: "completed",
      lastTraceId: traceId,
      messages: [
        { id: "msg_issue932_user", role: "user", title: "用户", text: "你好", status: "sent", traceId, conversationId, sessionId, threadId, createdAt: nowValue },
        { id: "msg_issue932_agent", role: "agent", title: "Code Agent 回复", text: "最终回复", status: "completed", traceId, conversationId, sessionId, threadId, createdAt: nowValue },
        { id: "msg_issue932_agent", role: "agent", title: "Code Agent 回复", text: "最终回复", status: "completed", traceId, conversationId, sessionId, threadId, createdAt: nowValue }
      ],
      messageCount: 3,
      firstUserMessagePreview: "你好",
      valuesRedacted: true,
      secretMaterialStored: false
    }
  });

  const session = await accessController.getAgentSession(sessionId);
  const conversation = await accessController.visibleConversationForActor({ id: ownerUserId, role: "user" }, conversationId, projectId, { includeArchived: true });

  assert.deepEqual(session.session.messages.map((message) => message.role), ["user", "agent"]);
  assert.deepEqual(conversation.messages.map((message) => message.role), ["user", "agent"]);
  assert.equal(session.session.messageCount, 2);
  assert.equal(conversation.messageCount, 2);
  assert.equal(conversation.messages[1].text, "最终回复");
});

test("access controller restores AgentRun mapping by Code Agent traceId", async () => {
  const accessController = createAccessController({ now: () => "2026-06-01T00:00:00.000Z" });
  await accessController.recordAgentSessionOwner({
    ownerUserId: "usr_agent_owner",
    sessionId: "ses_agentrun_trace_lookup",
    projectId: "prj_v02_code_agent",
    conversationId: "cnv_agentrun_trace_lookup",
    threadId: "thread-agentrun-trace-lookup",
    traceId: "trc_agentrun_trace_lookup",
    status: "running",
    session: {
      source: "hwlab-cloud-api-agentrun-v01-adapter",
      agentRun: {
        adapter: "agentrun-v01",
        runId: "run_trace_lookup",
        commandId: "cmd_trace_lookup",
        jobName: "agentrun-v01-runner-trace-lookup",
        namespace: "agentrun-v01",
        backendProfile: "deepseek",
        valuesPrinted: false
      },
      valuesRedacted: true
    }
  });

  const restored = await accessController.getAgentSessionByTraceId("trc_agentrun_trace_lookup");
  assert.equal(restored.id, "ses_agentrun_trace_lookup");
  assert.equal(restored.lastTraceId, "trc_agentrun_trace_lookup");
  assert.equal(restored.session.agentRun.runId, "run_trace_lookup");
  assert.equal(restored.session.agentRun.commandId, "cmd_trace_lookup");
  assert.equal(restored.session.agentRun.namespace, "agentrun-v01");
});

test("access controller preserves AgentRun runner mapping across conversation snapshot updates", async () => {
  const accessController = createAccessController({ now: () => "2026-06-01T00:00:00.000Z" });
  await accessController.recordAgentSessionOwner({
    ownerUserId: "usr_agent_owner",
    sessionId: "ses_agentrun_reuse_mapping",
    projectId: "prj_v02_code_agent",
    conversationId: "cnv_agentrun_reuse_mapping",
    threadId: "thread-agentrun-requested",
    traceId: "trc_agentrun_reuse_first",
    status: "running",
    session: {
      source: "hwlab-cloud-api-agentrun-v01-adapter",
      agentRun: {
        adapter: "agentrun-v01",
        runId: "run_reuse_mapping",
        commandId: "cmd_reuse_mapping_first",
        runnerId: "runner_reuse_mapping",
        jobName: "agentrun-v01-runner-reuse-mapping",
        namespace: "agentrun-v01",
        backendProfile: "deepseek",
        reuseEligible: true,
        valuesPrinted: false
      },
      valuesRedacted: true
    }
  });

  await accessController.recordAgentSessionOwner({
    ownerUserId: "usr_agent_owner",
    sessionId: "ses_agentrun_reuse_mapping",
    projectId: "prj_v02_code_agent",
    conversationId: "cnv_agentrun_reuse_mapping",
    threadId: "thread-agentrun-requested",
    traceId: "trc_agentrun_reuse_second",
    status: "active",
    session: {
      source: "workbench-conversation-snapshot",
      messages: [{ role: "assistant", content: "done" }],
      valuesRedacted: true
    }
  });

  const restored = await accessController.getAgentSession("ses_agentrun_reuse_mapping");
  assert.equal(restored.lastTraceId, "trc_agentrun_reuse_second");
  assert.equal(restored.session.source, "workbench-conversation-snapshot");
  assert.equal(restored.session.agentRun.runId, "run_reuse_mapping");
  assert.equal(restored.session.agentRun.commandId, "cmd_reuse_mapping_first");
  assert.equal(restored.session.agentRun.jobName, "agentrun-v01-runner-reuse-mapping");
  assert.equal(restored.session.agentRun.reuseEligible, true);
});

test("cloud api accepts user API keys as Bearer auth for the same account", async () => {
  const accessController = createAccessController({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_ID: "usr_v02_admin",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
    },
    now: () => "2026-05-28T00:00:00.000Z"
  });
  const server = createCloudApiServer({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_ID: "usr_v02_admin",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
    },
    accessController,
    now: () => "2026-05-28T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
    assert.equal(adminLogin.status, 200);

    const createdKey = await postJson(port, "/v1/api-keys", { name: "Code Agent runner" }, adminLogin.cookie);
    assert.equal(createdKey.status, 201);
    const apiKey = createdKey.body.key.displaySecret;
    assert.ok(apiKey.startsWith("hwl_live_"));

    const me = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${apiKey}` });
    assert.equal(me.status, 200);
    assert.equal(me.body.authMethod, "api-key");
    assert.equal(me.body.actor.id, "usr_v02_admin");
    assert.equal(me.body.actor.role, "admin");
    assert.equal(JSON.stringify(me.body).includes(apiKey), false);
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

test("cloud api bootstraps the master server admin API key from env", async () => {
  const apiKey = "hwl_live_master_server_admin_test_key_930";
  const openFgaAuthorizer = createFakeOpenFgaAuthorizer();
  const accessController = createAccessController({
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_ID: "usr_v02_admin",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
      HWLAB_BOOTSTRAP_ADMIN_API_KEY: apiKey,
      HWLAB_BOOTSTRAP_ADMIN_API_KEY_ID: "key_master_server_admin"
    },
    openFgaAuthorizer,
    now: () => "2026-06-05T00:00:00.000Z"
  });
  const server = createCloudApiServer({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
    accessController,
    now: () => "2026-06-05T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const me = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${apiKey}` });
    assert.equal(me.status, 200);
    assert.equal(me.body.authMethod, "api-key");
    assert.equal(me.body.actor.id, "usr_v02_admin");
    assert.equal(me.body.actor.username, "admin");
    assert.equal(me.body.actor.role, "admin");
    assert.equal(JSON.stringify(me.body).includes(apiKey), false);

    const summary = await getJson(port, "/v1/admin/access/summary", null, { authorization: `Bearer ${apiKey}` });
    assert.equal(summary.status, 200);
    assert.equal(summary.body.actor.id, "usr_v02_admin");
    assert.equal(summary.body.supported.toolIds.includes("hwpod"), true);

    for (const toolId of summary.body.supported.toolIds) {
      const check = await postJson(port, "/v1/admin/access/check", { userId: "usr_v02_admin", relation: "can_use", object: `tool:${toolId}` }, null, { authorization: `Bearer ${apiKey}` });
      assert.equal(check.status, 200);
      assert.equal(check.body.authorization.allowed, true, `expected admin key to access ${toolId}`);
    }

    const keys = await accessController.store.listApiKeysForUser("usr_v02_admin");
    const masterKey = keys.find((key) => key.id === "key_master_server_admin");
    assert.ok(masterKey);
    assert.equal(masterKey.name, "Master server admin API key");
    assert.equal(masterKey.displaySecret, null);
    assert.deepEqual(masterKey.scopes, ["admin", "system:hwlab", "tool:*"]);
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

test("cloud api exposes v1 access status routes and returns structured REST errors", async () => {
  const server = createCloudApiServer({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
    now: () => "2026-05-28T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const session = await getJson(port, "/v1/auth/session");
    assert.equal(session.status, 200);
    assert.equal(session.body.authenticated, false);
    assert.equal(session.body.setupRequired, true);
    assert.equal(session.body.contractVersion, "user-access-v1");

    const access = await getJson(port, "/v1/access/status");
    assert.equal(access.status, 200);
    assert.equal(access.body.accessControlRequired, true);
    assert.equal(access.body.routes.currentUser, "/v1/users/me");

    const setup = await getJson(port, "/v1/setup/status");
    assert.equal(setup.status, 200);
    assert.equal(setup.body.setupRequired, true);
    assert.equal(setup.body.bootstrapAdminConfigured, false);

    const me = await getJson(port, "/v1/users/me");
    assert.equal(me.status, 401);
    assert.equal(me.body.error.code, "auth_required");

    const missing = await getJson(port, "/v1/not-implemented");
    assert.equal(missing.status, 404);
    assert.equal(missing.body.error.code, "not_found");
    assert.equal(missing.body.error.audit.source.serviceId, "hwlab-cloud-api");
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

test("cloud api first-admin setup opens access when bootstrap secret is absent", async () => {
  const server = createCloudApiServer({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
    now: () => "2026-05-28T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const before = await getJson(port, "/v1/setup/status");
    assert.equal(before.status, 200);
    assert.equal(before.body.setupRequired, true);
    assert.equal(before.body.bootstrapAdminConfigured, false);
    assert.equal(before.body.routes.firstAdminSetup, "/v1/setup/first-admin");

    const setup = await postJson(port, "/v1/setup/first-admin", {
      username: "admin",
      password: "admin-pass",
      displayName: "Initial Admin"
    });
    assert.equal(setup.status, 201);
    assert.equal(setup.body.created, true);
    assert.equal(setup.body.authenticated, true);
    assert.equal(setup.body.actor.role, "admin");
    assert.equal(setup.body.actor.username, "admin");
    assert.equal(setup.body.setupRequired, false);
    assert.equal(JSON.stringify(setup.body).includes("admin-pass"), false);
    assert.equal(typeof setup.cookie, "string");

    const session = await getJson(port, "/v1/users/me", setup.cookie);
    assert.equal(session.status, 200);
    assert.equal(session.body.actor.role, "admin");
    assert.equal(JSON.stringify(session.body).includes("passwordHash"), false);

    const second = await postJson(port, "/v1/setup/first-admin", {
      username: "other-admin",
      password: "other-pass"
    });
    assert.equal(second.status, 409);
    assert.equal(second.body.error.code, "setup_already_completed");

    const login = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
    assert.equal(login.status, 200);
    assert.equal(login.body.actor.role, "admin");
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

test("cloud api bootstrap password synchronizes existing admin", async () => {
  const now = () => "2026-05-28T00:00:00.000Z";
  const firstAccessController = createAccessController({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
    now
  });
  const firstServer = createCloudApiServer({
    accessController: firstAccessController,
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
    now
  });
  await new Promise((resolve) => firstServer.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = firstServer.address();
    const setup = await postJson(port, "/v1/setup/first-admin", {
      username: "admin",
      password: "old-pass"
    });
    assert.equal(setup.status, 201);

    const downgraded = await patchJson(port, "/v1/admin/access/users/usr_bootstrap_admin", { role: "user", status: "active" }, setup.cookie);
    assert.equal(downgraded.status, 200);
    assert.equal(downgraded.body.user.role, "user");
  } finally {
    await new Promise((resolve, reject) => firstServer.close((error) => (error ? reject(error) : resolve())));
  }

  const syncAccessController = createAccessController({
    store: firstAccessController.store,
    env: {
      HWLAB_ACCESS_CONTROL_REQUIRED: "1",
      HWLAB_BOOTSTRAP_ADMIN_ID: "usr_v02_admin",
      HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
      HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "rotated-pass"
    },
    now
  });
  const server = createCloudApiServer({
    accessController: syncAccessController,
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
    now
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const oldLogin = await postJson(port, "/auth/login", { username: "admin", password: "old-pass" });
    assert.equal(oldLogin.status, 401);

    const rotatedLogin = await postJson(port, "/auth/login", { username: "admin", password: "rotated-pass" });
    assert.equal(rotatedLogin.status, 200);
    assert.equal(rotatedLogin.body.actor.username, "admin");
    assert.equal(rotatedLogin.body.actor.role, "admin");
    assert.equal(JSON.stringify(rotatedLogin.body).includes("rotated-pass"), false);

    const summary = await getJson(port, "/v1/admin/access/summary", rotatedLogin.cookie);
    assert.equal(summary.status, 200);
    assert.equal(summary.body.actor.username, "admin");
    assert.equal(summary.body.actor.role, "admin");
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

test("cloud api access matrix shows admin inherited tool authority", async () => {
  const env = {
    HWLAB_ACCESS_CONTROL_REQUIRED: "1",
    HWLAB_BOOTSTRAP_ADMIN_ID: "usr_v02_admin",
    HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
    HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
    HWLAB_OPENFGA_MODE: "enforce",
    HWLAB_OPENFGA_API_URL: "http://openfga.test"
  };
  const tuples = new Set();
  const accessController = createAccessController({
    env,
    fetchImpl: async (url, init = {}) => {
      const pathname = new URL(String(url)).pathname;
      if (pathname === "/healthz") return new Response("ok", { status: 200 });
      if (pathname === "/stores") return new Response(JSON.stringify({ id: "store_admin_tools" }), { status: 200 });
      if (pathname.endsWith("/authorization-models")) return new Response(JSON.stringify({ authorization_model_id: "model_admin_tools" }), { status: 200 });
      if (pathname.endsWith("/write")) {
        const body = JSON.parse(String(init.body ?? "{}"));
        for (const tuple of body.writes?.tuple_keys ?? []) tuples.add(`${tuple.user}|${tuple.relation}|${tuple.object}`);
        for (const tuple of body.deletes?.tuple_keys ?? []) tuples.delete(`${tuple.user}|${tuple.relation}|${tuple.object}`);
        return new Response(JSON.stringify({ ok: true }), { status: 200 });
      }
      if (pathname.endsWith("/check")) {
        const body = JSON.parse(String(init.body ?? "{}"));
        const tuple = body.tuple_key ?? {};
        return new Response(JSON.stringify({ allowed: tuples.has(`${tuple.user}|${tuple.relation}|${tuple.object}`) }), { status: 200 });
      }
      return new Response(JSON.stringify({ error: "unexpected route" }), { status: 404 });
    },
    now: () => "2026-05-28T00:00:00.000Z"
  });
  const server = createCloudApiServer({ env, accessController, now: () => "2026-05-28T00:00:00.000Z" });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));

  try {
    const { port } = server.address();
    const login = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
    assert.equal(login.status, 200);

    const users = await getJson(port, "/v1/admin/access/users", login.cookie);
    assert.equal(users.status, 200);
    const admin = users.body.users.find((item) => item.user.id === "usr_v02_admin");
    assert.equal(admin.user.role, "admin");
    assert.deepEqual(admin.tools, { hwpod: true, unidesk_ssh: true, trans_cmd: true, github_pr: true });

    const matrix = await getJson(port, "/v1/admin/access/users/usr_v02_admin", login.cookie);
    assert.equal(matrix.status, 200);
    assert.deepEqual(matrix.body.tools, { hwpod: true, unidesk_ssh: true, trans_cmd: true, github_pr: true });
    assert.deepEqual(matrix.body.tuples.map((tuple) => `${tuple.relation}:${tuple.object}`), ["admin:system:hwlab"]);
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

async function postJson(port, path, body, cookie = null, extraHeaders = {}) {
  const response = await fetch(`http://127.0.0.1:${port}${path}`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      ...(cookie ? { cookie } : {}),
      ...extraHeaders
    },
    body: JSON.stringify(body)
  });
  return {
    status: response.status,
    cookie: response.headers.get("set-cookie"),
    body: await response.json()
  };
}

async function putJson(port, path, body, cookie = null, extraHeaders = {}) {
  const response = await fetch(`http://127.0.0.1:${port}${path}`, {
    method: "PUT",
    headers: {
      "content-type": "application/json",
      ...(cookie ? { cookie } : {}),
      ...extraHeaders
    },
    body: JSON.stringify(body)
  });
  return {
    status: response.status,
    body: await response.json(),
    cookie: response.headers.get("set-cookie")
  };
}

async function patchJson(port, path, body, cookie = null, extraHeaders = {}) {
  const response = await fetch(`http://127.0.0.1:${port}${path}`, {
    method: "PATCH",
    headers: {
      "content-type": "application/json",
      ...(cookie ? { cookie } : {}),
      ...extraHeaders
    },
    body: JSON.stringify(body)
  });
  return {
    status: response.status,
    body: await response.json(),
    cookie: response.headers.get("set-cookie")
  };
}

async function getJson(port, path, cookie = null, extraHeaders = {}) {
  const response = await fetch(`http://127.0.0.1:${port}${path}`, {
    headers: {
      ...(cookie ? { cookie } : {}),
      ...extraHeaders
    }
  });
  return {
    status: response.status,
    body: await response.json()
  };
}

async function deleteJson(port, path, body = {}, cookie = null, extraHeaders = {}) {
  const response = await fetch(`http://127.0.0.1:${port}${path}`, {
    method: "DELETE",
    headers: {
      "content-type": "application/json",
      ...(cookie ? { cookie } : {}),
      ...extraHeaders
    },
    body: JSON.stringify(body)
  });
  return {
    status: response.status,
    body: await response.json(),
    cookie: response.headers.get("set-cookie")
  };
}

function createFakeOpenFgaAuthorizer({ mode = "enforce" } = {}) {
  const tuples = new Set();
  function key({ userId, relation, object }) { return `user:${userId}#${relation}@${object}`; }
  return {
    mode,
    tuples,
    async describe() {
      return { contractVersion: "openfga-authorization-v1", mode, configured: true, ready: true, storeId: "fake-store", modelId: "fake-model", tupleCount: tuples.size };
    },
    async check({ actor, userId, relation, object }) {
      const subject = actor?.id ?? userId;
      const allowed = tuples.has(key({ userId: subject, relation, object })) || tuples.has(key({ userId: subject, relation: "admin", object: "system:hwlab" }));
      return { contractVersion: "openfga-authorization-v1", mode, user: `user:${subject}`, relation, object, allowed, decisionSource: "fake-openfga", degraded: false };
    },
    async writeTuple({ userId, relation, object }) {
      tuples.add(key({ userId, relation, object }));
      return { ok: true, mode, written: 1, deleted: 0 };
    },
    async deleteTuple({ userId, relation, object }) {
      tuples.delete(key({ userId, relation, object }));
      return { ok: true, mode, written: 0, deleted: 1 };
    }
  };
}

async function requestJson(request) {
  const chunks = [];
  for await (const chunk of request) chunks.push(chunk);
  const text = Buffer.concat(chunks).toString("utf8").trim();
  return text ? JSON.parse(text) : {};
}

async function waitForCondition(predicate, timeoutMs = 500) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (predicate()) return;
    await new Promise((resolve) => setTimeout(resolve, 10));
  }
}

test("cloud api issues a default user API key and authenticates Bearer hwl_live_ tokens", async () => {
  const server = createCloudApiServer({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
    now: () => "2026-06-03T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
  try {
    const { port } = server.address();
    const firstAdmin = await postJson(port, "/v1/setup/first-admin", { username: "apikey-admin", password: "apikey-pass" });
    assert.equal(firstAdmin.status, 201);
    assert.equal(firstAdmin.body.actor.role, "admin");
    const cookie = firstAdmin.cookie;
    const defaultKey = await getJson(port, "/v1/api-keys/default", cookie);
    assert.equal(defaultKey.status, 200);
    assert.equal(defaultKey.body.created, false);
    assert.equal(defaultKey.body.key.displaySecret, undefined);
    const createdKey = await postJson(port, "/v1/api-keys", { name: "CLI test key" }, cookie);
    assert.equal(createdKey.status, 201);
    const apiKey = createdKey.body.key;
    assert.equal(apiKey.name, "CLI test key");
    assert.match(apiKey.keyPrefix, /^hwl_live_/u);
    assert.ok(apiKey.displaySecret, "created key should expose display secret once");
    assert.equal(apiKey.status, "active");
    const listed = await getJson(port, "/v1/api-keys", cookie);
    assert.equal(listed.status, 200);
    assert.equal(listed.body.contractVersion, "user-api-key-v1");
    assert.equal(listed.body.keys.length, 2);
    assert.equal(listed.body.keys.find((key) => key.id === apiKey.id)?.displaySecret, undefined);
    const me = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${apiKey.displaySecret}` });
    assert.equal(me.status, 200);
    assert.equal(me.body.actor.id, firstAdmin.body.actor.id);
    assert.equal(me.body.actor.username, "apikey-admin");
    assert.equal(me.body.actor.role, "admin");
    assert.equal(me.body.authMethod, "api-key");
    assert.equal(JSON.stringify(me.body).includes(apiKey.displaySecret), false);
    const bad = await getJson(port, "/v1/users/me", null, { authorization: "Bearer hwl_live_not-a-real-key" });
    assert.equal(bad.status, 401);
    assert.equal(bad.body.error.code, "api_key_invalid");
    const wrongScheme = await getJson(port, "/v1/users/me", null, { authorization: `Basic ${apiKey.displaySecret}` });
    assert.equal(wrongScheme.status, 401);
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

test("cloud api can list, create, regenerate, and revoke a user API key", async () => {
  const server = createCloudApiServer({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
    now: () => "2026-06-03T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
  try {
    const { port } = server.address();
    const setup = await postJson(port, "/v1/setup/first-admin", { username: "rotate-admin", password: "rotate-pass" });
    assert.equal(setup.status, 201);
    const cookie = setup.cookie;
    const originalKey = await postJson(port, "/v1/api-keys", { name: "Original CLI key" }, cookie);
    assert.equal(originalKey.status, 201);
    const original = originalKey.body.key.displaySecret;
    const created = await postJson(port, "/v1/api-keys", { name: "AgentRun runner" }, cookie);
    assert.equal(created.status, 201);
    assert.equal(created.body.key.name, "AgentRun runner");
    const createdSecret = created.body.key.displaySecret;
    const listAfterCreate = await getJson(port, "/v1/api-keys", cookie);
    assert.equal(listAfterCreate.body.keys.length, 3);
    assert.equal(listAfterCreate.body.keys.some((key) => key.displaySecret), false);
    const regenerate = await postJson(port, `/v1/api-keys/${created.body.key.id}/regenerate`, {}, cookie);
    assert.equal(regenerate.status, 200);
    assert.notEqual(regenerate.body.key.displaySecret, createdSecret);
    const usedOld = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${createdSecret}` });
    assert.equal(usedOld.status, 401);
    const usedNew = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${regenerate.body.key.displaySecret}` });
    assert.equal(usedNew.status, 200);
    const revoke = await deleteJson(port, `/v1/api-keys/${regenerate.body.key.id}`, {}, cookie);
    assert.equal(revoke.status, 200);
    const afterRevoke = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${regenerate.body.key.displaySecret}` });
    assert.equal(afterRevoke.status, 401);
    const originalStillWorks = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${original}` });
    assert.equal(originalStillWorks.status, 200);
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

test("cloud api rejects API key auth for a disabled user", async () => {
  const server = createCloudApiServer({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
    now: () => "2026-06-03T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
  try {
    const { port } = server.address();
    const adminSetup = await postJson(port, "/v1/setup/first-admin", { username: "root", password: "root-pass" });
    assert.equal(adminSetup.status, 201);
    const adminCookie = adminSetup.cookie;
    await postJson(port, "/v1/admin/users", { username: "soon-disabled", password: "pw", role: "user", status: "active" }, adminCookie);
    const userLogin = await postJson(port, "/auth/login", { username: "soon-disabled", password: "pw" });
    assert.equal(userLogin.status, 200);
    const userCookie = userLogin.cookie;
    const issued = await postJson(port, "/v1/api-keys", { name: "Disabled user CLI key" }, userCookie);
    const apiKey = issued.body.key.displaySecret;
    const ok = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${apiKey}` });
    assert.equal(ok.status, 200);
    const adminIssue = await postJson(port, "/v1/admin/users/soon-disabled/disable", {}, adminCookie);
    assert.ok([200, 204, 404].includes(adminIssue.status), `disable endpoint should be wired; got ${adminIssue.status}`);
    const blocked = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${apiKey}` });
    if (adminIssue.status === 404) {
      assert.equal(blocked.status, 200);
    } else {
      assert.equal(blocked.status, 401);
    }
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

test("cloud api issues a 24h Web session with Secure/SameSite cookie for HTTPS forwarded requests", async () => {
  const server = createCloudApiServer({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
    now: () => "2026-06-03T12:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
  try {
    const { port } = server.address();
    const setup = await postJson(port, "/v1/setup/first-admin", { username: "session-admin", password: "session-pass" }, null, { "x-forwarded-proto": "https" });
    assert.equal(setup.status, 201);
    assert.ok(setup.cookie, "first admin should set a session cookie");
    assert.match(setup.cookie, /HttpOnly/u);
    assert.match(setup.cookie, /(?:^|;\s*)Secure(?:;|$)/u);
    assert.match(setup.cookie, /SameSite=Lax/u);
    assert.match(setup.cookie, /Max-Age=86400/u);
    assert.equal(setup.body.session.expiresAt, "2026-06-04T12:00:00.000Z");

    const login = await postJson(port, "/auth/login", { username: "session-admin", password: "session-pass" }, null, { "x-forwarded-proto": "https" });
    assert.equal(login.status, 200);
    assert.match(login.cookie, /(?:^|;\s*)Secure(?:;|$)/u);
    assert.match(login.cookie, /Max-Age=86400/u);
    assert.equal(login.body.session.expiresAt, "2026-06-04T12:00:00.000Z");

    const session = await getJson(port, "/v1/auth/session", login.cookie);
    assert.equal(session.status, 200);
    assert.equal(session.body.authenticated, true);
    assert.equal(session.body.authMethod, "web-session");
    assert.equal(session.body.sessionExpiresAt, "2026-06-04T12:00:00.000Z");
    assert.equal(session.body.sessionTtlSeconds, 86400);

    const logout = await postJson(port, "/auth/logout", {}, login.cookie, { "x-forwarded-proto": "https" });
    assert.equal(logout.status, 200);
    assert.match(logout.cookie, /(?:^|;\s*)Secure(?:;|$)/u);
    assert.match(logout.cookie, /Max-Age=0/u);
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

test("cloud api keeps HTTP Web session cookies browser-storable for the v0.2 public entry", async () => {
  const server = createCloudApiServer({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_PUBLIC_ENDPOINT },
    now: () => "2026-06-03T12:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
  try {
    const { port } = server.address();
    const setup = await postJson(port, "/v1/setup/first-admin", { username: "http-session-admin", password: "session-pass" }, null, { host: "74.48.78.17:19666" });
    assert.equal(setup.status, 201);
    assert.ok(setup.cookie, "first admin should set a session cookie");
    assert.match(setup.cookie, /HttpOnly/u);
    assert.doesNotMatch(setup.cookie, /(?:^|;\s*)Secure(?:;|$)/u);
    assert.match(setup.cookie, /SameSite=Lax/u);
    assert.match(setup.cookie, /Max-Age=86400/u);

    const login = await postJson(port, "/auth/login", { username: "http-session-admin", password: "session-pass" }, null, { host: "74.48.78.17:19666" });
    assert.equal(login.status, 200);
    assert.doesNotMatch(login.cookie, /(?:^|;\s*)Secure(?:;|$)/u);

    const session = await getJson(port, "/v1/auth/session", login.cookie, { host: "74.48.78.17:19666" });
    assert.equal(session.status, 200);
    assert.equal(session.body.authenticated, true);
    assert.equal(session.body.authMethod, "web-session");

    const logout = await postJson(port, "/auth/logout", {}, login.cookie, { host: "74.48.78.17:19666" });
    assert.equal(logout.status, 200);
    assert.doesNotMatch(logout.cookie, /(?:^|;\s*)Secure(?:;|$)/u);
    assert.match(logout.cookie, /Max-Age=0/u);
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

test("cloud api /v1/users/me returns actor with authMethod=api-key for HWLAB_API_KEY Bearer token", async () => {
  const server = createCloudApiServer({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
    now: () => "2026-06-03T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
  try {
    const { port } = server.address();
    const setup = await postJson(port, "/v1/setup/first-admin", { username: "actor-admin", password: "actor-pass" });
    const cookie = setup.cookie;
    const apiKey = (await postJson(port, "/v1/api-keys", { name: "Actor CLI key" }, cookie)).body.key.displaySecret;
    const me = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${apiKey}` });
    assert.equal(me.status, 200);
    assert.equal(me.body.authMethod, "api-key");
    assert.equal(me.body.actor.username, "actor-admin");
    assert.equal(me.body.actor.role, "admin");
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});

test("cloud api AgentRun trace records real actor.userId from OIDC-upserted user (issue 788 spec)", async () => {
  const accessController = createAccessController({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_KEYCLOAK_ISSUER: OIDC_ISSUER, HWLAB_KEYCLOAK_CLIENT_ID: OIDC_CLIENT_ID, HWLAB_KEYCLOAK_CLIENT_SECRET: "secret", HWLAB_PUBLIC_ENDPOINT },
    fetchImpl: makeOidcFetchMock({ nonce: "actor-nonce" }),
    now: () => "2026-06-03T00:00:00.000Z"
  });
  const state = "actor-state";
  await accessController.store.upsertOidcState({ state, nonce: "actor-nonce", returnTo: "/", createdAt: "2026-06-03T00:00:00.000Z", expiresAt: "2026-06-03T00:10:00.000Z" });
  const server = createCloudApiServer({
    env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" },
    accessController,
    now: () => "2026-06-03T00:00:00.000Z"
  });
  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
  try {
    const { port } = server.address();
    const response = await fetch(`http://127.0.0.1:${port}/auth/oidc/callback?code=oidc-code&state=${state}`, { redirect: "manual" });
    const cookie = (response.headers.get("set-cookie") ?? "").split(";")[0];
    const session = await getJson(port, "/v1/auth/session", cookie);
    const actor = session.body.actor;
    assert.equal(actor.username, "oidc-user");
    assert.equal(actor.role, "user");
    const defaultApiKey = await getJson(port, "/v1/api-keys/default", cookie);
    assert.equal(defaultApiKey.body.created, false);
    assert.equal(defaultApiKey.body.key.displaySecret, undefined);
    const runnerKey = await postJson(port, "/v1/api-keys", { name: "OIDC runner key" }, cookie);
    const apiKey = runnerKey.body.key.displaySecret;
    const me = await getJson(port, "/v1/users/me", null, { authorization: `Bearer ${apiKey}` });
    assert.equal(me.body.actor.id, actor.id);
    assert.equal(me.body.authMethod, "api-key");
  } finally {
    await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
  }
});
