Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions src/agent/__tests__/skills.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { mkdir, rm, writeFile } from "node:fs/promises";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { afterEach, beforeEach, describe, expect, test } from "bun:test";

import { listSkills } from "../skills/list-skills";
import { readSkillFrontMatter } from "../skills/skill-reader";

describe("readSkillFrontMatter", () => {
let tempDir: string;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "helixent-skill-reader-"));
});

afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});

test("reads frontmatter from a SKILL.md file", async () => {
const skillPath = join(tempDir, "SKILL.md");
await writeFile(skillPath, `---
name: my-skill
description: A test skill
---

# My Skill

Some content here.
`);
const result = await readSkillFrontMatter(skillPath);
expect(result.name).toBe("my-skill");
expect(result.description).toBe("A test skill");
expect(result.path).toBe(skillPath);
});

test("throws when file does not exist", async () => {
await expect(readSkillFrontMatter(join(tempDir, "nonexistent.md"))).rejects.toThrow("does not exist");
});

test("handles SKILL.md with no frontmatter", async () => {
const skillPath = join(tempDir, "SKILL.md");
await writeFile(skillPath, "Just plain content, no frontmatter.");
const result = await readSkillFrontMatter(skillPath);
expect(result.name).toBeUndefined();
expect(result.description).toBeUndefined();
expect(result.path).toBe(skillPath);
});
});

describe("listSkills", () => {
let tempDir: string;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "helixent-list-skills-"));
});

afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});

test("discovers skills from a directory with SKILL.md subfolders", async () => {
const skillDir = join(tempDir, "skills");
await mkdir(join(skillDir, "skill-a"), { recursive: true });
await writeFile(
join(skillDir, "skill-a", "SKILL.md"),
`---\nname: skill-a\ndescription: Skill A\n---\n`,
);
await mkdir(join(skillDir, "skill-b"), { recursive: true });
await writeFile(
join(skillDir, "skill-b", "SKILL.md"),
`---\nname: skill-b\ndescription: Skill B\n---\n`,
);

const skills = await listSkills([skillDir]);
expect(skills).toHaveLength(2);
const names = skills.map((s) => s.name);
expect(names).toContain("skill-a");
expect(names).toContain("skill-b");
});

test("skips directories without SKILL.md", async () => {
const skillDir = join(tempDir, "skills");
await mkdir(join(skillDir, "no-skill"), { recursive: true });
// No SKILL.md created

const skills = await listSkills([skillDir]);
expect(skills).toHaveLength(0);
});

test("skips non-directory entries", async () => {
const skillDir = join(tempDir, "skills");
await mkdir(skillDir);
await writeFile(join(skillDir, "readme.txt"), "not a directory");

const skills = await listSkills([skillDir]);
expect(skills).toHaveLength(0);
});

test("returns empty array for non-existent directory", async () => {
const skills = await listSkills([join(tempDir, "does-not-exist")]);
expect(skills).toHaveLength(0);
});


});
92 changes: 92 additions & 0 deletions src/agent/__tests__/todos.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { describe, expect, test } from "bun:test";

import { createTodoSystem } from "../todos/todos";

const mockContext = { prompt: "", messages: [], tools: [] } as never;

describe("createTodoSystem", () => {
describe("tool invocation", () => {
test("replaces list when merge is false", async () => {
const { tool } = createTodoSystem();
const result = await tool.invoke({
todos: [
{ id: "1", content: "Task A", status: "pending" },
{ id: "2", content: "Task B", status: "in_progress" },
],
merge: false,
});
expect(result).toContain("2 items");
expect(result).toContain("1 pending");
expect(result).toContain("1 in_progress");
});

test("merges by id when merge is true", async () => {
const { tool } = createTodoSystem();
await tool.invoke({
todos: [
{ id: "1", content: "Task A", status: "pending" },
{ id: "2", content: "Task B", status: "pending" },
],
merge: false,
});

const result = await tool.invoke({
todos: [{ id: "1", content: "Task A updated", status: "completed" }],
merge: true,
});
expect(result).toContain("2 items");
expect(result).toContain("1 completed");
expect(result).toContain("1 pending");
});

test("appends new items when merging with new ids", async () => {
const { tool } = createTodoSystem();
await tool.invoke({
todos: [{ id: "1", content: "Task A", status: "pending" }],
merge: false,
});

const result = await tool.invoke({
todos: [{ id: "2", content: "Task B", status: "pending" }],
merge: true,
});
expect(result).toContain("2 items");
expect(result).toContain("2 pending");
});

test("handles empty todo list", async () => {
const { tool } = createTodoSystem();
const result = await tool.invoke({ todos: [], merge: false });
expect(result).toContain("0 items");
});

test("counts all status types correctly", async () => {
const { tool } = createTodoSystem();
const result = await tool.invoke({
todos: [
{ id: "1", content: "A", status: "pending" },
{ id: "2", content: "B", status: "in_progress" },
{ id: "3", content: "C", status: "completed" },
{ id: "4", content: "D", status: "cancelled" },
],
merge: false,
});
expect(result).toContain("4 items");
expect(result).toContain("1 pending");
expect(result).toContain("1 in_progress");
expect(result).toContain("1 completed");
expect(result).toContain("1 cancelled");
});
});

describe("middleware", () => {
test("beforeModel does nothing when store is empty", async () => {
const { middleware } = createTodoSystem();
const result = await middleware.beforeModel?.({
modelContext: { prompt: "hello", messages: [] },
agentContext: mockContext,
});
expect(result).toBeUndefined();
});
});
});
76 changes: 76 additions & 0 deletions src/agent/__tests__/tool-result-summary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, expect, test } from "bun:test";

import { summarizeToolResultText } from "../tool-result-summary";

describe("summarizeToolResultText", () => {
describe("error prefix passthrough", () => {
test("returns content as-is when it starts with 'Error:'", () => {
expect(summarizeToolResultText("Error: something went wrong")).toBe("Error: something went wrong");
});

test("returns content as-is for 'Error:' with no additional text", () => {
expect(summarizeToolResultText("Error:")).toBe("Error:");
});
});

describe("JSON success path", () => {
test("returns summary when ok is true and summary is a string", () => {
expect(summarizeToolResultText(JSON.stringify({ ok: true, summary: "3 files found" }))).toBe("3 files found");
});

test("returns null when ok is true but summary is not a string", () => {
expect(summarizeToolResultText(JSON.stringify({ ok: true, summary: 42 }))).toBeNull();
});

test("returns null when ok is true but summary is missing", () => {
expect(summarizeToolResultText(JSON.stringify({ ok: true }))).toBeNull();
});
});

describe("JSON error path", () => {
test("returns formatted error with code when ok is false", () => {
expect(summarizeToolResultText(JSON.stringify({ ok: false, error: "not found", code: "FILE_NOT_FOUND" }))).toBe(
"Error [FILE_NOT_FOUND]: not found",
);
});

test("prefers summary over error for the message when ok is false", () => {
expect(
summarizeToolResultText(JSON.stringify({ ok: false, summary: "custom msg", error: "raw error", code: "E1" })),
).toBe("Error [E1]: custom msg");
});

test("falls back to error string when summary is not a string and ok is false", () => {
expect(summarizeToolResultText(JSON.stringify({ ok: false, error: "fail", code: "E2" }))).toBe(
"Error [E2]: fail",
);
});

test("returns formatted error without code when ok is false and code is missing", () => {
expect(summarizeToolResultText(JSON.stringify({ ok: false, summary: "bad input" }))).toBe("Error: bad input");
});

test("returns formatted error using raw content when neither summary nor error is a string", () => {
const raw = JSON.stringify({ ok: false, summary: 123, error: 456 });
expect(summarizeToolResultText(raw)).toBe(`Error: ${raw}`);
});
});

describe("non-JSON content", () => {
test("returns null for plain text that is not an error", () => {
expect(summarizeToolResultText("some plain text output")).toBeNull();
});

test("returns null for empty string", () => {
expect(summarizeToolResultText("")).toBeNull();
});

test("returns null for invalid JSON", () => {
expect(summarizeToolResultText("{not valid json}")).toBeNull();
});

test("returns null for JSON with neither ok true nor ok false", () => {
expect(summarizeToolResultText(JSON.stringify({ data: "stuff" }))).toBeNull();
});
});
});
5 changes: 4 additions & 1 deletion src/agent/agent-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,10 @@ export interface AgentMiddleware {
* @param params - Hook parameters.
* @returns Optional context updates to merge into `context`, or a skip instruction to bypass tool execution.
*/
beforeToolUse?: (params: BeforeToolUseParams) => Promise<BeforeToolUseResult>;
beforeToolUse?: (params: {
agentContext: AgentContext;
toolUse: ToolUseContent<Record<string, unknown>>;
}) => Promise<BeforeToolUseResult>;
/**
* Runs immediately after a tool invocation resolves.
* @param params - Hook parameters.
Expand Down
Loading