diff --git a/CLAUDE.md b/CLAUDE.md index f7702cf..af30ad8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -270,7 +270,7 @@ node scripts/publish-all.mjs --dry-run # preview without publishing examples/solve-issue.yaml — write a workflow YAML definition │ uwf workflow put ▼ -~/.uncaged/workflow/cas/ — Workflow stored as CAS node +~/.uncaged/json-cas/ — Workflow stored as CAS node (unified CAS store) ~/.uncaged/workflow/registry.yaml — name → hash mapping updated │ uwf thread start -p "..." ▼ diff --git a/packages/cli-workflow/README.md b/packages/cli-workflow/README.md index 92f7eaf..2f9dc92 100644 --- a/packages/cli-workflow/README.md +++ b/packages/cli-workflow/README.md @@ -209,4 +209,13 @@ src/ | `~/.uncaged/workflow/.env` | API keys (referenced by `apiKeyEnv` in config) | | `~/.uncaged/workflow/registry.yaml` | Workflow name → CAS hash | | `~/.uncaged/workflow/threads.yaml` | Active thread head pointers | -| `~/.uncaged/workflow/cas/` | Content-addressed node storage | +| `~/.uncaged/json-cas/` | Content-addressed node storage (unified CAS store, shared with `json-cas` CLI) | + +### Environment Variables + +| Variable | Purpose | Default | +|----------|---------|---------| +| `UNCAGED_CAS_DIR` | Override the global CAS directory location | `~/.uncaged/json-cas` | +| `UNCAGED_WORKFLOW_STORAGE_ROOT` | Internal override for workflow metadata storage | `~/.uncaged/workflow` | +| `WORKFLOW_STORAGE_ROOT` | User override for workflow metadata storage | `~/.uncaged/workflow` | + diff --git a/packages/cli-workflow/src/__tests__/adapter-json-roundtrip.test.ts b/packages/cli-workflow/src/__tests__/adapter-json-roundtrip.test.ts index 250241d..711ddb7 100644 --- a/packages/cli-workflow/src/__tests__/adapter-json-roundtrip.test.ts +++ b/packages/cli-workflow/src/__tests__/adapter-json-roundtrip.test.ts @@ -129,7 +129,11 @@ describe("C1: adapter JSON round-trip integration", () => { { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], - env: { ...process.env, WORKFLOW_STORAGE_ROOT: tmpDir }, + env: { + ...process.env, + WORKFLOW_STORAGE_ROOT: tmpDir, + UNCAGED_CAS_DIR: casDir, + }, cwd: tmpDir, timeout: 30000, }, diff --git a/packages/cli-workflow/src/__tests__/cas-exit-code.test.ts b/packages/cli-workflow/src/__tests__/cas-exit-code.test.ts index dc53769..065eb7c 100644 --- a/packages/cli-workflow/src/__tests__/cas-exit-code.test.ts +++ b/packages/cli-workflow/src/__tests__/cas-exit-code.test.ts @@ -6,14 +6,22 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { cmdCasPutText } from "../commands/cas.js"; let storageRoot: string; +let casDir: string; let uwfPath: string; +let originalEnv: string | undefined; beforeEach(async () => { storageRoot = join( tmpdir(), `uwf-cas-exit-test-${Date.now()}-${Math.random().toString(36).slice(2)}`, ); + casDir = join(storageRoot, "cas"); await mkdir(storageRoot, { recursive: true }); + await mkdir(casDir, { recursive: true }); + + // Set UNCAGED_CAS_DIR for this test + originalEnv = process.env.UNCAGED_CAS_DIR; + process.env.UNCAGED_CAS_DIR = casDir; // Find the uwf CLI path uwfPath = join(__dirname, "../../src/cli.ts"); @@ -21,6 +29,13 @@ beforeEach(async () => { afterEach(async () => { await rm(storageRoot, { recursive: true, force: true }); + + // Restore original environment + if (originalEnv === undefined) { + delete process.env.UNCAGED_CAS_DIR; + } else { + process.env.UNCAGED_CAS_DIR = originalEnv; + } }); type ExecResult = { @@ -32,7 +47,11 @@ type ExecResult = { function execUwf(args: string[]): ExecResult { try { const stdout = execSync(`bun ${uwfPath} ${args.join(" ")}`, { - env: { ...process.env, WORKFLOW_STORAGE_ROOT: storageRoot }, + env: { + ...process.env, + WORKFLOW_STORAGE_ROOT: storageRoot, + UNCAGED_CAS_DIR: casDir, + }, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], }); diff --git a/packages/cli-workflow/src/__tests__/current-role.test.ts b/packages/cli-workflow/src/__tests__/current-role.test.ts index 05652e2..3a4d9bd 100644 --- a/packages/cli-workflow/src/__tests__/current-role.test.ts +++ b/packages/cli-workflow/src/__tests__/current-role.test.ts @@ -206,6 +206,8 @@ async function insertStepNode( describe("currentRole field", () => { let tmpDir: string; let storageRoot: string; + let casDir: string; + let originalEnv: string | undefined; async function setup() { tmpDir = join( @@ -213,13 +215,25 @@ describe("currentRole field", () => { `uwf-test-current-role-${Date.now()}-${Math.random().toString(36).slice(2)}`, ); storageRoot = join(tmpDir, "storage"); + casDir = join(tmpDir, "cas"); await mkdir(storageRoot, { recursive: true }); + await mkdir(casDir, { recursive: true }); + + // Set UNCAGED_CAS_DIR for this test + originalEnv = process.env.UNCAGED_CAS_DIR; + process.env.UNCAGED_CAS_DIR = casDir; } async function teardown() { if (tmpDir) { await rm(tmpDir, { recursive: true, force: true }); } + // Restore original environment + if (originalEnv === undefined) { + delete process.env.UNCAGED_CAS_DIR; + } else { + process.env.UNCAGED_CAS_DIR = originalEnv; + } } // T1: idle at start — currentRole = first role from graph diff --git a/packages/cli-workflow/src/__tests__/step-read.test.ts b/packages/cli-workflow/src/__tests__/step-read.test.ts index d16d45d..2e9fae9 100644 --- a/packages/cli-workflow/src/__tests__/step-read.test.ts +++ b/packages/cli-workflow/src/__tests__/step-read.test.ts @@ -66,13 +66,21 @@ function generateContent(size: number, prefix = "Content"): string { // ── fixture ─────────────────────────────────────────────────────────────────── let tmpDir: string; +let originalEnv: string | undefined; beforeEach(async () => { tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-step-read-test-")); + originalEnv = process.env.UNCAGED_CAS_DIR; }); afterEach(async () => { await rm(tmpDir, { recursive: true, force: true }); + // Restore original environment + if (originalEnv === undefined) { + delete process.env.UNCAGED_CAS_DIR; + } else { + process.env.UNCAGED_CAS_DIR = originalEnv; + } }); // ── step read tests ─────────────────────────────────────────────────────────── @@ -80,7 +88,10 @@ afterEach(async () => { describe("step read", () => { test("test 1: basic single-step read with 3 turns", async () => { const casDir = join(tmpDir, "cas"); + process.env.UNCAGED_CAS_DIR = casDir; await mkdir(casDir, { recursive: true }); + process.env.UNCAGED_CAS_DIR = casDir; + process.env.UNCAGED_CAS_DIR = casDir; const store = createFsStore(casDir); const schemas = await registerUwfSchemas(store); const detailSchemas = await registerDetailSchemas(store); @@ -166,7 +177,9 @@ describe("step read", () => { test("test 2: quota enforcement - multiple turns", async () => { const casDir = join(tmpDir, "cas"); + process.env.UNCAGED_CAS_DIR = casDir; await mkdir(casDir, { recursive: true }); + process.env.UNCAGED_CAS_DIR = casDir; const store = createFsStore(casDir); const schemas = await registerUwfSchemas(store); const detailSchemas = await registerDetailSchemas(store); @@ -250,7 +263,9 @@ describe("step read", () => { test("test 3: minimal quota edge case - always show at least one turn", async () => { const casDir = join(tmpDir, "cas"); + process.env.UNCAGED_CAS_DIR = casDir; await mkdir(casDir, { recursive: true }); + process.env.UNCAGED_CAS_DIR = casDir; const store = createFsStore(casDir); const schemas = await registerUwfSchemas(store); const detailSchemas = await registerDetailSchemas(store); @@ -325,7 +340,9 @@ describe("step read", () => { test("test 4: step with no detail field", async () => { const casDir = join(tmpDir, "cas"); + process.env.UNCAGED_CAS_DIR = casDir; await mkdir(casDir, { recursive: true }); + process.env.UNCAGED_CAS_DIR = casDir; const store = createFsStore(casDir); const schemas = await registerUwfSchemas(store); @@ -384,7 +401,9 @@ describe("step read", () => { test("test 5: step with detail but no turns array", async () => { const casDir = join(tmpDir, "cas"); + process.env.UNCAGED_CAS_DIR = casDir; await mkdir(casDir, { recursive: true }); + process.env.UNCAGED_CAS_DIR = casDir; const store = createFsStore(casDir); const schemas = await registerUwfSchemas(store); await registerDetailSchemas(store); @@ -460,7 +479,9 @@ describe("step read", () => { test("test 6: displays role and tool calls in turn body", async () => { const casDir = join(tmpDir, "cas"); + process.env.UNCAGED_CAS_DIR = casDir; await mkdir(casDir, { recursive: true }); + process.env.UNCAGED_CAS_DIR = casDir; const store = createFsStore(casDir); const schemas = await registerUwfSchemas(store); const detailSchemas = await registerDetailSchemas(store); @@ -532,7 +553,9 @@ describe("step read", () => { test("test 7: turn content with special characters", async () => { const casDir = join(tmpDir, "cas"); + process.env.UNCAGED_CAS_DIR = casDir; await mkdir(casDir, { recursive: true }); + process.env.UNCAGED_CAS_DIR = casDir; const store = createFsStore(casDir); const schemas = await registerUwfSchemas(store); const detailSchemas = await registerDetailSchemas(store); diff --git a/packages/cli-workflow/src/__tests__/step-show-json.test.ts b/packages/cli-workflow/src/__tests__/step-show-json.test.ts index feb505f..26eeaaf 100644 --- a/packages/cli-workflow/src/__tests__/step-show-json.test.ts +++ b/packages/cli-workflow/src/__tests__/step-show-json.test.ts @@ -125,15 +125,23 @@ async function createTestStep( describe("cmdStepShow JSON serialization", () => { let testDir: string; let casDir: string; + let originalEnv: string | undefined; beforeEach(async () => { testDir = await mkdtemp(join(tmpdir(), "uwf-test-")); casDir = join(testDir, "cas"); await mkdir(casDir, { recursive: true }); + originalEnv = process.env.UNCAGED_CAS_DIR; + process.env.UNCAGED_CAS_DIR = casDir; }); afterEach(async () => { await rm(testDir, { recursive: true, force: true }); + if (originalEnv === undefined) { + delete process.env.UNCAGED_CAS_DIR; + } else { + process.env.UNCAGED_CAS_DIR = originalEnv; + } }); test("escapes newlines in tool call args", async () => { diff --git a/packages/cli-workflow/src/__tests__/step-timing.test.ts b/packages/cli-workflow/src/__tests__/step-timing.test.ts index 8fe713b..24ae758 100644 --- a/packages/cli-workflow/src/__tests__/step-timing.test.ts +++ b/packages/cli-workflow/src/__tests__/step-timing.test.ts @@ -63,13 +63,22 @@ async function registerDetailSchemas(store: ReturnType) { // ── fixture ────────────────────────────────────────────────────────────────── let tmpDir: string; +let originalEnv: string | undefined; beforeEach(async () => { tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-step-timing-test-")); + originalEnv = process.env.UNCAGED_CAS_DIR; + process.env.UNCAGED_CAS_DIR = join(tmpDir, "cas"); + await mkdir(process.env.UNCAGED_CAS_DIR, { recursive: true }); }); afterEach(async () => { await rm(tmpDir, { recursive: true, force: true }); + if (originalEnv === undefined) { + delete process.env.UNCAGED_CAS_DIR; + } else { + process.env.UNCAGED_CAS_DIR = originalEnv; + } }); // ── 1. Protocol types (compile-time) ───────────────────────────────────────── diff --git a/packages/cli-workflow/src/__tests__/store-global-cas.test.ts b/packages/cli-workflow/src/__tests__/store-global-cas.test.ts new file mode 100644 index 0000000..07aacbf --- /dev/null +++ b/packages/cli-workflow/src/__tests__/store-global-cas.test.ts @@ -0,0 +1,224 @@ +import { mkdir, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { createUwfStore, getCasDir, getGlobalCasDir } from "../store.js"; + +describe("Global CAS directory", () => { + let tmpDir: string; + let originalEnv: string | undefined; + + beforeEach(async () => { + tmpDir = join(tmpdir(), `uwf-test-global-cas-${Date.now()}`); + await mkdir(tmpDir, { recursive: true }); + originalEnv = process.env.UNCAGED_CAS_DIR; + }); + + afterEach(async () => { + if (tmpDir) { + await rm(tmpDir, { recursive: true, force: true }); + } + if (originalEnv === undefined) { + delete process.env.UNCAGED_CAS_DIR; + } else { + process.env.UNCAGED_CAS_DIR = originalEnv; + } + }); + + test("getGlobalCasDir returns default path when no env var set", () => { + delete process.env.UNCAGED_CAS_DIR; + const casDir = getGlobalCasDir(); + // Should return ~/.uncaged/json-cas + expect(casDir).toContain(".uncaged"); + expect(casDir).toContain("json-cas"); + }); + + test("getGlobalCasDir respects UNCAGED_CAS_DIR environment variable", () => { + const customPath = join(tmpDir, "custom-cas"); + process.env.UNCAGED_CAS_DIR = customPath; + const casDir = getGlobalCasDir(); + expect(casDir).toBe(customPath); + }); + + test("getGlobalCasDir ignores empty UNCAGED_CAS_DIR", () => { + process.env.UNCAGED_CAS_DIR = ""; + const casDir = getGlobalCasDir(); + expect(casDir).toContain(".uncaged"); + expect(casDir).toContain("json-cas"); + }); + + test("getCasDir is deprecated but still works for backward compatibility", () => { + const storageRoot = join(tmpDir, "storage"); + const casDir = getCasDir(storageRoot); + expect(casDir).toBe(join(storageRoot, "cas")); + }); + + test("createUwfStore uses global CAS directory", async () => { + const globalCasDir = join(tmpDir, "global-cas"); + process.env.UNCAGED_CAS_DIR = globalCasDir; + + const storageRoot = join(tmpDir, "storage"); + await mkdir(storageRoot, { recursive: true }); + + const uwf = await createUwfStore(storageRoot); + + // Verify the store was created in the global CAS directory + expect(uwf.storageRoot).toBe(storageRoot); + expect(uwf.store).toBeDefined(); + expect(uwf.schemas).toBeDefined(); + + // The global CAS directory should be created + const { stat } = await import("node:fs/promises"); + const stats = await stat(globalCasDir); + expect(stats.isDirectory()).toBe(true); + }); + + test("createUwfStore creates global CAS directory if it does not exist", async () => { + const globalCasDir = join(tmpDir, "new-global-cas"); + process.env.UNCAGED_CAS_DIR = globalCasDir; + + const storageRoot = join(tmpDir, "storage"); + await mkdir(storageRoot, { recursive: true }); + + await createUwfStore(storageRoot); + + // Verify the directory was created + const { stat } = await import("node:fs/promises"); + const stats = await stat(globalCasDir); + expect(stats.isDirectory()).toBe(true); + }); + + test("multiple uwfStore instances share the same global CAS filesystem", async () => { + const globalCasDir = join(tmpDir, "shared-cas"); + process.env.UNCAGED_CAS_DIR = globalCasDir; + + const storageRoot1 = join(tmpDir, "storage1"); + const storageRoot2 = join(tmpDir, "storage2"); + await mkdir(storageRoot1, { recursive: true }); + await mkdir(storageRoot2, { recursive: true }); + + const uwf1 = await createUwfStore(storageRoot1); + const uwf2 = await createUwfStore(storageRoot2); + + // Both should use the same global CAS directory + expect(uwf1.store).toBeDefined(); + expect(uwf2.store).toBeDefined(); + + // Store a node in the first store + const testData = { test: "data" }; + const _hash = uwf1.store.put(uwf1.schemas.text, JSON.stringify(testData)); + + // Both stores share the same CAS filesystem directory + // Since schemas are registered idempotently, they should have the same hash + expect(uwf2.schemas.text).toBe(uwf1.schemas.text); + + // Verify the CAS files are written to the shared directory + const { readdir } = await import("node:fs/promises"); + const files = await readdir(globalCasDir); + expect(files.length).toBeGreaterThan(0); + }); + + test("workflow metadata remains in storageRoot, not global CAS", async () => { + const globalCasDir = join(tmpDir, "global-cas"); + process.env.UNCAGED_CAS_DIR = globalCasDir; + + const storageRoot = join(tmpDir, "storage"); + await mkdir(storageRoot, { recursive: true }); + + const _uwf = await createUwfStore(storageRoot); + + // Write workflow registry file + const { saveWorkflowRegistry } = await import("../store.js"); + await saveWorkflowRegistry(storageRoot, { "test-workflow": "ABC123" }); + + // Verify registry is in storageRoot, not global CAS + const { readFile } = await import("node:fs/promises"); + const registryPath = join(storageRoot, "workflows.yaml"); + const content = await readFile(registryPath, "utf8"); + expect(content).toContain("test-workflow"); + expect(content).toContain("ABC123"); + + // Verify registry is NOT in global CAS directory + const globalRegistryPath = join(globalCasDir, "workflows.yaml"); + await expect(readFile(globalRegistryPath, "utf8")).rejects.toThrow(); + }); + + test("thread metadata remains in storageRoot", async () => { + const globalCasDir = join(tmpDir, "global-cas"); + process.env.UNCAGED_CAS_DIR = globalCasDir; + + const storageRoot = join(tmpDir, "storage"); + await mkdir(storageRoot, { recursive: true }); + + await createUwfStore(storageRoot); + + // Write threads index + const { saveThreadsIndex } = await import("../store.js"); + await saveThreadsIndex(storageRoot, { "thread-123": "hash-456" }); + + // Verify threads.yaml is in storageRoot, not global CAS + const { readFile } = await import("node:fs/promises"); + const threadsPath = join(storageRoot, "threads.yaml"); + const content = await readFile(threadsPath, "utf8"); + expect(content).toContain("thread-123"); + expect(content).toContain("hash-456"); + + // Verify threads.yaml is NOT in global CAS directory + const globalThreadsPath = join(globalCasDir, "threads.yaml"); + await expect(readFile(globalThreadsPath, "utf8")).rejects.toThrow(); + }); + + test("history remains in storageRoot", async () => { + const globalCasDir = join(tmpDir, "global-cas"); + process.env.UNCAGED_CAS_DIR = globalCasDir; + + const storageRoot = join(tmpDir, "storage"); + await mkdir(storageRoot, { recursive: true }); + + await createUwfStore(storageRoot); + + // Write history + const { appendThreadHistory } = await import("../store.js"); + await appendThreadHistory(storageRoot, { + thread: "thread-123" as any, + workflow: "workflow-456", + head: "hash-789", + completedAt: Date.now(), + reason: "completed", + }); + + // Verify history.jsonl is in storageRoot, not global CAS + const { readFile } = await import("node:fs/promises"); + const historyPath = join(storageRoot, "history.jsonl"); + const content = await readFile(historyPath, "utf8"); + expect(content).toContain("thread-123"); + expect(content).toContain("workflow-456"); + + // Verify history.jsonl is NOT in global CAS directory + const globalHistoryPath = join(globalCasDir, "history.jsonl"); + await expect(readFile(globalHistoryPath, "utf8")).rejects.toThrow(); + }); + + test("CAS nodes are stored in global directory", async () => { + const globalCasDir = join(tmpDir, "global-cas"); + process.env.UNCAGED_CAS_DIR = globalCasDir; + + const storageRoot = join(tmpDir, "storage"); + await mkdir(storageRoot, { recursive: true }); + + const uwf = await createUwfStore(storageRoot); + + // Store a CAS node + const testPayload = JSON.stringify({ test: "node" }); + const _hash = uwf.store.put(uwf.schemas.text, testPayload); + + // Verify the node is in global CAS directory + const { readdir } = await import("node:fs/promises"); + const files = await readdir(globalCasDir); + expect(files.length).toBeGreaterThan(0); + + // Verify the node is NOT in the old storageRoot/cas location + const oldCasDir = join(storageRoot, "cas"); + await expect(readdir(oldCasDir)).rejects.toThrow(); + }); +}); diff --git a/packages/cli-workflow/src/__tests__/thread-list-filters.test.ts b/packages/cli-workflow/src/__tests__/thread-list-filters.test.ts index 2d7ee2d..e69328d 100644 --- a/packages/cli-workflow/src/__tests__/thread-list-filters.test.ts +++ b/packages/cli-workflow/src/__tests__/thread-list-filters.test.ts @@ -15,6 +15,8 @@ import { appendThreadHistory, createUwfStore, saveThreadsIndex } from "../store. async function makeUwfStore(storageRoot: string): Promise { const casDir = join(storageRoot, "cas"); await mkdir(casDir, { recursive: true }); + // Set UNCAGED_CAS_DIR to use the test's CAS directory + process.env.UNCAGED_CAS_DIR = casDir; return createUwfStore(storageRoot); } diff --git a/packages/cli-workflow/src/__tests__/thread-location.test.ts b/packages/cli-workflow/src/__tests__/thread-location.test.ts index 10f5230..bf297bb 100644 --- a/packages/cli-workflow/src/__tests__/thread-location.test.ts +++ b/packages/cli-workflow/src/__tests__/thread-location.test.ts @@ -9,17 +9,31 @@ import { createUwfStore } from "../store.js"; describe("Thread and edge location integration", () => { let tmpDir: string; let storageRoot: string; + let casDir: string; + let originalEnv: string | undefined; async function setupTestEnv() { tmpDir = join(tmpdir(), `uwf-test-location-${Date.now()}`); storageRoot = join(tmpDir, "storage"); + casDir = join(tmpDir, "cas"); await mkdir(storageRoot, { recursive: true }); + await mkdir(casDir, { recursive: true }); + + // Set UNCAGED_CAS_DIR for this test + originalEnv = process.env.UNCAGED_CAS_DIR; + process.env.UNCAGED_CAS_DIR = casDir; } async function teardown() { if (tmpDir) { await rm(tmpDir, { recursive: true, force: true }); } + // Restore original environment + if (originalEnv === undefined) { + delete process.env.UNCAGED_CAS_DIR; + } else { + process.env.UNCAGED_CAS_DIR = originalEnv; + } } test("thread start captures cwd in StartNode", async () => { diff --git a/packages/cli-workflow/src/__tests__/thread-read-quota.test.ts b/packages/cli-workflow/src/__tests__/thread-read-quota.test.ts index 2634863..17b8585 100644 --- a/packages/cli-workflow/src/__tests__/thread-read-quota.test.ts +++ b/packages/cli-workflow/src/__tests__/thread-read-quota.test.ts @@ -67,13 +67,22 @@ function generateContent(size: number, prefix = "Content"): string { // ── fixture ─────────────────────────────────────────────────────────────────── let tmpDir: string; +let originalEnv: string | undefined; beforeEach(async () => { tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-quota-test-")); + originalEnv = process.env.UNCAGED_CAS_DIR; + process.env.UNCAGED_CAS_DIR = join(tmpDir, "cas"); + await mkdir(process.env.UNCAGED_CAS_DIR, { recursive: true }); }); afterEach(async () => { await rm(tmpDir, { recursive: true, force: true }); + if (originalEnv === undefined) { + delete process.env.UNCAGED_CAS_DIR; + } else { + process.env.UNCAGED_CAS_DIR = originalEnv; + } }); // ── thread read quota enforcement ───────────────────────────────────────────── @@ -143,7 +152,7 @@ describe("thread read --quota flag", () => { agent: "uwf-test", startedAtMs: 1000000000000, completedAtMs: 1000000005000, - assembledPrompt: null, + assembledPrompt: null, }); steps.push(stepHash); } @@ -339,7 +348,7 @@ describe("thread read --quota flag", () => { agent: "uwf-test", startedAtMs: 1000000000000, completedAtMs: 1000000005000, - assembledPrompt: null, + assembledPrompt: null, }); steps.push(stepHash); } @@ -497,7 +506,7 @@ describe("thread read --quota flag", () => { agent: "uwf-test", startedAtMs: 1000000000000, completedAtMs: 1000000005000, - assembledPrompt: null, + assembledPrompt: null, }); steps.push(stepHash); } @@ -579,7 +588,7 @@ describe("thread read --quota flag", () => { agent: "uwf-test", startedAtMs: 1000000000000, completedAtMs: 1000000005000, - assembledPrompt: null, + assembledPrompt: null, }); steps.push(stepHash); } diff --git a/packages/cli-workflow/src/__tests__/thread-start-cwd-cli.test.ts b/packages/cli-workflow/src/__tests__/thread-start-cwd-cli.test.ts index 27a35c8..2da3ff0 100644 --- a/packages/cli-workflow/src/__tests__/thread-start-cwd-cli.test.ts +++ b/packages/cli-workflow/src/__tests__/thread-start-cwd-cli.test.ts @@ -10,17 +10,31 @@ import { createUwfStore, loadThreadsIndex } from "../store.js"; describe("thread start --cwd CLI option", () => { let tmpDir: string; let storageRoot: string; + let casDir: string; + let originalEnv: string | undefined; async function setupTestEnv() { tmpDir = join(tmpdir(), `uwf-test-cwd-cli-${Date.now()}`); storageRoot = join(tmpDir, "storage"); + casDir = join(tmpDir, "cas"); await mkdir(storageRoot, { recursive: true }); + await mkdir(casDir, { recursive: true }); + + // Set UNCAGED_CAS_DIR for this test + originalEnv = process.env.UNCAGED_CAS_DIR; + process.env.UNCAGED_CAS_DIR = casDir; } async function teardown() { if (tmpDir) { await rm(tmpDir, { recursive: true, force: true }); } + // Restore original environment + if (originalEnv === undefined) { + delete process.env.UNCAGED_CAS_DIR; + } else { + process.env.UNCAGED_CAS_DIR = originalEnv; + } } async function createTestWorkflow(): Promise { @@ -123,7 +137,7 @@ graph: // Register the workflow execFileSync("node", [uwfBin, "workflow", "add", workflowPath], { - env: { ...process.env, UWF_STORAGE_ROOT: storageRoot }, + env: { ...process.env, UWF_STORAGE_ROOT: storageRoot, UNCAGED_CAS_DIR: casDir }, encoding: "utf8", }); @@ -132,7 +146,7 @@ graph: "node", [uwfBin, "thread", "start", "test-cwd-cli", "-p", "test prompt", "--cwd", testCwd], { - env: { ...process.env, UWF_STORAGE_ROOT: storageRoot }, + env: { ...process.env, UWF_STORAGE_ROOT: storageRoot, UNCAGED_CAS_DIR: casDir }, encoding: "utf8", }, ); diff --git a/packages/cli-workflow/src/__tests__/thread.test.ts b/packages/cli-workflow/src/__tests__/thread.test.ts index b77f27d..d93b34b 100644 --- a/packages/cli-workflow/src/__tests__/thread.test.ts +++ b/packages/cli-workflow/src/__tests__/thread.test.ts @@ -58,6 +58,8 @@ const DETAIL_SCHEMA = { async function makeUwfStore(storageRoot: string): Promise { const casDir = join(storageRoot, "cas"); await mkdir(casDir, { recursive: true }); + // Set UNCAGED_CAS_DIR to use the test's CAS directory + process.env.UNCAGED_CAS_DIR = casDir; const store = createFsStore(casDir); const schemas = await registerUwfSchemas(store); return { storageRoot, store, schemas }; diff --git a/packages/cli-workflow/src/__tests__/workflow-resolution.test.ts b/packages/cli-workflow/src/__tests__/workflow-resolution.test.ts index d3c5d12..9c38ae0 100644 --- a/packages/cli-workflow/src/__tests__/workflow-resolution.test.ts +++ b/packages/cli-workflow/src/__tests__/workflow-resolution.test.ts @@ -15,6 +15,8 @@ import { loadWorkflowRegistry, saveWorkflowRegistry } from "../store.js"; async function makeUwfStore(storageRoot: string): Promise { const casDir = join(storageRoot, "cas"); await mkdir(casDir, { recursive: true }); + // Set UNCAGED_CAS_DIR to use the test's CAS directory + process.env.UNCAGED_CAS_DIR = casDir; const store = createFsStore(casDir); const schemas = await registerUwfSchemas(store); return { storageRoot, store, schemas }; diff --git a/packages/cli-workflow/src/store.ts b/packages/cli-workflow/src/store.ts index 3016ab5..be4dec2 100644 --- a/packages/cli-workflow/src/store.ts +++ b/packages/cli-workflow/src/store.ts @@ -70,10 +70,26 @@ export function resolveStorageRoot(): string { return getDefaultStorageRoot(); } +/** + * Deprecated: Use `getGlobalCasDir()` instead. + * Returns the old CAS directory for backward compatibility. + */ export function getCasDir(storageRoot: string): string { return join(storageRoot, "cas"); } +/** + * Returns the global CAS directory shared by all uwf and json-cas tools. + * Priority: UNCAGED_CAS_DIR environment variable → default ~/.uncaged/json-cas + */ +export function getGlobalCasDir(): string { + const envPath = process.env.UNCAGED_CAS_DIR; + if (envPath !== undefined && envPath !== "") { + return envPath; + } + return join(homedir(), ".uncaged", "json-cas"); +} + export function getRegistryPath(storageRoot: string): string { return join(storageRoot, "workflows.yaml"); } @@ -98,7 +114,7 @@ export type UwfStore = { }; export async function createUwfStore(storageRoot: string): Promise { - const casDir = getCasDir(storageRoot); + const casDir = getGlobalCasDir(); await mkdir(casDir, { recursive: true }); const store = createFsStore(casDir); const schemas = await registerUwfSchemas(store);