/** * @file server-s3-onboarded.test.js * @description S3: a member's Claude pane on a pool account (or an AI Proxy) * must not stop at the first-run "Select login method" screen a fresh home * shows even with CLAUDE_CODE_OAUTH_TOKEN set — `run-as.markClaudeOnboarded` * adds `hasCompletedOnboarding: true` to the member's `~/.claude.json`, AS the * member (write-as, played by fixtures/fake-sudo.js), keeping every other key * and leaving a file it cannot trust alone. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, beforeEach, after } = require("node:test"); const assert = require("node:assert/strict"); const path = require("node:path"); const fs = require("node:fs"); const os = require("node:os"); const TMP = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "server-s3-onboarded-"))); const USERS = path.join(TMP, "users"); const HOME = path.join(USERS, "mia"); const FILE = path.join(HOME, ".claude.json"); process.env.AGENTHUB_SERVER_CONF = path.join(TMP, "server.conf"); process.env.AGENTHUB_PASSWD = path.join(TMP, "passwd"); process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db"); process.env.DASHBOARD_DATA_DIR = path.join(TMP, "data"); process.env.CLAUDE_HOME = path.join(TMP, ".claude"); const auth = require("../lib/auth"); const linux = require("../lib/linux-accounts"); const runAs = require("../lib/run-as"); const { fakeSudo } = require("./fixtures/fake-sudo"); const sudo = fakeSudo(); let acct; before(async () => { fs.mkdirSync(HOME, { recursive: true }); // The member's uid is this test's own, so its files pass the owner check. const uid = process.getuid(); fs.writeFileSync(process.env.AGENTHUB_PASSWD, `ah-mia:x:${uid}:${uid}::/:/bin/sh\n`); fs.writeFileSync( process.env.AGENTHUB_SERVER_CONF, `HOST_USER=h\nHOST_UID=1\nUSERS_ROOT=${USERS}\nPORT=4820\n` ); linux.__resetConfCache(); runAs.__resetCache(); linux.__setRunImpl(() => "ok"); runAs.__setExecImpl(sudo.exec); process.env.AGENTHUB_MODE = "server"; await auth.createUser("mia", "member-pass-1", "developer"); acct = runAs.accountForPath(HOME); assert.ok(acct, "mia is a provisioned member"); }); after(() => { delete process.env.AGENTHUB_MODE; }); beforeEach(() => { fs.rmSync(FILE, { force: true }); sudo.calls.length = 0; }); describe("markClaudeOnboarded", () => { it("creates ~/.claude.json, 0600, as the member", () => { assert.equal(runAs.markClaudeOnboarded(acct), true); assert.deepEqual(JSON.parse(fs.readFileSync(FILE, "utf8")), { hasCompletedOnboarding: true }); assert.equal(fs.statSync(FILE).mode & 0o777, 0o600); assert.ok(sudo.calls.some((c) => c.includes(FILE) && c.includes("private"))); }); it("adds only the flag, keeping the member's own state", () => { fs.writeFileSync(FILE, JSON.stringify({ theme: "light", projects: { "/x": {} } })); assert.equal(runAs.markClaudeOnboarded(acct), true); assert.deepEqual(JSON.parse(fs.readFileSync(FILE, "utf8")), { theme: "light", projects: { "/x": {} }, hasCompletedOnboarding: true, }); }); it("writes nothing when the member is already onboarded", () => { fs.writeFileSync(FILE, JSON.stringify({ hasCompletedOnboarding: true })); assert.equal(runAs.markClaudeOnboarded(acct), false); assert.equal(sudo.calls.length, 0); }); it("leaves a file that is not JSON, or not a regular file, alone", () => { fs.writeFileSync(FILE, "{ half-written"); assert.equal(runAs.markClaudeOnboarded(acct), false); assert.equal(fs.readFileSync(FILE, "utf8"), "{ half-written"); fs.rmSync(FILE); fs.mkdirSync(FILE); assert.equal(runAs.markClaudeOnboarded(acct), false); fs.rmdirSync(FILE); assert.equal(sudo.calls.length, 0); }); it("never writes through a link the member left at the name", () => { const target = path.join(TMP, "elsewhere.json"); fs.writeFileSync(target, "{}"); fs.symlinkSync(target, FILE); runAs.markClaudeOnboarded(acct); assert.equal(fs.readFileSync(target, "utf8"), "{}"); }); });