refactor: unify env vars (UWF_HOME, OCAS_HOME) + env only in CLI (#37)
CI / check (pull_request) Failing after 3m6s

Breaking changes:
- UWF_STORAGE_ROOT → UWF_HOME
- WORKFLOW_STORAGE_ROOT removed (no fallback)
- OCAS_DIR → OCAS_HOME (aligned with ocas CLI)

Library functions no longer read process.env:
- util-agent/storage.ts: resolveStorageRoot(override), getGlobalCasDir(override)
- agent-hermes: isResumeDisabled(flag) pure function, CLI reads env
- agent-claude-code: CLI reads CLAUDE_MODEL and passes to agent

Fixes #37
This commit is contained in:
2026-06-04 05:12:05 +00:00
parent 84bdd81317
commit 6b7636b088
45 changed files with 394 additions and 333 deletions
@@ -4,37 +4,31 @@ import type { ThreadId } from "@united-workforce/protocol";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { getCachedSessionId, getCachePath, setCachedSessionId } from "../src/session-cache.js";
import { resolveStorageRoot } from "../src/storage.js";
import { getDefaultStorageRoot } from "../src/storage.js";
describe("session-cache", () => {
let originalStorageRoot: string;
let testStorageRoot: string;
beforeEach(async () => {
// Create a temporary test storage root
originalStorageRoot = resolveStorageRoot();
testStorageRoot = join(originalStorageRoot, "test-cache", `test-${Date.now()}`);
testStorageRoot = join(getDefaultStorageRoot(), "test-cache", `test-${Date.now()}`);
await mkdir(testStorageRoot, { recursive: true });
// Override the storage root for testing
process.env.WORKFLOW_STORAGE_ROOT = testStorageRoot;
});
afterEach(async () => {
// Clean up test storage root
await rm(testStorageRoot, { recursive: true, force: true });
delete process.env.WORKFLOW_STORAGE_ROOT;
});
describe("getCachePath", () => {
test("returns agent-specific file path", () => {
const path = getCachePath("claude-code");
const path = getCachePath("claude-code", testStorageRoot);
expect(path).toMatch(/\/cache\/claude-code-sessions\.json$/);
});
test("returns different paths for different agents", () => {
const pathClaudeCode = getCachePath("claude-code");
const pathHermes = getCachePath("hermes");
const pathClaudeCode = getCachePath("claude-code", testStorageRoot);
const pathHermes = getCachePath("hermes", testStorageRoot);
expect(pathClaudeCode).not.toBe(pathHermes);
expect(pathClaudeCode).toMatch(/claude-code-sessions\.json$/);
@@ -42,8 +36,8 @@ describe("session-cache", () => {
});
test("handles agent names with special characters", () => {
const path1 = getCachePath("my-agent");
const path2 = getCachePath("my_agent");
const path1 = getCachePath("my-agent", testStorageRoot);
const path2 = getCachePath("my_agent", testStorageRoot);
expect(path1).toMatch(/my-agent-sessions\.json$/);
expect(path2).toMatch(/my_agent-sessions\.json$/);
@@ -56,12 +50,12 @@ describe("session-cache", () => {
test("sessions are isolated per agent", async () => {
// Cache different session IDs for each agent
await setCachedSessionId("claude-code", threadId, role, "session-cc-001");
await setCachedSessionId("hermes", threadId, role, "session-hermes-001");
await setCachedSessionId("claude-code", threadId, role, "session-cc-001", testStorageRoot);
await setCachedSessionId("hermes", threadId, role, "session-hermes-001", testStorageRoot);
// Each agent should retrieve its own session ID
const sessionCC = await getCachedSessionId("claude-code", threadId, role);
const sessionHermes = await getCachedSessionId("hermes", threadId, role);
const sessionCC = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
const sessionHermes = await getCachedSessionId("hermes", threadId, role, testStorageRoot);
expect(sessionCC).toBe("session-cc-001");
expect(sessionHermes).toBe("session-hermes-001");
@@ -69,30 +63,30 @@ describe("session-cache", () => {
test("updating one agent's cache does not affect another", async () => {
// Set initial sessions for both agents
await setCachedSessionId("claude-code", threadId, role, "session-cc-001");
await setCachedSessionId("hermes", threadId, role, "session-hermes-001");
await setCachedSessionId("claude-code", threadId, role, "session-cc-001", testStorageRoot);
await setCachedSessionId("hermes", threadId, role, "session-hermes-001", testStorageRoot);
// Update claude-code's session
await setCachedSessionId("claude-code", threadId, role, "session-cc-002");
await setCachedSessionId("claude-code", threadId, role, "session-cc-002", testStorageRoot);
// Hermes's session should remain unchanged
const sessionHermes = await getCachedSessionId("hermes", threadId, role);
const sessionHermes = await getCachedSessionId("hermes", threadId, role, testStorageRoot);
expect(sessionHermes).toBe("session-hermes-001");
// Claude-code should have the new session
const sessionCC = await getCachedSessionId("claude-code", threadId, role);
const sessionCC = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
expect(sessionCC).toBe("session-cc-002");
});
test("missing session returns null for specific agent", async () => {
const session = await getCachedSessionId("claude-code", threadId, role);
const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
expect(session).toBeNull();
});
test("empty session ID is treated as missing", async () => {
await setCachedSessionId("claude-code", threadId, role, "");
await setCachedSessionId("claude-code", threadId, role, "", testStorageRoot);
const session = await getCachedSessionId("claude-code", threadId, role);
const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
expect(session).toBeNull();
});
});
@@ -102,14 +96,14 @@ describe("session-cache", () => {
const role = "developer";
test("cache directory is created if missing", async () => {
const cachePath = getCachePath("claude-code");
const cachePath = getCachePath("claude-code", testStorageRoot);
const cacheDir = dirname(cachePath);
// Ensure cache dir doesn't exist
await rm(cacheDir, { recursive: true, force: true });
// Write a session
await setCachedSessionId("claude-code", threadId, role, "session-001");
await setCachedSessionId("claude-code", threadId, role, "session-001", testStorageRoot);
// Cache directory should be created
const stats = await stat(cacheDir);
@@ -118,12 +112,12 @@ describe("session-cache", () => {
test("multiple agents create separate cache files", async () => {
// Cache sessions for multiple agents
await setCachedSessionId("claude-code", threadId, role, "session-cc-001");
await setCachedSessionId("hermes", threadId, role, "session-hermes-001");
await setCachedSessionId("claude-code", threadId, role, "session-cc-001", testStorageRoot);
await setCachedSessionId("hermes", threadId, role, "session-hermes-001", testStorageRoot);
// Separate cache files should exist
const pathCC = getCachePath("claude-code");
const pathHermes = getCachePath("hermes");
const pathCC = getCachePath("claude-code", testStorageRoot);
const pathHermes = getCachePath("hermes", testStorageRoot);
const contentCC = JSON.parse(await readFile(pathCC, "utf8")) as Record<string, string>;
const contentHermes = JSON.parse(await readFile(pathHermes, "utf8")) as Record<
@@ -137,10 +131,10 @@ describe("session-cache", () => {
test("atomic writes prevent partial reads", async () => {
// Write a session
await setCachedSessionId("claude-code", threadId, role, "session-001");
await setCachedSessionId("claude-code", threadId, role, "session-001", testStorageRoot);
// The final file should exist (no .tmp files left behind)
const cachePath = getCachePath("claude-code");
const cachePath = getCachePath("claude-code", testStorageRoot);
const dir = dirname(cachePath);
const files = await readdir(dir);
@@ -155,7 +149,7 @@ describe("session-cache", () => {
test("old agent-sessions.json is ignored", async () => {
// Create old agent-sessions.json file
const oldCachePath = join(resolveStorageRoot(), "cache", "agent-sessions.json");
const oldCachePath = join(testStorageRoot, "cache", "agent-sessions.json");
await mkdir(dirname(oldCachePath), { recursive: true });
await writeFile(
oldCachePath,
@@ -166,7 +160,7 @@ describe("session-cache", () => {
);
// Query with the new per-agent cache
const session = await getCachedSessionId("claude-code", threadId, role);
const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
// Should return null (old cache is ignored)
expect(session).toBeNull();
@@ -174,7 +168,7 @@ describe("session-cache", () => {
test("new per-agent cache takes precedence", async () => {
// Create both old and new cache files
const oldPath = join(resolveStorageRoot(), "cache", "agent-sessions.json");
const oldPath = join(testStorageRoot, "cache", "agent-sessions.json");
await mkdir(dirname(oldPath), { recursive: true });
await writeFile(
oldPath,
@@ -184,10 +178,10 @@ describe("session-cache", () => {
"utf8",
);
await setCachedSessionId("claude-code", threadId, role, "new-session");
await setCachedSessionId("claude-code", threadId, role, "new-session", testStorageRoot);
// The new per-agent cache value should be returned
const session = await getCachedSessionId("claude-code", threadId, role);
const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
expect(session).toBe("new-session");
});
});
@@ -198,29 +192,29 @@ describe("session-cache", () => {
test("invalid JSON in cache file returns empty cache", async () => {
// Create a corrupted cache file
const cachePath = getCachePath("claude-code");
const cachePath = getCachePath("claude-code", testStorageRoot);
await mkdir(dirname(cachePath), { recursive: true });
await writeFile(cachePath, "{ invalid json }", "utf8");
// Should return null (treating corrupted cache as empty)
const session = await getCachedSessionId("claude-code", threadId, role);
const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
expect(session).toBeNull();
});
test("non-object JSON in cache file returns empty cache", async () => {
// Create a cache file with non-object JSON
const cachePath = getCachePath("claude-code");
const cachePath = getCachePath("claude-code", testStorageRoot);
await mkdir(dirname(cachePath), { recursive: true });
await writeFile(cachePath, JSON.stringify(["not", "an", "object"]), "utf8");
// Should return null
const session = await getCachedSessionId("claude-code", threadId, role);
const session = await getCachedSessionId("claude-code", threadId, role, testStorageRoot);
expect(session).toBeNull();
});
test("cache entries with non-string values are ignored", async () => {
// Create a cache file with mixed types
const cachePath = getCachePath("claude-code");
const cachePath = getCachePath("claude-code", testStorageRoot);
const cacheData = {
"thread1:role1": "valid-session",
"thread2:role2": 12345, // number
@@ -231,13 +225,33 @@ describe("session-cache", () => {
await writeFile(cachePath, JSON.stringify(cacheData), "utf8");
// Valid string entries should be returned
const session1 = await getCachedSessionId("claude-code", "thread1" as ThreadId, "role1");
const session1 = await getCachedSessionId(
"claude-code",
"thread1" as ThreadId,
"role1",
testStorageRoot,
);
expect(session1).toBe("valid-session");
// Invalid entries should return null
const session2 = await getCachedSessionId("claude-code", "thread2" as ThreadId, "role2");
const session3 = await getCachedSessionId("claude-code", "thread3" as ThreadId, "role3");
const session4 = await getCachedSessionId("claude-code", "thread4" as ThreadId, "role4");
const session2 = await getCachedSessionId(
"claude-code",
"thread2" as ThreadId,
"role2",
testStorageRoot,
);
const session3 = await getCachedSessionId(
"claude-code",
"thread3" as ThreadId,
"role3",
testStorageRoot,
);
const session4 = await getCachedSessionId(
"claude-code",
"thread4" as ThreadId,
"role4",
testStorageRoot,
);
expect(session2).toBeNull();
expect(session3).toBeNull();