mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
203 lines
8.0 KiB
TypeScript
203 lines
8.0 KiB
TypeScript
/**
|
|
* mention.test.ts — the `@handle` grammar.
|
|
*
|
|
* Both halves are load-bearing in a way that fails silently. A handle that
|
|
* isn't `[\w-]` can never be typed back (the trigger regex would not match it),
|
|
* and a collision that reuses a name makes an older sibling permanently
|
|
* unreachable. On the parse side, every rejection here is a case where being
|
|
* too eager would swallow input the user meant for the main model — a leading
|
|
* file path, a bare handle, a mention mid-sentence.
|
|
*/
|
|
import { describe, expect, it } from "vitest";
|
|
import { agentMentionReminder, assignHandle, describeMention, handleBase, isReservedHandle, MENTION_TRIGGER, parseMention, resolveHandleToType, stripAgentPrefix } from "../src/mention.js";
|
|
|
|
describe("handleBase", () => {
|
|
it("lowercases so the handle matches how it is typed", () => {
|
|
expect(handleBase("Explore")).toBe("explore");
|
|
});
|
|
|
|
it("keeps a hyphenated type as-is", () => {
|
|
expect(handleBase("general-purpose")).toBe("general-purpose");
|
|
});
|
|
|
|
it("reduces anything outside [\\w-] to hyphens, without leaving edge hyphens", () => {
|
|
expect(handleBase("Code Review!")).toBe("code-review");
|
|
expect(handleBase(" spaced out ")).toBe("spaced-out");
|
|
});
|
|
|
|
it("always produces something typeable", () => {
|
|
// A type made entirely of stripped characters would otherwise slug to "",
|
|
// and `@` alone can address nothing.
|
|
expect(handleBase("!!!")).toBe("agent");
|
|
expect(handleBase("")).toBe("agent");
|
|
});
|
|
|
|
it("caps a long name so one agent can't own an unreadable row", () => {
|
|
const slug = handleBase("x".repeat(200));
|
|
expect(slug).toHaveLength(64);
|
|
});
|
|
|
|
it("never leaves a trailing hyphen the cap sliced into", () => {
|
|
// 63 chars, then a hyphen run: a naive slice(0, 64) keeps that hyphen.
|
|
expect(handleBase(`${"x".repeat(63)} tail`).endsWith("-")).toBe(false);
|
|
});
|
|
|
|
it("only ever produces handles the suggestion trigger can match", () => {
|
|
for (const type of ["Explore", "general-purpose", "Code Review!", "!!!", "デバッグ"]) {
|
|
expect(MENTION_TRIGGER.test(`@${handleBase(type)}`)).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("assignHandle", () => {
|
|
it("takes the plain base when it is free", () => {
|
|
expect(assignHandle("explore", new Set())).toBe("explore");
|
|
});
|
|
|
|
it("numbers from 2 on the first collision", () => {
|
|
expect(assignHandle("explore", new Set(["explore"]))).toBe("explore-2");
|
|
});
|
|
|
|
it("keeps counting past every taken form", () => {
|
|
expect(assignHandle("explore", new Set(["explore", "explore-2"]))).toBe("explore-3");
|
|
});
|
|
|
|
it("never hands out the reserved main handle", () => {
|
|
// `@main` addresses the main conversation. An agent holding that name
|
|
// would silently swallow the one escape hatch out of the mention grammar.
|
|
expect(assignHandle("main", new Set())).toBe("main-2");
|
|
});
|
|
|
|
it("skips a gap rather than reusing a live handle", () => {
|
|
// explore-2 finished and was evicted; reusing it is fine, but explore-3
|
|
// is still running and must not be shadowed.
|
|
expect(assignHandle("explore", new Set(["explore", "explore-3"]))).toBe("explore-2");
|
|
});
|
|
});
|
|
|
|
describe("resolveHandleToType", () => {
|
|
const TYPES = ["general-purpose", "Explore", "Code Review!"];
|
|
|
|
it("finds the type a handle was derived from, whatever its casing", () => {
|
|
expect(resolveHandleToType("explore", TYPES)).toBe("Explore");
|
|
expect(resolveHandleToType("EXPLORE", TYPES)).toBe("Explore");
|
|
});
|
|
|
|
it("resolves a type whose slug differs from its name", () => {
|
|
expect(resolveHandleToType("code-review", TYPES)).toBe("Code Review!");
|
|
});
|
|
|
|
it("is exact, not a prefix match — a partial handle must not start an agent", () => {
|
|
expect(resolveHandleToType("ex", TYPES)).toBeUndefined();
|
|
expect(resolveHandleToType("explore-2", TYPES)).toBeUndefined();
|
|
});
|
|
|
|
it("round-trips every registered type", () => {
|
|
for (const type of TYPES) expect(resolveHandleToType(handleBase(type), TYPES)).toBe(type);
|
|
});
|
|
|
|
it("refuses to resolve the reserved handle, even to a type named for it", () => {
|
|
// Otherwise `@main do this` would start an agent instead of reaching the
|
|
// main model — and `assignHandle` already denies its instances that name,
|
|
// so resolving the type here would promise something unreachable.
|
|
expect(resolveHandleToType("main", ["main", ...TYPES])).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("isReservedHandle", () => {
|
|
it("recognizes main whatever its casing", () => {
|
|
expect(isReservedHandle("main")).toBe(true);
|
|
expect(isReservedHandle("MAIN")).toBe(true);
|
|
});
|
|
|
|
it("leaves every ordinary handle alone", () => {
|
|
for (const handle of ["explore", "mainframe", "main-2", "ma"]) {
|
|
expect(isReservedHandle(handle)).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("stripAgentPrefix", () => {
|
|
it("unwraps Claude Code's manual @agent-<type> spelling", () => {
|
|
expect(stripAgentPrefix("agent-explore")).toBe("explore");
|
|
});
|
|
|
|
it("keeps the remainder intact when it is itself prefixed", () => {
|
|
expect(stripAgentPrefix("agent-agent-foo")).toBe("agent-foo");
|
|
});
|
|
|
|
it("returns nothing when there is no prefix or nothing behind it", () => {
|
|
expect(stripAgentPrefix("explore")).toBeUndefined();
|
|
expect(stripAgentPrefix("agent-")).toBeUndefined();
|
|
expect(stripAgentPrefix("agentexplore")).toBeUndefined();
|
|
});
|
|
|
|
it("only unwraps a prefix at the very start", () => {
|
|
// `@sub-agent-explore` names an agent called `sub-agent-explore`. Matching
|
|
// `agent-` anywhere would silently redirect it to `@explore`.
|
|
expect(stripAgentPrefix("sub-agent-explore")).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("describeMention", () => {
|
|
it("uses the message as the agent's short label", () => {
|
|
expect(describeMention("find every retry marker")).toBe("find every retry marker");
|
|
});
|
|
|
|
it("takes the first line and collapses whitespace", () => {
|
|
expect(describeMention(" audit the RPC path\nthen report back ")).toBe("audit the RPC path");
|
|
});
|
|
|
|
it("clips a long message rather than putting a paragraph in every agent surface", () => {
|
|
const label = describeMention("x".repeat(200));
|
|
expect(label).toHaveLength(40);
|
|
expect(label.endsWith("…")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("parseMention", () => {
|
|
it("splits a leading handle from its message", () => {
|
|
expect(parseMention("@explore check the RPC path")).toEqual({
|
|
handle: "explore",
|
|
message: "check the RPC path",
|
|
});
|
|
});
|
|
|
|
it("trims the message and accepts a newline as the separator", () => {
|
|
expect(parseMention("@explore spaced ")).toEqual({ handle: "explore", message: "spaced" });
|
|
expect(parseMention("@explore\nline1\nline2")).toEqual({ handle: "explore", message: "line1\nline2" });
|
|
});
|
|
|
|
it("rejects a bare handle — that belongs to the main model", () => {
|
|
expect(parseMention("@explore")).toBeNull();
|
|
expect(parseMention("@explore ")).toBeNull();
|
|
expect(parseMention("@explore \t ")).toBeNull();
|
|
});
|
|
|
|
it("rejects a leading file path so pi's @-attachment keeps working", () => {
|
|
expect(parseMention("@src/index.ts summarize this")).toBeNull();
|
|
expect(parseMention("@README.md what changed")).toBeNull();
|
|
});
|
|
|
|
it("rejects a mention that is not at the start of the input", () => {
|
|
expect(parseMention("hey @explore look at this")).toBeNull();
|
|
expect(parseMention(" @explore look at this")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("agentMentionReminder", () => {
|
|
it("is Claude Code's string, byte for byte", () => {
|
|
// Ported from the 2.1.233 bundle rather than paraphrased, so the model gets
|
|
// the wording it was trained against. Asserted whole — including the
|
|
// trailing space before the closing newline, which is in the original
|
|
// template literal and is exactly the kind of thing a tidy-up would drop.
|
|
expect(agentMentionReminder("code-review")).toBe(
|
|
'<system-reminder>\nThe user has expressed a desire to invoke the agent "code-review". Please invoke the agent appropriately, passing in the required context to it. \n</system-reminder>',
|
|
);
|
|
});
|
|
|
|
it("names the agent it was given", () => {
|
|
expect(agentMentionReminder("Plan")).toContain('invoke the agent "Plan"');
|
|
});
|
|
});
|