Cover high-priority untested modules: - util: base32, result, refs-field, storage-root, log-tag - util-agent: storage (normalizeWorkflowConfig, resolveStorageRoot), run (parseArgv) - agent-builtin: tools (read-file, write-file, run-command), session, detail 627 → 719 tests (+92), all passing. Refs #35
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import { createMemoryStore } from "@ocas/core";
|
||||
import { storeBuiltinDetail } from "../src/detail.js";
|
||||
import { appendSessionTurn, initSessionDir } from "../src/session.js";
|
||||
import type { BuiltinTurnPayload } from "../src/types.js";
|
||||
|
||||
describe("storeBuiltinDetail", () => {
|
||||
let storageRoot: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
storageRoot = await mkdtemp(join(tmpdir(), "builtin-detail-storage-"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(storageRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const makeTurn = (role: "assistant" | "tool", content: string): BuiltinTurnPayload => ({
|
||||
role,
|
||||
content,
|
||||
toolCalls: null,
|
||||
reasoning: null,
|
||||
});
|
||||
|
||||
test("stores detail with turns, returns hash and turnCount", async () => {
|
||||
const store = createMemoryStore();
|
||||
await initSessionDir(storageRoot);
|
||||
const sid = "detail-test";
|
||||
await appendSessionTurn(storageRoot, sid, makeTurn("tool", "question"));
|
||||
await appendSessionTurn(storageRoot, sid, makeTurn("assistant", "answer"));
|
||||
|
||||
const result = await storeBuiltinDetail(store, storageRoot, sid, "test-model", 1000, 2000);
|
||||
expect(result.turnCount).toBe(2);
|
||||
expect(typeof result.detailHash).toBe("string");
|
||||
expect(result.detailHash.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("empty session returns turnCount 0", async () => {
|
||||
const store = createMemoryStore();
|
||||
const sid = "empty-session";
|
||||
|
||||
const result = await storeBuiltinDetail(store, storageRoot, sid, "test-model", 1000, 2000);
|
||||
expect(result.turnCount).toBe(0);
|
||||
expect(typeof result.detailHash).toBe("string");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import { readFileTool } from "../src/tools/read-file.js";
|
||||
import { writeFile, mkdir, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const testDir = join(tmpdir(), `read-file-test-${Date.now()}`);
|
||||
const ctx = { cwd: testDir, storageRoot: testDir };
|
||||
|
||||
beforeAll(async () => {
|
||||
await mkdir(testDir, { recursive: true });
|
||||
await writeFile(join(testDir, "hello.txt"), "hello world", "utf8");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await rm(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("readFileTool", () => {
|
||||
it("reads a file successfully", async () => {
|
||||
const result = await readFileTool.execute({ path: "hello.txt" }, ctx);
|
||||
expect(result).toBe("hello world");
|
||||
});
|
||||
|
||||
it("returns error for non-existent file", async () => {
|
||||
const result = await readFileTool.execute({ path: "nope.txt" }, ctx);
|
||||
expect(result).toMatch(/^Error:/);
|
||||
});
|
||||
|
||||
it("returns error for directory", async () => {
|
||||
const result = await readFileTool.execute({ path: "." }, ctx);
|
||||
expect(result).toBe("Error: not a file");
|
||||
});
|
||||
|
||||
it("returns error when path is not a string", async () => {
|
||||
const result = await readFileTool.execute({ path: 123 }, ctx);
|
||||
expect(result).toBe("Error: path must be a string");
|
||||
});
|
||||
|
||||
it("returns error when args is null", async () => {
|
||||
const result = await readFileTool.execute(null, ctx);
|
||||
expect(result).toBe("Error: path must be a string");
|
||||
});
|
||||
|
||||
it("returns error for file exceeding 512KB limit", async () => {
|
||||
const bigFile = join(testDir, "big.txt");
|
||||
await writeFile(bigFile, Buffer.alloc(512 * 1024 + 1, 65));
|
||||
const result = await readFileTool.execute({ path: "big.txt" }, ctx);
|
||||
expect(result).toMatch(/Error:.*limit/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { runCommandTool } from "../src/tools/run-command.js";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const ctx = { cwd: tmpdir(), storageRoot: tmpdir() };
|
||||
|
||||
describe("runCommandTool", () => {
|
||||
it("runs echo command and checks stdout", async () => {
|
||||
const result = await runCommandTool.execute({ command: "echo hello" }, ctx);
|
||||
expect(result).toContain("hello");
|
||||
expect(result).toContain("stdout");
|
||||
});
|
||||
|
||||
it("returns exit code", async () => {
|
||||
const result = await runCommandTool.execute({ command: "exit 0" }, ctx);
|
||||
expect(result).toContain("exit_code: 0");
|
||||
});
|
||||
|
||||
it("returns non-zero exit code", async () => {
|
||||
const result = await runCommandTool.execute({ command: "exit 42" }, ctx);
|
||||
expect(result).toContain("exit_code: 42");
|
||||
});
|
||||
|
||||
it("returns error when command is not a string", async () => {
|
||||
const result = await runCommandTool.execute({ command: 123 }, ctx);
|
||||
expect(result).toBe("Error: command must be a string");
|
||||
});
|
||||
|
||||
it("returns error when args is null", async () => {
|
||||
const result = await runCommandTool.execute(null, ctx);
|
||||
expect(result).toBe("Error: command must be a string");
|
||||
});
|
||||
|
||||
it("custom cwd works", async () => {
|
||||
const result = await runCommandTool.execute({ command: "pwd", cwd: "/tmp" }, ctx);
|
||||
expect(result).toContain("/tmp");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import type { BuiltinTurnPayload } from "../src/types.js";
|
||||
import {
|
||||
appendSessionTurn,
|
||||
initSessionDir,
|
||||
readSessionTurns,
|
||||
removeSession,
|
||||
} from "../src/session.js";
|
||||
|
||||
describe("session", () => {
|
||||
let storageRoot: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
storageRoot = await mkdtemp(join(tmpdir(), "builtin-session-"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(storageRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const makeTurn = (role: "assistant" | "tool", content: string): BuiltinTurnPayload => ({
|
||||
role,
|
||||
content,
|
||||
toolCalls: null,
|
||||
reasoning: null,
|
||||
});
|
||||
|
||||
test("initSessionDir creates directory", async () => {
|
||||
await initSessionDir(storageRoot);
|
||||
expect(existsSync(join(storageRoot, "sessions"))).toBe(true);
|
||||
});
|
||||
|
||||
test("append + read roundtrip", async () => {
|
||||
await initSessionDir(storageRoot);
|
||||
const sid = "test-session-1";
|
||||
const t1 = makeTurn("tool", "hello");
|
||||
const t2 = makeTurn("assistant", "hi there");
|
||||
await appendSessionTurn(storageRoot, sid, t1);
|
||||
await appendSessionTurn(storageRoot, sid, t2);
|
||||
const turns = await readSessionTurns(storageRoot, sid);
|
||||
expect(turns).toEqual([t1, t2]);
|
||||
});
|
||||
|
||||
test("read from non-existent returns []", async () => {
|
||||
const turns = await readSessionTurns(storageRoot, "no-such-session");
|
||||
expect(turns).toEqual([]);
|
||||
});
|
||||
|
||||
test("removeSession deletes file", async () => {
|
||||
await initSessionDir(storageRoot);
|
||||
const sid = "to-remove";
|
||||
await appendSessionTurn(storageRoot, sid, makeTurn("tool", "bye"));
|
||||
await removeSession(storageRoot, sid);
|
||||
const turns = await readSessionTurns(storageRoot, sid);
|
||||
expect(turns).toEqual([]);
|
||||
});
|
||||
|
||||
test("removeSession on non-existent does not throw", async () => {
|
||||
await expect(removeSession(storageRoot, "ghost")).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, it, expect, afterAll } from "vitest";
|
||||
import { writeFileTool } from "../src/tools/write-file.js";
|
||||
import { readFile, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const testDir = join(tmpdir(), `write-file-test-${Date.now()}`);
|
||||
const ctx = { cwd: testDir, storageRoot: testDir };
|
||||
|
||||
afterAll(async () => {
|
||||
await rm(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("writeFileTool", () => {
|
||||
it("writes file successfully", async () => {
|
||||
const result = await writeFileTool.execute({ path: "out.txt", content: "hi" }, ctx);
|
||||
expect(result).toMatch(/Wrote 2 bytes/);
|
||||
const content = await readFile(join(testDir, "out.txt"), "utf8");
|
||||
expect(content).toBe("hi");
|
||||
});
|
||||
|
||||
it("creates parent directories", async () => {
|
||||
const result = await writeFileTool.execute({ path: "a/b/c.txt", content: "nested" }, ctx);
|
||||
expect(result).toMatch(/Wrote/);
|
||||
const content = await readFile(join(testDir, "a/b/c.txt"), "utf8");
|
||||
expect(content).toBe("nested");
|
||||
});
|
||||
|
||||
it("returns error when path is not a string", async () => {
|
||||
const result = await writeFileTool.execute({ path: 123, content: "x" }, ctx);
|
||||
expect(result).toBe("Error: path and content must be strings");
|
||||
});
|
||||
|
||||
it("returns error when content is not a string", async () => {
|
||||
const result = await writeFileTool.execute({ path: "x.txt", content: 42 }, ctx);
|
||||
expect(result).toBe("Error: path and content must be strings");
|
||||
});
|
||||
|
||||
it("returns error when args is null", async () => {
|
||||
const result = await writeFileTool.execute(null, ctx);
|
||||
expect(result).toBe("Error: path and content must be strings");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user