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
144 changes: 144 additions & 0 deletions src/agent/__tests__/message-param-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { describe, expect, test } from "bun:test";
import { extractTextFromMessageParam, wrapMessageContent } from "../message-param-utils.ts";

// The MessageParam type is { role: string; content: string | unknown[] }
// We construct values matching that shape for testing.

describe("extractTextFromMessageParam", () => {
test("extracts text from string content", () => {
const msg = { role: "user", content: "Hello world" };
expect(extractTextFromMessageParam(msg)).toBe("Hello world");
});

test("extracts text from array with single text block", () => {
const msg = {
role: "user",
content: [{ type: "text", text: "Hello from array" }],
};
expect(extractTextFromMessageParam(msg)).toBe("Hello from array");
});

test("joins multiple text blocks with newline", () => {
const msg = {
role: "user",
content: [
{ type: "text", text: "First" },
{ type: "text", text: "Second" },
],
};
expect(extractTextFromMessageParam(msg)).toBe("First\nSecond");
});

test("skips non-text blocks", () => {
const msg = {
role: "user",
content: [
{ type: "image", source: { data: "abc" } },
{ type: "text", text: "Only text" },
],
};
expect(extractTextFromMessageParam(msg)).toBe("Only text");
});

test("returns empty string for non-array non-string content", () => {
const msg = { role: "user", content: 42 };
expect(extractTextFromMessageParam(msg as unknown as { role: string; content: string })).toBe("");
});

test("returns empty string for empty array", () => {
const msg = { role: "user", content: [] };
expect(extractTextFromMessageParam(msg)).toBe("");
});

test("skips text blocks with empty text", () => {
const msg = {
role: "user",
content: [
{ type: "text", text: "" },
{ type: "text", text: "Real content" },
],
};
expect(extractTextFromMessageParam(msg)).toBe("Real content");
});
});

describe("wrapMessageContent", () => {
const wrapper = (text: string) => `<wrapped>${text}</wrapped>`;

test("wraps string content directly", () => {
const msg = { role: "user", content: "Hello" };
const result = wrapMessageContent(msg, wrapper);
expect(result.content).toBe("<wrapped>Hello</wrapped>");
});

test("wraps the last text block in array content", () => {
const msg = {
role: "user",
content: [
{ type: "text", text: "First" },
{ type: "text", text: "Last" },
],
};
const result = wrapMessageContent(msg, wrapper);
const blocks = result.content as { type: string; text: string }[];
expect(blocks[0].text).toBe("First");
expect(blocks[1].text).toBe("<wrapped>Last</wrapped>");
});

test("only wraps the last text block, not earlier ones", () => {
const msg = {
role: "user",
content: [
{ type: "text", text: "A" },
{ type: "image", source: { data: "x" } },
{ type: "text", text: "B" },
{ type: "text", text: "C" },
],
};
const result = wrapMessageContent(msg, wrapper);
const blocks = result.content as { type: string; text?: string }[];
expect(blocks[0].text).toBe("A");
expect(blocks[2].text).toBe("B");
expect(blocks[3].text).toBe("<wrapped>C</wrapped>");
});

test("handles array with no text blocks", () => {
const msg = {
role: "user",
content: [{ type: "image", source: { data: "x" } }],
};
const result = wrapMessageContent(msg, wrapper);
const blocks = result.content as { type: string }[];
// No text block to wrap, array stays unchanged
expect(blocks).toHaveLength(1);
expect(blocks[0].type).toBe("image");
});

test("wraps empty string for non-array non-string content", () => {
const msg = { role: "user", content: 42 };
const result = wrapMessageContent(msg as unknown as { role: string; content: string }, wrapper);
expect(result.content).toBe("<wrapped></wrapped>");
});

test("preserves other message properties", () => {
const msg = { role: "user", content: "Hello", extra: "keep" };
const result = wrapMessageContent(msg as unknown as { role: string; content: string }, wrapper);
expect((result as unknown as { role: string }).role).toBe("user");
});

test("preserves non-text blocks in their original positions", () => {
const msg = {
role: "user",
content: [
{ type: "image", source: { data: "img1" } },
{ type: "text", text: "Caption" },
{ type: "image", source: { data: "img2" } },
],
};
const result = wrapMessageContent(msg, wrapper);
const blocks = result.content as { type: string; text?: string; source?: { data: string } }[];
expect(blocks[0].source?.data).toBe("img1");
expect(blocks[1].text).toBe("<wrapped>Caption</wrapped>");
expect(blocks[2].source?.data).toBe("img2");
});
});
182 changes: 182 additions & 0 deletions src/agent/prompt-blocks/__tests__/evolved.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { describe, expect, test } from "bun:test";
import type { EvolvedConfig } from "../../../evolution/types.ts";
import { buildEvolvedSections } from "../evolved.ts";

function makeConfig(overrides: Partial<EvolvedConfig> = {}): EvolvedConfig {
return {
constitution: "",
persona: "",
userProfile: "",
domainKnowledge: "",
strategies: {
taskPatterns: "",
toolPreferences: "",
errorRecovery: "",
},
meta: {
version: 1,
metricsSnapshot: {
session_count: 0,
success_rate_7d: 0,
},
},
...overrides,
};
}

describe("buildEvolvedSections", () => {
test("returns empty string when all fields are empty", () => {
const result = buildEvolvedSections(makeConfig());
expect(result).toBe("");
});

test("returns empty string when all fields are whitespace-only", () => {
const result = buildEvolvedSections(
makeConfig({
constitution: " ",
persona: " \n ",
userProfile: "\t",
}),
);
expect(result).toBe("");
});

test("includes constitution section when populated", () => {
const result = buildEvolvedSections(
makeConfig({
constitution: "Always be helpful.\nNever lie.",
}),
);
expect(result).toContain("# Constitution");
expect(result).toContain("Always be helpful.");
expect(result).toContain("Never lie.");
});

test("includes persona section when it has multiple content lines", () => {
const result = buildEvolvedSections(
makeConfig({
persona: "Be concise.\nUse technical language.\nAvoid jargon.",
}),
);
expect(result).toContain("# Communication Style");
expect(result).toContain("Be concise.");
});

test("skips persona when it has only one content line", () => {
const result = buildEvolvedSections(
makeConfig({
constitution: "Rule one.\nRule two.",
persona: "Be friendly.",
}),
);
expect(result).not.toContain("# Communication Style");
});

test("includes userProfile section when it has multiple content lines", () => {
const result = buildEvolvedSections(
makeConfig({
userProfile: "Senior engineer.\nPrefers TypeScript.\nUses Vim.",
}),
);
expect(result).toContain("# User Profile");
expect(result).toContain("Senior engineer.");
});

test("skips userProfile when it has only one content line", () => {
const result = buildEvolvedSections(
makeConfig({
constitution: "Be honest.\nBe helpful.",
userProfile: "Developer.",
}),
);
expect(result).not.toContain("# User Profile");
});

test("includes domainKnowledge when it has multiple content lines", () => {
const result = buildEvolvedSections(
makeConfig({
domainKnowledge: "Uses PostgreSQL.\nRuns on Kubernetes.\nHosts on AWS.",
}),
);
expect(result).toContain("# Domain Knowledge");
expect(result).toContain("Uses PostgreSQL.");
});

test("skips domainKnowledge with only one content line", () => {
const result = buildEvolvedSections(
makeConfig({
constitution: "Help.\nAlways.",
domainKnowledge: "Uses React.",
}),
);
expect(result).not.toContain("# Domain Knowledge");
});

test("includes learned strategies when subsections have multiple content lines", () => {
const result = buildEvolvedSections(
makeConfig({
strategies: {
taskPatterns: "Break into small steps.\nVerify each step.",
toolPreferences: "Prefer grep over find.\nUse ripgrep for speed.",
errorRecovery: "Retry once.\nLog the error.",
},
}),
);
expect(result).toContain("# Learned Strategies");
expect(result).toContain("Break into small steps.");
expect(result).toContain("Prefer grep over find.");
expect(result).toContain("Retry once.");
});

test("includes strategy section when only some subsections qualify", () => {
const result = buildEvolvedSections(
makeConfig({
strategies: {
taskPatterns: "Break into steps.\nVerify.",
toolPreferences: "One liner only.",
errorRecovery: "",
},
}),
);
expect(result).toContain("# Learned Strategies");
expect(result).toContain("Break into steps.");
expect(result).not.toContain("One liner only.");
});

test("omits strategy section when no subsections qualify", () => {
const result = buildEvolvedSections(
makeConfig({
constitution: "Be good.\nBe great.",
strategies: {
taskPatterns: "Single line.",
toolPreferences: "",
errorRecovery: "",
},
}),
);
expect(result).not.toContain("# Learned Strategies");
});

test("combines multiple sections with double newlines", () => {
const result = buildEvolvedSections(
makeConfig({
constitution: "Always help.\nNever harm.",
persona: "Be terse.\nBe direct.\nNo fluff.",
}),
);
expect(result).toContain("# Constitution\n\nAlways help.\nNever harm.");
expect(result).toContain("# Communication Style\n\nBe terse.\nBe direct.\nNo fluff.");
expect(result.indexOf("# Constitution")).toBeLessThan(result.indexOf("# Communication Style"));
});

test("heading-only lines are not counted as content", () => {
const result = buildEvolvedSections(
makeConfig({
constitution: "Be good.\nStay honest.",
persona: "# Style Guide\nBe concise.",
}),
);
// "# Style Guide" is a heading, "Be concise." is the only content line
expect(result).not.toContain("# Communication Style");
});
});
Loading
Loading