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,259 +398,259 @@ 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 }); const claudeCreds = {
const claudeCreds = { claudeAiOauth: {
claudeAiOauth: { accessToken: "fresh-access-token",
accessToken: "fresh-access-token", refreshToken: "fresh-refresh-token",
refreshToken: "fresh-refresh-token", expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour from now
expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour from now
},
};
fs.writeFileSync(
path.join(claudeDir, ".credentials.json"),
JSON.stringify(claudeCreds),
);
// Create empty auth-profiles.json
const authPath = path.join(agentDir, "auth-profiles.json");
fs.writeFileSync(
authPath,
JSON.stringify({
version: 1,
profiles: {
"anthropic:default": {
type: "api_key",
provider: "anthropic",
key: "sk-default",
}, },
}, };
}), fs.writeFileSync(
); path.join(claudeDir, ".credentials.json"),
JSON.stringify(claudeCreds),
);
// Load the store - should sync from CLI // Create empty auth-profiles.json
const store = ensureAuthProfileStore(agentDir); const authPath = path.join(agentDir, "auth-profiles.json");
fs.writeFileSync(
authPath,
JSON.stringify({
version: 1,
profiles: {
"anthropic:default": {
type: "api_key",
provider: "anthropic",
key: "sk-default",
},
},
}),
);
expect(store.profiles["anthropic:default"]).toBeDefined(); // Load the store - should sync from CLI
expect((store.profiles["anthropic:default"] as { key: string }).key).toBe( const store = ensureAuthProfileStore(agentDir);
"sk-default",
expect(store.profiles["anthropic:default"]).toBeDefined();
expect(
(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] as { token: string }).token,
).toBe("fresh-access-token");
expect(
(store.profiles[CLAUDE_CLI_PROFILE_ID] as { expires: number })
.expires,
).toBeGreaterThan(Date.now());
},
{ prefix: "clawdbot-home-" },
); );
expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined();
expect(
(store.profiles[CLAUDE_CLI_PROFILE_ID] as { token: string }).token,
).toBe("fresh-access-token");
expect(
(store.profiles[CLAUDE_CLI_PROFILE_ID] as { expires: number }).expires,
).toBeGreaterThan(Date.now());
} 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
const codexDir = path.join(tempHome, ".codex");
fs.mkdirSync(codexDir, { recursive: true });
const codexCreds = {
tokens: {
access_token: "codex-access-token",
refresh_token: "codex-refresh-token",
},
};
const codexAuthPath = path.join(codexDir, "auth.json");
fs.writeFileSync(codexAuthPath, JSON.stringify(codexCreds));
// Create Codex CLI credentials // Create empty auth-profiles.json
const codexDir = path.join(tempHome, ".codex"); const authPath = path.join(agentDir, "auth-profiles.json");
fs.mkdirSync(codexDir, { recursive: true }); fs.writeFileSync(
const codexCreds = { authPath,
tokens: { JSON.stringify({
access_token: "codex-access-token", version: 1,
refresh_token: "codex-refresh-token", profiles: {},
}),
);
const store = ensureAuthProfileStore(agentDir);
expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeDefined();
expect(
(store.profiles[CODEX_CLI_PROFILE_ID] as { access: string }).access,
).toBe("codex-access-token");
}, },
}; { prefix: "clawdbot-home-" },
const codexAuthPath = path.join(codexDir, "auth.json");
fs.writeFileSync(codexAuthPath, JSON.stringify(codexCreds));
// Create empty auth-profiles.json
const authPath = path.join(agentDir, "auth-profiles.json");
fs.writeFileSync(
authPath,
JSON.stringify({
version: 1,
profiles: {},
}),
); );
const store = ensureAuthProfileStore(agentDir);
expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeDefined();
expect(
(store.profiles[CODEX_CLI_PROFILE_ID] as { access: string }).access,
).toBe("codex-access-token");
} 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 }); const claudeCreds = {
const claudeCreds = { claudeAiOauth: {
claudeAiOauth: { accessToken: "cli-access",
accessToken: "cli-access", refreshToken: "cli-refresh",
refreshToken: "cli-refresh", expiresAt: Date.now() + 30 * 60 * 1000,
expiresAt: Date.now() + 30 * 60 * 1000,
},
};
fs.writeFileSync(
path.join(claudeDir, ".credentials.json"),
JSON.stringify(claudeCreds),
);
// Create auth-profiles.json with an API key
const authPath = path.join(agentDir, "auth-profiles.json");
fs.writeFileSync(
authPath,
JSON.stringify({
version: 1,
profiles: {
"anthropic:default": {
type: "api_key",
provider: "anthropic",
key: "sk-store",
}, },
}, };
}), fs.writeFileSync(
); path.join(claudeDir, ".credentials.json"),
JSON.stringify(claudeCreds),
);
const store = ensureAuthProfileStore(agentDir); // Create auth-profiles.json with an API key
const authPath = path.join(agentDir, "auth-profiles.json");
fs.writeFileSync(
authPath,
JSON.stringify({
version: 1,
profiles: {
"anthropic:default": {
type: "api_key",
provider: "anthropic",
key: "sk-store",
},
},
}),
);
// Should keep the store's API key and still add the CLI profile. const store = ensureAuthProfileStore(agentDir);
expect((store.profiles["anthropic:default"] as { key: string }).key).toBe(
"sk-store", // Should keep the store's API key and still add the CLI profile.
expect(
(store.profiles["anthropic:default"] as { key: string }).key,
).toBe("sk-store");
expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined();
},
{ prefix: "clawdbot-home-" },
); );
expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined();
} 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");
fs.mkdirSync(claudeDir, { recursive: true });
fs.writeFileSync(
path.join(claudeDir, ".credentials.json"),
JSON.stringify({
claudeAiOauth: {
accessToken: "cli-access",
refreshToken: "cli-refresh",
expiresAt: Date.now() + 30 * 60 * 1000,
},
}),
);
const claudeDir = path.join(tempHome, ".claude"); const authPath = path.join(agentDir, "auth-profiles.json");
fs.mkdirSync(claudeDir, { recursive: true }); fs.writeFileSync(
fs.writeFileSync( authPath,
path.join(claudeDir, ".credentials.json"), JSON.stringify({
JSON.stringify({ version: 1,
claudeAiOauth: { profiles: {
accessToken: "cli-access", [CLAUDE_CLI_PROFILE_ID]: {
refreshToken: "cli-refresh", type: "token",
expiresAt: Date.now() + 30 * 60 * 1000, provider: "anthropic",
}, token: "store-access",
}), expires: Date.now() + 60 * 60 * 1000,
},
},
}),
);
const store = ensureAuthProfileStore(agentDir);
expect(
(store.profiles[CLAUDE_CLI_PROFILE_ID] as { token: string }).token,
).toBe("store-access");
},
{ prefix: "clawdbot-home-" },
); );
const authPath = path.join(agentDir, "auth-profiles.json");
fs.writeFileSync(
authPath,
JSON.stringify({
version: 1,
profiles: {
[CLAUDE_CLI_PROFILE_ID]: {
type: "token",
provider: "anthropic",
token: "store-access",
expires: Date.now() + 60 * 60 * 1000,
},
},
}),
);
const store = ensureAuthProfileStore(agentDir);
expect(
(store.profiles[CLAUDE_CLI_PROFILE_ID] as { token: string }).token,
).toBe("store-access");
} 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");
fs.mkdirSync(codexDir, { recursive: true });
const codexAuthPath = path.join(codexDir, "auth.json");
fs.writeFileSync(
codexAuthPath,
JSON.stringify({
tokens: {
access_token: "same-access",
refresh_token: "new-refresh",
},
}),
);
fs.utimesSync(codexAuthPath, new Date(), new Date());
const codexDir = path.join(tempHome, ".codex"); const authPath = path.join(agentDir, "auth-profiles.json");
fs.mkdirSync(codexDir, { recursive: true }); fs.writeFileSync(
const codexAuthPath = path.join(codexDir, "auth.json"); authPath,
fs.writeFileSync( JSON.stringify({
codexAuthPath, version: 1,
JSON.stringify({ profiles: {
tokens: { access_token: "same-access", refresh_token: "new-refresh" }, [CODEX_CLI_PROFILE_ID]: {
}), type: "oauth",
provider: "openai-codex",
access: "same-access",
refresh: "old-refresh",
expires: Date.now() - 1000,
},
},
}),
);
const store = ensureAuthProfileStore(agentDir);
expect(
(store.profiles[CODEX_CLI_PROFILE_ID] as { refresh: string })
.refresh,
).toBe("new-refresh");
},
{ prefix: "clawdbot-home-" },
); );
fs.utimesSync(codexAuthPath, new Date(), new Date());
const authPath = path.join(agentDir, "auth-profiles.json");
fs.writeFileSync(
authPath,
JSON.stringify({
version: 1,
profiles: {
[CODEX_CLI_PROFILE_ID]: {
type: "oauth",
provider: "openai-codex",
access: "same-access",
refresh: "old-refresh",
expires: Date.now() - 1000,
},
},
}),
);
const store = ensureAuthProfileStore(agentDir);
expect(
(store.profiles[CODEX_CLI_PROFILE_ID] as { refresh: string }).refresh,
).toBe("new-refresh");
} 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(home);
return await fn(base); } finally {
} finally { if (previousStateDir === undefined)
process.env.HOME = previousHome; delete process.env.CLAWDBOT_STATE_DIR;
if (previousStateDir === undefined) delete process.env.CLAWDBOT_STATE_DIR; else process.env.CLAWDBOT_STATE_DIR = previousStateDir;
else process.env.CLAWDBOT_STATE_DIR = previousStateDir; if (previousAgentDir === undefined)
if (previousAgentDir === undefined) delete process.env.CLAWDBOT_AGENT_DIR; 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; runEmbeddedPiAgentMock.mockClear();
try { return await fn(home);
runEmbeddedPiAgentMock.mockClear(); },
return await fn(base); { prefix: "clawdbot-typing-" },
} finally { );
process.env.HOME = previousHome;
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,
try { "bundled-skills",
vi.mocked(runEmbeddedPiAgent).mockReset(); );
return await fn(base); try {
} finally { vi.mocked(runEmbeddedPiAgent).mockReset();
process.env.HOME = previousHome; return await fn(home);
if (previousBundledSkills === undefined) { } finally {
delete process.env.CLAWDBOT_BUNDLED_SKILLS_DIR; if (previousBundledSkills === undefined) {
} else { delete process.env.CLAWDBOT_BUNDLED_SKILLS_DIR;
process.env.CLAWDBOT_BUNDLED_SKILLS_DIR = previousBundledSkills; } else {
} 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; vi.mocked(runEmbeddedPiAgent).mockReset();
try { return await fn(home);
vi.mocked(runEmbeddedPiAgent).mockReset(); },
return await fn(base); { prefix: "clawdbot-queue-" },
} finally { );
process.env.HOME = previousHome;
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 previousStateDir = process.env.CLAWDBOT_STATE_DIR;
const previousHomeDrive = process.env.HOMEDRIVE; const previousClawdisStateDir = process.env.CLAWDIS_STATE_DIR;
const previousHomePath = process.env.HOMEPATH; process.env.CLAWDBOT_STATE_DIR = join(home, ".clawdbot");
const previousStateDir = process.env.CLAWDBOT_STATE_DIR; process.env.CLAWDIS_STATE_DIR = join(home, ".clawdbot");
const previousClawdisStateDir = process.env.CLAWDIS_STATE_DIR; try {
process.env.HOME = base; vi.mocked(runEmbeddedPiAgent).mockClear();
process.env.CLAWDBOT_STATE_DIR = join(base, ".clawdbot"); vi.mocked(abortEmbeddedPiRun).mockClear();
process.env.CLAWDIS_STATE_DIR = join(base, ".clawdbot"); return await fn(home);
if (process.platform === "win32") { } finally {
process.env.USERPROFILE = base; if (previousStateDir === undefined) delete process.env.CLAWDBOT_STATE_DIR;
const driveMatch = base.match(/^([A-Za-z]:)(.*)$/); else process.env.CLAWDBOT_STATE_DIR = previousStateDir;
if (driveMatch) { if (previousClawdisStateDir === undefined)
process.env.HOMEDRIVE = driveMatch[1]; delete process.env.CLAWDIS_STATE_DIR;
process.env.HOMEPATH = driveMatch[2] || "\\"; else process.env.CLAWDIS_STATE_DIR = previousClawdisStateDir;
} }
} },
try { { prefix: "clawdbot-triggers-" },
vi.mocked(runEmbeddedPiAgent).mockClear(); );
vi.mocked(abortEmbeddedPiRun).mockClear();
return await fn(base);
} finally {
process.env.HOME = previousHome;
process.env.USERPROFILE = previousUserProfile;
process.env.HOMEDRIVE = previousHomeDrive;
process.env.HOMEPATH = previousHomePath;
process.env.CLAWDBOT_STATE_DIR = previousStateDir;
process.env.CLAWDIS_STATE_DIR = previousClawdisStateDir;
await fs.rm(base, { recursive: true, force: true });
}
} }
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,69 +226,66 @@ 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); vi.resetModules();
try { const { buildStatusMessage: buildStatusMessageDynamic } = await import(
vi.resetModules(); "./status.js"
const { buildStatusMessage: buildStatusMessageDynamic } = await import( );
"./status.js"
);
const sessionId = "sess-1"; const sessionId = "sess-1";
const logPath = path.join( const logPath = path.join(
dir, dir,
".clawdbot", ".clawdbot",
"agents", "agents",
"main", "main",
"sessions", "sessions",
`${sessionId}.jsonl`, `${sessionId}.jsonl`,
); );
fs.mkdirSync(path.dirname(logPath), { recursive: true }); fs.mkdirSync(path.dirname(logPath), { recursive: true });
fs.writeFileSync( fs.writeFileSync(
logPath, logPath,
[ [
JSON.stringify({ JSON.stringify({
type: "message", type: "message",
message: { message: {
role: "assistant", role: "assistant",
model: "claude-opus-4-5", model: "claude-opus-4-5",
usage: { usage: {
input: 1, input: 1,
output: 2, output: 2,
cacheRead: 1000, cacheRead: 1000,
cacheWrite: 0, cacheWrite: 0,
totalTokens: 1003, totalTokens: 1003,
},
}, },
}, }),
}), ].join("\n"),
].join("\n"), "utf-8",
"utf-8", );
);
const text = buildStatusMessageDynamic({ const text = buildStatusMessageDynamic({
agent: { agent: {
model: "anthropic/claude-opus-4-5", model: "anthropic/claude-opus-4-5",
contextTokens: 32_000, contextTokens: 32_000,
}, },
sessionEntry: { sessionEntry: {
sessionId, sessionId,
updatedAt: 0, updatedAt: 0,
totalTokens: 3, // would be wrong if cached prompt tokens exist totalTokens: 3, // would be wrong if cached prompt tokens exist
contextTokens: 32_000, contextTokens: 32_000,
}, },
sessionKey: "agent:main:main", sessionKey: "agent:main:main",
sessionScope: "per-sender", sessionScope: "per-sender",
queue: { mode: "collect", depth: 0 }, queue: { mode: "collect", depth: 0 },
includeTranscriptUsage: true, includeTranscriptUsage: true,
modelAuth: "api-key", modelAuth: "api-key",
}); });
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,94 +136,98 @@ 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-"), process.env.CLAWDBOT_STATE_DIR = path.join(tempHome, ".clawdbot");
); const agentDir = path.join(
try { process.env.CLAWDBOT_STATE_DIR,
setTempHome(tempHome); "agents",
process.env.CLAWDBOT_STATE_DIR = path.join(tempHome, ".clawdbot"); "main",
const agentDir = path.join( "agent",
process.env.CLAWDBOT_STATE_DIR, );
"agents", try {
"main", fs.mkdirSync(agentDir, { recursive: true, mode: 0o700 });
"agent", fs.writeFileSync(
); path.join(agentDir, "auth-profiles.json"),
fs.mkdirSync(agentDir, { recursive: true, mode: 0o700 }); `${JSON.stringify(
fs.writeFileSync( {
path.join(agentDir, "auth-profiles.json"), version: 1,
`${JSON.stringify( order: { anthropic: ["anthropic:default"] },
{ profiles: {
version: 1, "anthropic:default": {
order: { anthropic: ["anthropic:default"] }, type: "token",
profiles: { provider: "anthropic",
"anthropic:default": { token: "token-1",
type: "token", expires: Date.UTC(2100, 0, 1, 0, 0, 0),
provider: "anthropic", },
token: "token-1", },
expires: Date.UTC(2100, 0, 1, 0, 0, 0),
}, },
}, null,
}, 2,
null, )}\n`,
2, "utf8",
)}\n`, );
"utf8", const store = ensureAuthProfileStore(agentDir, {
); allowKeychainPrompt: false,
const store = ensureAuthProfileStore(agentDir, {
allowKeychainPrompt: false,
});
expect(listProfilesForProvider(store, "anthropic")).toContain(
"anthropic:default",
);
const makeResponse = (status: number, body: unknown): Response => {
const payload = typeof body === "string" ? body : JSON.stringify(body);
const headers =
typeof body === "string"
? undefined
: { "Content-Type": "application/json" };
return new Response(payload, { status, headers });
};
const mockFetch = vi.fn<
Parameters<typeof fetch>,
ReturnType<typeof fetch>
>(async (input, init) => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.toString()
: input.url;
if (url.includes("api.anthropic.com/api/oauth/usage")) {
const headers = (init?.headers ?? {}) as Record<string, string>;
expect(headers.Authorization).toBe("Bearer token-1");
return makeResponse(200, {
five_hour: { utilization: 20, resets_at: "2026-01-07T01:00:00Z" },
}); });
expect(listProfilesForProvider(store, "anthropic")).toContain(
"anthropic:default",
);
const makeResponse = (status: number, body: unknown): Response => {
const payload =
typeof body === "string" ? body : JSON.stringify(body);
const headers =
typeof body === "string"
? undefined
: { "Content-Type": "application/json" };
return new Response(payload, { status, headers });
};
const mockFetch = vi.fn<
Parameters<typeof fetch>,
ReturnType<typeof fetch>
>(async (input, init) => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.toString()
: input.url;
if (url.includes("api.anthropic.com/api/oauth/usage")) {
const headers = (init?.headers ?? {}) as Record<string, string>;
expect(headers.Authorization).toBe("Bearer token-1");
return makeResponse(200, {
five_hour: {
utilization: 20,
resets_at: "2026-01-07T01:00:00Z",
},
});
}
return makeResponse(404, "not found");
});
const summary = await loadProviderUsageSummary({
now: Date.UTC(2026, 0, 7, 0, 0, 0),
providers: ["anthropic"],
agentDir,
fetch: mockFetch,
});
expect(summary.providers).toHaveLength(1);
const claude = summary.providers[0];
expect(claude?.provider).toBe("anthropic");
expect(claude?.windows[0]?.label).toBe("5h");
expect(mockFetch).toHaveBeenCalled();
} finally {
if (previousStateDir === undefined)
delete process.env.CLAWDBOT_STATE_DIR;
else process.env.CLAWDBOT_STATE_DIR = previousStateDir;
} }
return makeResponse(404, "not found"); },
}); { prefix: "clawdbot-provider-usage-" },
);
const summary = await loadProviderUsageSummary({
now: Date.UTC(2026, 0, 7, 0, 0, 0),
providers: ["anthropic"],
agentDir,
fetch: mockFetch,
});
expect(summary.providers).toHaveLength(1);
const claude = summary.providers[0];
expect(claude?.provider).toBe("anthropic");
expect(claude?.windows[0]?.label).toBe("5h");
expect(mockFetch).toHaveBeenCalled();
} finally {
restoreHomeEnv(homeSnapshot);
if (stateSnapshot === undefined) delete process.env.CLAWDBOT_STATE_DIR;
else process.env.CLAWDBOT_STATE_DIR = stateSnapshot;
}
}); });
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
}
}
}