1import * as NodeOS from "node:os";
2
3import * as NodeServices from "@effect/platform-node/NodeServices";
4import { describe, expect, it } from "@effect/vitest";
5import * as Effect from "effect/Effect";
6import * as Path from "effect/Path";
7
8import {
9 claudeSignedOutMessage,
10 makeClaudeCapabilitiesCacheKey,
11 makeClaudeContinuationGroupKey,
12 makeClaudeEnvironment,
13 resolveClaudeHomePath,
14} from "./ClaudeHome.ts";
15
16it.layer(NodeServices.layer)("ClaudeHome", (it) => {
17 describe("Claude home resolution", () => {
18 it.effect("treats empty, ~/.claude, and the expanded default as the same Claude home", () =>
19 Effect.gen(function* () {
20 const path = yield* Path.Path;
21 const resolved = path.resolve(path.join(NodeOS.homedir(), ".claude"));
22
23 expect(yield* resolveClaudeHomePath({ homePath: "" })).toBe(resolved);
24 expect(yield* resolveClaudeHomePath({ homePath: "~/.claude" })).toBe(resolved);
25 expect(yield* resolveClaudeHomePath({ homePath: resolved })).toBe(resolved);
26 expect(yield* makeClaudeEnvironment({ homePath: "" })).toBe(process.env);
27
28 const key = `claude:home:${resolved}`;
29 expect(yield* makeClaudeContinuationGroupKey({ homePath: "" })).toBe(key);
30 expect(yield* makeClaudeContinuationGroupKey({ homePath: "~/.claude" })).toBe(key);
31 expect(yield* makeClaudeContinuationGroupKey({ homePath: resolved })).toBe(key);
32 }),
33 );
34
35 it.effect("resolves configured Claude HOME and stamps continuation/cache keys with it", () =>
36 Effect.gen(function* () {
37 const path = yield* Path.Path;
38 const homePath = "~/.claude-work";
39 const resolved = path.resolve(NodeOS.homedir(), ".claude-work");
40
41 expect(yield* resolveClaudeHomePath({ homePath })).toBe(resolved);
42 expect((yield* makeClaudeEnvironment({ homePath })).CLAUDE_CONFIG_DIR).toBe(resolved);
43 expect(yield* makeClaudeContinuationGroupKey({ homePath })).toBe(`claude:home:${resolved}`);
44 expect(yield* makeClaudeCapabilitiesCacheKey({ binaryPath: "claude", homePath })).toBe(
45 `claude\0${resolved}\0`,
46 );
47 }),
48 );
49
50 it.effect("uses inherited CLAUDE_CONFIG_DIR when homePath is empty", () =>
51 Effect.gen(function* () {
52 const path = yield* Path.Path;
53 const inherited = path.resolve("/tmp/claude-inherited");
54 const environment = { CLAUDE_CONFIG_DIR: inherited };
55
56 expect(yield* resolveClaudeHomePath({ homePath: "" }, environment)).toBe(inherited);
57 expect(yield* makeClaudeContinuationGroupKey({ homePath: "" }, environment)).toBe(
58 `claude:home:${inherited}`,
59 );
60
61 const explicit = path.resolve(NodeOS.homedir(), ".claude-work");
62 expect(yield* resolveClaudeHomePath({ homePath: "~/.claude-work" }, environment)).toBe(
63 explicit,
64 );
65 }),
66 );
67
68 it("points the signed-out hint at the configured Claude home", () => {
69 expect(claudeSignedOutMessage({ configDir: undefined, cwd: "/synthetic" })).toContain(
70 "run `claude auth login`",
71 );
72 const configDir = "/synthetic/Claude work's $literal";
73 const message = claudeSignedOutMessage({ configDir, cwd: "/synthetic/project" });
74 expect(message).toContain(`CLAUDE_CONFIG_DIR set to "${configDir}"`);
75 expect(message).not.toContain("CLAUDE_CONFIG_DIR=");
76 expect(message).toContain("then start a new thread");
77 });
78
79 it.effect("separates capability probes by cwd", () =>
80 Effect.gen(function* () {
81 const config = { binaryPath: "claude", homePath: "" };
82 const first = yield* makeClaudeCapabilitiesCacheKey(config, "/repo-a");
83 const second = yield* makeClaudeCapabilitiesCacheKey(config, "/repo-b");
84 expect(first).not.toBe(second);
85 }),
86 );
87 });
88});