refactor(test): consolidate temp home + vitest setup
This commit is contained in:
37
.github/workflows/workflow-sanity.yml
vendored
Normal file
37
.github/workflows/workflow-sanity.yml
vendored
Normal 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
|
||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 });
|
);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
68
test/helpers/temp-home.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user