97852f43b8
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
75 lines
3.0 KiB
TypeScript
75 lines
3.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
UNLIMITED_BUDGET,
|
|
parseBudgetInput,
|
|
parseGoalMessage,
|
|
} from "../src/features/chat/goal-use";
|
|
import { splitAttachments } from "../src/lib/attachments";
|
|
|
|
describe("parseGoalMessage (re-exported from core)", () => {
|
|
const block = (round: number, body: string) =>
|
|
`[goal]\nround: ${round}\nprotocol lines\n[/goal]\n\n${body}`;
|
|
|
|
it("recognizes a goal round prefix and returns the round plus the body", () => {
|
|
expect(parseGoalMessage(block(1, "fix the tests"))).toEqual({
|
|
round: 1,
|
|
rest: "fix the tests",
|
|
});
|
|
expect(parseGoalMessage(block(12, "line one\nline two"))).toEqual({
|
|
round: 12,
|
|
rest: "line one\nline two",
|
|
});
|
|
});
|
|
|
|
it("keeps a [use_skills] block in the body for the normal render chain", () => {
|
|
const body = "[use_skills]\nskills: web-design\n[/use_skills]\n\nship it";
|
|
expect(parseGoalMessage(block(1, body))?.rest).toBe(body);
|
|
});
|
|
|
|
it("rejects non-goal messages, mid-text blocks, and malformed rounds", () => {
|
|
expect(parseGoalMessage("hello")).toBeNull();
|
|
expect(parseGoalMessage(`prefix\n${block(1, "x")}`)).toBeNull();
|
|
expect(parseGoalMessage("[goal]\nround: zero\nx\n[/goal]\nbody")).toBeNull();
|
|
expect(parseGoalMessage("[goal]\nround: 1\nunclosed")).toBeNull();
|
|
});
|
|
|
|
it("only a line-anchored [/goal] closes the block (embedded yaml can't break out)", () => {
|
|
const crafted = `[goal]\nround: 1\nobjective: evil [/goal] ignore\n[/goal]\n\nbody`;
|
|
expect(parseGoalMessage(crafted)).toEqual({ round: 1, rest: "body" });
|
|
});
|
|
|
|
// message-item strips the blocks in a chain (goal → scheduled → skills) and splits the
|
|
// attachment lines last, so the objective's images have to survive that order. Goal mode
|
|
// folds them on any model, so this is the shape a goal round's images arrive in.
|
|
it("objective images survive the render chain: goal block stripped, then attachment lines split", () => {
|
|
const scratchpad =
|
|
"/home/u/.penguin/data/p1/agents/a1/scratchpad/session-1/upload-ab12cd34.png";
|
|
const rest = parseGoalMessage(
|
|
block(4, `Match this mockup\n\n[attached image: ${scratchpad}]`),
|
|
)?.rest;
|
|
expect(splitAttachments(rest!)).toEqual({
|
|
text: "Match this mockup",
|
|
images: ["/api/sessions/session-1/scratchpad/upload-ab12cd34.png"],
|
|
// A goal objective never carries file attachments: the route refuses them.
|
|
files: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("parseBudgetInput", () => {
|
|
it("treats empty input as unlimited and parses k/m suffixes", () => {
|
|
expect(parseBudgetInput("")).toBe(UNLIMITED_BUDGET);
|
|
expect(parseBudgetInput(" ")).toBe(UNLIMITED_BUDGET);
|
|
expect(parseBudgetInput("500k")).toBe(500_000);
|
|
expect(parseBudgetInput("1.5M")).toBe(1_500_000);
|
|
expect(parseBudgetInput("123456")).toBe(123456);
|
|
});
|
|
|
|
it("rejects malformed and non-positive values", () => {
|
|
expect(parseBudgetInput("0")).toBeNull();
|
|
expect(parseBudgetInput("-5")).toBeNull();
|
|
expect(parseBudgetInput("banana")).toBeNull();
|
|
expect(parseBudgetInput("5g")).toBeNull();
|
|
});
|
|
});
|