chore: remove Bun, migrate to pnpm + Node + vitest + esbuild #29

Merged
xingyue merged 3 commits from chore/26-remove-bun into main 2026-06-03 15:15:40 +00:00
96 changed files with 6722 additions and 721 deletions
+1
View File
@@ -0,0 +1 @@
{"better-sqlite3@12.10.0": true, "esbuild@0.27.7": true}
+11 -13
View File
@@ -1,23 +1,21 @@
{ {
"name": "@united-workforce/monorepo", "name": "@united-workforce/monorepo",
"private": true, "private": true,
"packageManager": "bun@1.3.14",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],
"scripts": { "scripts": {
"uwf": "bun packages/cli/src/cli.ts", "uwf": "pnpm tsx packages/cli/src/cli.ts",
"preinstall": "npx only-allow bun", "prepublishOnly": "echo 'Use pnpm run release instead' && exit 1",
"prepublishOnly": "echo 'Use bun run release instead' && exit 1", "build": "npx tsc --build",
"build": "bunx tsc --build", "check": "npx tsc --build && biome check . && bash scripts/lint-log-tags.sh",
"check": "bunx tsc --build && biome check . && bash scripts/lint-log-tags.sh", "typecheck": "npx tsc --build",
"typecheck": "bunx tsc --build",
"format": "biome format --write .", "format": "biome format --write .",
"test": "bun run --filter './packages/*' test", "test": "pnpm -r run test",
"test:ci": "bun run --filter './packages/*' test:ci", "test:ci": "pnpm -r run test:ci",
"changeset": "bunx changeset", "changeset": "npx changeset",
"version": "bunx changeset version", "version": "npx changeset version",
"release": "bun run build && bun run test && node scripts/publish-all.mjs" "release": "pnpm run build && pnpm run test && node scripts/publish-all.mjs"
}, },
"devDependencies": { "devDependencies": {
"@agentclientprotocol/sdk": "^0.22.1", "@agentclientprotocol/sdk": "^0.22.1",
@@ -26,8 +24,8 @@
"@types/node": "^25.7.0", "@types/node": "^25.7.0",
"@types/xxhashjs": "^0.2.4", "@types/xxhashjs": "^0.2.4",
"@united-workforce/agent-hermes": "workspace:*", "@united-workforce/agent-hermes": "workspace:*",
"bun-types": "^1.3.13",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vitest": "^3.2.1",
"yaml": "^2.9.0" "yaml": "^2.9.0"
}, },
"repository": { "repository": {
@@ -1,5 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import type { LlmToolCall } from "../src/llm/types.js"; import type { LlmToolCall } from "../src/llm/types.js";
/** Mirror OpenAI response shape for parser coverage via chatCompletionWithTools integration later. */ /** Mirror OpenAI response shape for parser coverage via chatCompletionWithTools integration later. */
+11 -10
View File
@@ -1,19 +1,20 @@
import { beforeEach, describe, expect, mock, test } from "bun:test"; import { beforeEach, describe, expect, test, vi } from 'vitest';
const { mockChatCompletionWithTools, mockAppendSessionTurn, mockExecuteBuiltinTool } = vi.hoisted(() => ({
const mockChatCompletionWithTools = mock(async () => ({ mockChatCompletionWithTools: vi.fn(async () => ({
content: "---\nstatus: done\n---", content: "---\nstatus: done\n---",
toolCalls: [], toolCalls: [],
})),
mockAppendSessionTurn: vi.fn(async () => {}),
mockExecuteBuiltinTool: vi.fn(async () => "tool-result"),
})); }));
const mockAppendSessionTurn = mock(async () => {});
const mockExecuteBuiltinTool = mock(async () => "tool-result");
mock.module("../src/llm/index.js", () => ({ vi.mock("../src/llm/index.js", () => ({
chatCompletionWithTools: mockChatCompletionWithTools, chatCompletionWithTools: mockChatCompletionWithTools,
})); }));
mock.module("../src/session.js", () => ({ vi.mock("../src/session.js", () => ({
appendSessionTurn: mockAppendSessionTurn, appendSessionTurn: mockAppendSessionTurn,
})); }));
mock.module("../src/tools/index.js", () => ({ vi.mock("../src/tools/index.js", () => ({
builtinToolsToOpenAi: () => [], builtinToolsToOpenAi: () => [],
executeBuiltinTool: mockExecuteBuiltinTool, executeBuiltinTool: mockExecuteBuiltinTool,
getBuiltinTools: () => [], getBuiltinTools: () => [],
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { resolve } from "node:path"; import { resolve } from "node:path";
import { resolvePath } from "../src/tools/path.js"; import { resolvePath } from "../src/tools/path.js";
@@ -1,5 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import type { AgentContext } from "@united-workforce/util-agent"; import type { AgentContext } from "@united-workforce/util-agent";
import { buildBuiltinMessages } from "../src/prompt.js"; import { buildBuiltinMessages } from "../src/prompt.js";
+6 -7
View File
@@ -12,20 +12,19 @@
}, },
"exports": { "exports": {
".": { ".": {
"bun": "./src/index.ts",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js" "import": "./dist/index.js"
} }
}, },
"scripts": { "scripts": {
"prepublishOnly": "echo 'Use bun run release from repo root' && exit 1", "prepublishOnly": "echo 'Use pnpm run release from repo root' && exit 1",
"test": "bun test __tests__/", "test": "vitest run __tests__/",
"test:ci": "bun test __tests__/" "test:ci": "vitest run __tests__/"
}, },
"dependencies": { "dependencies": {
"@ocas/core": "^0.1.1", "@ocas/core": "^0.2.2",
"@united-workforce/util-agent": "workspace:^", "@united-workforce/util": "workspace:^",
"@united-workforce/util": "workspace:^" "@united-workforce/util-agent": "workspace:^"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.8.3" "typescript": "^5.8.3"
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun #!/usr/bin/env node
import { createBuiltinAgent } from "./agent.js"; import { createBuiltinAgent } from "./agent.js";
+2 -2
View File
@@ -32,7 +32,7 @@ export async function storeBuiltinDetail(
const turnHashes: string[] = []; const turnHashes: string[] = [];
for (const turn of turns) { for (const turn of turns) {
const hash = await store.put(schemas.turn, turn); const hash = await store.cas.put(schemas.turn, turn);
turnHashes.push(hash); turnHashes.push(hash);
} }
@@ -44,6 +44,6 @@ export async function storeBuiltinDetail(
turnCount: turnHashes.length, turnCount: turnHashes.length,
turns: turnHashes, turns: turnHashes,
}; };
const detailHash = await store.put(schemas.detail, detail); const detailHash = await store.cas.put(schemas.detail, detail);
return { detailHash, turnCount: turnHashes.length }; return { detailHash, turnCount: turnHashes.length };
} }
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import type { ThreadId } from "@united-workforce/protocol"; import type { ThreadId } from "@united-workforce/protocol";
import type { AgentContext } from "@united-workforce/util-agent"; import type { AgentContext } from "@united-workforce/util-agent";
import { buildClaudeCodePrompt } from "../src/claude-code.js"; import { buildClaudeCodePrompt } from "../src/claude-code.js";
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { createMemoryStore, walk } from "@ocas/core"; import { createMemoryStore, walk } from "@ocas/core";
import { import {
parseClaudeCodeJsonOutput, parseClaudeCodeJsonOutput,
@@ -278,7 +278,7 @@ describe("storeClaudeCodeDetail", () => {
expect(output).toBe("The answer"); expect(output).toBe("The answer");
expect(sessionId).toBe("abc-123"); expect(sessionId).toBe("abc-123");
const node = await store.get(detailHash); const node = await store.cas.get(detailHash);
expect(node).not.toBeNull(); expect(node).not.toBeNull();
expect(node!.payload.model).toBe("claude-sonnet-4.5"); expect(node!.payload.model).toBe("claude-sonnet-4.5");
expect(node!.payload.stopReason).toBe("end_turn"); expect(node!.payload.stopReason).toBe("end_turn");
@@ -286,7 +286,7 @@ describe("storeClaudeCodeDetail", () => {
expect(node!.payload.turns).toHaveLength(2); expect(node!.payload.turns).toHaveLength(2);
// Verify turn CAS nodes // Verify turn CAS nodes
const turn0 = await store.get(node!.payload.turns[0]); const turn0 = await store.cas.get(node!.payload.turns[0]);
expect(turn0).not.toBeNull(); expect(turn0).not.toBeNull();
expect(turn0!.payload.role).toBe("assistant"); expect(turn0!.payload.role).toBe("assistant");
expect(turn0!.payload.content).toBe("hello"); expect(turn0!.payload.content).toBe("hello");
@@ -466,7 +466,7 @@ describe("storeClaudeCodeDetail — incomplete results", () => {
expect(output).toBe("Partial output"); expect(output).toBe("Partial output");
expect(sessionId).toBe("sess-incomplete"); expect(sessionId).toBe("sess-incomplete");
const node = await store.get(detailHash); const node = await store.cas.get(detailHash);
expect(node).not.toBeNull(); expect(node).not.toBeNull();
expect(node!.payload.subtype).toBe("incomplete"); expect(node!.payload.subtype).toBe("incomplete");
expect(node!.payload.stopReason).toBe("incomplete_no_result_line"); expect(node!.payload.stopReason).toBe("incomplete_no_result_line");
@@ -480,7 +480,7 @@ describe("storeClaudeCodeRawOutput", () => {
const rawText = "Claude produced plain text without JSON"; const rawText = "Claude produced plain text without JSON";
const hash = await storeClaudeCodeRawOutput(store, rawText); const hash = await storeClaudeCodeRawOutput(store, rawText);
expect(hash).toHaveLength(13); expect(hash).toHaveLength(13);
const node = await store.get(hash); const node = await store.cas.get(hash);
expect(node).not.toBeNull(); expect(node).not.toBeNull();
expect(node!.payload.text).toBe(rawText); expect(node!.payload.text).toBe(rawText);
}); });
+6 -7
View File
@@ -12,20 +12,19 @@
}, },
"exports": { "exports": {
".": { ".": {
"bun": "./src/index.ts",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js" "import": "./dist/index.js"
} }
}, },
"scripts": { "scripts": {
"prepublishOnly": "echo 'Use bun run release from repo root' && exit 1", "prepublishOnly": "echo 'Use pnpm run release from repo root' && exit 1",
"test": "bun test __tests__/", "test": "vitest run __tests__/",
"test:ci": "bun test __tests__/" "test:ci": "vitest run __tests__/"
}, },
"dependencies": { "dependencies": {
"@ocas/core": "^0.1.1", "@ocas/core": "^0.2.2",
"@united-workforce/util-agent": "workspace:^", "@united-workforce/util": "workspace:^",
"@united-workforce/util": "workspace:^" "@united-workforce/util-agent": "workspace:^"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.8.3" "typescript": "^5.8.3"
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun #!/usr/bin/env node
import { createClaudeCodeAgent } from "./claude-code.js"; import { createClaudeCodeAgent } from "./claude-code.js";
@@ -290,7 +290,7 @@ export async function storeClaudeCodeDetail(
// Store each turn as an individual CAS node // Store each turn as an individual CAS node
const turnHashes: string[] = []; const turnHashes: string[] = [];
for (const turn of parsed.turns) { for (const turn of parsed.turns) {
const hash = await store.put(schemas.turn, turn); const hash = await store.cas.put(schemas.turn, turn);
turnHashes.push(hash); turnHashes.push(hash);
} }
@@ -306,12 +306,12 @@ export async function storeClaudeCodeDetail(
turns: turnHashes, turns: turnHashes,
}; };
const detailHash = await store.put(schemas.detail, detail); const detailHash = await store.cas.put(schemas.detail, detail);
return { detailHash, output: parsed.result, sessionId: parsed.sessionId }; return { detailHash, output: parsed.result, sessionId: parsed.sessionId };
} }
/** Fallback: store raw text output when JSON parsing fails. */ /** Fallback: store raw text output when JSON parsing fails. */
export async function storeClaudeCodeRawOutput(store: Store, rawOutput: string): Promise<string> { export async function storeClaudeCodeRawOutput(store: Store, rawOutput: string): Promise<string> {
const schemas = await registerSchemas(store); const schemas = await registerSchemas(store);
return store.put(schemas.rawOutput, { text: rawOutput }); return store.cas.put(schemas.rawOutput, { text: rawOutput });
} }
@@ -1,5 +1,4 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { HermesAcpClient } from "../src/acp-client.js"; import { HermesAcpClient } from "../src/acp-client.js";
describe("handleSessionUpdate — text extraction", () => { describe("handleSessionUpdate — text extraction", () => {
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import type { ThreadId } from "@united-workforce/protocol"; import type { ThreadId } from "@united-workforce/protocol";
import type { AgentContext } from "@united-workforce/util-agent"; import type { AgentContext } from "@united-workforce/util-agent";
import { buildHermesPrompt } from "../src/hermes.js"; import { buildHermesPrompt } from "../src/hermes.js";
@@ -1,5 +1,4 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { HermesAcpClient } from "../../src/acp-client.js"; import { HermesAcpClient } from "../../src/acp-client.js";
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -1,5 +1,4 @@
import { afterEach, describe, expect, it } from "bun:test"; import { afterEach, describe, expect, it } from 'vitest';
import { HermesAcpClient } from "../../src/acp-client.js"; import { HermesAcpClient } from "../../src/acp-client.js";
/** /**
@@ -1,22 +1,22 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
const PKG_ROOT = join(import.meta.dir, ".."); const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
describe("Issue #551 — bin entry & engines", () => { describe("Issue #551 — bin entry & engines", () => {
test("package.json declares bun in engines", () => { test("package.json no longer declares bun in engines", () => {
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf-8")); const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf-8"));
expect(pkg.engines).toBeDefined(); expect(pkg.engines?.bun).toBeUndefined();
expect(pkg.engines.bun).toBeDefined();
expect(pkg.engines.bun).toMatch(/^>=?\s*[\d.]+/);
}); });
test("bin entry file has bun shebang", () => { test("bin entry file has node shebang", () => {
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf-8")); const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf-8"));
const binPath = pkg.bin["uwf-hermes"]; const binPath = pkg.bin["uwf-hermes"];
const content = readFileSync(join(PKG_ROOT, binPath), "utf-8"); const content = readFileSync(join(PKG_ROOT, binPath), "utf-8");
expect(content.startsWith("#!/usr/bin/env bun")).toBe(true); expect(content.startsWith("#!/usr/bin/env node")).toBe(true);
}); });
test("README.md explains uwf-hermes is an adapter", () => { test("README.md explains uwf-hermes is an adapter", () => {
@@ -1,5 +1,5 @@
import { Database } from "bun:sqlite"; import Database from "better-sqlite3";
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -106,7 +106,7 @@ describe("storeHermesSessionDetail", () => {
expect(output).toBe("done"); expect(output).toBe("done");
const detailNode = store.get(detailHash); const detailNode = store.cas.get(detailHash);
expect(detailNode).not.toBeNull(); expect(detailNode).not.toBeNull();
if (detailNode === null) { if (detailNode === null) {
return; return;
@@ -133,14 +133,16 @@ describe("storeHermesSessionDetail", () => {
// ── SQLite fallback tests ────────────────────────────────────────── // ── SQLite fallback tests ──────────────────────────────────────────
function createTestDb(dbPath: string): Database { type TestDb = InstanceType<typeof Database>;
function createTestDb(dbPath: string): TestDb {
const db = new Database(dbPath); const db = new Database(dbPath);
db.run(`CREATE TABLE sessions ( db.exec(`CREATE TABLE sessions (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
model TEXT NOT NULL, model TEXT NOT NULL,
started_at INTEGER NOT NULL started_at INTEGER NOT NULL
)`); )`);
db.run(`CREATE TABLE messages ( db.exec(`CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL, session_id TEXT NOT NULL,
role TEXT NOT NULL, role TEXT NOT NULL,
@@ -152,6 +154,27 @@ function createTestDb(dbPath: string): Database {
return db; return db;
} }
function insertSession(db: TestDb, id: string, model: string, startedAt: number): void {
db.prepare("INSERT INTO sessions (id, model, started_at) VALUES (?, ?, ?)").run(
id,
model,
startedAt,
);
}
function insertMessage(
db: TestDb,
sessionId: string,
role: string,
content: string | null,
reasoning: string | null,
toolCalls: string | null,
): void {
db.prepare(
"INSERT INTO messages (session_id, role, content, reasoning, tool_calls) VALUES (?, ?, ?, ?, ?)",
).run(sessionId, role, content, reasoning, toolCalls);
}
describe("getHermesDbPath", () => { describe("getHermesDbPath", () => {
test("returns correct path", () => { test("returns correct path", () => {
const { homedir } = require("node:os"); const { homedir } = require("node:os");
@@ -168,19 +191,9 @@ describe("loadHermesSessionFromDb", () => {
const sessionId = "test-session-001"; const sessionId = "test-session-001";
const startedAt = 1748099519; const startedAt = 1748099519;
db.run("INSERT INTO sessions (id, model, started_at) VALUES (?, ?, ?)", [ insertSession(db, sessionId, "claude-opus-4.6", startedAt);
sessionId, insertMessage(db, sessionId, "user", "hello", null, null);
"claude-opus-4.6", insertMessage(db, sessionId, "assistant", "hi there", "thinking...", null);
startedAt,
]);
db.run(
"INSERT INTO messages (session_id, role, content, reasoning, tool_calls) VALUES (?, ?, ?, ?, ?)",
[sessionId, "user", "hello", null, null],
);
db.run(
"INSERT INTO messages (session_id, role, content, reasoning, tool_calls) VALUES (?, ?, ?, ?, ?)",
[sessionId, "assistant", "hi there", "thinking...", null],
);
db.close(); db.close();
const result = await loadHermesSessionFromDb(sessionId, dbPath); const result = await loadHermesSessionFromDb(sessionId, dbPath);
@@ -220,18 +233,11 @@ describe("loadHermesSessionFromDb", () => {
const db = createTestDb(dbPath); const db = createTestDb(dbPath);
const sessionId = "test-tool-calls"; const sessionId = "test-tool-calls";
db.run("INSERT INTO sessions (id, model, started_at) VALUES (?, ?, ?)", [ insertSession(db, sessionId, "gpt-4", 1748099519);
sessionId,
"gpt-4",
1748099519,
]);
const toolCallsJson = JSON.stringify([ const toolCallsJson = JSON.stringify([
{ function: { name: "read_file", arguments: '{"path":"x"}' } }, { function: { name: "read_file", arguments: '{"path":"x"}' } },
]); ]);
db.run( insertMessage(db, sessionId, "assistant", "", null, toolCallsJson);
"INSERT INTO messages (session_id, role, content, reasoning, tool_calls) VALUES (?, ?, ?, ?, ?)",
[sessionId, "assistant", "", null, toolCallsJson],
);
db.close(); db.close();
const result = await loadHermesSessionFromDb(sessionId, dbPath); const result = await loadHermesSessionFromDb(sessionId, dbPath);
@@ -249,15 +255,8 @@ describe("loadHermesSessionFromDb", () => {
const db = createTestDb(dbPath); const db = createTestDb(dbPath);
const sessionId = "test-nulls"; const sessionId = "test-nulls";
db.run("INSERT INTO sessions (id, model, started_at) VALUES (?, ?, ?)", [ insertSession(db, sessionId, "model", 1748099519);
sessionId, insertMessage(db, sessionId, "assistant", null, null, null);
"model",
1748099519,
]);
db.run(
"INSERT INTO messages (session_id, role, content, reasoning, tool_calls) VALUES (?, ?, ?, ?, ?)",
[sessionId, "assistant", null, null, null],
);
db.close(); db.close();
const result = await loadHermesSessionFromDb(sessionId, dbPath); const result = await loadHermesSessionFromDb(sessionId, dbPath);
@@ -276,23 +275,10 @@ describe("loadHermesSessionFromDb", () => {
const db = createTestDb(dbPath); const db = createTestDb(dbPath);
const sessionId = "test-order"; const sessionId = "test-order";
db.run("INSERT INTO sessions (id, model, started_at) VALUES (?, ?, ?)", [ insertSession(db, sessionId, "model", 1748099519);
sessionId, insertMessage(db, sessionId, "user", "first", null, null);
"model", insertMessage(db, sessionId, "assistant", "second", null, null);
1748099519, insertMessage(db, sessionId, "user", "third", null, null);
]);
db.run(
"INSERT INTO messages (session_id, role, content, reasoning, tool_calls) VALUES (?, ?, ?, ?, ?)",
[sessionId, "user", "first", null, null],
);
db.run(
"INSERT INTO messages (session_id, role, content, reasoning, tool_calls) VALUES (?, ?, ?, ?, ?)",
[sessionId, "assistant", "second", null, null],
);
db.run(
"INSERT INTO messages (session_id, role, content, reasoning, tool_calls) VALUES (?, ?, ?, ?, ?)",
[sessionId, "user", "third", null, null],
);
db.close(); db.close();
const result = await loadHermesSessionFromDb(sessionId, dbPath); const result = await loadHermesSessionFromDb(sessionId, dbPath);
@@ -309,11 +295,7 @@ describe("loadHermesSessionFromDb", () => {
const sessionId = "test-timestamp"; const sessionId = "test-timestamp";
const startedAt = 1748099519; const startedAt = 1748099519;
db.run("INSERT INTO sessions (id, model, started_at) VALUES (?, ?, ?)", [ insertSession(db, sessionId, "model", startedAt);
sessionId,
"model",
startedAt,
]);
db.close(); db.close();
const result = await loadHermesSessionFromDb(sessionId, dbPath); const result = await loadHermesSessionFromDb(sessionId, dbPath);
@@ -333,15 +315,8 @@ describe("loadHermesSession with SQLite fallback", () => {
// Create DB with one model value // Create DB with one model value
const db = createTestDb(dbPath); const db = createTestDb(dbPath);
const sessionId = "test-priority"; const sessionId = "test-priority";
db.run("INSERT INTO sessions (id, model, started_at) VALUES (?, ?, ?)", [ insertSession(db, sessionId, "db-model", 1748099519);
sessionId, insertMessage(db, sessionId, "user", "from db", null, null);
"db-model",
1748099519,
]);
db.run(
"INSERT INTO messages (session_id, role, content, reasoning, tool_calls) VALUES (?, ?, ?, ?, ?)",
[sessionId, "user", "from db", null, null],
);
db.close(); db.close();
// Create JSON file with a different model value // Create JSON file with a different model value
+8 -10
View File
@@ -12,23 +12,24 @@
}, },
"exports": { "exports": {
".": { ".": {
"bun": "./src/index.ts",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js" "import": "./dist/index.js"
} }
}, },
"scripts": { "scripts": {
"prepublishOnly": "echo 'Use bun run release from repo root' && exit 1", "prepublishOnly": "echo 'Use pnpm run release from repo root' && exit 1",
"test": "bun test __tests__/", "test": "vitest run __tests__/",
"test:ci": "bun test __tests__/" "test:ci": "vitest run __tests__/"
}, },
"dependencies": { "dependencies": {
"@ocas/core": "^0.1.1", "@ocas/core": "^0.2.2",
"@united-workforce/util-agent": "workspace:^",
"@united-workforce/protocol": "workspace:^", "@united-workforce/protocol": "workspace:^",
"@united-workforce/util": "workspace:^" "@united-workforce/util": "workspace:^",
"@united-workforce/util-agent": "workspace:^",
"better-sqlite3": "^12.10.0"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"publishConfig": { "publishConfig": {
@@ -43,8 +44,5 @@
"bugs": { "bugs": {
"url": "https://git.shazhou.work/shazhou/united-workforce/issues" "url": "https://git.shazhou.work/shazhou/united-workforce/issues"
}, },
"engines": {
"bun": ">= 1.0.0"
},
"license": "MIT" "license": "MIT"
} }
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun #!/usr/bin/env node
import { createHermesAgent } from "./hermes.js"; import { createHermesAgent } from "./hermes.js";
+6 -6
View File
@@ -1,4 +1,4 @@
import { Database } from "bun:sqlite"; import Database from "better-sqlite3";
import { readFile } from "node:fs/promises"; import { readFile } from "node:fs/promises";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -156,13 +156,13 @@ export function loadHermesSessionFromDb(
try { try {
db = new Database(resolvedPath, { readonly: true }); db = new Database(resolvedPath, { readonly: true });
const session = db const session = db
.query("SELECT id, model, started_at FROM sessions WHERE id = ?") .prepare("SELECT id, model, started_at FROM sessions WHERE id = ?")
.get(sessionId) as DbSessionRow | null; .get(sessionId) as DbSessionRow | null;
if (session === null) { if (session === null) {
return null; return null;
} }
const rows = db const rows = db
.query( .prepare(
"SELECT role, content, reasoning, tool_calls FROM messages WHERE session_id = ? ORDER BY id", "SELECT role, content, reasoning, tool_calls FROM messages WHERE session_id = ? ORDER BY id",
) )
.all(sessionId) as DbMessageRow[]; .all(sessionId) as DbMessageRow[];
@@ -285,7 +285,7 @@ export async function storeHermesSessionDetail(
if (turn === null) { if (turn === null) {
continue; continue;
} }
const hash = await store.put(schemas.turn, turn); const hash = await store.cas.put(schemas.turn, turn);
turnHashes.push(hash); turnHashes.push(hash);
turnIndex += 1; turnIndex += 1;
} }
@@ -297,12 +297,12 @@ export async function storeHermesSessionDetail(
turnCount: turnHashes.length, turnCount: turnHashes.length,
turns: turnHashes, turns: turnHashes,
}; };
const detailHash = await store.put(schemas.detail, detail); const detailHash = await store.cas.put(schemas.detail, detail);
const output = extractLastAssistantContent(session.messages); const output = extractLastAssistantContent(session.messages);
return { detailHash, output }; return { detailHash, output };
} }
export async function storeHermesRawOutput(store: Store, rawOutput: string): Promise<string> { export async function storeHermesRawOutput(store: Store, rawOutput: string): Promise<string> {
const schemas = await registerHermesSchemas(store); const schemas = await registerHermesSchemas(store);
return store.put(schemas.rawOutput, { text: rawOutput }); return store.cas.put(schemas.rawOutput, { text: rawOutput });
} }
+5 -5
View File
@@ -11,8 +11,8 @@
"uwf": "./dist/cli.js" "uwf": "./dist/cli.js"
}, },
"dependencies": { "dependencies": {
"@ocas/core": "^0.1.1", "@ocas/core": "^0.2.2",
"@ocas/fs": "^0.1.1", "@ocas/fs": "^0.2.2",
"@united-workforce/protocol": "workspace:^", "@united-workforce/protocol": "workspace:^",
"@united-workforce/util": "workspace:^", "@united-workforce/util": "workspace:^",
"@united-workforce/util-agent": "workspace:^", "@united-workforce/util-agent": "workspace:^",
@@ -22,9 +22,9 @@
"yaml": "^2.8.4" "yaml": "^2.8.4"
}, },
"scripts": { "scripts": {
"prepublishOnly": "echo 'Use bun run release from repo root' && exit 1", "prepublishOnly": "echo 'Use pnpm run release from repo root' && exit 1",
"test": "bun test src/", "test": "vitest run src/",
"test:ci": "bun test src/" "test:ci": "vitest run src/"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
@@ -1,10 +1,12 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { putSchema } from "@ocas/core"; import { putSchema } from "@ocas/core";
import { createFsStore } from "@ocas/fs"; import { openStore } from "@ocas/fs";
import type { CasRef, StepNodePayload, ThreadId } from "@united-workforce/protocol"; import type { CasRef, StepNodePayload, ThreadId } from "@united-workforce/protocol";
import { registerUwfSchemas } from "../schemas.js"; import { registerUwfSchemas } from "../schemas.js";
import { seedThreads } from "./thread-test-helpers.js"; import { seedThreads } from "./thread-test-helpers.js";
@@ -38,12 +40,12 @@ describe("C1: adapter JSON round-trip integration", () => {
// 1. Set up CAS store with workflow, start node, and output schema // 1. Set up CAS store with workflow, start node, and output schema
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const outputSchemaHash = await putSchema(store, OUTPUT_SCHEMA); const outputSchemaHash = await putSchema(store, OUTPUT_SCHEMA);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-roundtrip", name: "test-roundtrip",
description: "roundtrip integration test", description: "roundtrip integration test",
roles: { roles: {
@@ -62,7 +64,7 @@ describe("C1: adapter JSON round-trip integration", () => {
}, },
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test round-trip task", prompt: "Test round-trip task",
}); });
@@ -73,18 +75,18 @@ describe("C1: adapter JSON round-trip integration", () => {
await seedThreads(tmpDir, { [threadId]: startHash }); await seedThreads(tmpDir, { [threadId]: startHash });
// 2. Pre-create CAS nodes that the mock agent would produce // 2. Pre-create CAS nodes that the mock agent would produce
const outputHash = await store.put(outputSchemaHash, { const outputHash = await store.cas.put(outputSchemaHash, {
$status: "done", $status: "done",
result: "test-ok", result: "test-ok",
}); });
// Use text schema for detail (simple placeholder) // Use text schema for detail (simple placeholder)
const detailHash = await store.put(schemas.text, "mock detail"); const detailHash = await store.cas.put(schemas.text, "mock detail");
const startedAtMs = 1716600000000; const startedAtMs = 1716600000000;
const completedAtMs = 1716600001500; const completedAtMs = 1716600001500;
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -119,15 +121,15 @@ describe("C1: adapter JSON round-trip integration", () => {
); );
// 5. Run CLI with agent override pointing to our mock // 5. Run CLI with agent override pointing to our mock
const cliPath = join(import.meta.dirname, "..", "cli.js"); const cliPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "dist", "cli.js");
let stdout: string; let stdout: string;
let stderr: string; let stderr: string;
let exitCode: number; let exitCode: number;
try { try {
stdout = execFileSync( stdout = execFileSync(
"bun", process.execPath,
["run", cliPath, "thread", "exec", threadId, "--agent", mockAgentPath], [cliPath, "thread", "exec", threadId, "--agent", mockAgentPath],
{ {
encoding: "utf8", encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
@@ -165,8 +167,8 @@ describe("C1: adapter JSON round-trip integration", () => {
expect(cliOutput.head).toMatch(/^[0-9A-HJ-NP-TV-Z]{13}$/); expect(cliOutput.head).toMatch(/^[0-9A-HJ-NP-TV-Z]{13}$/);
// Verify the CAS step node exists and has correct metadata // Verify the CAS step node exists and has correct metadata
const storeAfter = createFsStore(casDir); const storeAfter = await openStore(casDir);
const stepNode = storeAfter.get(cliOutput.head as CasRef); const stepNode = storeAfter.cas.get(cliOutput.head as CasRef);
expect(stepNode).not.toBeNull(); expect(stepNode).not.toBeNull();
const payload = stepNode!.payload as StepNodePayload; const payload = stepNode!.payload as StepNodePayload;
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { mkdir, rm, writeFile } from "node:fs/promises"; import { mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -181,18 +181,18 @@ async function insertStepNode(
const head = headEntry.head; const head = headEntry.head;
const outputSchemaHash = await putSchema(uwf.store, OUTPUT_SCHEMA); const outputSchemaHash = await putSchema(uwf.store, OUTPUT_SCHEMA);
const outputHash = await uwf.store.put(outputSchemaHash, outputPayload); const outputHash = await uwf.store.cas.put(outputSchemaHash, outputPayload);
// Use text schema for detail (simple placeholder) // Use text schema for detail (simple placeholder)
const detailHash = await uwf.store.put(uwf.schemas.text, "detail-placeholder"); const detailHash = await uwf.store.cas.put(uwf.schemas.text, "detail-placeholder");
// Resolve start hash from head // Resolve start hash from head
const headNode = uwf.store.get(head); const headNode = uwf.store.cas.get(head);
if (headNode === null) throw new Error(`head ${head} not found`); if (headNode === null) throw new Error(`head ${head} not found`);
const isStart = headNode.type === uwf.schemas.startNode; const isStart = headNode.type === uwf.schemas.startNode;
const startHash = isStart ? head : (headNode.payload as { start: CasRef }).start; const startHash = isStart ? head : (headNode.payload as { start: CasRef }).start;
const stepHash = (await uwf.store.put(uwf.schemas.stepNode, { const stepHash = (await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: isStart ? null : head, prev: isStart ? null : head,
role, role,
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
+1 -1
View File
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { mkdir, readdir, rm, writeFile } from "node:fs/promises"; import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import type { Target, WorkflowPayload } from "@united-workforce/protocol"; import type { Target, WorkflowPayload } from "@united-workforce/protocol";
import { evaluate } from "../moderator/evaluate.js"; import { evaluate } from "../moderator/evaluate.js";
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { mkdir, mkdtemp, rm } from "node:fs/promises"; import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -23,7 +23,7 @@ describe("resolveHeadHash", () => {
test("returns head hash from variable store for active thread", async () => { test("returns head hash from variable store for active thread", async () => {
const threadId = "01JTEST0000000000000000001" as ThreadId; const threadId = "01JTEST0000000000000000001" as ThreadId;
const uwf = await createUwfStore(tmpDir); const uwf = await createUwfStore(tmpDir);
const headHash = (await uwf.store.put(uwf.schemas.text, "active")) as CasRef; const headHash = (await uwf.store.cas.put(uwf.schemas.text, "active")) as CasRef;
setThread(uwf.varStore, threadId, createThreadIndexEntry(headHash as CasRef)); setThread(uwf.varStore, threadId, createThreadIndexEntry(headHash as CasRef));
const result = await resolveHeadHash(tmpDir, threadId); const result = await resolveHeadHash(tmpDir, threadId);
@@ -36,7 +36,7 @@ describe("resolveHeadHash", () => {
const workflowHash = "workflow_hash_789" as CasRef; const workflowHash = "workflow_hash_789" as CasRef;
const uwf = await createUwfStore(tmpDir); const uwf = await createUwfStore(tmpDir);
const headHash = (await uwf.store.put(uwf.schemas.text, "completed-head")) as CasRef; const headHash = (await uwf.store.cas.put(uwf.schemas.text, "completed-head")) as CasRef;
addHistoryEntry(uwf.varStore, { addHistoryEntry(uwf.varStore, {
thread: threadId, thread: threadId,
workflow: workflowHash, workflow: workflowHash,
@@ -59,8 +59,8 @@ describe("resolveHeadHash", () => {
const workflowHash = "workflow_hash_xyz" as CasRef; const workflowHash = "workflow_hash_xyz" as CasRef;
const uwf = await createUwfStore(tmpDir); const uwf = await createUwfStore(tmpDir);
const activeHead = (await uwf.store.put(uwf.schemas.text, "active-v2")) as CasRef; const activeHead = (await uwf.store.cas.put(uwf.schemas.text, "active-v2")) as CasRef;
const historicalHash = (await uwf.store.put(uwf.schemas.text, "historical-v1")) as CasRef; const historicalHash = (await uwf.store.cas.put(uwf.schemas.text, "historical-v1")) as CasRef;
setThread(uwf.varStore, threadId, createThreadIndexEntry(activeHead)); setThread(uwf.varStore, threadId, createThreadIndexEntry(activeHead));
addHistoryEntry(uwf.varStore, { addHistoryEntry(uwf.varStore, {
thread: threadId, thread: threadId,
@@ -82,9 +82,9 @@ describe("resolveHeadHash", () => {
const threadId3 = "01JTEST0000000000000000007" as ThreadId; const threadId3 = "01JTEST0000000000000000007" as ThreadId;
const workflowHash = "workflow_hash_abc" as CasRef; const workflowHash = "workflow_hash_abc" as CasRef;
const uwf = await createUwfStore(tmpDir); const uwf = await createUwfStore(tmpDir);
const hash1 = (await uwf.store.put(uwf.schemas.text, "hash-thread1")) as CasRef; const hash1 = (await uwf.store.cas.put(uwf.schemas.text, "hash-thread1")) as CasRef;
const hash2 = (await uwf.store.put(uwf.schemas.text, "hash-thread2")) as CasRef; const hash2 = (await uwf.store.cas.put(uwf.schemas.text, "hash-thread2")) as CasRef;
const hash3 = (await uwf.store.put(uwf.schemas.text, "hash-thread3")) as CasRef; const hash3 = (await uwf.store.cas.put(uwf.schemas.text, "hash-thread3")) as CasRef;
addHistoryEntry(uwf.varStore, { addHistoryEntry(uwf.varStore, {
thread: threadId1, thread: threadId1,
workflow: workflowHash, workflow: workflowHash,
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { mkdtemp, rm } from "node:fs/promises"; import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
@@ -31,7 +31,7 @@ describe("_agentNameFromBinary", () => {
describe("_printAgentMenu", () => { describe("_printAgentMenu", () => {
test("prints known agents with labels", () => { test("prints known agents with labels", () => {
const logs: string[] = []; const logs: string[] = [];
spyOn(console, "log").mockImplementation((...args: unknown[]) => { vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
logs.push(args.join(" ")); logs.push(args.join(" "));
}); });
@@ -40,12 +40,12 @@ describe("_printAgentMenu", () => {
expect(logs.some((l) => l.includes("Hermes"))).toBe(true); expect(logs.some((l) => l.includes("Hermes"))).toBe(true);
expect(logs.some((l) => l.includes("Claude Code"))).toBe(true); expect(logs.some((l) => l.includes("Claude Code"))).toBe(true);
mock.restore(); vi.restoreAllMocks();
}); });
test("prints unknown agents with binary name as label", () => { test("prints unknown agents with binary name as label", () => {
const logs: string[] = []; const logs: string[] = [];
spyOn(console, "log").mockImplementation((...args: unknown[]) => { vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
logs.push(args.join(" ")); logs.push(args.join(" "));
}); });
@@ -53,7 +53,7 @@ describe("_printAgentMenu", () => {
expect(logs.some((l) => l.includes("uwf-custom-agent"))).toBe(true); expect(logs.some((l) => l.includes("uwf-custom-agent"))).toBe(true);
mock.restore(); vi.restoreAllMocks();
}); });
}); });
@@ -67,7 +67,7 @@ describe("cmdSetup agent configuration", () => {
}); });
afterEach(async () => { afterEach(async () => {
mock.restore(); vi.restoreAllMocks();
await rm(storageRoot, { recursive: true, force: true }); await rm(storageRoot, { recursive: true, force: true });
}); });
@@ -80,7 +80,7 @@ describe("cmdSetup agent configuration", () => {
}); });
test("defaults to hermes agent when no agent specified", async () => { test("defaults to hermes agent when no agent specified", async () => {
spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({}), { status: 200 })); vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
const result = await cmdSetup(baseArgs()); const result = await cmdSetup(baseArgs());
@@ -91,7 +91,7 @@ describe("cmdSetup agent configuration", () => {
}); });
test("writes specified agent as default", async () => { test("writes specified agent as default", async () => {
spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({}), { status: 200 })); vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
const result = await cmdSetup({ ...baseArgs(), agent: "claude-code" }); const result = await cmdSetup({ ...baseArgs(), agent: "claude-code" });
@@ -102,7 +102,7 @@ describe("cmdSetup agent configuration", () => {
}); });
test("preserves existing agents when adding new one", async () => { test("preserves existing agents when adding new one", async () => {
spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({}), { status: 200 })); vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
// First setup with hermes // First setup with hermes
await cmdSetup(baseArgs()); await cmdSetup(baseArgs());
@@ -116,7 +116,7 @@ describe("cmdSetup agent configuration", () => {
}); });
test("updates defaultAgent on re-run with different agent", async () => { test("updates defaultAgent on re-run with different agent", async () => {
spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({}), { status: 200 })); vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
await cmdSetup(baseArgs()); await cmdSetup(baseArgs());
const config1 = parse(readFileSync(join(storageRoot, "config.yaml"), "utf8")); const config1 = parse(readFileSync(join(storageRoot, "config.yaml"), "utf8"));
@@ -128,7 +128,7 @@ describe("cmdSetup agent configuration", () => {
}); });
test("normalizes agent name with uwf- prefix to bare name", async () => { test("normalizes agent name with uwf- prefix to bare name", async () => {
spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({}), { status: 200 })); vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
const result = await cmdSetup({ ...baseArgs(), agent: "uwf-hermes" }); const result = await cmdSetup({ ...baseArgs(), agent: "uwf-hermes" });
@@ -141,7 +141,7 @@ describe("cmdSetup agent configuration", () => {
}); });
test("normalizes uwf-claude-code to claude-code", async () => { test("normalizes uwf-claude-code to claude-code", async () => {
spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({}), { status: 200 })); vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
const result = await cmdSetup({ ...baseArgs(), agent: "uwf-claude-code" }); const result = await cmdSetup({ ...baseArgs(), agent: "uwf-claude-code" });
@@ -1,4 +1,4 @@
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"; import { afterEach, describe, expect, test, vi } from 'vitest';
import { mkdirSync, writeFileSync } from "node:fs"; import { mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -178,7 +178,7 @@ describe("_isBackspace", () => {
describe("_printProviderMenu", () => { describe("_printProviderMenu", () => {
afterEach(() => { afterEach(() => {
mock.restore(); vi.restoreAllMocks();
}); });
const providers = [ const providers = [
@@ -188,7 +188,7 @@ describe("_printProviderMenu", () => {
test("prints correct number of lines (one per provider + custom)", () => { test("prints correct number of lines (one per provider + custom)", () => {
const lines: string[] = []; const lines: string[] = [];
spyOn(console, "log").mockImplementation((msg: string) => { vi.spyOn(console, "log").mockImplementation((msg: string) => {
lines.push(msg); lines.push(msg);
}); });
_printProviderMenu(providers); _printProviderMenu(providers);
@@ -198,7 +198,7 @@ describe("_printProviderMenu", () => {
test("custom option number = providers.length + 1", () => { test("custom option number = providers.length + 1", () => {
const lines: string[] = []; const lines: string[] = [];
spyOn(console, "log").mockImplementation((msg: string) => { vi.spyOn(console, "log").mockImplementation((msg: string) => {
lines.push(msg); lines.push(msg);
}); });
_printProviderMenu(providers); _printProviderMenu(providers);
@@ -208,7 +208,7 @@ describe("_printProviderMenu", () => {
test("each provider line contains its label and baseUrl", () => { test("each provider line contains its label and baseUrl", () => {
const lines: string[] = []; const lines: string[] = [];
spyOn(console, "log").mockImplementation((msg: string) => { vi.spyOn(console, "log").mockImplementation((msg: string) => {
lines.push(msg); lines.push(msg);
}); });
_printProviderMenu(providers); _printProviderMenu(providers);
@@ -294,12 +294,12 @@ describe("_resolveModelChoice", () => {
describe("_printModelMenu", () => { describe("_printModelMenu", () => {
afterEach(() => { afterEach(() => {
mock.restore(); vi.restoreAllMocks();
}); });
test("prints all models — each model name appears in output", () => { test("prints all models — each model name appears in output", () => {
const output: string[] = []; const output: string[] = [];
spyOn(console, "log").mockImplementation((msg: string) => { vi.spyOn(console, "log").mockImplementation((msg: string) => {
output.push(msg); output.push(msg);
}); });
const models = ["model-a", "model-b", "model-c"]; const models = ["model-a", "model-b", "model-c"];
@@ -312,7 +312,7 @@ describe("_printModelMenu", () => {
test("single column when termCols is very small", () => { test("single column when termCols is very small", () => {
const output: string[] = []; const output: string[] = [];
spyOn(console, "log").mockImplementation((msg: string) => { vi.spyOn(console, "log").mockImplementation((msg: string) => {
output.push(msg); output.push(msg);
}); });
_printModelMenu(["a", "b", "c"], 1); _printModelMenu(["a", "b", "c"], 1);
@@ -322,7 +322,7 @@ describe("_printModelMenu", () => {
test("wide terminal fits multiple columns", () => { test("wide terminal fits multiple columns", () => {
const output: string[] = []; const output: string[] = [];
spyOn(console, "log").mockImplementation((msg: string) => { vi.spyOn(console, "log").mockImplementation((msg: string) => {
output.push(msg); output.push(msg);
}); });
const models = Array.from({ length: 6 }, (_, i) => `m${i}`); const models = Array.from({ length: 6 }, (_, i) => `m${i}`);
@@ -338,12 +338,12 @@ describe("_printModelMenu", () => {
describe("_printValidationResult", () => { describe("_printValidationResult", () => {
afterEach(() => { afterEach(() => {
mock.restore(); vi.restoreAllMocks();
}); });
test("ok=true prints success message containing '✓'", () => { test("ok=true prints success message containing '✓'", () => {
const lines: string[] = []; const lines: string[] = [];
spyOn(console, "log").mockImplementation((msg: string) => { vi.spyOn(console, "log").mockImplementation((msg: string) => {
lines.push(msg); lines.push(msg);
}); });
_printValidationResult({ ok: true, error: null }); _printValidationResult({ ok: true, error: null });
@@ -352,7 +352,7 @@ describe("_printValidationResult", () => {
test("ok=false prints warning message containing '⚠'", () => { test("ok=false prints warning message containing '⚠'", () => {
const lines: string[] = []; const lines: string[] = [];
spyOn(console, "log").mockImplementation((msg: string) => { vi.spyOn(console, "log").mockImplementation((msg: string) => {
lines.push(msg); lines.push(msg);
}); });
_printValidationResult({ ok: false, error: "HTTP 401" }); _printValidationResult({ ok: false, error: "HTTP 401" });
@@ -361,7 +361,7 @@ describe("_printValidationResult", () => {
test("ok=false includes the error string in output", () => { test("ok=false includes the error string in output", () => {
const lines: string[] = []; const lines: string[] = [];
spyOn(console, "log").mockImplementation((msg: string) => { vi.spyOn(console, "log").mockImplementation((msg: string) => {
lines.push(msg); lines.push(msg);
}); });
_printValidationResult({ ok: false, error: "HTTP 401" }); _printValidationResult({ ok: false, error: "HTTP 401" });
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { mkdtemp, rm } from "node:fs/promises"; import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -10,11 +10,11 @@ describe("validateModel", () => {
const MODEL = "test-model"; const MODEL = "test-model";
afterEach(() => { afterEach(() => {
mock.restore(); vi.restoreAllMocks();
}); });
test("success path — returns ok on 200", async () => { test("success path — returns ok on 200", async () => {
const mockFetch = spyOn(globalThis, "fetch").mockResolvedValue( const mockFetch = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify({}), { status: 200 }), new Response(JSON.stringify({}), { status: 200 }),
); );
@@ -37,7 +37,7 @@ describe("validateModel", () => {
}); });
test("HTTP 401 — returns error containing 401", async () => { test("HTTP 401 — returns error containing 401", async () => {
spyOn(globalThis, "fetch").mockResolvedValue( vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response("Unauthorized", { status: 401, statusText: "Unauthorized" }), new Response("Unauthorized", { status: 401, statusText: "Unauthorized" }),
); );
@@ -50,7 +50,7 @@ describe("validateModel", () => {
}); });
test("HTTP 404 — returns error containing 404", async () => { test("HTTP 404 — returns error containing 404", async () => {
spyOn(globalThis, "fetch").mockResolvedValue( vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response("Not Found", { status: 404, statusText: "Not Found" }), new Response("Not Found", { status: 404, statusText: "Not Found" }),
); );
@@ -64,7 +64,7 @@ describe("validateModel", () => {
test("network timeout — returns error mentioning timeout", async () => { test("network timeout — returns error mentioning timeout", async () => {
const err = new DOMException("signal timed out", "AbortError"); const err = new DOMException("signal timed out", "AbortError");
spyOn(globalThis, "fetch").mockRejectedValue(err); vi.spyOn(globalThis, "fetch").mockRejectedValue(err);
const result = await validateModel(BASE_URL, API_KEY, MODEL); const result = await validateModel(BASE_URL, API_KEY, MODEL);
@@ -75,7 +75,7 @@ describe("validateModel", () => {
}); });
test("network error (DNS/connection) — returns error mentioning connectivity", async () => { test("network error (DNS/connection) — returns error mentioning connectivity", async () => {
spyOn(globalThis, "fetch").mockRejectedValue(new TypeError("fetch failed")); vi.spyOn(globalThis, "fetch").mockRejectedValue(new TypeError("fetch failed"));
const result = await validateModel(BASE_URL, API_KEY, MODEL); const result = await validateModel(BASE_URL, API_KEY, MODEL);
@@ -86,7 +86,7 @@ describe("validateModel", () => {
}); });
test("request body correctness", async () => { test("request body correctness", async () => {
const mockFetch = spyOn(globalThis, "fetch").mockResolvedValue( const mockFetch = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify({}), { status: 200 }), new Response(JSON.stringify({}), { status: 200 }),
); );
@@ -109,7 +109,7 @@ describe("cmdSetup with validation", () => {
}); });
afterEach(async () => { afterEach(async () => {
mock.restore(); vi.restoreAllMocks();
await rm(storageRoot, { recursive: true, force: true }); await rm(storageRoot, { recursive: true, force: true });
}); });
@@ -122,7 +122,7 @@ describe("cmdSetup with validation", () => {
}); });
test("includes validation result on success", async () => { test("includes validation result on success", async () => {
spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({}), { status: 200 })); vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
const result = await cmdSetup(setupArgs()); const result = await cmdSetup(setupArgs());
@@ -132,7 +132,7 @@ describe("cmdSetup with validation", () => {
}); });
test("includes validation failure — config still saved", async () => { test("includes validation failure — config still saved", async () => {
spyOn(globalThis, "fetch").mockResolvedValue( vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response("Unauthorized", { status: 401, statusText: "Unauthorized" }), new Response("Unauthorized", { status: 401, statusText: "Unauthorized" }),
); );
@@ -1,4 +1,6 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { readFile } from "node:fs/promises"; import { readFile } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import type { WorkflowPayload } from "@united-workforce/protocol"; import type { WorkflowPayload } from "@united-workforce/protocol";
@@ -15,7 +17,7 @@ import { parse } from "yaml";
describe("solve-issue workflow: Gitea API PR creation", () => { describe("solve-issue workflow: Gitea API PR creation", () => {
// Navigate up from packages/cli/src/__tests__ to repo root // Navigate up from packages/cli/src/__tests__ to repo root
const workflowPath = join( const workflowPath = join(
import.meta.dirname, dirname(fileURLToPath(import.meta.url)),
"..", "..",
"..", "..",
"..", "..",
@@ -1,5 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
/** /**
* B-group tests: validate JSON parsing logic used by spawnAgent. * B-group tests: validate JSON parsing logic used by spawnAgent.
* *
+49 -49
View File
@@ -1,9 +1,9 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { mkdir, mkdtemp, rm } from "node:fs/promises"; import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { bootstrap, putSchema } from "@ocas/core"; import { bootstrap, putSchema } from "@ocas/core";
import { createFsStore } from "@ocas/fs"; import { openStore } from "@ocas/fs";
import type { CasRef } from "@united-workforce/protocol"; import type { CasRef } from "@united-workforce/protocol";
import { cmdStepRead } from "../commands/step.js"; import { cmdStepRead } from "../commands/step.js";
import { registerUwfSchemas } from "../schemas.js"; import { registerUwfSchemas } from "../schemas.js";
@@ -48,7 +48,7 @@ const DETAIL_SCHEMA = {
// ── helpers ─────────────────────────────────────────────────────────────────── // ── helpers ───────────────────────────────────────────────────────────────────
async function registerDetailSchemas(store: ReturnType<typeof createFsStore>) { async function registerDetailSchemas(store: Awaited<ReturnType<typeof openStore>>) {
await bootstrap(store); await bootstrap(store);
const [turn, detail] = await Promise.all([ const [turn, detail] = await Promise.all([
putSchema(store, TURN_SCHEMA), putSchema(store, TURN_SCHEMA),
@@ -92,11 +92,11 @@ describe("step read", () => {
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -113,12 +113,12 @@ describe("step read", () => {
graph: {}, graph: {},
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test task", prompt: "Test task",
}); });
const outputHash = await store.put(schemas.workflow, { const outputHash = await store.cas.put(schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -130,7 +130,7 @@ describe("step read", () => {
const turnHashes: CasRef[] = []; const turnHashes: CasRef[] = [];
for (let i = 1; i <= 3; i++) { for (let i = 1; i <= 3; i++) {
const content = `Turn ${i} content with some text to make it readable.`; const content = `Turn ${i} content with some text to make it readable.`;
const turnHash = await store.put(detailSchemas.turn, { const turnHash = await store.cas.put(detailSchemas.turn, {
index: i - 1, index: i - 1,
role: "assistant", role: "assistant",
content, content,
@@ -140,7 +140,7 @@ describe("step read", () => {
turnHashes.push(turnHash); turnHashes.push(turnHash);
} }
const detailHash = await store.put(detailSchemas.detail, { const detailHash = await store.cas.put(detailSchemas.detail, {
sessionId: "session-1", sessionId: "session-1",
model: "test-model", model: "test-model",
duration: 1000, duration: 1000,
@@ -148,7 +148,7 @@ describe("step read", () => {
turns: turnHashes, turns: turnHashes,
}); });
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -180,11 +180,11 @@ describe("step read", () => {
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -201,12 +201,12 @@ describe("step read", () => {
graph: {}, graph: {},
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test task", prompt: "Test task",
}); });
const outputHash = await store.put(schemas.workflow, { const outputHash = await store.cas.put(schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -218,7 +218,7 @@ describe("step read", () => {
const turnHashes: CasRef[] = []; const turnHashes: CasRef[] = [];
for (let i = 1; i <= 4; i++) { for (let i = 1; i <= 4; i++) {
const content = generateContent(300, `Turn${i}`); const content = generateContent(300, `Turn${i}`);
const turnHash = await store.put(detailSchemas.turn, { const turnHash = await store.cas.put(detailSchemas.turn, {
index: i - 1, index: i - 1,
role: "assistant", role: "assistant",
content, content,
@@ -228,7 +228,7 @@ describe("step read", () => {
turnHashes.push(turnHash); turnHashes.push(turnHash);
} }
const detailHash = await store.put(detailSchemas.detail, { const detailHash = await store.cas.put(detailSchemas.detail, {
sessionId: "session-1", sessionId: "session-1",
model: "test-model", model: "test-model",
duration: 1000, duration: 1000,
@@ -236,7 +236,7 @@ describe("step read", () => {
turns: turnHashes, turns: turnHashes,
}); });
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -266,11 +266,11 @@ describe("step read", () => {
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -287,12 +287,12 @@ describe("step read", () => {
graph: {}, graph: {},
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test task", prompt: "Test task",
}); });
const outputHash = await store.put(schemas.workflow, { const outputHash = await store.cas.put(schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -302,7 +302,7 @@ describe("step read", () => {
// Create 1 turn of 500 chars // Create 1 turn of 500 chars
const content = generateContent(500, "LongTurn"); const content = generateContent(500, "LongTurn");
const turnHash = await store.put(detailSchemas.turn, { const turnHash = await store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content, content,
@@ -310,7 +310,7 @@ describe("step read", () => {
reasoning: null, reasoning: null,
}); });
const detailHash = await store.put(detailSchemas.detail, { const detailHash = await store.cas.put(detailSchemas.detail, {
sessionId: "session-1", sessionId: "session-1",
model: "test-model", model: "test-model",
duration: 1000, duration: 1000,
@@ -318,7 +318,7 @@ describe("step read", () => {
turns: [turnHash], turns: [turnHash],
}); });
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -343,10 +343,10 @@ describe("step read", () => {
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -363,12 +363,12 @@ describe("step read", () => {
graph: {}, graph: {},
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test task", prompt: "Test task",
}); });
const outputHash = await store.put(schemas.workflow, { const outputHash = await store.cas.put(schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -376,7 +376,7 @@ describe("step read", () => {
graph: {}, graph: {},
}); });
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -404,11 +404,11 @@ describe("step read", () => {
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
await registerDetailSchemas(store); await registerDetailSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -425,12 +425,12 @@ describe("step read", () => {
graph: {}, graph: {},
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test task", prompt: "Test task",
}); });
const outputHash = await store.put(schemas.workflow, { const outputHash = await store.cas.put(schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -451,11 +451,11 @@ describe("step read", () => {
await bootstrap(store); await bootstrap(store);
const simpleDetailType = await putSchema(store, SIMPLE_DETAIL_SCHEMA); const simpleDetailType = await putSchema(store, SIMPLE_DETAIL_SCHEMA);
const detailHash = await store.put(simpleDetailType, { const detailHash = await store.cas.put(simpleDetailType, {
sessionId: "session-1", sessionId: "session-1",
}); });
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -482,11 +482,11 @@ describe("step read", () => {
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -503,12 +503,12 @@ describe("step read", () => {
graph: {}, graph: {},
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test task", prompt: "Test task",
}); });
const outputHash = await store.put(schemas.workflow, { const outputHash = await store.cas.put(schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -516,7 +516,7 @@ describe("step read", () => {
graph: {}, graph: {},
}); });
const turnHash = await store.put(detailSchemas.turn, { const turnHash = await store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content: "", content: "",
@@ -524,7 +524,7 @@ describe("step read", () => {
reasoning: null, reasoning: null,
}); });
const detailHash = await store.put(detailSchemas.detail, { const detailHash = await store.cas.put(detailSchemas.detail, {
sessionId: "session-1", sessionId: "session-1",
model: "test-model", model: "test-model",
duration: 1000, duration: 1000,
@@ -532,7 +532,7 @@ describe("step read", () => {
turns: [turnHash], turns: [turnHash],
}); });
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -556,11 +556,11 @@ describe("step read", () => {
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -577,12 +577,12 @@ describe("step read", () => {
graph: {}, graph: {},
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test task", prompt: "Test task",
}); });
const outputHash = await store.put(schemas.workflow, { const outputHash = await store.cas.put(schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -592,7 +592,7 @@ describe("step read", () => {
// Create turn with special markdown characters // Create turn with special markdown characters
const content = "This has `backticks`, **bold**, *italic*, and [links](http://example.com)"; const content = "This has `backticks`, **bold**, *italic*, and [links](http://example.com)";
const turnHash = await store.put(detailSchemas.turn, { const turnHash = await store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content, content,
@@ -600,7 +600,7 @@ describe("step read", () => {
reasoning: null, reasoning: null,
}); });
const detailHash = await store.put(detailSchemas.detail, { const detailHash = await store.cas.put(detailSchemas.detail, {
sessionId: "session-1", sessionId: "session-1",
model: "test-model", model: "test-model",
duration: 1000, duration: 1000,
@@ -608,7 +608,7 @@ describe("step read", () => {
turns: [turnHash], turns: [turnHash],
}); });
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -1,9 +1,9 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { mkdir, mkdtemp, rm } from "node:fs/promises"; import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { bootstrap, type Hash, type JSONSchema, putSchema } from "@ocas/core"; import { bootstrap, type Hash, type JSONSchema, putSchema } from "@ocas/core";
import { createFsStore } from "@ocas/fs"; import { openStore } from "@ocas/fs";
import type { CasRef, StepNodePayload } from "@united-workforce/protocol"; import type { CasRef, StepNodePayload } from "@united-workforce/protocol";
import { cmdStepShow } from "../commands/step.js"; import { cmdStepShow } from "../commands/step.js";
import { formatOutput } from "../format.js"; import { formatOutput } from "../format.js";
@@ -52,7 +52,7 @@ const DETAIL_SCHEMA: JSONSchema = {
}; };
type TestSetup = { type TestSetup = {
store: ReturnType<typeof createFsStore>; store: Awaited<ReturnType<typeof openStore>>;
schemas: { schemas: {
workflow: Hash; workflow: Hash;
startNode: Hash; startNode: Hash;
@@ -64,7 +64,7 @@ type TestSetup = {
}; };
async function setupTest(casDir: string): Promise<TestSetup> { async function setupTest(casDir: string): Promise<TestSetup> {
const store = createFsStore(casDir); const store = await openStore(casDir);
await bootstrap(store); await bootstrap(store);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const [turnType, detailType] = await Promise.all([ const [turnType, detailType] = await Promise.all([
@@ -88,22 +88,22 @@ async function createTestStep(
// Create turn nodes // Create turn nodes
const turnHashes: CasRef[] = []; const turnHashes: CasRef[] = [];
for (const payload of turnPayloads) { for (const payload of turnPayloads) {
const turnHash = await store.put(turnType, payload); const turnHash = await store.cas.put(turnType, payload);
turnHashes.push(turnHash); turnHashes.push(turnHash);
} }
// Create detail node // Create detail node
const detailHash = await store.put(detailType, { turns: turnHashes }); const detailHash = await store.cas.put(detailType, { turns: turnHashes });
// Create dummy start node // Create dummy start node
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: "0000000000000" as CasRef, workflow: "0000000000000" as CasRef,
prompt: "test prompt", prompt: "test prompt",
cwd: "/tmp", cwd: "/tmp",
}); });
// Create dummy output node // Create dummy output node
const outputHash = await store.put(schemas.text, { $status: "done" }); const outputHash = await store.cas.put(schemas.text, { $status: "done" });
// Create step node // Create step node
const stepPayload: StepNodePayload = { const stepPayload: StepNodePayload = {
@@ -119,7 +119,7 @@ async function createTestStep(
assembledPrompt: null, assembledPrompt: null,
cwd: "/tmp", cwd: "/tmp",
}; };
return store.put(schemas.stepNode, stepPayload); return store.cas.put(schemas.stepNode, stepPayload);
} }
describe("cmdStepShow JSON serialization", () => { describe("cmdStepShow JSON serialization", () => {
+28 -28
View File
@@ -1,9 +1,9 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { mkdir, mkdtemp, rm } from "node:fs/promises"; import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { bootstrap, putSchema } from "@ocas/core"; import { bootstrap, putSchema } from "@ocas/core";
import { createFsStore } from "@ocas/fs"; import { openStore } from "@ocas/fs";
import type { CasRef, ThreadId } from "@united-workforce/protocol"; import type { CasRef, ThreadId } from "@united-workforce/protocol";
import { STEP_NODE_SCHEMA } from "@united-workforce/protocol"; import { STEP_NODE_SCHEMA } from "@united-workforce/protocol";
import { cmdStepList } from "../commands/step.js"; import { cmdStepList } from "../commands/step.js";
@@ -51,7 +51,7 @@ const DETAIL_SCHEMA = {
// ── helpers ────────────────────────────────────────────────────────────────── // ── helpers ──────────────────────────────────────────────────────────────────
async function registerDetailSchemas(store: ReturnType<typeof createFsStore>) { async function registerDetailSchemas(store: Awaited<ReturnType<typeof openStore>>) {
await bootstrap(store); await bootstrap(store);
const [turn, detail] = await Promise.all([ const [turn, detail] = await Promise.all([
putSchema(store, TURN_SCHEMA), putSchema(store, TURN_SCHEMA),
@@ -133,18 +133,18 @@ describe("StepNode JSON schema", () => {
test("StepNode with timing fields passes CAS validation", async () => { test("StepNode with timing fields passes CAS validation", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: "placeholder0000" as CasRef, workflow: "placeholder0000" as CasRef,
prompt: "test", prompt: "test",
}); });
const outputHash = await store.put(schemas.text, "output text"); const outputHash = await store.cas.put(schemas.text, "output text");
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
const detailHash = await store.put(detailSchemas.detail, { const detailHash = await store.cas.put(detailSchemas.detail, {
sessionId: "s1", sessionId: "s1",
model: "m1", model: "m1",
duration: 100, duration: 100,
@@ -153,7 +153,7 @@ describe("StepNode JSON schema", () => {
}); });
// Should succeed — valid timing fields // Should succeed — valid timing fields
const hash = await store.put(schemas.stepNode, { const hash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -175,24 +175,24 @@ describe("step list timing", () => {
test("step list includes durationMs = completedAtMs - startedAtMs", async () => { test("step list includes durationMs = completedAtMs - startedAtMs", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: {}, roles: {},
graph: {}, graph: {},
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "test", prompt: "test",
}); });
const outputHash = await store.put(schemas.text, "output"); const outputHash = await store.cas.put(schemas.text, "output");
const detailHash = await store.put(detailSchemas.detail, { const detailHash = await store.cas.put(detailSchemas.detail, {
sessionId: "s1", sessionId: "s1",
model: "m1", model: "m1",
duration: 100, duration: 100,
@@ -203,7 +203,7 @@ describe("step list timing", () => {
const startedAt = 1716600000000; const startedAt = 1716600000000;
const completedAt = 1716600003500; const completedAt = 1716600003500;
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -233,11 +233,11 @@ describe("thread read timing", () => {
test("thread read header includes Duration", async () => { test("thread read header includes Duration", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -256,28 +256,28 @@ describe("thread read timing", () => {
}, },
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "test task", prompt: "test task",
}); });
const turnHash = await store.put(detailSchemas.turn, { const turnHash = await store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content: "Done.", content: "Done.",
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const detailHash = await store.put(detailSchemas.detail, { const detailHash = await store.cas.put(detailSchemas.detail, {
sessionId: "s1", sessionId: "s1",
model: "m1", model: "m1",
duration: 100, duration: 100,
turnCount: 1, turnCount: 1,
turns: [turnHash], turns: [turnHash],
}); });
const outputHash = await store.put(schemas.text, "output"); const outputHash = await store.cas.put(schemas.text, "output");
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -299,11 +299,11 @@ describe("thread read timing", () => {
test("thread read shows sub-second duration as ms", async () => { test("thread read shows sub-second duration as ms", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -322,28 +322,28 @@ describe("thread read timing", () => {
}, },
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "test", prompt: "test",
}); });
const turnHash = await store.put(detailSchemas.turn, { const turnHash = await store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content: "Done.", content: "Done.",
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const detailHash = await store.put(detailSchemas.detail, { const detailHash = await store.cas.put(detailSchemas.detail, {
sessionId: "s1", sessionId: "s1",
model: "m1", model: "m1",
duration: 100, duration: 100,
turnCount: 1, turnCount: 1,
turns: [turnHash], turns: [turnHash],
}); });
const outputHash = await store.put(schemas.text, "output"); const outputHash = await store.cas.put(schemas.text, "output");
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { mkdir, rm } from "node:fs/promises"; import { mkdir, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -113,7 +113,7 @@ describe("Global CAS directory", () => {
// Store a node in the first store // Store a node in the first store
const testData = { test: "data" }; const testData = { test: "data" };
const _hash = uwf1.store.put(uwf1.schemas.text, JSON.stringify(testData)); const _hash = uwf1.store.cas.put(uwf1.schemas.text, JSON.stringify(testData));
// Both stores share the same CAS filesystem directory // Both stores share the same CAS filesystem directory
// Since schemas are registered idempotently, they should have the same hash // Since schemas are registered idempotently, they should have the same hash
@@ -133,14 +133,14 @@ describe("Global CAS directory", () => {
await mkdir(storageRoot, { recursive: true }); await mkdir(storageRoot, { recursive: true });
const uwf = await createUwfStore(storageRoot); const uwf = await createUwfStore(storageRoot);
const hash = await uwf.store.put(uwf.schemas.text, "registry-test"); const hash = await uwf.store.cas.put(uwf.schemas.text, "registry-test");
saveWorkflowRegistry(uwf.varStore, "test-workflow", hash); saveWorkflowRegistry(uwf.varStore, "test-workflow", hash);
const registry = loadWorkflowRegistry(uwf.varStore); const registry = loadWorkflowRegistry(uwf.varStore);
expect(registry["test-workflow"]).toBe(hash); expect(registry["test-workflow"]).toBe(hash);
const { access } = await import("node:fs/promises"); const { access } = await import("node:fs/promises");
await access(join(globalCasDir, "variables.db")); await access(join(globalCasDir, "vars"));
const registryPath = join(storageRoot, "workflows.yaml"); const registryPath = join(storageRoot, "workflows.yaml");
await expect(access(registryPath)).rejects.toThrow(); await expect(access(registryPath)).rejects.toThrow();
@@ -154,7 +154,7 @@ describe("Global CAS directory", () => {
await mkdir(storageRoot, { recursive: true }); await mkdir(storageRoot, { recursive: true });
const uwfSeed = await createUwfStore(storageRoot); const uwfSeed = await createUwfStore(storageRoot);
const hash = await uwfSeed.store.put(uwfSeed.schemas.text, "migrated-workflow"); const hash = await uwfSeed.store.cas.put(uwfSeed.schemas.text, "migrated-workflow");
const registryPath = getRegistryPath(storageRoot); const registryPath = getRegistryPath(storageRoot);
const { writeFile, access, readFile } = await import("node:fs/promises"); const { writeFile, access, readFile } = await import("node:fs/promises");
@@ -180,7 +180,7 @@ describe("Global CAS directory", () => {
const threadId = "01JTEST0000000000000000AB" as ThreadId; const threadId = "01JTEST0000000000000000AB" as ThreadId;
const uwfSeed = await createUwfStore(storageRoot); const uwfSeed = await createUwfStore(storageRoot);
const headHash = await uwfSeed.store.put(uwfSeed.schemas.text, "migrated-thread-head"); const headHash = await uwfSeed.store.cas.put(uwfSeed.schemas.text, "migrated-thread-head");
const { writeFile, access, readFile } = await import("node:fs/promises"); const { writeFile, access, readFile } = await import("node:fs/promises");
const threadsPath = join(storageRoot, "threads.yaml"); const threadsPath = join(storageRoot, "threads.yaml");
await writeFile(threadsPath, `${threadId}: ${headHash}\n`, "utf8"); await writeFile(threadsPath, `${threadId}: ${headHash}\n`, "utf8");
@@ -204,7 +204,7 @@ describe("Global CAS directory", () => {
const threadId = "01JTEST000000000000000123" as ThreadId; const threadId = "01JTEST000000000000000123" as ThreadId;
const uwfSeed = await createUwfStore(storageRoot); const uwfSeed = await createUwfStore(storageRoot);
const headHash = await uwfSeed.store.put(uwfSeed.schemas.text, "hash-456"); const headHash = await uwfSeed.store.cas.put(uwfSeed.schemas.text, "hash-456");
setThread(uwfSeed.varStore, threadId, createThreadIndexEntry(headHash)); setThread(uwfSeed.varStore, threadId, createThreadIndexEntry(headHash));
const uwf = await createUwfStore(storageRoot); const uwf = await createUwfStore(storageRoot);
@@ -225,7 +225,7 @@ describe("Global CAS directory", () => {
const uwf = await createUwfStore(storageRoot); const uwf = await createUwfStore(storageRoot);
const threadId = "thread-123" as ThreadId; const threadId = "thread-123" as ThreadId;
const headHash = await uwf.store.put(uwf.schemas.text, "history-head"); const headHash = await uwf.store.cas.put(uwf.schemas.text, "history-head");
const { addHistoryEntry, findHistoryEntry } = await import("../store.js"); const { addHistoryEntry, findHistoryEntry } = await import("../store.js");
addHistoryEntry(uwf.varStore, { addHistoryEntry(uwf.varStore, {
thread: threadId, thread: threadId,
@@ -241,7 +241,7 @@ describe("Global CAS directory", () => {
expect(entry?.head).toBe(headHash); expect(entry?.head).toBe(headHash);
const { access } = await import("node:fs/promises"); const { access } = await import("node:fs/promises");
await access(join(globalCasDir, "variables.db")); await access(join(globalCasDir, "vars"));
const historyPath = join(storageRoot, "history.jsonl"); const historyPath = join(storageRoot, "history.jsonl");
await expect(access(historyPath)).rejects.toThrow(); await expect(access(historyPath)).rejects.toThrow();
@@ -256,8 +256,8 @@ describe("Global CAS directory", () => {
const threadId = "01JTEST0000000000000000CD" as ThreadId; const threadId = "01JTEST0000000000000000CD" as ThreadId;
const uwfSeed = await createUwfStore(storageRoot); const uwfSeed = await createUwfStore(storageRoot);
const workflowHash = await uwfSeed.store.put(uwfSeed.schemas.text, "migrated-workflow"); const workflowHash = await uwfSeed.store.cas.put(uwfSeed.schemas.text, "migrated-workflow");
const headHash = await uwfSeed.store.put(uwfSeed.schemas.text, "migrated-head"); const headHash = await uwfSeed.store.cas.put(uwfSeed.schemas.text, "migrated-head");
const completedAt = 1780410000000; const completedAt = 1780410000000;
const { writeFile, access, readFile } = await import("node:fs/promises"); const { writeFile, access, readFile } = await import("node:fs/promises");
const historyPath = join(storageRoot, "history.jsonl"); const historyPath = join(storageRoot, "history.jsonl");
@@ -301,7 +301,7 @@ describe("Global CAS directory", () => {
// Store a CAS node // Store a CAS node
const testPayload = JSON.stringify({ test: "node" }); const testPayload = JSON.stringify({ test: "node" });
const _hash = uwf.store.put(uwf.schemas.text, testPayload); const _hash = uwf.store.cas.put(uwf.schemas.text, testPayload);
// Verify the node is in global CAS directory // Verify the node is in global CAS directory
const { readdir } = await import("node:fs/promises"); const { readdir } = await import("node:fs/promises");
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { homedir } from "node:os"; import { homedir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { getDefaultStorageRoot, getGlobalCasDir, resolveStorageRoot } from "../store.js"; import { getDefaultStorageRoot, getGlobalCasDir, resolveStorageRoot } from "../store.js";
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { mkdir, mkdtemp } from "node:fs/promises"; import { mkdir, mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -16,7 +16,7 @@ async function seedHistoryHead(
uwf: Awaited<ReturnType<typeof createUwfStore>>, uwf: Awaited<ReturnType<typeof createUwfStore>>,
label: string, label: string,
): Promise<CasRef> { ): Promise<CasRef> {
return (await uwf.store.put(uwf.schemas.text, label)) as CasRef; return (await uwf.store.cas.put(uwf.schemas.text, label)) as CasRef;
} }
describe("thread cancel status", () => { describe("thread cancel status", () => {
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { mkdir, mkdtemp, rm } from "node:fs/promises"; import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -39,7 +39,7 @@ async function createTestWorkflow(uwf: UwfStore): Promise<CasRef> {
graph: { start: "role1" }, graph: { start: "role1" },
conditions: {}, conditions: {},
}; };
return await uwf.store.put(uwf.schemas.workflow, workflowPayload); return await uwf.store.cas.put(uwf.schemas.workflow, workflowPayload);
} }
async function createTestThread( async function createTestThread(
@@ -54,7 +54,7 @@ async function createTestThread(
prompt: "test prompt", prompt: "test prompt",
cwd: storageRoot, cwd: storageRoot,
}; };
const headHash = await uwf.store.put(uwf.schemas.startNode, startPayload); const headHash = await uwf.store.cas.put(uwf.schemas.startNode, startPayload);
setThread(uwf.varStore, threadId, createThreadIndexEntry(headHash)); setThread(uwf.varStore, threadId, createThreadIndexEntry(headHash));
@@ -494,7 +494,7 @@ describe("edge cases", () => {
const uwfIdx = await createUwfStore(tmpDir); const uwfIdx = await createUwfStore(tmpDir);
const index = loadAllThreads(uwfIdx.varStore); const index = loadAllThreads(uwfIdx.varStore);
const placeholderHead = (await uwfIdx.store.put( const placeholderHead = (await uwfIdx.store.cas.put(
uwfIdx.schemas.text, uwfIdx.schemas.text,
"invalid-ulid-placeholder", "invalid-ulid-placeholder",
)) as CasRef; )) as CasRef;
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { mkdir, rm, writeFile } from "node:fs/promises"; import { mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -82,7 +82,7 @@ graph:
const headHash = getThread(uwf.varStore, result.thread as ThreadId)!.head; const headHash = getThread(uwf.varStore, result.thread as ThreadId)!.head;
expect(headHash).toBeDefined(); expect(headHash).toBeDefined();
const startNode = uwf.store.get(headHash as CasRef); const startNode = uwf.store.cas.get(headHash as CasRef);
expect(startNode).not.toBe(null); expect(startNode).not.toBe(null);
expect(startNode?.type).toBe(uwf.schemas.startNode); expect(startNode?.type).toBe(uwf.schemas.startNode);
@@ -175,7 +175,7 @@ graph:
const uwf = await createUwfStore(storageRoot); const uwf = await createUwfStore(storageRoot);
const headHash = getThread(uwf.varStore, result.thread as ThreadId)!.head; const headHash = getThread(uwf.varStore, result.thread as ThreadId)!.head;
const startNode = uwf.store.get(headHash as CasRef); const startNode = uwf.store.cas.get(headHash as CasRef);
const startPayload = startNode?.payload as StartNodePayload; const startPayload = startNode?.payload as StartNodePayload;
// Should default to process.cwd() // Should default to process.cwd()
@@ -1,9 +1,9 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { mkdir, mkdtemp, rm } from "node:fs/promises"; import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { bootstrap, putSchema } from "@ocas/core"; import { bootstrap, putSchema } from "@ocas/core";
import { createFsStore } from "@ocas/fs"; import { openStore } from "@ocas/fs";
import type { CasRef, ThreadId } from "@united-workforce/protocol"; import type { CasRef, ThreadId } from "@united-workforce/protocol";
import { cmdThreadRead } from "../commands/thread.js"; import { cmdThreadRead } from "../commands/thread.js";
import { registerUwfSchemas } from "../schemas.js"; import { registerUwfSchemas } from "../schemas.js";
@@ -49,7 +49,7 @@ const DETAIL_SCHEMA = {
// ── helpers ─────────────────────────────────────────────────────────────────── // ── helpers ───────────────────────────────────────────────────────────────────
async function registerDetailSchemas(store: ReturnType<typeof createFsStore>) { async function registerDetailSchemas(store: Awaited<ReturnType<typeof openStore>>) {
await bootstrap(store); await bootstrap(store);
const [turn, detail] = await Promise.all([ const [turn, detail] = await Promise.all([
putSchema(store, TURN_SCHEMA), putSchema(store, TURN_SCHEMA),
@@ -91,11 +91,11 @@ describe("thread read --quota flag", () => {
test("test 1: basic quota enforcement with 3 steps", async () => { test("test 1: basic quota enforcement with 3 steps", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -112,12 +112,12 @@ describe("thread read --quota flag", () => {
graph: {}, graph: {},
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test task", prompt: "Test task",
}); });
const outputHash = await store.put(schemas.workflow, { const outputHash = await store.cas.put(schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -129,21 +129,21 @@ describe("thread read --quota flag", () => {
const steps: CasRef[] = []; const steps: CasRef[] = [];
for (let i = 1; i <= 3; i++) { for (let i = 1; i <= 3; i++) {
const content = generateContent(500, `Step${i}`); const content = generateContent(500, `Step${i}`);
const turnHash = await store.put(detailSchemas.turn, { const turnHash = await store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content, content,
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const detailHash = await store.put(detailSchemas.detail, { const detailHash = await store.cas.put(detailSchemas.detail, {
sessionId: `session-${i}`, sessionId: `session-${i}`,
model: "test-model", model: "test-model",
duration: 1000, duration: 1000,
turnCount: 1, turnCount: 1,
turns: [turnHash], turns: [turnHash],
}); });
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: steps[i - 2] ?? null, prev: steps[i - 2] ?? null,
role: "worker", role: "worker",
@@ -176,11 +176,11 @@ describe("thread read --quota flag", () => {
test("test 2: quota check order - verifies bug is fixed", async () => { test("test 2: quota check order - verifies bug is fixed", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -197,12 +197,12 @@ describe("thread read --quota flag", () => {
graph: {}, graph: {},
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test task", prompt: "Test task",
}); });
const outputHash = await store.put(schemas.workflow, { const outputHash = await store.cas.put(schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -212,21 +212,21 @@ describe("thread read --quota flag", () => {
// Create 2 steps: first=300 chars, second=600 chars // Create 2 steps: first=300 chars, second=600 chars
const step1Content = generateContent(300, "First"); const step1Content = generateContent(300, "First");
const step1TurnHash = await store.put(detailSchemas.turn, { const step1TurnHash = await store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content: step1Content, content: step1Content,
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const step1DetailHash = await store.put(detailSchemas.detail, { const step1DetailHash = await store.cas.put(detailSchemas.detail, {
sessionId: "session-1", sessionId: "session-1",
model: "test-model", model: "test-model",
duration: 1000, duration: 1000,
turnCount: 1, turnCount: 1,
turns: [step1TurnHash], turns: [step1TurnHash],
}); });
const step1Hash = await store.put(schemas.stepNode, { const step1Hash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -239,21 +239,21 @@ describe("thread read --quota flag", () => {
}); });
const step2Content = generateContent(600, "Second"); const step2Content = generateContent(600, "Second");
const step2TurnHash = await store.put(detailSchemas.turn, { const step2TurnHash = await store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content: step2Content, content: step2Content,
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const step2DetailHash = await store.put(detailSchemas.detail, { const step2DetailHash = await store.cas.put(detailSchemas.detail, {
sessionId: "session-2", sessionId: "session-2",
model: "test-model", model: "test-model",
duration: 1000, duration: 1000,
turnCount: 1, turnCount: 1,
turns: [step2TurnHash], turns: [step2TurnHash],
}); });
const step2Hash = await store.put(schemas.stepNode, { const step2Hash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: step1Hash, prev: step1Hash,
role: "worker", role: "worker",
@@ -287,11 +287,11 @@ describe("thread read --quota flag", () => {
test("test 3: quota with --start section", async () => { test("test 3: quota with --start section", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -308,12 +308,12 @@ describe("thread read --quota flag", () => {
graph: {}, graph: {},
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test task with a moderately long prompt to test quota accounting", prompt: "Test task with a moderately long prompt to test quota accounting",
}); });
const outputHash = await store.put(schemas.workflow, { const outputHash = await store.cas.put(schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -325,21 +325,21 @@ describe("thread read --quota flag", () => {
const steps: CasRef[] = []; const steps: CasRef[] = [];
for (let i = 1; i <= 2; i++) { for (let i = 1; i <= 2; i++) {
const content = generateContent(400, `Step${i}`); const content = generateContent(400, `Step${i}`);
const turnHash = await store.put(detailSchemas.turn, { const turnHash = await store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content, content,
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const detailHash = await store.put(detailSchemas.detail, { const detailHash = await store.cas.put(detailSchemas.detail, {
sessionId: `session-${i}`, sessionId: `session-${i}`,
model: "test-model", model: "test-model",
duration: 1000, duration: 1000,
turnCount: 1, turnCount: 1,
turns: [turnHash], turns: [turnHash],
}); });
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: steps[i - 2] ?? null, prev: steps[i - 2] ?? null,
role: "worker", role: "worker",
@@ -370,11 +370,11 @@ describe("thread read --quota flag", () => {
test("test 5a: quota edge case - minimal quota", async () => { test("test 5a: quota edge case - minimal quota", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -391,12 +391,12 @@ describe("thread read --quota flag", () => {
graph: {}, graph: {},
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test task", prompt: "Test task",
}); });
const outputHash = await store.put(schemas.workflow, { const outputHash = await store.cas.put(schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -405,21 +405,21 @@ describe("thread read --quota flag", () => {
}); });
const content = generateContent(500, "Test"); const content = generateContent(500, "Test");
const turnHash = await store.put(detailSchemas.turn, { const turnHash = await store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content, content,
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const detailHash = await store.put(detailSchemas.detail, { const detailHash = await store.cas.put(detailSchemas.detail, {
sessionId: "session-1", sessionId: "session-1",
model: "test-model", model: "test-model",
duration: 1000, duration: 1000,
turnCount: 1, turnCount: 1,
turns: [turnHash], turns: [turnHash],
}); });
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -445,11 +445,11 @@ describe("thread read --quota flag", () => {
test("test 5b: quota edge case - very large quota", async () => { test("test 5b: quota edge case - very large quota", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -466,12 +466,12 @@ describe("thread read --quota flag", () => {
graph: {}, graph: {},
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test task", prompt: "Test task",
}); });
const outputHash = await store.put(schemas.workflow, { const outputHash = await store.cas.put(schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -483,21 +483,21 @@ describe("thread read --quota flag", () => {
const steps: CasRef[] = []; const steps: CasRef[] = [];
for (let i = 1; i <= 3; i++) { for (let i = 1; i <= 3; i++) {
const content = generateContent(300, `Step${i}`); const content = generateContent(300, `Step${i}`);
const turnHash = await store.put(detailSchemas.turn, { const turnHash = await store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content, content,
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const detailHash = await store.put(detailSchemas.detail, { const detailHash = await store.cas.put(detailSchemas.detail, {
sessionId: `session-${i}`, sessionId: `session-${i}`,
model: "test-model", model: "test-model",
duration: 1000, duration: 1000,
turnCount: 1, turnCount: 1,
turns: [turnHash], turns: [turnHash],
}); });
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: steps[i - 2] ?? null, prev: steps[i - 2] ?? null,
role: "worker", role: "worker",
@@ -527,11 +527,11 @@ describe("thread read --quota flag", () => {
test("test 6: quota with --before parameter", async () => { test("test 6: quota with --before parameter", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const detailSchemas = await registerDetailSchemas(store); const detailSchemas = await registerDetailSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -548,12 +548,12 @@ describe("thread read --quota flag", () => {
graph: {}, graph: {},
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test task", prompt: "Test task",
}); });
const outputHash = await store.put(schemas.workflow, { const outputHash = await store.cas.put(schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -565,21 +565,21 @@ describe("thread read --quota flag", () => {
const steps: CasRef[] = []; const steps: CasRef[] = [];
for (let i = 1; i <= 5; i++) { for (let i = 1; i <= 5; i++) {
const content = generateContent(300, `Step${i}`); const content = generateContent(300, `Step${i}`);
const turnHash = await store.put(detailSchemas.turn, { const turnHash = await store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content, content,
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const detailHash = await store.put(detailSchemas.detail, { const detailHash = await store.cas.put(detailSchemas.detail, {
sessionId: `session-${i}`, sessionId: `session-${i}`,
model: "test-model", model: "test-model",
duration: 1000, duration: 1000,
turnCount: 1, turnCount: 1,
turns: [turnHash], turns: [turnHash],
}); });
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: steps[i - 2] ?? null, prev: steps[i - 2] ?? null,
role: "worker", role: "worker",
@@ -1,9 +1,8 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { mkdir, mkdtemp, rm } from "node:fs/promises"; import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { bootstrap, putSchema } from "@ocas/core"; import { bootstrap, putSchema, type Store } from "@ocas/core";
import type { createFsStore } from "@ocas/fs";
import type { CasRef, ThreadId } from "@united-workforce/protocol"; import type { CasRef, ThreadId } from "@united-workforce/protocol";
import { cmdThreadRead, THREAD_READ_DEFAULT_QUOTA } from "../commands/thread.js"; import { cmdThreadRead, THREAD_READ_DEFAULT_QUOTA } from "../commands/thread.js";
import type { UwfStore } from "../store.js"; import type { UwfStore } from "../store.js";
@@ -57,7 +56,7 @@ async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
return createUwfStore(storageRoot); return createUwfStore(storageRoot);
} }
async function registerDetailSchemas(store: ReturnType<typeof createFsStore>) { async function registerDetailSchemas(store: Store) {
await bootstrap(store); await bootstrap(store);
const [turn, detail] = await Promise.all([ const [turn, detail] = await Promise.all([
putSchema(store, TURN_SCHEMA), putSchema(store, TURN_SCHEMA),
@@ -85,7 +84,7 @@ describe("thread read XML tag isolation", () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const detailSchemas = await registerDetailSchemas(uwf.store); const detailSchemas = await registerDetailSchemas(uwf.store);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -102,12 +101,12 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Fix issue #459", prompt: "Fix issue #459",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -115,7 +114,7 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const turnHash = await uwf.store.put(detailSchemas.turn, { const turnHash = await uwf.store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content: content:
@@ -123,7 +122,7 @@ describe("thread read XML tag isolation", () => {
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const detailHash = await uwf.store.put(detailSchemas.detail, { const detailHash = await uwf.store.cas.put(detailSchemas.detail, {
sessionId: "sx", sessionId: "sx",
model: "mx", model: "mx",
duration: 500, duration: 500,
@@ -131,7 +130,7 @@ describe("thread read XML tag isolation", () => {
turns: [turnHash], turns: [turnHash],
}); });
const stepHash = await uwf.store.put(uwf.schemas.stepNode, { const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "planner", role: "planner",
@@ -164,7 +163,7 @@ describe("thread read XML tag isolation", () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const detailSchemas = await registerDetailSchemas(uwf.store); const detailSchemas = await registerDetailSchemas(uwf.store);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -181,12 +180,12 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Fix issue", prompt: "Fix issue",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -194,14 +193,14 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const turnHash = await uwf.store.put(detailSchemas.turn, { const turnHash = await uwf.store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content: "---\nstatus: ready\n---\n\nContent here...", content: "---\nstatus: ready\n---\n\nContent here...",
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const detailHash = await uwf.store.put(detailSchemas.detail, { const detailHash = await uwf.store.cas.put(detailSchemas.detail, {
sessionId: "sx", sessionId: "sx",
model: "mx", model: "mx",
duration: 500, duration: 500,
@@ -209,7 +208,7 @@ describe("thread read XML tag isolation", () => {
turns: [turnHash], turns: [turnHash],
}); });
const stepHash = await uwf.store.put(uwf.schemas.stepNode, { const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "planner", role: "planner",
@@ -242,7 +241,7 @@ describe("thread read XML tag isolation", () => {
test("scenario 3: same role repeated does not show prompt twice", async () => { test("scenario 3: same role repeated does not show prompt twice", async () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -259,12 +258,12 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Write something", prompt: "Write something",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -272,7 +271,7 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const step1 = await uwf.store.put(uwf.schemas.stepNode, { const step1 = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "writer", role: "writer",
@@ -284,7 +283,7 @@ describe("thread read XML tag isolation", () => {
assembledPrompt: null, assembledPrompt: null,
}); });
const step2 = await uwf.store.put(uwf.schemas.stepNode, { const step2 = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: step1 as CasRef, prev: step1 as CasRef,
role: "writer", role: "writer",
@@ -309,7 +308,7 @@ describe("thread read XML tag isolation", () => {
test("scenario 4: step with no detail shows no output tags", async () => { test("scenario 4: step with no detail shows no output tags", async () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -326,12 +325,12 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Do stuff", prompt: "Do stuff",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -339,7 +338,7 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const stepHash = await uwf.store.put(uwf.schemas.stepNode, { const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -370,7 +369,7 @@ describe("thread read XML tag isolation", () => {
test("scenario 5: empty content shows no output tags", async () => { test("scenario 5: empty content shows no output tags", async () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: {}, roles: {},
@@ -378,12 +377,12 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Do stuff", prompt: "Do stuff",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -394,7 +393,7 @@ describe("thread read XML tag isolation", () => {
// A detail ref that doesn't exist → extractLastAssistantContent returns null // A detail ref that doesn't exist → extractLastAssistantContent returns null
const missingDetailRef = "missingdetail0" as CasRef; const missingDetailRef = "missingdetail0" as CasRef;
const stepHash = await uwf.store.put(uwf.schemas.stepNode, { const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -419,7 +418,7 @@ describe("thread read XML tag isolation", () => {
test("scenario 6: thread read with --start flag shows task section", async () => { test("scenario 6: thread read with --start flag shows task section", async () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -436,12 +435,12 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Initial prompt", prompt: "Initial prompt",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -449,7 +448,7 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const stepHash = await uwf.store.put(uwf.schemas.stepNode, { const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "roleA", role: "roleA",
@@ -478,7 +477,7 @@ describe("thread read XML tag isolation", () => {
test("scenario 7: thread read with --before parameter", async () => { test("scenario 7: thread read with --before parameter", async () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -511,12 +510,12 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Initial prompt", prompt: "Initial prompt",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -524,7 +523,7 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const step1 = await uwf.store.put(uwf.schemas.stepNode, { const step1 = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "roleA", role: "roleA",
@@ -536,7 +535,7 @@ describe("thread read XML tag isolation", () => {
assembledPrompt: null, assembledPrompt: null,
}); });
const step2 = await uwf.store.put(uwf.schemas.stepNode, { const step2 = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: step1 as CasRef, prev: step1 as CasRef,
role: "roleB", role: "roleB",
@@ -548,7 +547,7 @@ describe("thread read XML tag isolation", () => {
assembledPrompt: null, assembledPrompt: null,
}); });
const step3 = await uwf.store.put(uwf.schemas.stepNode, { const step3 = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: step2 as CasRef, prev: step2 as CasRef,
role: "roleC", role: "roleC",
@@ -584,7 +583,7 @@ describe("thread read XML tag isolation", () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const detailSchemas = await registerDetailSchemas(uwf.store); const detailSchemas = await registerDetailSchemas(uwf.store);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -601,12 +600,12 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Write something", prompt: "Write something",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -614,14 +613,14 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const turnHash = await uwf.store.put(detailSchemas.turn, { const turnHash = await uwf.store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content: "Content with <special> & characters > like <this>", content: "Content with <special> & characters > like <this>",
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const detailHash = await uwf.store.put(detailSchemas.detail, { const detailHash = await uwf.store.cas.put(detailSchemas.detail, {
sessionId: "sx", sessionId: "sx",
model: "mx", model: "mx",
duration: 500, duration: 500,
@@ -629,7 +628,7 @@ describe("thread read XML tag isolation", () => {
turns: [turnHash], turns: [turnHash],
}); });
const stepHash = await uwf.store.put(uwf.schemas.stepNode, { const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "writer", role: "writer",
@@ -653,7 +652,7 @@ describe("thread read XML tag isolation", () => {
test("scenario 10: quota limit with XML tags", async () => { test("scenario 10: quota limit with XML tags", async () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -670,12 +669,12 @@ describe("thread read XML tag isolation", () => {
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Initial prompt", prompt: "Initial prompt",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -686,7 +685,7 @@ describe("thread read XML tag isolation", () => {
const steps: CasRef[] = []; const steps: CasRef[] = [];
let prev: CasRef | null = null; let prev: CasRef | null = null;
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
const step = (await uwf.store.put(uwf.schemas.stepNode, { const step = (await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev, prev,
role: "roleA", role: "roleA",
@@ -1,10 +1,12 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { putSchema } from "@ocas/core"; import { putSchema } from "@ocas/core";
import { createFsStore } from "@ocas/fs"; import { openStore } from "@ocas/fs";
import type { CasRef, StepNodePayload, ThreadId } from "@united-workforce/protocol"; import type { CasRef, StepNodePayload, ThreadId } from "@united-workforce/protocol";
import { cmdThreadShow } from "../commands/thread.js"; import { cmdThreadShow } from "../commands/thread.js";
import { registerUwfSchemas } from "../schemas.js"; import { registerUwfSchemas } from "../schemas.js";
@@ -43,11 +45,11 @@ async function setupSuspendedThread(mode: MockAgentMode): Promise<{
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const outputSchemaHash = await putSchema(store, OUTPUT_SCHEMA); const outputSchemaHash = await putSchema(store, OUTPUT_SCHEMA);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-resume", name: "test-resume",
description: "resume command integration test", description: "resume command integration test",
roles: { roles: {
@@ -82,7 +84,7 @@ async function setupSuspendedThread(mode: MockAgentMode): Promise<{
}, },
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test resume task", prompt: "Test resume task",
cwd: tmpDir, cwd: tmpDir,
@@ -91,16 +93,16 @@ async function setupSuspendedThread(mode: MockAgentMode): Promise<{
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
await seedThreads(tmpDir, { [THREAD_ID]: startHash }); await seedThreads(tmpDir, { [THREAD_ID]: startHash });
const outputHash = await store.put(outputSchemaHash, { const outputHash = await store.cas.put(outputSchemaHash, {
$status: "needs_input", $status: "needs_input",
question: "Which API?", question: "Which API?",
}); });
const detailHash = await store.put(schemas.text, "mock detail"); const detailHash = await store.cas.put(schemas.text, "mock detail");
const startedAtMs = 1716600000000; const startedAtMs = 1716600000000;
const completedAtMs = 1716600001500; const completedAtMs = 1716600001500;
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -129,11 +131,11 @@ async function setupSuspendedThread(mode: MockAgentMode): Promise<{
mode === "suspend" ? { $status: "needs_input", question: "Which API?" } : { $status: "ok" }; mode === "suspend" ? { $status: "needs_input", question: "Which API?" } : { $status: "ok" };
const adapterJson = JSON.stringify({ const adapterJson = JSON.stringify({
stepHash: await store.put(schemas.stepNode, { stepHash: await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: stepHash, prev: stepHash,
role: "worker", role: "worker",
output: await store.put(outputSchemaHash, frontmatter), output: await store.cas.put(outputSchemaHash, frontmatter),
detail: detailHash, detail: detailHash,
agent: "uwf-mock", agent: "uwf-mock",
edgePrompt: "resume prompt placeholder", edgePrompt: "resume prompt placeholder",
@@ -181,9 +183,9 @@ function runUwf(
args: string[], args: string[],
casDir: string, casDir: string,
): { stdout: string; stderr: string; status: number } { ): { stdout: string; stderr: string; status: number } {
const cliPath = join(import.meta.dirname, "..", "cli.js"); const cliPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "dist", "cli.js");
try { try {
const stdout = execFileSync("bun", ["run", cliPath, ...args], { const stdout = execFileSync(process.execPath, [cliPath, ...args], {
encoding: "utf8", encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
env: { env: {
@@ -213,10 +215,10 @@ describe("uwf thread resume", () => {
test("resume non-suspended thread returns error", async () => { test("resume non-suspended thread returns error", async () => {
const casDir = join(tmpDir, "cas"); const casDir = join(tmpDir, "cas");
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "idle-workflow", name: "idle-workflow",
description: "idle thread", description: "idle thread",
roles: { roles: {
@@ -235,7 +237,7 @@ describe("uwf thread resume", () => {
}, },
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "task", prompt: "task",
cwd: tmpDir, cwd: tmpDir,
@@ -382,22 +384,22 @@ async function setupOkMockAgent(
casDir: string, casDir: string,
prevHead: CasRef, prevHead: CasRef,
): Promise<{ mockAgentPath: string }> { ): Promise<{ mockAgentPath: string }> {
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const outputSchemaHash = await putSchema(store, OUTPUT_SCHEMA); const outputSchemaHash = await putSchema(store, OUTPUT_SCHEMA);
const prevNode = store.get(prevHead); const prevNode = store.cas.get(prevHead);
if (prevNode === null || prevNode.type !== schemas.stepNode) { if (prevNode === null || prevNode.type !== schemas.stepNode) {
throw new Error(`expected StepNode at ${prevHead}`); throw new Error(`expected StepNode at ${prevHead}`);
} }
const prevPayload = prevNode.payload as StepNodePayload; const prevPayload = prevNode.payload as StepNodePayload;
const outputHash = await store.put(outputSchemaHash, { $status: "ok" }); const outputHash = await store.cas.put(outputSchemaHash, { $status: "ok" });
const detailHash = await store.put(schemas.text, "ok detail"); const detailHash = await store.cas.put(schemas.text, "ok detail");
const startedAtMs = Date.now(); const startedAtMs = Date.now();
const completedAtMs = startedAtMs + 1; const completedAtMs = startedAtMs + 1;
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: prevPayload.start, start: prevPayload.start,
prev: prevHead, prev: prevHead,
role: "worker", role: "worker",
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { mkdir, rm, writeFile } from "node:fs/promises"; import { mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -96,15 +96,15 @@ async function insertStepNode(
const head = headEntry.head; const head = headEntry.head;
const outputSchemaHash = await putSchema(uwf.store, OUTPUT_SCHEMA); const outputSchemaHash = await putSchema(uwf.store, OUTPUT_SCHEMA);
const outputHash = await uwf.store.put(outputSchemaHash, outputPayload); const outputHash = await uwf.store.cas.put(outputSchemaHash, outputPayload);
const detailHash = await uwf.store.put(uwf.schemas.text, "detail-placeholder"); const detailHash = await uwf.store.cas.put(uwf.schemas.text, "detail-placeholder");
const headNode = uwf.store.get(head); const headNode = uwf.store.cas.get(head);
if (headNode === null) throw new Error(`head ${head} not found`); if (headNode === null) throw new Error(`head ${head} not found`);
const isStart = headNode.type === uwf.schemas.startNode; const isStart = headNode.type === uwf.schemas.startNode;
const startHash = isStart ? head : (headNode.payload as { start: CasRef }).start; const startHash = isStart ? head : (headNode.payload as { start: CasRef }).start;
const stepHash = (await uwf.store.put(uwf.schemas.stepNode, { const stepHash = (await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: isStart ? null : head, prev: isStart ? null : head,
role, role,
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { mkdir, rm, writeFile } from "node:fs/promises"; import { mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
@@ -78,7 +78,7 @@ graph:
const headHash = entry!.head; const headHash = entry!.head;
expect(headHash).toBeDefined(); expect(headHash).toBeDefined();
const startNode = uwf.store.get(headHash as CasRef); const startNode = uwf.store.cas.get(headHash as CasRef);
expect(startNode).not.toBe(null); expect(startNode).not.toBe(null);
expect(startNode?.type).toBe(uwf.schemas.startNode); expect(startNode?.type).toBe(uwf.schemas.startNode);
@@ -136,14 +136,14 @@ graph:
const uwfBin = join(process.cwd(), "dist", "cli.js"); const uwfBin = join(process.cwd(), "dist", "cli.js");
// Register the workflow // Register the workflow
execFileSync("bun", [uwfBin, "workflow", "add", workflowPath], { execFileSync(process.execPath, [uwfBin, "workflow", "add", workflowPath], {
env: { ...process.env, UWF_STORAGE_ROOT: storageRoot, OCAS_DIR: casDir }, env: { ...process.env, UWF_STORAGE_ROOT: storageRoot, OCAS_DIR: casDir },
encoding: "utf8", encoding: "utf8",
}); });
// Verify CLI accepts --cwd option (no error thrown) // Verify CLI accepts --cwd option (no error thrown)
const output = execFileSync( const output = execFileSync(
"bun", process.execPath,
[uwfBin, "thread", "start", "test-cwd-cli", "-p", "test prompt", "--cwd", testCwd], [uwfBin, "thread", "start", "test-cwd-cli", "-p", "test prompt", "--cwd", testCwd],
{ {
env: { ...process.env, UWF_STORAGE_ROOT: storageRoot, OCAS_DIR: casDir }, env: { ...process.env, UWF_STORAGE_ROOT: storageRoot, OCAS_DIR: casDir },
@@ -1,8 +1,10 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { join } from "node:path"; import { join } from "node:path";
const CLI_PATH = join(import.meta.dirname, "..", "cli.js"); const CLI_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "cli.js");
function runCli(args: string[]): { stdout: string; stderr: string; exitCode: number } { function runCli(args: string[]): { stdout: string; stderr: string; exitCode: number } {
try { try {
@@ -1,10 +1,12 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { putSchema } from "@ocas/core"; import { putSchema } from "@ocas/core";
import { createFsStore } from "@ocas/fs"; import { openStore } from "@ocas/fs";
import type { CasRef, StepNodePayload, ThreadId } from "@united-workforce/protocol"; import type { CasRef, StepNodePayload, ThreadId } from "@united-workforce/protocol";
import { cmdThreadShow } from "../commands/thread.js"; import { cmdThreadShow } from "../commands/thread.js";
import { registerUwfSchemas } from "../schemas.js"; import { registerUwfSchemas } from "../schemas.js";
@@ -38,12 +40,12 @@ describe("suspend step CAS chain and threads.yaml metadata", () => {
process.env.OCAS_DIR = casDir; process.env.OCAS_DIR = casDir;
try { try {
const store = createFsStore(casDir); const store = await openStore(casDir);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const outputSchemaHash = await putSchema(store, OUTPUT_SCHEMA); const outputSchemaHash = await putSchema(store, OUTPUT_SCHEMA);
const workflowHash = await store.put(schemas.workflow, { const workflowHash = await store.cas.put(schemas.workflow, {
name: "test-suspend-step", name: "test-suspend-step",
description: "suspend step integration test", description: "suspend step integration test",
roles: { roles: {
@@ -68,7 +70,7 @@ describe("suspend step CAS chain and threads.yaml metadata", () => {
}, },
}); });
const startHash = await store.put(schemas.startNode, { const startHash = await store.cas.put(schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test suspend task", prompt: "Test suspend task",
cwd: tmpDir, cwd: tmpDir,
@@ -77,16 +79,16 @@ describe("suspend step CAS chain and threads.yaml metadata", () => {
const threadId = "01SUSPENDSTEPTEST0000000" as ThreadId; const threadId = "01SUSPENDSTEPTEST0000000" as ThreadId;
await seedThreads(tmpDir, { [threadId]: startHash }); await seedThreads(tmpDir, { [threadId]: startHash });
const outputHash = await store.put(outputSchemaHash, { const outputHash = await store.cas.put(outputSchemaHash, {
$status: "needs_input", $status: "needs_input",
question: "Which API?", question: "Which API?",
}); });
const detailHash = await store.put(schemas.text, "mock detail"); const detailHash = await store.cas.put(schemas.text, "mock detail");
const startedAtMs = 1716600000000; const startedAtMs = 1716600000000;
const completedAtMs = 1716600001500; const completedAtMs = 1716600001500;
const stepHash = await store.put(schemas.stepNode, { const stepHash = await store.cas.put(schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -118,10 +120,10 @@ describe("suspend step CAS chain and threads.yaml metadata", () => {
`defaultAgent: uwf-hermes\ndefaultModel: test-model\nagentOverrides: null\nagents: {}\nproviders: {}\nmodels: {}\n`, `defaultAgent: uwf-hermes\ndefaultModel: test-model\nagentOverrides: null\nagents: {}\nproviders: {}\nmodels: {}\n`,
); );
const cliPath = join(import.meta.dirname, "..", "cli.js"); const cliPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "dist", "cli.js");
const stdout = execFileSync( const stdout = execFileSync(
"bun", process.execPath,
["run", cliPath, "thread", "exec", threadId, "--agent", mockAgentPath], [cliPath, "thread", "exec", threadId, "--agent", mockAgentPath],
{ {
encoding: "utf8", encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
@@ -141,14 +143,14 @@ describe("suspend step CAS chain and threads.yaml metadata", () => {
expect(cliOutput.suspendedRole).toBe("worker"); expect(cliOutput.suspendedRole).toBe("worker");
expect(cliOutput.suspendMessage).toBe("Please clarify: Which API?"); expect(cliOutput.suspendMessage).toBe("Please clarify: Which API?");
const storeAfter = createFsStore(casDir); const storeAfter = await openStore(casDir);
const stepNode = storeAfter.get(cliOutput.head as CasRef); const stepNode = storeAfter.cas.get(cliOutput.head as CasRef);
expect(stepNode).not.toBeNull(); expect(stepNode).not.toBeNull();
const payload = stepNode!.payload as StepNodePayload; const payload = stepNode!.payload as StepNodePayload;
expect(payload.role).toBe("worker"); expect(payload.role).toBe("worker");
expect(payload.output).toBe(outputHash); expect(payload.output).toBe(outputHash);
const outputNode = storeAfter.get(outputHash); const outputNode = storeAfter.cas.get(outputHash);
expect(outputNode?.payload).toEqual({ expect(outputNode?.payload).toEqual({
$status: "needs_input", $status: "needs_input",
question: "Which API?", question: "Which API?",
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { mkdir, mkdtemp, rm } from "node:fs/promises"; import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -41,7 +41,7 @@ describe("suspended thread display", () => {
const outputSchemaHash = await putSchema(uwf.store, OUTPUT_SCHEMA); const outputSchemaHash = await putSchema(uwf.store, OUTPUT_SCHEMA);
// Create test workflow with suspend capability // Create test workflow with suspend capability
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-suspend-display", name: "test-suspend-display",
description: "test suspended display", description: "test suspended display",
roles: { roles: {
@@ -66,7 +66,7 @@ describe("suspended thread display", () => {
}, },
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test task requiring input", prompt: "Test task requiring input",
cwd: tmpDir, cwd: tmpDir,
@@ -74,13 +74,13 @@ describe("suspended thread display", () => {
// Create suspended thread // Create suspended thread
const suspendedThreadId = "01SUSPENDEDTHREAD0000000" as ThreadId; const suspendedThreadId = "01SUSPENDEDTHREAD0000000" as ThreadId;
const outputHash = await uwf.store.put(outputSchemaHash, { const outputHash = await uwf.store.cas.put(outputSchemaHash, {
$status: "needs_input", $status: "needs_input",
question: "What is the target API?", question: "What is the target API?",
}); });
const detailHash = await uwf.store.put(uwf.schemas.text, "mock detail"); const detailHash = await uwf.store.cas.put(uwf.schemas.text, "mock detail");
const stepHash = await uwf.store.put(uwf.schemas.stepNode, { const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -103,7 +103,7 @@ describe("suspended thread display", () => {
// Create normal (idle) thread // Create normal (idle) thread
const idleThreadId = "01IDLETHREAD00000000000" as ThreadId; const idleThreadId = "01IDLETHREAD00000000000" as ThreadId;
const idleStartHash = await uwf.store.put(uwf.schemas.startNode, { const idleStartHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Normal task", prompt: "Normal task",
cwd: tmpDir, cwd: tmpDir,
@@ -148,7 +148,7 @@ describe("suspended thread display", () => {
const uwf = await createUwfStore(tmpDir); const uwf = await createUwfStore(tmpDir);
const outputSchemaHash = await putSchema(uwf.store, OUTPUT_SCHEMA); const outputSchemaHash = await putSchema(uwf.store, OUTPUT_SCHEMA);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-suspend-show", name: "test-suspend-show",
description: "test suspended show", description: "test suspended show",
roles: { roles: {
@@ -173,20 +173,20 @@ describe("suspended thread display", () => {
}, },
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Test task", prompt: "Test task",
cwd: tmpDir, cwd: tmpDir,
}); });
const threadId = "01SUSPENDSHOW000000000" as ThreadId; const threadId = "01SUSPENDSHOW000000000" as ThreadId;
const outputHash = await uwf.store.put(outputSchemaHash, { const outputHash = await uwf.store.cas.put(outputSchemaHash, {
$status: "needs_input", $status: "needs_input",
question: "Which database to use?", question: "Which database to use?",
}); });
const detailHash = await uwf.store.put(uwf.schemas.text, "mock detail"); const detailHash = await uwf.store.cas.put(uwf.schemas.text, "mock detail");
const stepHash = await uwf.store.put(uwf.schemas.stepNode, { const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -235,7 +235,7 @@ describe("suspended thread display", () => {
try { try {
const uwf = await createUwfStore(tmpDir); const uwf = await createUwfStore(tmpDir);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-normal", name: "test-normal",
description: "test normal thread", description: "test normal thread",
roles: { roles: {
@@ -252,7 +252,7 @@ describe("suspended thread display", () => {
}, },
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Normal task", prompt: "Normal task",
cwd: tmpDir, cwd: tmpDir,
@@ -7,10 +7,10 @@ async function ensureHeadInCas(
head: CasRef, head: CasRef,
threadId: ThreadId, threadId: ThreadId,
): Promise<CasRef> { ): Promise<CasRef> {
if (uwf.store.get(head) !== null) { if (uwf.store.cas.get(head) !== null) {
return head; return head;
} }
return (await uwf.store.put(uwf.schemas.text, `thread-head:${threadId}:${head}`)) as CasRef; return (await uwf.store.cas.put(uwf.schemas.text, `thread-head:${threadId}:${head}`)) as CasRef;
} }
export async function seedThread( export async function seedThread(
+74 -75
View File
@@ -1,9 +1,8 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { mkdir, mkdtemp, rm } from "node:fs/promises"; import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { bootstrap, putSchema } from "@ocas/core"; import { bootstrap, putSchema, type Store } from "@ocas/core";
import type { createFsStore } from "@ocas/fs";
import type { CasRef, ThreadId } from "@united-workforce/protocol"; import type { CasRef, ThreadId } from "@united-workforce/protocol";
import { cmdStepList, cmdStepShow } from "../commands/step.js"; import { cmdStepList, cmdStepShow } from "../commands/step.js";
import { import {
@@ -62,7 +61,7 @@ async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
return createUwfStore(storageRoot); return createUwfStore(storageRoot);
} }
async function registerDetailSchemas(store: ReturnType<typeof createFsStore>) { async function registerDetailSchemas(store: Store) {
await bootstrap(store); await bootstrap(store);
const [turn, detail] = await Promise.all([ const [turn, detail] = await Promise.all([
putSchema(store, TURN_SCHEMA), putSchema(store, TURN_SCHEMA),
@@ -90,21 +89,21 @@ describe("extractLastAssistantContent", () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const schemas = await registerDetailSchemas(uwf.store); const schemas = await registerDetailSchemas(uwf.store);
const turn1 = await uwf.store.put(schemas.turn, { const turn1 = await uwf.store.cas.put(schemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content: "intermediate", content: "intermediate",
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const turn2 = await uwf.store.put(schemas.turn, { const turn2 = await uwf.store.cas.put(schemas.turn, {
index: 1, index: 1,
role: "tool", role: "tool",
content: "ok", content: "ok",
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const turn3 = await uwf.store.put(schemas.turn, { const turn3 = await uwf.store.cas.put(schemas.turn, {
index: 2, index: 2,
role: "assistant", role: "assistant",
content: "final answer", content: "final answer",
@@ -112,7 +111,7 @@ describe("extractLastAssistantContent", () => {
reasoning: null, reasoning: null,
}); });
const detailHash = await uwf.store.put(schemas.detail, { const detailHash = await uwf.store.cas.put(schemas.detail, {
sessionId: "s1", sessionId: "s1",
model: "m1", model: "m1",
duration: 1000, duration: 1000,
@@ -132,7 +131,7 @@ describe("extractLastAssistantContent", () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const schemas = await registerDetailSchemas(uwf.store); const schemas = await registerDetailSchemas(uwf.store);
const detailHash = await uwf.store.put(schemas.detail, { const detailHash = await uwf.store.cas.put(schemas.detail, {
sessionId: "s2", sessionId: "s2",
model: "m2", model: "m2",
duration: 0, duration: 0,
@@ -147,7 +146,7 @@ describe("extractLastAssistantContent", () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const schemas = await registerDetailSchemas(uwf.store); const schemas = await registerDetailSchemas(uwf.store);
const turn1 = await uwf.store.put(schemas.turn, { const turn1 = await uwf.store.cas.put(schemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content: "", content: "",
@@ -155,7 +154,7 @@ describe("extractLastAssistantContent", () => {
reasoning: null, reasoning: null,
}); });
const detailHash = await uwf.store.put(schemas.detail, { const detailHash = await uwf.store.cas.put(schemas.detail, {
sessionId: "s3", sessionId: "s3",
model: "m3", model: "m3",
duration: 0, duration: 0,
@@ -170,14 +169,14 @@ describe("extractLastAssistantContent", () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const schemas = await registerDetailSchemas(uwf.store); const schemas = await registerDetailSchemas(uwf.store);
const turn1 = await uwf.store.put(schemas.turn, { const turn1 = await uwf.store.cas.put(schemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content: "real content", content: "real content",
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const turn2 = await uwf.store.put(schemas.turn, { const turn2 = await uwf.store.cas.put(schemas.turn, {
index: 1, index: 1,
role: "assistant", role: "assistant",
content: " ", content: " ",
@@ -185,7 +184,7 @@ describe("extractLastAssistantContent", () => {
reasoning: null, reasoning: null,
}); });
const detailHash = await uwf.store.put(schemas.detail, { const detailHash = await uwf.store.cas.put(schemas.detail, {
sessionId: "s4", sessionId: "s4",
model: "m4", model: "m4",
duration: 0, duration: 0,
@@ -204,7 +203,7 @@ describe("cmdThreadRead <output> section", () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const detailSchemas = await registerDetailSchemas(uwf.store); const detailSchemas = await registerDetailSchemas(uwf.store);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf", name: "test-wf",
description: "desc", description: "desc",
roles: { roles: {
@@ -221,12 +220,12 @@ describe("cmdThreadRead <output> section", () => {
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Write something", prompt: "Write something",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -234,14 +233,14 @@ describe("cmdThreadRead <output> section", () => {
graph: {}, graph: {},
}); });
const turnHash = await uwf.store.put(detailSchemas.turn, { const turnHash = await uwf.store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content: "The assistant response text", content: "The assistant response text",
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const detailHash = await uwf.store.put(detailSchemas.detail, { const detailHash = await uwf.store.cas.put(detailSchemas.detail, {
sessionId: "sx", sessionId: "sx",
model: "mx", model: "mx",
duration: 500, duration: 500,
@@ -249,7 +248,7 @@ describe("cmdThreadRead <output> section", () => {
turns: [turnHash], turns: [turnHash],
}); });
const stepHash = await uwf.store.put(uwf.schemas.stepNode, { const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "writer", role: "writer",
@@ -272,18 +271,18 @@ describe("cmdThreadRead <output> section", () => {
test("omits <output> tags when detail has no matching assistant turns", async () => { test("omits <output> tags when detail has no matching assistant turns", async () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf2", name: "test-wf2",
description: "desc", description: "desc",
roles: {}, roles: {},
conditions: {}, conditions: {},
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Do stuff", prompt: "Do stuff",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -294,7 +293,7 @@ describe("cmdThreadRead <output> section", () => {
// A detail ref that doesn't exist in the store → extractLastAssistantContent returns null // A detail ref that doesn't exist in the store → extractLastAssistantContent returns null
const missingDetailRef = "missingdetail0" as CasRef; const missingDetailRef = "missingdetail0" as CasRef;
const stepHash = await uwf.store.put(uwf.schemas.stepNode, { const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "worker", role: "worker",
@@ -321,18 +320,18 @@ describe("cmdStepShow", () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const detailSchemas = await registerDetailSchemas(uwf.store); const detailSchemas = await registerDetailSchemas(uwf.store);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "wf", name: "wf",
description: "", description: "",
roles: {}, roles: {},
conditions: {}, conditions: {},
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "p", prompt: "p",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -340,14 +339,14 @@ describe("cmdStepShow", () => {
graph: {}, graph: {},
}); });
const turnHash = await uwf.store.put(detailSchemas.turn, { const turnHash = await uwf.store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content: "done", content: "done",
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const detailHash = await uwf.store.put(detailSchemas.detail, { const detailHash = await uwf.store.cas.put(detailSchemas.detail, {
sessionId: "sess42", sessionId: "sess42",
model: "gpt-4o", model: "gpt-4o",
duration: 3000, duration: 3000,
@@ -355,7 +354,7 @@ describe("cmdStepShow", () => {
turns: [turnHash], turns: [turnHash],
}); });
const stepHash = await uwf.store.put(uwf.schemas.stepNode, { const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "coder", role: "coder",
@@ -400,18 +399,18 @@ describe("cmdThreadRead <prompt> deduplication", () => {
meta: "placeholder00" as CasRef, meta: "placeholder00" as CasRef,
}; };
} }
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "dedup-wf", name: "dedup-wf",
description: "desc", description: "desc",
roles: roleMap, roles: roleMap,
conditions: {}, conditions: {},
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Start", prompt: "Start",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -422,7 +421,7 @@ describe("cmdThreadRead <prompt> deduplication", () => {
let prev: string | null = null; let prev: string | null = null;
let stepHash = ""; let stepHash = "";
for (const role of roles) { for (const role of roles) {
stepHash = await uwf.store.put(uwf.schemas.stepNode, { stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: prev as CasRef | null, prev: prev as CasRef | null,
role, role,
@@ -477,7 +476,7 @@ describe("cmdThreadRead start section / before / quota", () => {
roles: string[], roles: string[],
): Promise<{ startHash: CasRef; stepHashes: CasRef[] }> { ): Promise<{ startHash: CasRef; stepHashes: CasRef[] }> {
const uniqueRoles = [...new Set(roles)]; const uniqueRoles = [...new Set(roles)];
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "simple-wf", name: "simple-wf",
description: "desc", description: "desc",
roles: Object.fromEntries( roles: Object.fromEntries(
@@ -496,11 +495,11 @@ describe("cmdThreadRead start section / before / quota", () => {
conditions: {}, conditions: {},
graph: {}, graph: {},
}); });
const startHash = (await uwf.store.put(uwf.schemas.startNode, { const startHash = (await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Initial prompt", prompt: "Initial prompt",
})) as CasRef; })) as CasRef;
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -511,7 +510,7 @@ describe("cmdThreadRead start section / before / quota", () => {
const stepHashes: CasRef[] = []; const stepHashes: CasRef[] = [];
let prev: CasRef | null = null; let prev: CasRef | null = null;
for (const role of roles) { for (const role of roles) {
const stepHash = (await uwf.store.put(uwf.schemas.stepNode, { const stepHash = (await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev, prev,
role, role,
@@ -593,7 +592,7 @@ describe("cmdStepShow (process.exit tests - must be last)", () => {
test("before with unknown hash rejects", async () => { test("before with unknown hash rejects", async () => {
const uwfStore = await makeUwfStore(tmpDir); const uwfStore = await makeUwfStore(tmpDir);
const workflowHash = await uwfStore.store.put(uwfStore.schemas.workflow, { const workflowHash = await uwfStore.store.cas.put(uwfStore.schemas.workflow, {
name: "wf2", name: "wf2",
description: "", description: "",
roles: { roles: {
@@ -609,18 +608,18 @@ describe("cmdStepShow (process.exit tests - must be last)", () => {
conditions: {}, conditions: {},
graph: {}, graph: {},
}); });
const startHash = await uwfStore.store.put(uwfStore.schemas.startNode, { const startHash = await uwfStore.store.cas.put(uwfStore.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "p", prompt: "p",
}); });
const outputHash = await uwfStore.store.put(uwfStore.schemas.workflow, { const outputHash = await uwfStore.store.cas.put(uwfStore.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
conditions: {}, conditions: {},
graph: {}, graph: {},
}); });
const stepHash = await uwfStore.store.put(uwfStore.schemas.stepNode, { const stepHash = await uwfStore.store.cas.put(uwfStore.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "roleA", role: "roleA",
@@ -648,18 +647,18 @@ describe("cmdStepList with completed threads", () => {
test("lists steps from active thread", async () => { test("lists steps from active thread", async () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf-active", name: "test-wf-active",
description: "desc", description: "desc",
roles: {}, roles: {},
conditions: {}, conditions: {},
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Start prompt", prompt: "Start prompt",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -667,7 +666,7 @@ describe("cmdStepList with completed threads", () => {
graph: {}, graph: {},
}); });
const step1Hash = await uwf.store.put(uwf.schemas.stepNode, { const step1Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "role1", role: "role1",
@@ -675,7 +674,7 @@ describe("cmdStepList with completed threads", () => {
detail: null, detail: null,
agent: "uwf-test", agent: "uwf-test",
}); });
const step2Hash = await uwf.store.put(uwf.schemas.stepNode, { const step2Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: step1Hash, prev: step1Hash,
role: "role2", role: "role2",
@@ -683,7 +682,7 @@ describe("cmdStepList with completed threads", () => {
detail: null, detail: null,
agent: "uwf-test", agent: "uwf-test",
}); });
const step3Hash = await uwf.store.put(uwf.schemas.stepNode, { const step3Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: step2Hash, prev: step2Hash,
role: "role3", role: "role3",
@@ -707,18 +706,18 @@ describe("cmdStepList with completed threads", () => {
test("lists steps from completed thread", async () => { test("lists steps from completed thread", async () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf-completed", name: "test-wf-completed",
description: "desc", description: "desc",
roles: {}, roles: {},
conditions: {}, conditions: {},
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Start prompt", prompt: "Start prompt",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -726,7 +725,7 @@ describe("cmdStepList with completed threads", () => {
graph: {}, graph: {},
}); });
const step1Hash = await uwf.store.put(uwf.schemas.stepNode, { const step1Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "roleA", role: "roleA",
@@ -734,7 +733,7 @@ describe("cmdStepList with completed threads", () => {
detail: null, detail: null,
agent: "uwf-test", agent: "uwf-test",
}); });
const step2Hash = await uwf.store.put(uwf.schemas.stepNode, { const step2Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: step1Hash, prev: step1Hash,
role: "roleB", role: "roleB",
@@ -768,18 +767,18 @@ describe("cmdStepShow with completed threads", () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const detailSchemas = await registerDetailSchemas(uwf.store); const detailSchemas = await registerDetailSchemas(uwf.store);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf-step-active", name: "test-wf-step-active",
description: "desc", description: "desc",
roles: {}, roles: {},
conditions: {}, conditions: {},
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "p", prompt: "p",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -787,14 +786,14 @@ describe("cmdStepShow with completed threads", () => {
graph: {}, graph: {},
}); });
const turnHash = await uwf.store.put(detailSchemas.turn, { const turnHash = await uwf.store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content: "Active thread response", content: "Active thread response",
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const detailHash = await uwf.store.put(detailSchemas.detail, { const detailHash = await uwf.store.cas.put(detailSchemas.detail, {
sessionId: "sess-active", sessionId: "sess-active",
model: "model-x", model: "model-x",
duration: 1234, duration: 1234,
@@ -802,7 +801,7 @@ describe("cmdStepShow with completed threads", () => {
turns: [turnHash], turns: [turnHash],
}); });
const stepHash = await uwf.store.put(uwf.schemas.stepNode, { const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "coder", role: "coder",
@@ -828,18 +827,18 @@ describe("cmdStepShow with completed threads", () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const detailSchemas = await registerDetailSchemas(uwf.store); const detailSchemas = await registerDetailSchemas(uwf.store);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf-step-completed", name: "test-wf-step-completed",
description: "desc", description: "desc",
roles: {}, roles: {},
conditions: {}, conditions: {},
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "p", prompt: "p",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -847,14 +846,14 @@ describe("cmdStepShow with completed threads", () => {
graph: {}, graph: {},
}); });
const turnHash = await uwf.store.put(detailSchemas.turn, { const turnHash = await uwf.store.cas.put(detailSchemas.turn, {
index: 0, index: 0,
role: "assistant", role: "assistant",
content: "Completed thread response", content: "Completed thread response",
toolCalls: null, toolCalls: null,
reasoning: null, reasoning: null,
}); });
const detailHash = await uwf.store.put(detailSchemas.detail, { const detailHash = await uwf.store.cas.put(detailSchemas.detail, {
sessionId: "sess-completed", sessionId: "sess-completed",
model: "model-y", model: "model-y",
duration: 5678, duration: 5678,
@@ -862,7 +861,7 @@ describe("cmdStepShow with completed threads", () => {
turns: [turnHash], turns: [turnHash],
}); });
const stepHash = await uwf.store.put(uwf.schemas.stepNode, { const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "reviewer", role: "reviewer",
@@ -897,7 +896,7 @@ describe("cmdThreadRead with completed threads", () => {
test("reads completed thread context", async () => { test("reads completed thread context", async () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf-read-completed", name: "test-wf-read-completed",
description: "desc", description: "desc",
roles: { roles: {
@@ -913,11 +912,11 @@ describe("cmdThreadRead with completed threads", () => {
conditions: {}, conditions: {},
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Write something", prompt: "Write something",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -925,7 +924,7 @@ describe("cmdThreadRead with completed threads", () => {
graph: {}, graph: {},
}); });
const stepHash = await uwf.store.put(uwf.schemas.stepNode, { const stepHash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "writer", role: "writer",
@@ -954,18 +953,18 @@ describe("cmdThreadRead with completed threads", () => {
test("reads completed thread with before filter", async () => { test("reads completed thread with before filter", async () => {
const uwf = await makeUwfStore(tmpDir); const uwf = await makeUwfStore(tmpDir);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, { const workflowHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "test-wf-read-before", name: "test-wf-read-before",
description: "desc", description: "desc",
roles: {}, roles: {},
conditions: {}, conditions: {},
graph: {}, graph: {},
}); });
const startHash = await uwf.store.put(uwf.schemas.startNode, { const startHash = await uwf.store.cas.put(uwf.schemas.startNode, {
workflow: workflowHash, workflow: workflowHash,
prompt: "Do task", prompt: "Do task",
}); });
const outputHash = await uwf.store.put(uwf.schemas.workflow, { const outputHash = await uwf.store.cas.put(uwf.schemas.workflow, {
name: "out", name: "out",
description: "", description: "",
roles: {}, roles: {},
@@ -973,7 +972,7 @@ describe("cmdThreadRead with completed threads", () => {
graph: {}, graph: {},
}); });
const step1Hash = await uwf.store.put(uwf.schemas.stepNode, { const step1Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: null, prev: null,
role: "roleX", role: "roleX",
@@ -981,7 +980,7 @@ describe("cmdThreadRead with completed threads", () => {
detail: null, detail: null,
agent: "uwf-test", agent: "uwf-test",
}); });
const step2Hash = await uwf.store.put(uwf.schemas.stepNode, { const step2Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: step1Hash, prev: step1Hash,
role: "roleY", role: "roleY",
@@ -989,7 +988,7 @@ describe("cmdThreadRead with completed threads", () => {
detail: null, detail: null,
agent: "uwf-test", agent: "uwf-test",
}); });
const step3Hash = await uwf.store.put(uwf.schemas.stepNode, { const step3Hash = await uwf.store.cas.put(uwf.schemas.stepNode, {
start: startHash, start: startHash,
prev: step2Hash, prev: step2Hash,
role: "roleZ", role: "roleZ",
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import type { WorkflowPayload } from "@united-workforce/protocol"; import type { WorkflowPayload } from "@united-workforce/protocol";
import { validateWorkflow } from "../validate-semantic.js"; import { validateWorkflow } from "../validate-semantic.js";
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -46,7 +46,7 @@ function makeMinimalPayload(name: string, description: string): WorkflowPayload
async function storeWorkflow(uwf: UwfStore, name: string): Promise<CasRef> { async function storeWorkflow(uwf: UwfStore, name: string): Promise<CasRef> {
const payload = makeMinimalPayload(name, "Test workflow"); const payload = makeMinimalPayload(name, "Test workflow");
return await uwf.store.put(uwf.schemas.workflow, payload); return await uwf.store.cas.put(uwf.schemas.workflow, payload);
} }
async function createWorkflowYaml(name: string, version: string | null = null): Promise<string> { async function createWorkflowYaml(name: string, version: string | null = null): Promise<string> {
@@ -124,7 +124,7 @@ describe("Strategy 2: File Path Resolution", () => {
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
const uwf = await makeUwfStore(storageRoot); const uwf = await makeUwfStore(storageRoot);
const node = uwf.store.get(result.workflow); const node = uwf.store.cas.get(result.workflow);
expect(node).not.toBeNull(); expect(node).not.toBeNull();
if (node !== null) { if (node !== null) {
expect((node.payload as WorkflowPayload).name).toBe("test-workflow"); expect((node.payload as WorkflowPayload).name).toBe("test-workflow");
@@ -187,7 +187,7 @@ describe("Strategy 3: Local Discovery", () => {
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
const uwf = await makeUwfStore(storageRoot); const uwf = await makeUwfStore(storageRoot);
const node = uwf.store.get(result.workflow); const node = uwf.store.cas.get(result.workflow);
expect(node).not.toBeNull(); expect(node).not.toBeNull();
if (node !== null) { if (node !== null) {
expect((node.payload as WorkflowPayload).name).toBe("solve-issue"); expect((node.payload as WorkflowPayload).name).toBe("solve-issue");
@@ -235,7 +235,7 @@ describe("Strategy 3: Local Discovery", () => {
const result = await cmdThreadStart(storageRoot, "solve-issue", "prompt", projectRoot); const result = await cmdThreadStart(storageRoot, "solve-issue", "prompt", projectRoot);
const uwf = await makeUwfStore(storageRoot); const uwf = await makeUwfStore(storageRoot);
const node = uwf.store.get(result.workflow); const node = uwf.store.cas.get(result.workflow);
expect(node).not.toBeNull(); expect(node).not.toBeNull();
if (node !== null) { if (node !== null) {
expect((node.payload as WorkflowPayload).description).toBe("Test workflow (1)"); expect((node.payload as WorkflowPayload).description).toBe("Test workflow (1)");
@@ -263,7 +263,7 @@ describe("Strategy 3: Local Discovery", () => {
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/); expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
const uwf = await makeUwfStore(storageRoot); const uwf = await makeUwfStore(storageRoot);
const node = uwf.store.get(result.workflow); const node = uwf.store.cas.get(result.workflow);
expect(node).not.toBeNull(); expect(node).not.toBeNull();
if (node !== null) { if (node !== null) {
expect((node.payload as WorkflowPayload).name).toBe("solve-issue"); expect((node.payload as WorkflowPayload).name).toBe("solve-issue");
@@ -289,7 +289,7 @@ describe("Strategy 3: Local Discovery", () => {
const result = await cmdThreadStart(storageRoot, "solve-issue", "prompt", projectRoot); const result = await cmdThreadStart(storageRoot, "solve-issue", "prompt", projectRoot);
const uwf = await makeUwfStore(storageRoot); const uwf = await makeUwfStore(storageRoot);
const node = uwf.store.get(result.workflow); const node = uwf.store.cas.get(result.workflow);
expect(node).not.toBeNull(); expect(node).not.toBeNull();
if (node !== null) { if (node !== null) {
expect((node.payload as WorkflowPayload).description).toBe("Test workflow (flat)"); expect((node.payload as WorkflowPayload).description).toBe("Test workflow (flat)");
@@ -341,7 +341,7 @@ describe("Resolution Priority", () => {
const result = await cmdThreadStart(storageRoot, explicitPath, "prompt", projectRoot); const result = await cmdThreadStart(storageRoot, explicitPath, "prompt", projectRoot);
const uwf = await makeUwfStore(storageRoot); const uwf = await makeUwfStore(storageRoot);
const node = uwf.store.get(result.workflow); const node = uwf.store.cas.get(result.workflow);
expect(node).not.toBeNull(); expect(node).not.toBeNull();
if (node !== null) { if (node !== null) {
expect((node.payload as WorkflowPayload).description).toBe("Test workflow (explicit)"); expect((node.payload as WorkflowPayload).description).toBe("Test workflow (explicit)");
@@ -364,7 +364,7 @@ describe("Resolution Priority", () => {
const result = await cmdThreadStart(storageRoot, "solve-issue", "prompt", projectRoot); const result = await cmdThreadStart(storageRoot, "solve-issue", "prompt", projectRoot);
const uwf2 = await makeUwfStore(storageRoot); const uwf2 = await makeUwfStore(storageRoot);
const node = uwf2.store.get(result.workflow); const node = uwf2.store.cas.get(result.workflow);
expect(node).not.toBeNull(); expect(node).not.toBeNull();
if (node !== null) { if (node !== null) {
expect((node.payload as WorkflowPayload).description).toBe("Test workflow (local)"); expect((node.payload as WorkflowPayload).description).toBe("Test workflow (local)");
+4 -6
View File
@@ -175,13 +175,11 @@ export async function _discoverAgents(): Promise<string[]> {
async function _tryWhichDiscovery(): Promise<string[] | null> { async function _tryWhichDiscovery(): Promise<string[] | null> {
try { try {
const proc = Bun.spawn(["which", "-a", "uwf-hermes", "uwf-claude-code", "uwf-cursor"], { const { execFileSync } = await import("node:child_process");
stdout: "pipe", const text = execFileSync("which", ["-a", "uwf-hermes", "uwf-claude-code", "uwf-cursor"], {
stderr: "pipe", encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
}); });
const text = await new Response(proc.stdout).text();
await proc.exited;
if (proc.exitCode !== 0) return null;
return _parseWhichOutput(text); return _parseWhichOutput(text);
} catch { } catch {
return null; return null;
+6 -6
View File
@@ -27,7 +27,7 @@ function fail(message: string): never {
} }
function walkChain(uwf: UwfStore, headHash: CasRef): ChainState { function walkChain(uwf: UwfStore, headHash: CasRef): ChainState {
const headNode = uwf.store.get(headHash); const headNode = uwf.store.cas.get(headHash);
if (headNode === null) { if (headNode === null) {
fail(`CAS node not found: ${headHash}`); fail(`CAS node not found: ${headHash}`);
} }
@@ -49,7 +49,7 @@ function walkChain(uwf: UwfStore, headHash: CasRef): ChainState {
let hash: CasRef | null = headHash; let hash: CasRef | null = headHash;
while (hash !== null) { while (hash !== null) {
const node = uwf.store.get(hash); const node = uwf.store.cas.get(hash);
if (node === null) { if (node === null) {
fail(`CAS node not found while walking chain: ${hash}`); fail(`CAS node not found while walking chain: ${hash}`);
} }
@@ -66,7 +66,7 @@ function walkChain(uwf: UwfStore, headHash: CasRef): ChainState {
fail(`empty step chain at head ${headHash}`); fail(`empty step chain at head ${headHash}`);
} }
const startNode = uwf.store.get(newest.start); const startNode = uwf.store.cas.get(newest.start);
if (startNode === null || startNode.type !== uwf.schemas.startNode) { if (startNode === null || startNode.type !== uwf.schemas.startNode) {
fail(`StartNode not found: ${newest.start}`); fail(`StartNode not found: ${newest.start}`);
} }
@@ -80,7 +80,7 @@ function walkChain(uwf: UwfStore, headHash: CasRef): ChainState {
} }
function expandOutput(uwf: UwfStore, outputRef: CasRef): unknown { function expandOutput(uwf: UwfStore, outputRef: CasRef): unknown {
const node = uwf.store.get(outputRef); const node = uwf.store.cas.get(outputRef);
if (node === null) { if (node === null) {
return {}; return {};
} }
@@ -96,7 +96,7 @@ function expandDeep(store: CasStore, hash: CasRef, visited?: Set<string>): unkno
if (seen.has(hash)) return hash; // cycle guard if (seen.has(hash)) return hash; // cycle guard
seen.add(hash); seen.add(hash);
const node = store.get(hash); const node = store.cas.get(hash);
if (node === null) return hash; if (node === null) return hash;
const schema = getSchema(store, node.type); const schema = getSchema(store, node.type);
@@ -177,7 +177,7 @@ function collectOrderedSteps(
let hash: CasRef | null = headHash; let hash: CasRef | null = headHash;
const hashToNode = new Map<string, { payload: StepNodePayload; timestamp: number }>(); const hashToNode = new Map<string, { payload: StepNodePayload; timestamp: number }>();
while (hash !== null) { while (hash !== null) {
const node = uwf.store.get(hash); const node = uwf.store.cas.get(hash);
if (node === null || node.type !== uwf.schemas.stepNode) { if (node === null || node.type !== uwf.schemas.stepNode) {
break; break;
} }
+11 -11
View File
@@ -1,4 +1,4 @@
import type { BootstrapCapableStore } from "@ocas/core"; import type { CasStore } from "@ocas/core";
import type { import type {
CasRef, CasRef,
StartEntry, StartEntry,
@@ -42,7 +42,7 @@ export async function cmdStepList(
const uwf = await createUwfStore(storageRoot); const uwf = await createUwfStore(storageRoot);
const chain = walkChain(uwf, headHash); const chain = walkChain(uwf, headHash);
const startNode = uwf.store.get(chain.startHash); const startNode = uwf.store.cas.get(chain.startHash);
if (startNode === null) { if (startNode === null) {
fail(`StartNode not found: ${chain.startHash}`); fail(`StartNode not found: ${chain.startHash}`);
} }
@@ -81,7 +81,7 @@ export async function cmdStepList(
*/ */
export async function cmdStepShow(storageRoot: string, stepHash: CasRef): Promise<unknown> { export async function cmdStepShow(storageRoot: string, stepHash: CasRef): Promise<unknown> {
const uwf = await createUwfStore(storageRoot); const uwf = await createUwfStore(storageRoot);
const node = uwf.store.get(stepHash); const node = uwf.store.cas.get(stepHash);
if (node === null) { if (node === null) {
fail(`CAS node not found: ${stepHash}`); fail(`CAS node not found: ${stepHash}`);
} }
@@ -103,7 +103,7 @@ export async function cmdStepFork(
stepHash: CasRef, stepHash: CasRef,
): Promise<ThreadForkOutput> { ): Promise<ThreadForkOutput> {
const uwf = await createUwfStore(storageRoot); const uwf = await createUwfStore(storageRoot);
const node = uwf.store.get(stepHash); const node = uwf.store.cas.get(stepHash);
if (node === null) { if (node === null) {
fail(`CAS node not found: ${stepHash}`); fail(`CAS node not found: ${stepHash}`);
} }
@@ -129,7 +129,7 @@ export async function cmdStepFork(
/** /**
* Load and validate step detail node from CAS store * Load and validate step detail node from CAS store
*/ */
function loadStepDetail(store: BootstrapCapableStore, detailRef: CasRef): Record<string, unknown> { function loadStepDetail(store: CasStore, detailRef: CasRef): Record<string, unknown> {
const detailNode = store.get(detailRef); const detailNode = store.get(detailRef);
if (detailNode === null) { if (detailNode === null) {
fail(`detail node not found: ${detailRef}`); fail(`detail node not found: ${detailRef}`);
@@ -178,7 +178,7 @@ function formatTurnBody(turn: TurnData): string {
} }
function parseSingleTurn( function parseSingleTurn(
store: BootstrapCapableStore, store: CasStore,
turnRef: unknown, turnRef: unknown,
fallbackIndex: number, fallbackIndex: number,
): TurnData | null { ): TurnData | null {
@@ -206,7 +206,7 @@ function parseSingleTurn(
/** /**
* Load all turn nodes from CAS store and extract display fields * Load all turn nodes from CAS store and extract display fields
*/ */
function loadTurnData(store: BootstrapCapableStore, turns: unknown): TurnData[] { function loadTurnData(store: CasStore, turns: unknown): TurnData[] {
if (!Array.isArray(turns) || turns.length === 0) { if (!Array.isArray(turns) || turns.length === 0) {
return []; return [];
} }
@@ -294,7 +294,7 @@ export async function cmdStepRead(
showPrompt: boolean, showPrompt: boolean,
): Promise<string> { ): Promise<string> {
const uwf = await createUwfStore(storageRoot); const uwf = await createUwfStore(storageRoot);
const node = uwf.store.get(stepHash); const node = uwf.store.cas.get(stepHash);
if (node === null) { if (node === null) {
fail(`CAS node not found: ${stepHash}`); fail(`CAS node not found: ${stepHash}`);
} }
@@ -309,7 +309,7 @@ export async function cmdStepRead(
if (typeof promptRef !== "string") { if (typeof promptRef !== "string") {
return `# Step ${stepHash}\n\n_Prompt not recorded (legacy step)._`; return `# Step ${stepHash}\n\n_Prompt not recorded (legacy step)._`;
} }
const promptNode = uwf.store.get(promptRef as CasRef); const promptNode = uwf.store.cas.get(promptRef as CasRef);
if (promptNode === null) { if (promptNode === null) {
return `# Step ${stepHash}\n\n_Prompt CAS node not found: ${promptRef}_`; return `# Step ${stepHash}\n\n_Prompt CAS node not found: ${promptRef}_`;
} }
@@ -324,8 +324,8 @@ export async function cmdStepRead(
return formatStepMarkdown(stepHash, payload.role, payload.agent, [], []); return formatStepMarkdown(stepHash, payload.role, payload.agent, [], []);
} }
const detail = loadStepDetail(uwf.store, payload.detail); const detail = loadStepDetail(uwf.store.cas, payload.detail);
const turnData = loadTurnData(uwf.store, detail.turns); const turnData = loadTurnData(uwf.store.cas, detail.turns);
if (turnData.length === 0) { if (turnData.length === 0) {
return formatStepMarkdown(stepHash, payload.role, payload.agent, [], []); return formatStepMarkdown(stepHash, payload.role, payload.agent, [], []);
+15 -15
View File
@@ -1,7 +1,7 @@
import { execFileSync, spawn } from "node:child_process"; import { execFileSync, spawn } from "node:child_process";
import { access, readFile } from "node:fs/promises"; import { access, readFile } from "node:fs/promises";
import { dirname, isAbsolute, resolve as resolvePath } from "node:path"; import { dirname, isAbsolute, resolve as resolvePath } from "node:path";
import type { VariableStore } from "@ocas/core"; import type { VarStore } from "@ocas/core";
import { validate } from "@ocas/core"; import { validate } from "@ocas/core";
import type { import type {
AgentAlias, AgentAlias,
@@ -139,7 +139,7 @@ function resolveSuspendFieldsForShow(
} }
async function ensureThreadSuspendMetadata( async function ensureThreadSuspendMetadata(
varStore: VariableStore, varStore: VarStore,
threadId: ThreadId, threadId: ThreadId,
entry: ThreadIndexEntry, entry: ThreadIndexEntry,
suspendedRole: string, suspendedRole: string,
@@ -352,8 +352,8 @@ async function materializeLocalWorkflow(uwf: UwfStore, filePath: string): Promis
} }
const materialized = await materializeWorkflowPayload(uwf, payload); const materialized = await materializeWorkflowPayload(uwf, payload);
const hash = await uwf.store.put(uwf.schemas.workflow, materialized); const hash = await uwf.store.cas.put(uwf.schemas.workflow, materialized);
const stored = uwf.store.get(hash); const stored = uwf.store.cas.get(hash);
if (stored === null || !validate(uwf.store, stored)) { if (stored === null || !validate(uwf.store, stored)) {
fail("stored local workflow failed schema validation"); fail("stored local workflow failed schema validation");
} }
@@ -374,7 +374,7 @@ async function resolveWorkflowCasRef(
// Strategy 1: Direct CAS hash // Strategy 1: Direct CAS hash
if (isCasRef(trimmed)) { if (isCasRef(trimmed)) {
const node = uwf.store.get(trimmed); const node = uwf.store.cas.get(trimmed);
if (node === null) { if (node === null) {
fail(`CAS node not found: ${trimmed}`); fail(`CAS node not found: ${trimmed}`);
} }
@@ -402,7 +402,7 @@ async function resolveWorkflowCasRef(
if (!isCasRef(hash)) { if (!isCasRef(hash)) {
fail(`workflow not found: ${trimmed}`); fail(`workflow not found: ${trimmed}`);
} }
const node = uwf.store.get(hash); const node = uwf.store.cas.get(hash);
if (node === null) { if (node === null) {
fail(`CAS node not found: ${hash}`); fail(`CAS node not found: ${hash}`);
} }
@@ -413,7 +413,7 @@ async function resolveWorkflowCasRef(
} }
function resolveWorkflowFromHead(uwf: UwfStore, head: CasRef): CasRef | null { function resolveWorkflowFromHead(uwf: UwfStore, head: CasRef): CasRef | null {
const node = uwf.store.get(head); const node = uwf.store.cas.get(head);
if (node === null) { if (node === null) {
return null; return null;
} }
@@ -428,7 +428,7 @@ function resolveWorkflowFromHead(uwf: UwfStore, head: CasRef): CasRef | null {
return null; return null;
} }
const startNode = uwf.store.get(payload.start); const startNode = uwf.store.cas.get(payload.start);
if (startNode === null || startNode.type !== uwf.schemas.startNode) { if (startNode === null || startNode.type !== uwf.schemas.startNode) {
return null; return null;
} }
@@ -462,8 +462,8 @@ export async function cmdThreadStart(
cwd, cwd,
}; };
const headHash = await uwf.store.put(uwf.schemas.startNode, startPayload); const headHash = await uwf.store.cas.put(uwf.schemas.startNode, startPayload);
const node = uwf.store.get(headHash); const node = uwf.store.cas.get(headHash);
if (node === null || !validate(uwf.store, node)) { if (node === null || !validate(uwf.store, node)) {
fail("stored StartNode failed schema validation"); fail("stored StartNode failed schema validation");
} }
@@ -594,7 +594,7 @@ async function collectActiveThreads(
} }
function collectCompletedThreads( function collectCompletedThreads(
varStore: VariableStore, varStore: VarStore,
activeIds: Set<ThreadId>, activeIds: Set<ThreadId>,
): ThreadListItemWithStatus[] { ): ThreadListItemWithStatus[] {
const items: ThreadListItemWithStatus[] = []; const items: ThreadListItemWithStatus[] = [];
@@ -691,7 +691,7 @@ export async function cmdThreadList(
} }
export function extractLastAssistantContent(uwf: UwfStore, detailRef: CasRef): string | null { export function extractLastAssistantContent(uwf: UwfStore, detailRef: CasRef): string | null {
const detailNode = uwf.store.get(detailRef); const detailNode = uwf.store.cas.get(detailRef);
if (detailNode === null) { if (detailNode === null) {
return null; return null;
} }
@@ -705,7 +705,7 @@ export function extractLastAssistantContent(uwf: UwfStore, detailRef: CasRef): s
if (typeof turnRef !== "string") { if (typeof turnRef !== "string") {
continue; continue;
} }
const turnNode = uwf.store.get(turnRef as CasRef); const turnNode = uwf.store.cas.get(turnRef as CasRef);
if (turnNode === null) { if (turnNode === null) {
continue; continue;
} }
@@ -940,7 +940,7 @@ function resolveEvaluateArgs(
} }
function loadWorkflowPayload(uwf: UwfStore, workflowRef: CasRef): WorkflowPayload { function loadWorkflowPayload(uwf: UwfStore, workflowRef: CasRef): WorkflowPayload {
const node = uwf.store.get(workflowRef); const node = uwf.store.cas.get(workflowRef);
if (node === null) { if (node === null) {
fail(`workflow CAS node not found: ${workflowRef}`); fail(`workflow CAS node not found: ${workflowRef}`);
} }
@@ -1436,7 +1436,7 @@ async function cmdThreadStepOnce(
plog.log(PL_AGENT_DONE, `agent returned head=${newHead}`, null); plog.log(PL_AGENT_DONE, `agent returned head=${newHead}`, null);
const uwfAfter = await createUwfStore(storageRoot); const uwfAfter = await createUwfStore(storageRoot);
const newNode = uwfAfter.store.get(newHead); const newNode = uwfAfter.store.cas.get(newHead);
if (newNode === null || newNode.type !== uwfAfter.schemas.stepNode) { if (newNode === null || newNode.type !== uwfAfter.schemas.stepNode) {
failStep(plog, `agent returned hash that is not a StepNode: ${newHead}`); failStep(plog, `agent returned hash that is not a StepNode: ${newHead}`);
} }
+3 -3
View File
@@ -150,8 +150,8 @@ export async function cmdWorkflowAdd(
const uwf = await createUwfStore(storageRoot); const uwf = await createUwfStore(storageRoot);
const materialized = await materializeWorkflowPayload(uwf, payload); const materialized = await materializeWorkflowPayload(uwf, payload);
const hash = await uwf.store.put(uwf.schemas.workflow, materialized); const hash = await uwf.store.cas.put(uwf.schemas.workflow, materialized);
const node = uwf.store.get(hash); const node = uwf.store.cas.get(hash);
if (node === null || !validate(uwf.store, node)) { if (node === null || !validate(uwf.store, node)) {
fail("stored workflow failed schema validation"); fail("stored workflow failed schema validation");
} }
@@ -169,7 +169,7 @@ export async function cmdWorkflowShow(
const registry = loadWorkflowRegistry(uwf.varStore); const registry = loadWorkflowRegistry(uwf.varStore);
const hash = resolveWorkflowHash(registry, id); const hash = resolveWorkflowHash(registry, id);
const node = uwf.store.get(hash); const node = uwf.store.cas.get(hash);
if (node === null) { if (node === null) {
fail(`CAS node not found: ${hash}`); fail(`CAS node not found: ${hash}`);
} }
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { evaluate } from "../evaluate.js"; import { evaluate } from "../evaluate.js";
import { isSuspendResult } from "../types.js"; import { isSuspendResult } from "../types.js";
+20 -19
View File
@@ -4,9 +4,8 @@ import { access, mkdir, readdir, readFile, rename } from "node:fs/promises";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import type { BootstrapCapableStore, Hash } from "@ocas/core"; import { bootstrap, type Hash, type Store, type VarStore } from "@ocas/core";
import { createVariableStore, type VariableStore } from "@ocas/core"; import { createFsStore, createSqliteVarStore } from "@ocas/fs";
import { createFsStore } from "@ocas/fs";
import type { import type {
CasRef, CasRef,
ThreadId, ThreadId,
@@ -168,17 +167,19 @@ export type ThreadHistoryLine = ThreadListItem & {
export type UwfStore = { export type UwfStore = {
storageRoot: string; storageRoot: string;
store: BootstrapCapableStore; store: Store;
schemas: UwfSchemaHashes; schemas: UwfSchemaHashes;
varStore: VariableStore; varStore: VarStore;
}; };
export async function createUwfStore(storageRoot: string): Promise<UwfStore> { export async function createUwfStore(storageRoot: string): Promise<UwfStore> {
const casDir = getGlobalCasDir(); const casDir = getGlobalCasDir();
await mkdir(casDir, { recursive: true }); await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir); const cas = createFsStore(casDir);
const { var: varStore, tag } = createSqliteVarStore(join(casDir, "vars"), cas);
const store: Store = { cas, var: varStore, tag };
bootstrap(store);
const schemas = await registerUwfSchemas(store); const schemas = await registerUwfSchemas(store);
const varStore = createVariableStore(join(casDir, "variables.db"), store);
await migrateWorkflowRegistryIfNeeded(storageRoot, varStore); await migrateWorkflowRegistryIfNeeded(storageRoot, varStore);
await migrateThreadsIndexIfNeeded(storageRoot, varStore); await migrateThreadsIndexIfNeeded(storageRoot, varStore);
await migrateHistoryIfNeeded(storageRoot, varStore); await migrateHistoryIfNeeded(storageRoot, varStore);
@@ -204,7 +205,7 @@ async function loadWorkflowRegistryFromYaml(storageRoot: string): Promise<Workfl
/** One-time migration: `~/.uwf/workflows.yaml` → `@uwf/registry/*` variables. */ /** One-time migration: `~/.uwf/workflows.yaml` → `@uwf/registry/*` variables. */
export async function migrateWorkflowRegistryIfNeeded( export async function migrateWorkflowRegistryIfNeeded(
storageRoot: string, storageRoot: string,
varStore: VariableStore, varStore: VarStore,
): Promise<void> { ): Promise<void> {
const path = getRegistryPath(storageRoot); const path = getRegistryPath(storageRoot);
if (!existsSync(path)) { if (!existsSync(path)) {
@@ -219,7 +220,7 @@ export async function migrateWorkflowRegistryIfNeeded(
await rename(path, `${path}.migrated`); await rename(path, `${path}.migrated`);
} }
export function loadWorkflowRegistry(varStore: VariableStore): WorkflowRegistry { export function loadWorkflowRegistry(varStore: VarStore): WorkflowRegistry {
const vars = varStore.list({ namePrefix: REGISTRY_VAR_PREFIX }); const vars = varStore.list({ namePrefix: REGISTRY_VAR_PREFIX });
const registry: WorkflowRegistry = {}; const registry: WorkflowRegistry = {};
for (const v of vars) { for (const v of vars) {
@@ -229,7 +230,7 @@ export function loadWorkflowRegistry(varStore: VariableStore): WorkflowRegistry
return registry; return registry;
} }
export function saveWorkflowRegistry(varStore: VariableStore, name: string, hash: CasRef): void { export function saveWorkflowRegistry(varStore: VarStore, name: string, hash: CasRef): void {
varStore.set(`${REGISTRY_VAR_PREFIX}${name}`, hash); varStore.set(`${REGISTRY_VAR_PREFIX}${name}`, hash);
} }
@@ -280,7 +281,7 @@ async function loadThreadsIndexFromYaml(storageRoot: string): Promise<ThreadsInd
/** One-time migration: `~/.uwf/threads.yaml` → `@uwf/thread/*` variables. */ /** One-time migration: `~/.uwf/threads.yaml` → `@uwf/thread/*` variables. */
export async function migrateThreadsIndexIfNeeded( export async function migrateThreadsIndexIfNeeded(
storageRoot: string, storageRoot: string,
varStore: VariableStore, varStore: VarStore,
): Promise<void> { ): Promise<void> {
const path = getThreadsPath(storageRoot); const path = getThreadsPath(storageRoot);
if (!existsSync(path)) { if (!existsSync(path)) {
@@ -308,7 +309,7 @@ function entryFromVariable(v: { value: string; tags: Record<string, string> }):
} }
/** Load all active threads (equivalent to legacy `loadThreadsIndex`). */ /** Load all active threads (equivalent to legacy `loadThreadsIndex`). */
export function loadAllThreads(varStore: VariableStore): ThreadsIndex { export function loadAllThreads(varStore: VarStore): ThreadsIndex {
const vars = varStore.list({ namePrefix: THREAD_VAR_PREFIX }); const vars = varStore.list({ namePrefix: THREAD_VAR_PREFIX });
const index: ThreadsIndex = {}; const index: ThreadsIndex = {};
for (const v of vars) { for (const v of vars) {
@@ -319,7 +320,7 @@ export function loadAllThreads(varStore: VariableStore): ThreadsIndex {
} }
/** Get a single active thread entry, or null if not found. */ /** Get a single active thread entry, or null if not found. */
export function getThread(varStore: VariableStore, threadId: ThreadId): ThreadIndexEntry | null { export function getThread(varStore: VarStore, threadId: ThreadId): ThreadIndexEntry | null {
const vars = varStore.list({ exactName: threadVarName(threadId) }); const vars = varStore.list({ exactName: threadVarName(threadId) });
const v = vars[0]; const v = vars[0];
if (v === undefined) { if (v === undefined) {
@@ -330,7 +331,7 @@ export function getThread(varStore: VariableStore, threadId: ThreadId): ThreadIn
/** Set or update a single active thread entry. */ /** Set or update a single active thread entry. */
export function setThread( export function setThread(
varStore: VariableStore, varStore: VarStore,
threadId: ThreadId, threadId: ThreadId,
entry: ThreadIndexEntry, entry: ThreadIndexEntry,
): void { ): void {
@@ -348,7 +349,7 @@ export function setThread(
} }
/** Remove an active thread entry (on complete/cancel). */ /** Remove an active thread entry (on complete/cancel). */
export function deleteThread(varStore: VariableStore, threadId: ThreadId): void { export function deleteThread(varStore: VarStore, threadId: ThreadId): void {
varStore.remove(threadVarName(threadId)); varStore.remove(threadVarName(threadId));
} }
@@ -389,7 +390,7 @@ function parseHistoryJsonlLine(trimmed: string): ThreadHistoryLine | null {
/** One-time migration: `~/.uwf/history.jsonl` → `@uwf/history/*` variables. */ /** One-time migration: `~/.uwf/history.jsonl` → `@uwf/history/*` variables. */
export async function migrateHistoryIfNeeded( export async function migrateHistoryIfNeeded(
storageRoot: string, storageRoot: string,
varStore: VariableStore, varStore: VarStore,
): Promise<void> { ): Promise<void> {
const path = join(storageRoot, "history.jsonl"); const path = join(storageRoot, "history.jsonl");
if (!existsSync(path)) { if (!existsSync(path)) {
@@ -411,7 +412,7 @@ export async function migrateHistoryIfNeeded(
await rename(path, `${path}.migrated`); await rename(path, `${path}.migrated`);
} }
export function loadAllHistory(varStore: VariableStore): ThreadHistoryLine[] { export function loadAllHistory(varStore: VarStore): ThreadHistoryLine[] {
const vars = varStore.list({ namePrefix: HISTORY_VAR_PREFIX }); const vars = varStore.list({ namePrefix: HISTORY_VAR_PREFIX });
return vars.map((v) => ({ return vars.map((v) => ({
thread: v.name.slice(HISTORY_VAR_PREFIX.length) as ThreadId, thread: v.name.slice(HISTORY_VAR_PREFIX.length) as ThreadId,
@@ -423,7 +424,7 @@ export function loadAllHistory(varStore: VariableStore): ThreadHistoryLine[] {
} }
export function findHistoryEntry( export function findHistoryEntry(
varStore: VariableStore, varStore: VarStore,
threadId: ThreadId, threadId: ThreadId,
): ThreadHistoryLine | null { ): ThreadHistoryLine | null {
const vars = varStore.list({ namePrefix: `${HISTORY_VAR_PREFIX}${threadId}` }); const vars = varStore.list({ namePrefix: `${HISTORY_VAR_PREFIX}${threadId}` });
@@ -440,7 +441,7 @@ export function findHistoryEntry(
}; };
} }
export function addHistoryEntry(varStore: VariableStore, entry: ThreadHistoryLine): void { export function addHistoryEntry(varStore: VarStore, entry: ThreadHistoryLine): void {
varStore.set(`${HISTORY_VAR_PREFIX}${entry.thread}`, entry.head, { varStore.set(`${HISTORY_VAR_PREFIX}${entry.thread}`, entry.head, {
tags: { tags: {
workflow: entry.workflow, workflow: entry.workflow,
+5 -5
View File
@@ -4,10 +4,10 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "bun server.ts", "dev": "npx tsx server.ts",
"build": "vite build", "build": "vite build",
"test": "bun test src/", "test": "vitest run src/",
"test:ci": "bun test src/" "test:ci": "vitest run src/"
}, },
"dependencies": { "dependencies": {
"@base-ui/react": "^1.5.0", "@base-ui/react": "^1.5.0",
@@ -29,12 +29,12 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.3.0", "@tailwindcss/vite": "^4.3.0",
"@types/bun": "^1.2.14",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2", "@vitejs/plugin-react": "^6.0.2",
"tailwindcss": "^4.2.4", "tailwindcss": "^4.2.4",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^8.0.13" "vite": "^8.0.13",
"@types/node": "^25.7.0"
} }
} }
@@ -1,4 +1,4 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from 'vitest';
import type { Edge, Node } from "@xyflow/react"; import type { Edge, Node } from "@xyflow/react";
import { LayoutLR } from "../index.js"; import { LayoutLR } from "../index.js";
@@ -1,4 +1,4 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from 'vitest';
import { transIn } from "../trans-in.js"; import { transIn } from "../trans-in.js";
import type { WorkFlowStep } from "../type.js"; import type { WorkFlowStep } from "../type.js";
@@ -1,4 +1,4 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from 'vitest';
import type { AnyWorkEdge, AnyWorkNode } from "../../type.js"; import type { AnyWorkEdge, AnyWorkNode } from "../../type.js";
import { validate } from "../validate.js"; import { validate } from "../validate.js";
+1 -1
View File
@@ -13,7 +13,7 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"types": ["bun-types"], "types": ["node"],
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }
+5 -6
View File
@@ -9,19 +9,18 @@
"type": "module", "type": "module",
"exports": { "exports": {
".": { ".": {
"bun": "./src/index.ts",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js" "import": "./dist/index.js"
} }
}, },
"scripts": { "scripts": {
"prepublishOnly": "echo 'Use bun run release from repo root' && exit 1", "prepublishOnly": "echo 'Use pnpm run release from repo root' && exit 1",
"test": "bun test src/__tests__/", "test": "vitest run src/__tests__/",
"test:ci": "bun test src/__tests__/" "test:ci": "vitest run src/__tests__/"
}, },
"dependencies": { "dependencies": {
"@ocas/core": "^0.1.1", "@ocas/core": "^0.2.2",
"@ocas/fs": "^0.1.1" "@ocas/fs": "^0.2.2"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.8.3" "typescript": "^5.8.3"
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { import {
createThreadIndexEntry, createThreadIndexEntry,
markThreadSuspended, markThreadSuspended,
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import type { StartNodePayload, StepRecord, Target } from "../types.js"; import type { StartNodePayload, StepRecord, Target } from "../types.js";
describe("Protocol types for thread/edge location", () => { describe("Protocol types for thread/edge location", () => {
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { createMemoryStore, putSchema } from "@ocas/core"; import { createMemoryStore, putSchema, bootstrap } from "@ocas/core";
import { tryFrontmatterFastPath } from "../src/frontmatter.js"; import { tryFrontmatterFastPath } from "../src/frontmatter.js";
@@ -18,6 +18,7 @@ const PLANNER_SCHEMA = {
describe("adapter-stdout: A4 retry loop survives JSON output", () => { describe("adapter-stdout: A4 retry loop survives JSON output", () => {
test("A4. first extraction fails, second succeeds — final result has correct data", async () => { test("A4. first extraction fails, second succeeds — final result has correct data", async () => {
const store = createMemoryStore(); const store = createMemoryStore();
bootstrap(store);
const schemaHash = await putSchema(store, PLANNER_SCHEMA); const schemaHash = await putSchema(store, PLANNER_SCHEMA);
// Simulate the retry loop from createAgent (run.ts lines 163-173): // Simulate the retry loop from createAgent (run.ts lines 163-173):
@@ -56,6 +57,7 @@ describe("adapter-stdout: A4 retry loop survives JSON output", () => {
test("A4. all retries fail — extraction returns null on every attempt", async () => { test("A4. all retries fail — extraction returns null on every attempt", async () => {
const store = createMemoryStore(); const store = createMemoryStore();
bootstrap(store);
const schemaHash = await putSchema(store, PLANNER_SCHEMA); const schemaHash = await putSchema(store, PLANNER_SCHEMA);
const MAX_RETRIES = 2; const MAX_RETRIES = 2;
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { createMemoryStore, putSchema } from "@ocas/core"; import { createMemoryStore, putSchema, bootstrap } from "@ocas/core";
import { tryFrontmatterFastPath } from "../src/frontmatter.js"; import { tryFrontmatterFastPath } from "../src/frontmatter.js";
@@ -31,6 +31,7 @@ const FRONTMATTER_SCHEMA = {
describe("adapter-stdout: FrontmatterFastPathResult includes frontmatter", () => { describe("adapter-stdout: FrontmatterFastPathResult includes frontmatter", () => {
test("A2. frontmatter field contains the parsed YAML frontmatter object", async () => { test("A2. frontmatter field contains the parsed YAML frontmatter object", async () => {
const store = createMemoryStore(); const store = createMemoryStore();
bootstrap(store);
const schemaHash = await putSchema(store, PLANNER_SCHEMA); const schemaHash = await putSchema(store, PLANNER_SCHEMA);
const raw = `---\n$status: ready\nplan: abc123\n---\nSome body text`; const raw = `---\n$status: ready\nplan: abc123\n---\nSome body text`;
@@ -42,6 +43,7 @@ describe("adapter-stdout: FrontmatterFastPathResult includes frontmatter", () =>
test("A3. body field contains the markdown body after frontmatter", async () => { test("A3. body field contains the markdown body after frontmatter", async () => {
const store = createMemoryStore(); const store = createMemoryStore();
bootstrap(store);
const schemaHash = await putSchema(store, PLANNER_SCHEMA); const schemaHash = await putSchema(store, PLANNER_SCHEMA);
const raw = `---\n$status: ready\nplan: hash123\n---\nHere is the body.\n\nWith multiple paragraphs.`; const raw = `---\n$status: ready\nplan: hash123\n---\nHere is the body.\n\nWith multiple paragraphs.`;
@@ -53,6 +55,7 @@ describe("adapter-stdout: FrontmatterFastPathResult includes frontmatter", () =>
test("A1. result contains outputHash as valid CasRef", async () => { test("A1. result contains outputHash as valid CasRef", async () => {
const store = createMemoryStore(); const store = createMemoryStore();
bootstrap(store);
const schemaHash = await putSchema(store, FRONTMATTER_SCHEMA); const schemaHash = await putSchema(store, FRONTMATTER_SCHEMA);
const raw = `---\nstatus: done\nnext: null\nconfidence: 0.9\nartifacts: []\nscope: test\n---\nBody`; const raw = `---\nstatus: done\nnext: null\nconfidence: 0.9\nartifacts: []\nscope: test\n---\nBody`;
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import type { StepContext } from "@united-workforce/protocol"; import type { StepContext } from "@united-workforce/protocol";
import { buildContinuationPrompt } from "../src/build-continuation-prompt.js"; import { buildContinuationPrompt } from "../src/build-continuation-prompt.js";
@@ -1,5 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { buildOutputFormatInstruction } from "../src/build-output-format-instruction.js"; import { buildOutputFormatInstruction } from "../src/build-output-format-instruction.js";
const PLANNER_SCHEMA = { const PLANNER_SCHEMA = {
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import type { RoleDefinition } from "@united-workforce/protocol"; import type { RoleDefinition } from "@united-workforce/protocol";
import { buildRolePrompt } from "../src/build-role-prompt.js"; import { buildRolePrompt } from "../src/build-role-prompt.js";
@@ -1,5 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
// We need to test buildHistory indirectly through buildContext // We need to test buildHistory indirectly through buildContext
// since buildHistory is not exported. For now, we'll test the integration // since buildHistory is not exported. For now, we'll test the integration
// through the public API in a separate integration test. // through the public API in a separate integration test.
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import { createMemoryStore, putSchema } from "@ocas/core"; import { createMemoryStore, putSchema, bootstrap } from "@ocas/core";
import { tryFrontmatterFastPath } from "../src/frontmatter.js"; import { tryFrontmatterFastPath } from "../src/frontmatter.js";
@@ -48,6 +48,7 @@ const PLANNER_SCHEMA = {
async function makeStoreWithSchema(schema: Record<string, unknown>) { async function makeStoreWithSchema(schema: Record<string, unknown>) {
const store = createMemoryStore(); const store = createMemoryStore();
bootstrap(store);
const schemaHash = await putSchema(store, schema); const schemaHash = await putSchema(store, schema);
return { store, schemaHash }; return { store, schemaHash };
} }
@@ -68,7 +69,7 @@ describe("STANDARD_KEYS contains only status", () => {
const result = await tryFrontmatterFastPath(raw, schemaHash, store); const result = await tryFrontmatterFastPath(raw, schemaHash, store);
expect(result).not.toBeNull(); expect(result).not.toBeNull();
const node = store.get(result!.outputHash); const node = store.cas.get(result!.outputHash);
expect(node).not.toBeNull(); expect(node).not.toBeNull();
const payload = node!.payload as Record<string, unknown>; const payload = node!.payload as Record<string, unknown>;
expect(payload.status).toBe("done"); expect(payload.status).toBe("done");
@@ -106,7 +107,7 @@ describe("tryFrontmatterFastPath — happy path", () => {
const result = await tryFrontmatterFastPath(raw, schemaHash, store); const result = await tryFrontmatterFastPath(raw, schemaHash, store);
expect(result).not.toBeNull(); expect(result).not.toBeNull();
const node = store.get(result!.outputHash); const node = store.cas.get(result!.outputHash);
expect(node).not.toBeNull(); expect(node).not.toBeNull();
const payload = node!.payload as Record<string, unknown>; const payload = node!.payload as Record<string, unknown>;
expect(payload.status).toBe("done"); expect(payload.status).toBe("done");
@@ -126,7 +127,7 @@ describe("tryFrontmatterFastPath — legacy fields ignored", () => {
const result = await tryFrontmatterFastPath(raw, schemaHash, store); const result = await tryFrontmatterFastPath(raw, schemaHash, store);
expect(result).not.toBeNull(); expect(result).not.toBeNull();
const node = store.get(result!.outputHash); const node = store.cas.get(result!.outputHash);
const payload = node!.payload as Record<string, unknown>; const payload = node!.payload as Record<string, unknown>;
expect(payload.status).toBe("done"); expect(payload.status).toBe("done");
expect(payload.next).toBeUndefined(); expect(payload.next).toBeUndefined();
@@ -176,7 +177,7 @@ describe("tryFrontmatterFastPath — role-specific fields", () => {
const result = await tryFrontmatterFastPath(raw, schemaHash, store); const result = await tryFrontmatterFastPath(raw, schemaHash, store);
expect(result).not.toBeNull(); expect(result).not.toBeNull();
const node = store.get(result!.outputHash); const node = store.cas.get(result!.outputHash);
expect(node).not.toBeNull(); expect(node).not.toBeNull();
const payload = node!.payload as Record<string, unknown>; const payload = node!.payload as Record<string, unknown>;
expect(payload).toEqual({ approved: true }); expect(payload).toEqual({ approved: true });
@@ -192,7 +193,7 @@ describe("tryFrontmatterFastPath — role-specific fields", () => {
const result = await tryFrontmatterFastPath(raw, schemaHash, store); const result = await tryFrontmatterFastPath(raw, schemaHash, store);
expect(result).not.toBeNull(); expect(result).not.toBeNull();
const node = store.get(result!.outputHash); const node = store.cas.get(result!.outputHash);
expect(node).not.toBeNull(); expect(node).not.toBeNull();
const payload = node!.payload as Record<string, unknown>; const payload = node!.payload as Record<string, unknown>;
expect(payload.status).toBe("ready"); expect(payload.status).toBe("ready");
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from 'vitest';
import type { WorkflowConfig } from "@united-workforce/protocol"; import type { WorkflowConfig } from "@united-workforce/protocol";
import { resolveExtractModelAlias } from "../src/extract.js"; import { resolveExtractModelAlias } from "../src/extract.js";
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import type { ThreadId } from "@united-workforce/protocol"; import type { ThreadId } from "@united-workforce/protocol";
+5 -6
View File
@@ -9,19 +9,18 @@
"type": "module", "type": "module",
"exports": { "exports": {
".": { ".": {
"bun": "./src/index.ts",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js" "import": "./dist/index.js"
} }
}, },
"scripts": { "scripts": {
"prepublishOnly": "echo 'Use bun run release from repo root' && exit 1", "prepublishOnly": "echo 'Use pnpm run release from repo root' && exit 1",
"test": "bun test __tests__/ src/__tests__/", "test": "vitest run __tests__/ src/__tests__/",
"test:ci": "bun test __tests__/ src/__tests__/" "test:ci": "vitest run __tests__/ src/__tests__/"
}, },
"dependencies": { "dependencies": {
"@ocas/core": "^0.1.1", "@ocas/core": "^0.2.2",
"@ocas/fs": "^0.1.1", "@ocas/fs": "^0.2.2",
"@united-workforce/protocol": "workspace:^", "@united-workforce/protocol": "workspace:^",
"@united-workforce/util": "workspace:^", "@united-workforce/util": "workspace:^",
"dotenv": "^16.6.1", "dotenv": "^16.6.1",
@@ -1,5 +1,4 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
describe("parseArgv empty prompt error message", () => { describe("parseArgv empty prompt error message", () => {
let stderrOutput: string; let stderrOutput: string;
let _exitCode: number | null; let _exitCode: number | null;
+7 -7
View File
@@ -22,7 +22,7 @@ function fail(message: string): never {
} }
function walkChain(store: Store, schemas: AgentStore["schemas"], headHash: CasRef): ChainState { function walkChain(store: Store, schemas: AgentStore["schemas"], headHash: CasRef): ChainState {
const headNode = store.get(headHash); const headNode = store.cas.get(headHash);
if (headNode === null) { if (headNode === null) {
fail(`CAS node not found: ${headHash}`); fail(`CAS node not found: ${headHash}`);
} }
@@ -44,7 +44,7 @@ function walkChain(store: Store, schemas: AgentStore["schemas"], headHash: CasRe
let hash: CasRef | null = headHash; let hash: CasRef | null = headHash;
while (hash !== null) { while (hash !== null) {
const node = store.get(hash); const node = store.cas.get(hash);
if (node === null) { if (node === null) {
fail(`CAS node not found while walking chain: ${hash}`); fail(`CAS node not found while walking chain: ${hash}`);
} }
@@ -61,7 +61,7 @@ function walkChain(store: Store, schemas: AgentStore["schemas"], headHash: CasRe
fail(`empty step chain at head ${headHash}`); fail(`empty step chain at head ${headHash}`);
} }
const startNode = store.get(newest.start); const startNode = store.cas.get(newest.start);
if (startNode === null || startNode.type !== schemas.startNode) { if (startNode === null || startNode.type !== schemas.startNode) {
fail(`StartNode not found: ${newest.start}`); fail(`StartNode not found: ${newest.start}`);
} }
@@ -75,7 +75,7 @@ function walkChain(store: Store, schemas: AgentStore["schemas"], headHash: CasRe
} }
function expandOutput(store: Store, outputRef: CasRef): unknown { function expandOutput(store: Store, outputRef: CasRef): unknown {
const node = store.get(outputRef); const node = store.cas.get(outputRef);
if (node === null) { if (node === null) {
return {}; return {};
} }
@@ -83,7 +83,7 @@ function expandOutput(store: Store, outputRef: CasRef): unknown {
} }
function extractStepContent(store: Store, detailRef: CasRef): string | null { function extractStepContent(store: Store, detailRef: CasRef): string | null {
const detailNode = store.get(detailRef); const detailNode = store.cas.get(detailRef);
if (detailNode === null) { if (detailNode === null) {
return null; return null;
} }
@@ -98,7 +98,7 @@ function extractStepContent(store: Store, detailRef: CasRef): string | null {
if (typeof turnRef !== "string") { if (typeof turnRef !== "string") {
continue; continue;
} }
const turnNode = store.get(turnRef as CasRef); const turnNode = store.cas.get(turnRef as CasRef);
if (turnNode === null) { if (turnNode === null) {
continue; continue;
} }
@@ -139,7 +139,7 @@ async function buildHistory(
} }
async function loadWorkflow(store: Store, schemas: AgentStore["schemas"], workflowRef: CasRef) { async function loadWorkflow(store: Store, schemas: AgentStore["schemas"], workflowRef: CasRef) {
const node = store.get(workflowRef); const node = store.cas.get(workflowRef);
if (node === null) { if (node === null) {
fail(`workflow CAS node not found: ${workflowRef}`); fail(`workflow CAS node not found: ${workflowRef}`);
} }
+2 -2
View File
@@ -169,8 +169,8 @@ export async function extract(
throw new Error(`failed to parse extracted JSON: ${message}`); throw new Error(`failed to parse extracted JSON: ${message}`);
} }
const outputHash = await store.put(outputSchema, structured); const outputHash = await store.cas.put(outputSchema, structured);
const node = store.get(outputHash); const node = store.cas.get(outputHash);
if (node === null || !validate(store, node)) { if (node === null || !validate(store, node)) {
throw new Error("extracted output failed JSON Schema validation"); throw new Error("extracted output failed JSON Schema validation");
} }
+5 -5
View File
@@ -162,13 +162,13 @@ export async function tryFrontmatterFastPath(
const candidate = buildCandidate(frontmatter, rawFields, schemaFields); const candidate = buildCandidate(frontmatter, rawFields, schemaFields);
let outputHash: CasRef; let outputHash: CasRef;
let node: ReturnType<Store["get"]>; let node: ReturnType<Store["cas"]["get"]>;
try { try {
outputHash = await store.put(outputSchema, candidate); outputHash = await store.cas.put(outputSchema, candidate);
node = store.get(outputHash); node = store.cas.get(outputHash);
} catch { } catch (e) {
log("2KMQT7NR", "failed to store frontmatter candidate in CAS"); log("2KMQT7NR", `failed to store frontmatter candidate in CAS: ${e}`);
return null; return null;
} }
+10 -6
View File
@@ -79,8 +79,8 @@ async function writeStepNode(options: {
cwd: process.cwd(), cwd: process.cwd(),
assembledPrompt: options.assembledPromptHash, assembledPrompt: options.assembledPromptHash,
}; };
const hash = await options.store.put(options.schemas.stepNode, payload); const hash = await options.store.cas.put(options.schemas.stepNode, payload);
const node = options.store.get(hash); const node = options.store.cas.get(hash);
if (node === null || !validate(options.store, node)) { if (node === null || !validate(options.store, node)) {
fail("stored StepNode failed schema validation"); fail("stored StepNode failed schema validation");
} }
@@ -189,10 +189,14 @@ export function createAgent(options: AgentOptions): () => Promise<void> {
// Store the assembled prompt in CAS for later inspection via `step read --prompt` // Store the assembled prompt in CAS for later inspection via `step read --prompt`
const promptText = agentResult.assembledPrompt; const promptText = agentResult.assembledPrompt;
const assembledPromptHash = let assembledPromptHash: CasRef | null = null;
promptText !== "" if (promptText !== "") {
? await ctx.meta.store.put(ctx.meta.schemas.text, promptText).catch(() => null) try {
: null; assembledPromptHash = await ctx.meta.store.cas.put(ctx.meta.schemas.text, promptText);
} catch {
assembledPromptHash = null;
}
}
const stepHash = await persistStep({ const stepHash = await persistStep({
ctx, ctx,
+9 -5
View File
@@ -2,8 +2,8 @@ import { readFile } from "node:fs/promises";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { createVariableStore, type Store } from "@ocas/core"; import { bootstrap, type Store } from "@ocas/core";
import { createFsStore } from "@ocas/fs"; import { createFsStore, createSqliteVarStore } from "@ocas/fs";
import type { import type {
AgentAlias, AgentAlias,
AgentConfig, AgentConfig,
@@ -79,8 +79,8 @@ export async function getActiveThreadEntry(
threadId: ThreadId, threadId: ThreadId,
): Promise<ThreadIndexEntry | null> { ): Promise<ThreadIndexEntry | null> {
const casDir = getGlobalCasDir(); const casDir = getGlobalCasDir();
const store = createFsStore(casDir); const cas = createFsStore(casDir);
const varStore = createVariableStore(join(casDir, "variables.db"), store); const { var: varStore } = createSqliteVarStore(join(casDir, "vars"), cas);
const vars = varStore.list({ exactName: threadVarName(threadId) }); const vars = varStore.list({ exactName: threadVarName(threadId) });
const v = vars[0]; const v = vars[0];
if (v === undefined) { if (v === undefined) {
@@ -100,7 +100,11 @@ export type AgentStore = {
}; };
export async function createAgentStore(storageRoot: string): Promise<AgentStore> { export async function createAgentStore(storageRoot: string): Promise<AgentStore> {
const store = createFsStore(getGlobalCasDir()); const casDir = getGlobalCasDir();
const cas = createFsStore(casDir);
const { var: varSub, tag } = createSqliteVarStore(join(casDir, "vars"), cas);
const store: Store = { cas, var: varSub, tag };
bootstrap(store);
const schemas = await registerAgentSchemas(store); const schemas = await registerAgentSchemas(store);
return { storageRoot, store, schemas }; return { storageRoot, store, schemas };
} }
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from 'vitest';
import { env } from "../src/env.js"; import { env } from "../src/env.js";
describe("env", () => { describe("env", () => {
@@ -1,4 +1,4 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from 'vitest';
import type { AgentFrontmatter } from "../src/index.js"; import type { AgentFrontmatter } from "../src/index.js";
import { parseFrontmatterMarkdown, validateFrontmatter } from "../src/index.js"; import { parseFrontmatterMarkdown, validateFrontmatter } from "../src/index.js";
@@ -1,4 +1,4 @@
import { afterEach, describe, expect, test } from "bun:test"; import { afterEach, describe, expect, test } from 'vitest';
import { mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
+3 -4
View File
@@ -9,15 +9,14 @@
"type": "module", "type": "module",
"exports": { "exports": {
".": { ".": {
"bun": "./src/index.ts",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js" "import": "./dist/index.js"
} }
}, },
"scripts": { "scripts": {
"prepublishOnly": "echo 'Use bun run release from repo root' && exit 1", "prepublishOnly": "echo 'Use pnpm run release from repo root' && exit 1",
"test": "bun test __tests__/ src/__tests__/", "test": "vitest run __tests__/ src/__tests__/",
"test:ci": "bun test __tests__/ src/__tests__/" "test:ci": "vitest run __tests__/ src/__tests__/"
}, },
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
@@ -1,4 +1,4 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from 'vitest';
import { extractUlidTimestamp, generateUlid } from "../ulid.js"; import { extractUlidTimestamp, generateUlid } from "../ulid.js";
describe("extractUlidTimestamp", () => { describe("extractUlidTimestamp", () => {
+6019
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
packages:
- 'packages/*'
+1 -1
View File
@@ -15,7 +15,7 @@
"sourceMap": true, "sourceMap": true,
"composite": true, "composite": true,
"outDir": "dist", "outDir": "dist",
"types": ["bun-types", "node"] "types": ["node"]
}, },
"references": [ "references": [
{ "path": "packages/util" }, { "path": "packages/util" },