import { afterAll, beforeAll, describe, expect, it } from "bun:test"; import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { findSoulfiles, parseAgentContent, parseSoulfile } from "../src/agent/parser.ts"; const TEST_DIR = join(tmpdir(), `mozart-test-${Date.now()}`); beforeAll(() => { mkdirSync(TEST_DIR, { recursive: true }); }); afterAll(() => { rmSync(TEST_DIR, { recursive: true, force: true }); }); function writeTestFile(name: string, content: string): string { const path = join(TEST_DIR, name); mkdirSync(join(path, ".."), { recursive: true }); writeFileSync(path, content); return path; } describe("parseSoulfile", () => { it("parses a minimal 2-line .soul file", () => { const path = writeTestFile( "minimal.soul", ["MODEL anthropic/claude-sonnet-4", "SOUL You are a helpful assistant."].join("\n"), ); const config = parseSoulfile(path); expect(config.model).toBe("anthropic/claude-sonnet-4"); expect(config.identity).toBe("You are a helpful assistant."); expect(config.name).toBe("minimal"); expect(config.ifThen).toEqual([]); expect(config.schedule).toEqual([]); }); it("parses heredoc SOUL", () => { const path = writeTestFile( "heredoc.soul", [ "MODEL anthropic/claude-sonnet-4", "SOUL < { const path = writeTestFile("bad/Agentfile", ["MODEL anthropic/claude-sonnet-4", "SOUL Test agent."].join("\n")); expect(() => parseSoulfile(path)).toThrow("must use the .soul extension"); }); it("parses IF/THEN instruction with free-form action", () => { const path = writeTestFile( "ifthen.soul", [ "MODEL anthropic/claude-sonnet-4", "SOUL Test agent.", 'IF "the bug is in the UI" THEN "route it to frontend-agent using the send tool"', 'IF "the bug is in the API" THEN "route it to backend-agent using the send tool"', ].join("\n"), ); const config = parseSoulfile(path); expect(config.ifThen).toHaveLength(2); expect(config.ifThen[0]).toEqual({ condition: "the bug is in the UI", action: "route it to frontend-agent using the send tool", }); expect(config.ifThen[1]).toEqual({ condition: "the bug is in the API", action: "route it to backend-agent using the send tool", }); }); it("parses IF/THEN with non-routing action", () => { const path = writeTestFile( "ifthen-tool.soul", [ "MODEL anthropic/claude-sonnet-4", "SOUL Test agent.", 'IF "the user asks about pricing" THEN "search memory for pricing info and respond with a summary"', ].join("\n"), ); const config = parseSoulfile(path); expect(config.ifThen).toHaveLength(1); expect(config.ifThen[0]).toEqual({ condition: "the user asks about pricing", action: "search memory for pricing info and respond with a summary", }); }); it("parses SCHEDULE instruction", () => { const path = writeTestFile( "scheduler.soul", [ "MODEL anthropic/claude-sonnet-4", "SOUL Test agent.", 'SCHEDULE "every weekday at 9am" "Check for new issues"', ].join("\n"), ); const config = parseSoulfile(path); expect(config.schedule).toHaveLength(1); expect(config.schedule[0]).toEqual({ timing: "every weekday at 9am", task: "Check for new issues" }); }); it("ignores comments and blank lines", () => { const path = writeTestFile( "comments.soul", ["# This is a comment", "", "MODEL anthropic/claude-sonnet-4", "# Another comment", "", "SOUL Test agent."].join( "\n", ), ); const config = parseSoulfile(path); expect(config.model).toBe("anthropic/claude-sonnet-4"); }); it("derives name from parent directory when filename is agent.soul", () => { const path = writeTestFile( "my-agent/agent.soul", ["MODEL anthropic/claude-sonnet-4", "SOUL Test agent."].join("\n"), ); const config = parseSoulfile(path); expect(config.name).toBe("my-agent"); }); it("throws on missing MODEL", () => { const path = writeTestFile("no-model.soul", "SOUL Test agent."); expect(() => parseSoulfile(path)).toThrow("Missing required MODEL"); }); it("throws on missing SOUL", () => { const path = writeTestFile("no-identity.soul", "MODEL anthropic/claude-sonnet-4"); expect(() => parseSoulfile(path)).toThrow("Missing required SOUL"); }); it("throws on unknown instruction", () => { const path = writeTestFile( "unknown.soul", ["MODEL anthropic/claude-sonnet-4", "SOUL Test.", "FOOBAR something"].join("\n"), ); expect(() => parseSoulfile(path)).toThrow("Unknown instruction"); }); }); describe("parseAgentContent", () => { it("parses content string with a given name", () => { const content = "MODEL openai/gpt-4.1-mini\nSOUL A helpful assistant."; const config = parseAgentContent(content, "my-agent"); expect(config.model).toBe("openai/gpt-4.1-mini"); expect(config.identity).toBe("A helpful assistant."); expect(config.name).toBe("my-agent"); expect(config.sourcePath).toBe(""); }); it("parses heredoc SOUL from string", () => { const content = [ "MODEL openai/gpt-4.1-mini", "SOUL < { const content = [ "MODEL openai/gpt-4.1-mini", "SOUL Triage agent.", 'IF "bug in UI" THEN "send it to the frontend team"', 'SCHEDULE "every hour" "check issues"', ].join("\n"); const config = parseAgentContent(content, "triage"); expect(config.ifThen).toHaveLength(1); expect(config.ifThen[0]!.action).toBe("send it to the frontend team"); expect(config.schedule).toHaveLength(1); expect(config.schedule[0]!.task).toBe("check issues"); }); it("preserves custom sourcePath when provided", () => { const config = parseAgentContent("MODEL openai/gpt-4.1-mini\nSOUL y", "test", "/custom/path.soul"); expect(config.sourcePath).toBe("/custom/path.soul"); }); it("throws on missing MODEL", () => { expect(() => parseAgentContent("SOUL y", "test")).toThrow("Missing required MODEL"); }); }); describe("findSoulfiles", () => { it("finds .soul file by direct path", () => { const path = writeTestFile("direct.soul", "MODEL openai/gpt-4.1-mini\nSOUL y"); const found = findSoulfiles(path); expect(found).toHaveLength(1); }); it("finds .soul files in a directory", () => { writeTestFile("multi/a.soul", "MODEL openai/gpt-4.1-mini\nSOUL y"); writeTestFile("multi/b.soul", "MODEL openai/gpt-4.1-mini\nSOUL y"); const found = findSoulfiles(join(TEST_DIR, "multi")); expect(found.length).toBeGreaterThanOrEqual(2); }); }); describe("SANCTUM instruction", () => { it("parses absolute SANCTUM path", () => { const config = parseAgentContent("MODEL openai/gpt-4.1-mini\nSANCTUM /Users/sean/projects/app\nSOUL Test.", "test"); expect(config.sanctums).toHaveLength(1); expect(config.sanctums[0]!.path).toBe("/Users/sean/projects/app"); expect(config.sanctums[0]!.readonly).toBe(false); }); it("parses SANCTUM with :ro suffix", () => { const config = parseAgentContent("MODEL openai/gpt-4.1-mini\nSANCTUM /data/shared:ro\nSOUL Test.", "test"); expect(config.sanctums).toHaveLength(1); expect(config.sanctums[0]!.path).toBe("/data/shared"); expect(config.sanctums[0]!.readonly).toBe(true); }); it("parses SANCTUM with explicit :rw suffix", () => { const config = parseAgentContent("MODEL openai/gpt-4.1-mini\nSANCTUM /data/work:rw\nSOUL Test.", "test"); expect(config.sanctums[0]!.path).toBe("/data/work"); expect(config.sanctums[0]!.readonly).toBe(false); }); it("parses multiple SANCTUM instructions", () => { const config = parseAgentContent("MODEL openai/gpt-4.1-mini\nSANCTUM /app\nSANCTUM /data:ro\nSOUL Test.", "test"); expect(config.sanctums).toHaveLength(2); expect(config.sanctums[0]!.path).toBe("/app"); expect(config.sanctums[0]!.readonly).toBe(false); expect(config.sanctums[1]!.path).toBe("/data"); expect(config.sanctums[1]!.readonly).toBe(true); }); it("resolves ~/ to home directory", () => { const config = parseAgentContent("MODEL openai/gpt-4.1-mini\nSANCTUM ~/projects/app\nSOUL Test.", "test"); const { homedir } = require("node:os"); expect(config.sanctums[0]!.path).toBe(join(homedir(), "projects/app")); }); it("resolves relative paths to cwd", () => { const config = parseAgentContent("MODEL openai/gpt-4.1-mini\nSANCTUM ./my-project\nSOUL Test.", "test"); const { resolve } = require("node:path"); expect(config.sanctums[0]!.path).toBe(resolve("./my-project")); }); it("rejects SANCTUM with no path", () => { expect(() => parseAgentContent("MODEL openai/gpt-4.1-mini\nSANCTUM \nSOUL Test.", "test")).toThrow( "SANCTUM requires a path", ); }); it("defaults to empty sanctums when none declared", () => { const config = parseAgentContent("MODEL openai/gpt-4.1-mini\nSOUL Test.", "test"); expect(config.sanctums).toEqual([]); }); }); describe("model validation", () => { it("accepts any model string (validated at runtime by OpenRouter)", () => { const config = parseAgentContent("MODEL openai/gpt-4.1-mini\nSOUL Test.", "test"); expect(config.model).toBe("openai/gpt-4.1-mini"); }); it("accepts unknown models since validation is now runtime", () => { const config = parseAgentContent("MODEL fake/nonexistent\nSOUL Test.", "test"); expect(config.model).toBe("fake/nonexistent"); }); });