refactor(test): consolidate temp home + vitest setup

This commit is contained in:
Peter Steinberger
2026-01-09 16:39:02 +01:00
parent 1eecce9a15
commit 4ffbd9802a
15 changed files with 549 additions and 629 deletions

37
.github/workflows/workflow-sanity.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Workflow Sanity
on:
pull_request:
push:
jobs:
no-tabs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Fail on tabs in workflow files
run: |
python - <<'PY'
from __future__ import annotations
import pathlib
import sys
root = pathlib.Path(".github/workflows")
bad: list[str] = []
for path in sorted(root.rglob("*.yml")):
if b"\t" in path.read_bytes():
bad.append(str(path))
for path in sorted(root.rglob("*.yaml")):
if b"\t" in path.read_bytes():
bad.append(str(path))
if bad:
print("Tabs found in workflow file(s):")
for path in bad:
print(f"- {path}")
sys.exit(1)
PY

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { withTempHome } from "../../test/helpers/temp-home.js";
import { import {
type AuthProfileStore, type AuthProfileStore,
CLAUDE_CLI_PROFILE_ID, CLAUDE_CLI_PROFILE_ID,
@@ -13,40 +14,6 @@ import {
resolveAuthProfileOrder, resolveAuthProfileOrder,
} from "./auth-profiles.js"; } from "./auth-profiles.js";
const HOME_ENV_KEYS = ["HOME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH"] as const;
type HomeEnvSnapshot = Record<
(typeof HOME_ENV_KEYS)[number],
string | undefined
>;
const snapshotHomeEnv = (): HomeEnvSnapshot => ({
HOME: process.env.HOME,
USERPROFILE: process.env.USERPROFILE,
HOMEDRIVE: process.env.HOMEDRIVE,
HOMEPATH: process.env.HOMEPATH,
});
const restoreHomeEnv = (snapshot: HomeEnvSnapshot) => {
for (const key of HOME_ENV_KEYS) {
const value = snapshot[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
};
const setTempHome = (tempHome: string) => {
process.env.HOME = tempHome;
if (process.platform === "win32") {
process.env.USERPROFILE = tempHome;
const root = path.parse(tempHome).root;
process.env.HOMEDRIVE = root.replace(/\\$/, "");
process.env.HOMEPATH = tempHome.slice(root.length - 1);
}
};
describe("resolveAuthProfileOrder", () => { describe("resolveAuthProfileOrder", () => {
const store: AuthProfileStore = { const store: AuthProfileStore = {
version: 1, version: 1,
@@ -431,17 +398,14 @@ describe("auth profile cooldowns", () => {
}); });
describe("external CLI credential sync", () => { describe("external CLI credential sync", () => {
it("syncs Claude CLI credentials into anthropic:claude-cli", () => { it("syncs Claude CLI credentials into anthropic:claude-cli", async () => {
const agentDir = fs.mkdtempSync( const agentDir = fs.mkdtempSync(
path.join(os.tmpdir(), "clawdbot-cli-sync-"), path.join(os.tmpdir(), "clawdbot-cli-sync-"),
); );
const originalHome = snapshotHomeEnv();
try { try {
// Create a temp home with Claude CLI credentials // Create a temp home with Claude CLI credentials
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-home-")); await withTempHome(
setTempHome(tempHome); async (tempHome) => {
// Create Claude CLI credentials // Create Claude CLI credentials
const claudeDir = path.join(tempHome, ".claude"); const claudeDir = path.join(tempHome, ".claude");
fs.mkdirSync(claudeDir, { recursive: true }); fs.mkdirSync(claudeDir, { recursive: true });
@@ -477,32 +441,32 @@ describe("external CLI credential sync", () => {
const store = ensureAuthProfileStore(agentDir); const store = ensureAuthProfileStore(agentDir);
expect(store.profiles["anthropic:default"]).toBeDefined(); expect(store.profiles["anthropic:default"]).toBeDefined();
expect((store.profiles["anthropic:default"] as { key: string }).key).toBe( expect(
"sk-default", (store.profiles["anthropic:default"] as { key: string }).key,
); ).toBe("sk-default");
expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined(); expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined();
expect( expect(
(store.profiles[CLAUDE_CLI_PROFILE_ID] as { token: string }).token, (store.profiles[CLAUDE_CLI_PROFILE_ID] as { token: string }).token,
).toBe("fresh-access-token"); ).toBe("fresh-access-token");
expect( expect(
(store.profiles[CLAUDE_CLI_PROFILE_ID] as { expires: number }).expires, (store.profiles[CLAUDE_CLI_PROFILE_ID] as { expires: number })
.expires,
).toBeGreaterThan(Date.now()); ).toBeGreaterThan(Date.now());
},
{ prefix: "clawdbot-home-" },
);
} finally { } finally {
restoreHomeEnv(originalHome);
fs.rmSync(agentDir, { recursive: true, force: true }); fs.rmSync(agentDir, { recursive: true, force: true });
} }
}); });
it("syncs Codex CLI credentials into openai-codex:codex-cli", () => { it("syncs Codex CLI credentials into openai-codex:codex-cli", async () => {
const agentDir = fs.mkdtempSync( const agentDir = fs.mkdtempSync(
path.join(os.tmpdir(), "clawdbot-codex-sync-"), path.join(os.tmpdir(), "clawdbot-codex-sync-"),
); );
const originalHome = snapshotHomeEnv();
try { try {
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-home-")); await withTempHome(
setTempHome(tempHome); async (tempHome) => {
// Create Codex CLI credentials // Create Codex CLI credentials
const codexDir = path.join(tempHome, ".codex"); const codexDir = path.join(tempHome, ".codex");
fs.mkdirSync(codexDir, { recursive: true }); fs.mkdirSync(codexDir, { recursive: true });
@@ -531,22 +495,21 @@ describe("external CLI credential sync", () => {
expect( expect(
(store.profiles[CODEX_CLI_PROFILE_ID] as { access: string }).access, (store.profiles[CODEX_CLI_PROFILE_ID] as { access: string }).access,
).toBe("codex-access-token"); ).toBe("codex-access-token");
},
{ prefix: "clawdbot-home-" },
);
} finally { } finally {
restoreHomeEnv(originalHome);
fs.rmSync(agentDir, { recursive: true, force: true }); fs.rmSync(agentDir, { recursive: true, force: true });
} }
}); });
it("does not overwrite API keys when syncing external CLI creds", () => { it("does not overwrite API keys when syncing external CLI creds", async () => {
const agentDir = fs.mkdtempSync( const agentDir = fs.mkdtempSync(
path.join(os.tmpdir(), "clawdbot-no-overwrite-"), path.join(os.tmpdir(), "clawdbot-no-overwrite-"),
); );
const originalHome = snapshotHomeEnv();
try { try {
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-home-")); await withTempHome(
setTempHome(tempHome); async (tempHome) => {
// Create Claude CLI credentials // Create Claude CLI credentials
const claudeDir = path.join(tempHome, ".claude"); const claudeDir = path.join(tempHome, ".claude");
fs.mkdirSync(claudeDir, { recursive: true }); fs.mkdirSync(claudeDir, { recursive: true });
@@ -581,26 +544,25 @@ describe("external CLI credential sync", () => {
const store = ensureAuthProfileStore(agentDir); const store = ensureAuthProfileStore(agentDir);
// Should keep the store's API key and still add the CLI profile. // Should keep the store's API key and still add the CLI profile.
expect((store.profiles["anthropic:default"] as { key: string }).key).toBe( expect(
"sk-store", (store.profiles["anthropic:default"] as { key: string }).key,
); ).toBe("sk-store");
expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined(); expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined();
},
{ prefix: "clawdbot-home-" },
);
} finally { } finally {
restoreHomeEnv(originalHome);
fs.rmSync(agentDir, { recursive: true, force: true }); fs.rmSync(agentDir, { recursive: true, force: true });
} }
}); });
it("does not overwrite fresher store token with older Claude CLI credentials", () => { it("does not overwrite fresher store token with older Claude CLI credentials", async () => {
const agentDir = fs.mkdtempSync( const agentDir = fs.mkdtempSync(
path.join(os.tmpdir(), "clawdbot-cli-no-downgrade-"), path.join(os.tmpdir(), "clawdbot-cli-no-downgrade-"),
); );
const originalHome = snapshotHomeEnv();
try { try {
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-home-")); await withTempHome(
setTempHome(tempHome); async (tempHome) => {
const claudeDir = path.join(tempHome, ".claude"); const claudeDir = path.join(tempHome, ".claude");
fs.mkdirSync(claudeDir, { recursive: true }); fs.mkdirSync(claudeDir, { recursive: true });
fs.writeFileSync( fs.writeFileSync(
@@ -634,29 +596,31 @@ describe("external CLI credential sync", () => {
expect( expect(
(store.profiles[CLAUDE_CLI_PROFILE_ID] as { token: string }).token, (store.profiles[CLAUDE_CLI_PROFILE_ID] as { token: string }).token,
).toBe("store-access"); ).toBe("store-access");
},
{ prefix: "clawdbot-home-" },
);
} finally { } finally {
restoreHomeEnv(originalHome);
fs.rmSync(agentDir, { recursive: true, force: true }); fs.rmSync(agentDir, { recursive: true, force: true });
} }
}); });
it("updates codex-cli profile when Codex CLI refresh token changes", () => { it("updates codex-cli profile when Codex CLI refresh token changes", async () => {
const agentDir = fs.mkdtempSync( const agentDir = fs.mkdtempSync(
path.join(os.tmpdir(), "clawdbot-codex-refresh-sync-"), path.join(os.tmpdir(), "clawdbot-codex-refresh-sync-"),
); );
const originalHome = snapshotHomeEnv();
try { try {
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-home-")); await withTempHome(
setTempHome(tempHome); async (tempHome) => {
const codexDir = path.join(tempHome, ".codex"); const codexDir = path.join(tempHome, ".codex");
fs.mkdirSync(codexDir, { recursive: true }); fs.mkdirSync(codexDir, { recursive: true });
const codexAuthPath = path.join(codexDir, "auth.json"); const codexAuthPath = path.join(codexDir, "auth.json");
fs.writeFileSync( fs.writeFileSync(
codexAuthPath, codexAuthPath,
JSON.stringify({ JSON.stringify({
tokens: { access_token: "same-access", refresh_token: "new-refresh" }, tokens: {
access_token: "same-access",
refresh_token: "new-refresh",
},
}), }),
); );
fs.utimesSync(codexAuthPath, new Date(), new Date()); fs.utimesSync(codexAuthPath, new Date(), new Date());
@@ -680,10 +644,13 @@ describe("external CLI credential sync", () => {
const store = ensureAuthProfileStore(agentDir); const store = ensureAuthProfileStore(agentDir);
expect( expect(
(store.profiles[CODEX_CLI_PROFILE_ID] as { refresh: string }).refresh, (store.profiles[CODEX_CLI_PROFILE_ID] as { refresh: string })
.refresh,
).toBe("new-refresh"); ).toBe("new-refresh");
},
{ prefix: "clawdbot-home-" },
);
} finally { } finally {
restoreHomeEnv(originalHome);
fs.rmSync(agentDir, { recursive: true, force: true }); fs.rmSync(agentDir, { recursive: true, force: true });
} }
}); });

View File

@@ -1,20 +1,12 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> { async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
const base = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-models-")); return withTempHomeBase(fn, { prefix: "clawdbot-models-" });
const previousHome = process.env.HOME;
process.env.HOME = base;
try {
return await fn(base);
} finally {
process.env.HOME = previousHome;
await fs.rm(base, { recursive: true, force: true });
}
} }
const MODELS_CONFIG: ClawdbotConfig = { const MODELS_CONFIG: ClawdbotConfig = {

View File

@@ -1,9 +1,8 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
import { loadModelCatalog } from "../agents/model-catalog.js"; import { loadModelCatalog } from "../agents/model-catalog.js";
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
import { getReplyFromConfig } from "./reply.js"; import { getReplyFromConfig } from "./reply.js";
@@ -22,15 +21,7 @@ vi.mock("../agents/model-catalog.js", () => ({
})); }));
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> { async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
const base = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-stream-")); return withTempHomeBase(fn, { prefix: "clawdbot-stream-" });
const previousHome = process.env.HOME;
process.env.HOME = base;
try {
return await fn(base);
} finally {
process.env.HOME = previousHome;
await fs.rm(base, { recursive: true, force: true });
}
} }
describe("block streaming", () => { describe("block streaming", () => {

View File

@@ -1,9 +1,9 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
import { loadModelCatalog } from "../agents/model-catalog.js"; import { loadModelCatalog } from "../agents/model-catalog.js";
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
import { import {
@@ -28,28 +28,30 @@ vi.mock("../agents/model-catalog.js", () => ({
})); }));
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> { async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
const base = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-reply-")); return withTempHomeBase(
const previousHome = process.env.HOME; async (home) => {
const previousStateDir = process.env.CLAWDBOT_STATE_DIR; const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR; const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR;
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
process.env.HOME = base; process.env.CLAWDBOT_STATE_DIR = path.join(home, ".clawdbot");
process.env.CLAWDBOT_STATE_DIR = path.join(base, ".clawdbot"); process.env.CLAWDBOT_AGENT_DIR = path.join(home, ".clawdbot", "agent");
process.env.CLAWDBOT_AGENT_DIR = path.join(base, ".clawdbot", "agent");
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR; process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
try { try {
return await fn(base); return await fn(home);
} finally { } finally {
process.env.HOME = previousHome; if (previousStateDir === undefined)
if (previousStateDir === undefined) delete process.env.CLAWDBOT_STATE_DIR; delete process.env.CLAWDBOT_STATE_DIR;
else process.env.CLAWDBOT_STATE_DIR = previousStateDir; else process.env.CLAWDBOT_STATE_DIR = previousStateDir;
if (previousAgentDir === undefined) delete process.env.CLAWDBOT_AGENT_DIR; if (previousAgentDir === undefined)
delete process.env.CLAWDBOT_AGENT_DIR;
else process.env.CLAWDBOT_AGENT_DIR = previousAgentDir; else process.env.CLAWDBOT_AGENT_DIR = previousAgentDir;
if (previousPiAgentDir === undefined) if (previousPiAgentDir === undefined)
delete process.env.PI_CODING_AGENT_DIR; delete process.env.PI_CODING_AGENT_DIR;
else process.env.PI_CODING_AGENT_DIR = previousPiAgentDir; else process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
await fs.rm(base, { recursive: true, force: true });
} }
},
{ prefix: "clawdbot-reply-" },
);
} }
describe("directive behavior", () => { describe("directive behavior", () => {

View File

@@ -1,9 +1,9 @@
import fs from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
const runEmbeddedPiAgentMock = vi.fn(); const runEmbeddedPiAgentMock = vi.fn();
vi.mock("../agents/model-fallback.js", () => ({ vi.mock("../agents/model-fallback.js", () => ({
@@ -43,16 +43,13 @@ vi.mock("../web/session.js", () => webMocks);
import { getReplyFromConfig } from "./reply.js"; import { getReplyFromConfig } from "./reply.js";
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> { async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
const base = await fs.mkdtemp(join(tmpdir(), "clawdbot-typing-")); return withTempHomeBase(
const previousHome = process.env.HOME; async (home) => {
process.env.HOME = base;
try {
runEmbeddedPiAgentMock.mockClear(); runEmbeddedPiAgentMock.mockClear();
return await fn(base); return await fn(home);
} finally { },
process.env.HOME = previousHome; { prefix: "clawdbot-typing-" },
await fs.rm(base, { recursive: true, force: true }); );
}
} }
function makeCfg(home: string) { function makeCfg(home: string) {

View File

@@ -1,9 +1,8 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
import { getReplyFromConfig } from "./reply.js"; import { getReplyFromConfig } from "./reply.js";
@@ -28,27 +27,26 @@ function makeResult(text: string) {
} }
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> { async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
const base = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-media-note-")); return withTempHomeBase(
const previousHome = process.env.HOME; async (home) => {
const previousBundledSkills = process.env.CLAWDBOT_BUNDLED_SKILLS_DIR; const previousBundledSkills = process.env.CLAWDBOT_BUNDLED_SKILLS_DIR;
process.env.HOME = base; process.env.CLAWDBOT_BUNDLED_SKILLS_DIR = path.join(
process.env.CLAWDBOT_BUNDLED_SKILLS_DIR = path.join(base, "bundled-skills"); home,
"bundled-skills",
);
try { try {
vi.mocked(runEmbeddedPiAgent).mockReset(); vi.mocked(runEmbeddedPiAgent).mockReset();
return await fn(base); return await fn(home);
} finally { } finally {
process.env.HOME = previousHome;
if (previousBundledSkills === undefined) { if (previousBundledSkills === undefined) {
delete process.env.CLAWDBOT_BUNDLED_SKILLS_DIR; delete process.env.CLAWDBOT_BUNDLED_SKILLS_DIR;
} else { } else {
process.env.CLAWDBOT_BUNDLED_SKILLS_DIR = previousBundledSkills; process.env.CLAWDBOT_BUNDLED_SKILLS_DIR = previousBundledSkills;
} }
try {
await fs.rm(base, { recursive: true, force: true });
} catch {
// ignore cleanup failures in tests
}
} }
},
{ prefix: "clawdbot-media-note-" },
);
} }
function makeCfg(home: string) { function makeCfg(home: string) {

View File

@@ -1,9 +1,8 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
import { import {
isEmbeddedPiRunActive, isEmbeddedPiRunActive,
isEmbeddedPiRunStreaming, isEmbeddedPiRunStreaming,
@@ -32,20 +31,13 @@ function makeResult(text: string) {
} }
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> { async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
const base = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-queue-")); return withTempHomeBase(
const previousHome = process.env.HOME; async (home) => {
process.env.HOME = base;
try {
vi.mocked(runEmbeddedPiAgent).mockReset(); vi.mocked(runEmbeddedPiAgent).mockReset();
return await fn(base); return await fn(home);
} finally { },
process.env.HOME = previousHome; { prefix: "clawdbot-queue-" },
try { );
await fs.rm(base, { recursive: true, force: true });
} catch {
// ignore cleanup failures in tests
}
}
} }
function makeCfg(home: string, queue?: Record<string, unknown>) { function makeCfg(home: string, queue?: Record<string, unknown>) {

View File

@@ -3,6 +3,8 @@ import { tmpdir } from "node:os";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
vi.mock("../agents/pi-embedded.js", () => ({ vi.mock("../agents/pi-embedded.js", () => ({
abortEmbeddedPiRun: vi.fn().mockReturnValue(false), abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
compactEmbeddedPiSession: vi.fn(), compactEmbeddedPiSession: vi.fn(),
@@ -51,37 +53,26 @@ const webMocks = vi.hoisted(() => ({
vi.mock("../web/session.js", () => webMocks); vi.mock("../web/session.js", () => webMocks);
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> { async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
const base = await fs.mkdtemp(join(tmpdir(), "clawdbot-triggers-")); return withTempHomeBase(
const previousHome = process.env.HOME; async (home) => {
const previousUserProfile = process.env.USERPROFILE;
const previousHomeDrive = process.env.HOMEDRIVE;
const previousHomePath = process.env.HOMEPATH;
const previousStateDir = process.env.CLAWDBOT_STATE_DIR; const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
const previousClawdisStateDir = process.env.CLAWDIS_STATE_DIR; const previousClawdisStateDir = process.env.CLAWDIS_STATE_DIR;
process.env.HOME = base; process.env.CLAWDBOT_STATE_DIR = join(home, ".clawdbot");
process.env.CLAWDBOT_STATE_DIR = join(base, ".clawdbot"); process.env.CLAWDIS_STATE_DIR = join(home, ".clawdbot");
process.env.CLAWDIS_STATE_DIR = join(base, ".clawdbot");
if (process.platform === "win32") {
process.env.USERPROFILE = base;
const driveMatch = base.match(/^([A-Za-z]:)(.*)$/);
if (driveMatch) {
process.env.HOMEDRIVE = driveMatch[1];
process.env.HOMEPATH = driveMatch[2] || "\\";
}
}
try { try {
vi.mocked(runEmbeddedPiAgent).mockClear(); vi.mocked(runEmbeddedPiAgent).mockClear();
vi.mocked(abortEmbeddedPiRun).mockClear(); vi.mocked(abortEmbeddedPiRun).mockClear();
return await fn(base); return await fn(home);
} finally { } finally {
process.env.HOME = previousHome; if (previousStateDir === undefined) delete process.env.CLAWDBOT_STATE_DIR;
process.env.USERPROFILE = previousUserProfile; else process.env.CLAWDBOT_STATE_DIR = previousStateDir;
process.env.HOMEDRIVE = previousHomeDrive; if (previousClawdisStateDir === undefined)
process.env.HOMEPATH = previousHomePath; delete process.env.CLAWDIS_STATE_DIR;
process.env.CLAWDBOT_STATE_DIR = previousStateDir; else process.env.CLAWDIS_STATE_DIR = previousClawdisStateDir;
process.env.CLAWDIS_STATE_DIR = previousClawdisStateDir;
await fs.rm(base, { recursive: true, force: true });
} }
},
{ prefix: "clawdbot-triggers-" },
);
} }
function makeCfg(home: string) { function makeCfg(home: string) {
@@ -320,7 +311,7 @@ describe("trigger handling", () => {
); );
const text = Array.isArray(res) ? res[0]?.text : res?.text; const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("api-key"); expect(text).toContain("api-key");
expect(text).toContain("…"); expect(text).toMatch(/…|\.{3}/);
expect(text).toContain("(anthropic:work)"); expect(text).toContain("(anthropic:work)");
expect(text).not.toContain("mixed"); expect(text).not.toContain("mixed");
expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); expect(runEmbeddedPiAgent).not.toHaveBeenCalled();

View File

@@ -1,44 +1,10 @@
import fs from "node:fs"; import fs from "node:fs";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { withTempHome } from "../../test/helpers/temp-home.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { buildStatusMessage } from "./status.js"; import { buildStatusMessage } from "./status.js";
const HOME_ENV_KEYS = ["HOME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH"] as const;
type HomeEnvSnapshot = Record<
(typeof HOME_ENV_KEYS)[number],
string | undefined
>;
const snapshotHomeEnv = (): HomeEnvSnapshot => ({
HOME: process.env.HOME,
USERPROFILE: process.env.USERPROFILE,
HOMEDRIVE: process.env.HOMEDRIVE,
HOMEPATH: process.env.HOMEPATH,
});
const restoreHomeEnv = (snapshot: HomeEnvSnapshot) => {
for (const key of HOME_ENV_KEYS) {
const value = snapshot[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
};
const setTempHome = (tempHome: string) => {
process.env.HOME = tempHome;
if (process.platform === "win32") {
process.env.USERPROFILE = tempHome;
const root = path.parse(tempHome).root;
process.env.HOMEDRIVE = root.replace(/\\$/, "");
process.env.HOMEPATH = tempHome.slice(root.length - 1);
}
};
afterEach(() => { afterEach(() => {
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
@@ -260,10 +226,8 @@ describe("buildStatusMessage", () => {
}); });
it("prefers cached prompt tokens from the session log", async () => { it("prefers cached prompt tokens from the session log", async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-status-")); await withTempHome(
const previousHome = snapshotHomeEnv(); async (dir) => {
setTempHome(dir);
try {
vi.resetModules(); vi.resetModules();
const { buildStatusMessage: buildStatusMessageDynamic } = await import( const { buildStatusMessage: buildStatusMessageDynamic } = await import(
"./status.js" "./status.js"
@@ -320,9 +284,8 @@ describe("buildStatusMessage", () => {
}); });
expect(text).toContain("Context: 1.0k/32k"); expect(text).toContain("Context: 1.0k/32k");
} finally { },
restoreHomeEnv(previousHome); { prefix: "clawdbot-status-" },
fs.rmSync(dir, { recursive: true, force: true }); );
}
}); });
}); });

View File

@@ -1,5 +1,4 @@
import fs from "node:fs"; import fs from "node:fs";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { import {
@@ -11,6 +10,8 @@ import {
vi, vi,
} from "vitest"; } from "vitest";
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
vi.mock("../agents/pi-embedded.js", () => ({ vi.mock("../agents/pi-embedded.js", () => ({
abortEmbeddedPiRun: vi.fn().mockReturnValue(false), abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
runEmbeddedPiAgent: vi.fn(), runEmbeddedPiAgent: vi.fn(),
@@ -39,15 +40,7 @@ const runtime: RuntimeEnv = {
const configSpy = vi.spyOn(configModule, "loadConfig"); const configSpy = vi.spyOn(configModule, "loadConfig");
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> { async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
const base = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-agent-")); return withTempHomeBase(fn, { prefix: "clawdbot-agent-" });
const previousHome = process.env.HOME;
process.env.HOME = base;
try {
return await fn(base);
} finally {
process.env.HOME = previousHome;
fs.rmSync(base, { recursive: true, force: true });
}
} }
function mockConfig( function mockConfig(

View File

@@ -1,41 +1,13 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os"; import { tmpdir } from "node:os";
import path from "node:path"; import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> { async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
const base = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-config-")); return withTempHomeBase(fn, { prefix: "clawdbot-config-" });
const previousHome = process.env.HOME;
const previousUserProfile = process.env.USERPROFILE;
const previousHomeDrive = process.env.HOMEDRIVE;
const previousHomePath = process.env.HOMEPATH;
process.env.HOME = base;
process.env.USERPROFILE = base;
if (process.platform === "win32") {
const parsed = path.parse(base);
process.env.HOMEDRIVE = parsed.root.replace(/\\$/, "");
process.env.HOMEPATH = base.slice(Math.max(parsed.root.length - 1, 0));
}
try {
return await fn(base);
} finally {
process.env.HOME = previousHome;
process.env.USERPROFILE = previousUserProfile;
if (process.platform === "win32") {
if (previousHomeDrive === undefined) {
delete process.env.HOMEDRIVE;
} else {
process.env.HOMEDRIVE = previousHomeDrive;
}
if (previousHomePath === undefined) {
delete process.env.HOMEPATH;
} else {
process.env.HOMEPATH = previousHomePath;
}
}
await fs.rm(base, { recursive: true, force: true });
}
} }
/** /**
@@ -1277,7 +1249,7 @@ describe("multi-agent agentDir validation", () => {
it("rejects shared agents.list agentDir", async () => { it("rejects shared agents.list agentDir", async () => {
vi.resetModules(); vi.resetModules();
const { validateConfigObject } = await import("./config.js"); const { validateConfigObject } = await import("./config.js");
const shared = path.join(os.tmpdir(), "clawdbot-shared-agentdir"); const shared = path.join(tmpdir(), "clawdbot-shared-agentdir");
const res = validateConfigObject({ const res = validateConfigObject({
agents: { agents: {
list: [ list: [

View File

@@ -1,9 +1,9 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
import type { CliDeps } from "../cli/deps.js"; import type { CliDeps } from "../cli/deps.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import type { CronJob } from "./types.js"; import type { CronJob } from "./types.js";
@@ -23,15 +23,7 @@ import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
import { runCronIsolatedAgentTurn } from "./isolated-agent.js"; import { runCronIsolatedAgentTurn } from "./isolated-agent.js";
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> { async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
const base = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-cron-")); return withTempHomeBase(fn, { prefix: "clawdbot-cron-" });
const previousHome = process.env.HOME;
process.env.HOME = base;
try {
return await fn(base);
} finally {
process.env.HOME = previousHome;
await fs.rm(base, { recursive: true, force: true });
}
} }
async function writeSessionStore(home: string) { async function writeSessionStore(home: string) {

View File

@@ -1,7 +1,7 @@
import fs from "node:fs"; import fs from "node:fs";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { withTempHome } from "../../test/helpers/temp-home.js";
import { import {
ensureAuthProfileStore, ensureAuthProfileStore,
listProfilesForProvider, listProfilesForProvider,
@@ -73,45 +73,6 @@ describe("provider usage formatting", () => {
}); });
describe("provider usage loading", () => { describe("provider usage loading", () => {
const HOME_ENV_KEYS = [
"HOME",
"USERPROFILE",
"HOMEDRIVE",
"HOMEPATH",
] as const;
type HomeEnvSnapshot = Record<
(typeof HOME_ENV_KEYS)[number],
string | undefined
>;
const snapshotHomeEnv = (): HomeEnvSnapshot => ({
HOME: process.env.HOME,
USERPROFILE: process.env.USERPROFILE,
HOMEDRIVE: process.env.HOMEDRIVE,
HOMEPATH: process.env.HOMEPATH,
});
const restoreHomeEnv = (snapshot: HomeEnvSnapshot) => {
for (const key of HOME_ENV_KEYS) {
const value = snapshot[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
};
const setTempHome = (tempHome: string) => {
process.env.HOME = tempHome;
if (process.platform === "win32") {
process.env.USERPROFILE = tempHome;
const root = path.parse(tempHome).root;
process.env.HOMEDRIVE = root.replace(/\\$/, "");
process.env.HOMEPATH = tempHome.slice(root.length - 1);
}
};
it("loads usage snapshots with injected auth", async () => { it("loads usage snapshots with injected auth", async () => {
const makeResponse = (status: number, body: unknown): Response => { const makeResponse = (status: number, body: unknown): Response => {
const payload = typeof body === "string" ? body : JSON.stringify(body); const payload = typeof body === "string" ? body : JSON.stringify(body);
@@ -175,13 +136,9 @@ describe("provider usage loading", () => {
}); });
it("discovers Claude usage from token auth profiles", async () => { it("discovers Claude usage from token auth profiles", async () => {
const homeSnapshot = snapshotHomeEnv(); await withTempHome(
const stateSnapshot = process.env.CLAWDBOT_STATE_DIR; async (tempHome) => {
const tempHome = fs.mkdtempSync( const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
path.join(os.tmpdir(), "clawdbot-provider-usage-"),
);
try {
setTempHome(tempHome);
process.env.CLAWDBOT_STATE_DIR = path.join(tempHome, ".clawdbot"); process.env.CLAWDBOT_STATE_DIR = path.join(tempHome, ".clawdbot");
const agentDir = path.join( const agentDir = path.join(
process.env.CLAWDBOT_STATE_DIR, process.env.CLAWDBOT_STATE_DIR,
@@ -189,6 +146,7 @@ describe("provider usage loading", () => {
"main", "main",
"agent", "agent",
); );
try {
fs.mkdirSync(agentDir, { recursive: true, mode: 0o700 }); fs.mkdirSync(agentDir, { recursive: true, mode: 0o700 });
fs.writeFileSync( fs.writeFileSync(
path.join(agentDir, "auth-profiles.json"), path.join(agentDir, "auth-profiles.json"),
@@ -218,7 +176,8 @@ describe("provider usage loading", () => {
); );
const makeResponse = (status: number, body: unknown): Response => { const makeResponse = (status: number, body: unknown): Response => {
const payload = typeof body === "string" ? body : JSON.stringify(body); const payload =
typeof body === "string" ? body : JSON.stringify(body);
const headers = const headers =
typeof body === "string" typeof body === "string"
? undefined ? undefined
@@ -240,7 +199,10 @@ describe("provider usage loading", () => {
const headers = (init?.headers ?? {}) as Record<string, string>; const headers = (init?.headers ?? {}) as Record<string, string>;
expect(headers.Authorization).toBe("Bearer token-1"); expect(headers.Authorization).toBe("Bearer token-1");
return makeResponse(200, { return makeResponse(200, {
five_hour: { utilization: 20, resets_at: "2026-01-07T01:00:00Z" }, five_hour: {
utilization: 20,
resets_at: "2026-01-07T01:00:00Z",
},
}); });
} }
return makeResponse(404, "not found"); return makeResponse(404, "not found");
@@ -259,10 +221,13 @@ describe("provider usage loading", () => {
expect(claude?.windows[0]?.label).toBe("5h"); expect(claude?.windows[0]?.label).toBe("5h");
expect(mockFetch).toHaveBeenCalled(); expect(mockFetch).toHaveBeenCalled();
} finally { } finally {
restoreHomeEnv(homeSnapshot); if (previousStateDir === undefined)
if (stateSnapshot === undefined) delete process.env.CLAWDBOT_STATE_DIR; delete process.env.CLAWDBOT_STATE_DIR;
else process.env.CLAWDBOT_STATE_DIR = stateSnapshot; else process.env.CLAWDBOT_STATE_DIR = previousStateDir;
} }
},
{ prefix: "clawdbot-provider-usage-" },
);
}); });
it("falls back to claude.ai web usage when OAuth scope is missing", async () => { it("falls back to claude.ai web usage when OAuth scope is missing", async () => {

68
test/helpers/temp-home.ts Normal file
View File

@@ -0,0 +1,68 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
type EnvSnapshot = {
home: string | undefined;
userProfile: string | undefined;
homeDrive: string | undefined;
homePath: string | undefined;
};
function snapshotEnv(): EnvSnapshot {
return {
home: process.env.HOME,
userProfile: process.env.USERPROFILE,
homeDrive: process.env.HOMEDRIVE,
homePath: process.env.HOMEPATH,
};
}
function restoreEnv(snapshot: EnvSnapshot) {
const restoreKey = (key: string, value: string | undefined) => {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
};
restoreKey("HOME", snapshot.home);
restoreKey("USERPROFILE", snapshot.userProfile);
restoreKey("HOMEDRIVE", snapshot.homeDrive);
restoreKey("HOMEPATH", snapshot.homePath);
}
function setTempHome(base: string) {
process.env.HOME = base;
process.env.USERPROFILE = base;
if (process.platform !== "win32") return;
const match = base.match(/^([A-Za-z]:)(.*)$/);
if (!match) return;
process.env.HOMEDRIVE = match[1];
process.env.HOMEPATH = match[2] || "\\";
}
export async function withTempHome<T>(
fn: (home: string) => Promise<T>,
opts: { prefix?: string } = {},
): Promise<T> {
const base = await fs.mkdtemp(
path.join(os.tmpdir(), opts.prefix ?? "clawdbot-test-home-"),
);
const snapshot = snapshotEnv();
setTempHome(base);
try {
return await fn(base);
} finally {
restoreEnv(snapshot);
try {
await fs.rm(base, {
recursive: true,
force: true,
maxRetries: 10,
retryDelay: 50,
});
} catch {
// ignore cleanup failures in tests
}
}
}