357 lines
11 KiB
TypeScript
357 lines
11 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { Mode } from "../modes.ts";
|
|
import { resolveMode, truncateOutput } from "./delegate.ts";
|
|
|
|
// ─── mode resolution tests ─────────────────────────────────────────────
|
|
|
|
const testModes: Mode[] = [
|
|
{ name: "Build", description: "build things", prompt: "build prompt" },
|
|
{ name: "Plan", description: "plan things", prompt: "plan prompt" },
|
|
{ name: "Review", description: "review things", prompt: "review prompt" },
|
|
{ name: "Fix", description: "fix things", prompt: "fix prompt" },
|
|
{ name: "AddressReviews", description: "address reviews", prompt: "address prompt" },
|
|
];
|
|
|
|
describe("delegate - mode resolution", () => {
|
|
it("resolves valid mode name", () => {
|
|
const mode = resolveMode(testModes, "Build");
|
|
expect(mode).not.toBeNull();
|
|
expect(mode!.name).toBe("Build");
|
|
});
|
|
|
|
it("resolves case-insensitively (lowercase)", () => {
|
|
const mode = resolveMode(testModes, "build");
|
|
expect(mode).not.toBeNull();
|
|
expect(mode!.name).toBe("Build");
|
|
});
|
|
|
|
it("resolves case-insensitively (uppercase)", () => {
|
|
const mode = resolveMode(testModes, "BUILD");
|
|
expect(mode).not.toBeNull();
|
|
expect(mode!.name).toBe("Build");
|
|
});
|
|
|
|
it("resolves case-insensitively (mixed case)", () => {
|
|
const mode = resolveMode(testModes, "pLaN");
|
|
expect(mode).not.toBeNull();
|
|
expect(mode!.name).toBe("Plan");
|
|
});
|
|
|
|
it("returns null for invalid mode name", () => {
|
|
const mode = resolveMode(testModes, "nonexistent");
|
|
expect(mode).toBeNull();
|
|
});
|
|
|
|
it("returns null for empty string", () => {
|
|
const mode = resolveMode(testModes, "");
|
|
expect(mode).toBeNull();
|
|
});
|
|
|
|
it("resolves custom modes appended alongside built-in modes", () => {
|
|
const modesWithCustom: Mode[] = [
|
|
...testModes,
|
|
{ name: "CustomLabel", description: "label issues", prompt: "label prompt" },
|
|
];
|
|
const mode = resolveMode(modesWithCustom, "customlabel");
|
|
expect(mode).not.toBeNull();
|
|
expect(mode!.name).toBe("CustomLabel");
|
|
});
|
|
|
|
it("returns null when modes list is empty", () => {
|
|
const mode = resolveMode([], "Build");
|
|
expect(mode).toBeNull();
|
|
});
|
|
|
|
it("resolves all built-in modes", () => {
|
|
for (const m of testModes) {
|
|
const resolved = resolveMode(testModes, m.name);
|
|
expect(resolved).not.toBeNull();
|
|
expect(resolved!.name).toBe(m.name);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── output truncation tests ────────────────────────────────────────────
|
|
|
|
describe("delegate - output truncation", () => {
|
|
it("returns undefined for undefined input", () => {
|
|
expect(truncateOutput(undefined)).toBeUndefined();
|
|
});
|
|
|
|
it("returns empty string as-is", () => {
|
|
expect(truncateOutput("")).toBe("");
|
|
});
|
|
|
|
it("returns short output unchanged", () => {
|
|
const short = "a".repeat(100);
|
|
expect(truncateOutput(short)).toBe(short);
|
|
});
|
|
|
|
it("returns output at exactly the limit unchanged", () => {
|
|
const exact = "x".repeat(20_000);
|
|
expect(truncateOutput(exact)).toBe(exact);
|
|
});
|
|
|
|
it("truncates output exceeding the limit", () => {
|
|
const long = "a".repeat(30_000);
|
|
const result = truncateOutput(long);
|
|
expect(result).not.toBe(long);
|
|
expect(result).toContain("[truncated");
|
|
expect(result).toContain("20000");
|
|
});
|
|
|
|
it("keeps the tail of the output (last N chars)", () => {
|
|
const prefix = "START_".repeat(5000);
|
|
const suffix = "END_MARKER";
|
|
const long = prefix + suffix;
|
|
const result = truncateOutput(long)!;
|
|
expect(result).toContain("END_MARKER");
|
|
// the very beginning of the original is lost (starts with truncation prefix, not original content)
|
|
expect(result.startsWith("START_")).toBe(false);
|
|
});
|
|
|
|
it("adds truncation prefix before the content", () => {
|
|
const long = "x".repeat(25_000);
|
|
const result = truncateOutput(long)!;
|
|
expect(result).toMatch(/^\[truncated.*\]\n/);
|
|
});
|
|
});
|
|
|
|
// ─── effort validation tests ────────────────────────────────────────────
|
|
|
|
// mirrors the effort default logic in the delegate handler
|
|
function resolveEffort(effort: string | undefined): string {
|
|
return effort ?? "auto";
|
|
}
|
|
|
|
describe("delegate - effort defaults", () => {
|
|
it("defaults to 'auto' when undefined", () => {
|
|
expect(resolveEffort(undefined)).toBe("auto");
|
|
});
|
|
|
|
it("passes through 'mini'", () => {
|
|
expect(resolveEffort("mini")).toBe("mini");
|
|
});
|
|
|
|
it("passes through 'auto'", () => {
|
|
expect(resolveEffort("auto")).toBe("auto");
|
|
});
|
|
|
|
it("passes through 'max'", () => {
|
|
expect(resolveEffort("max")).toBe("max");
|
|
});
|
|
});
|
|
|
|
// ─── delegation guard tests ─────────────────────────────────────────────
|
|
|
|
// mirrors the delegationActive guard logic
|
|
function checkDelegationGuard(delegationActive: boolean): string | null {
|
|
if (delegationActive) {
|
|
return "delegation is not available inside a delegated subagent";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
describe("delegate - delegation guard", () => {
|
|
it("allows delegation when delegationActive is false", () => {
|
|
expect(checkDelegationGuard(false)).toBeNull();
|
|
});
|
|
|
|
it("blocks delegation when delegationActive is true", () => {
|
|
const error = checkDelegationGuard(true);
|
|
expect(error).not.toBeNull();
|
|
expect(error).toContain("not available");
|
|
});
|
|
});
|
|
|
|
// ─── delegation lifecycle tests ─────────────────────────────────────────
|
|
|
|
// simulates the delegationActive lifecycle across sequential delegations
|
|
describe("delegate - delegation lifecycle", () => {
|
|
it("delegationActive resets after successful delegation", () => {
|
|
let delegationActive = false;
|
|
|
|
// first delegation
|
|
delegationActive = true;
|
|
// ... agent.run() succeeds ...
|
|
delegationActive = false; // finally block
|
|
|
|
expect(delegationActive).toBe(false);
|
|
});
|
|
|
|
it("delegationActive resets after failed delegation (finally block)", () => {
|
|
let delegationActive = false;
|
|
|
|
// delegation that fails — finally block still runs
|
|
delegationActive = true;
|
|
try {
|
|
throw new Error("agent failed");
|
|
} catch {
|
|
// agent error handled
|
|
} finally {
|
|
delegationActive = false;
|
|
}
|
|
|
|
expect(delegationActive).toBe(false);
|
|
});
|
|
|
|
it("supports sequential delegations", () => {
|
|
let delegationActive = false;
|
|
let selectedMode: string | undefined;
|
|
|
|
// first delegation: Plan
|
|
expect(checkDelegationGuard(delegationActive)).toBeNull();
|
|
delegationActive = true;
|
|
selectedMode = "Plan";
|
|
delegationActive = false; // completed
|
|
|
|
expect(selectedMode).toBe("Plan");
|
|
|
|
// second delegation: Build
|
|
expect(checkDelegationGuard(delegationActive)).toBeNull();
|
|
delegationActive = true;
|
|
selectedMode = "Build";
|
|
delegationActive = false; // completed
|
|
|
|
expect(selectedMode).toBe("Build");
|
|
});
|
|
|
|
it("blocks during active delegation", () => {
|
|
let delegationActive = false;
|
|
|
|
// start delegation
|
|
delegationActive = true;
|
|
|
|
// attempt second delegation while first is active
|
|
expect(checkDelegationGuard(delegationActive)).not.toBeNull();
|
|
|
|
// first completes
|
|
delegationActive = false;
|
|
|
|
// now second should be allowed
|
|
expect(checkDelegationGuard(delegationActive)).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ─── subagent payload construction tests ────────────────────────────────
|
|
|
|
type MinimalPayload = {
|
|
effort: string;
|
|
prompt: string;
|
|
bash: string;
|
|
push: string;
|
|
web: string;
|
|
};
|
|
|
|
function buildSubagentPayload(payload: MinimalPayload, delegatedEffort: string): MinimalPayload {
|
|
return { ...payload, effort: delegatedEffort };
|
|
}
|
|
|
|
describe("delegate - subagent payload construction", () => {
|
|
const basePayload: MinimalPayload = {
|
|
effort: "auto",
|
|
prompt: "test prompt",
|
|
bash: "restricted",
|
|
push: "restricted",
|
|
web: "enabled",
|
|
};
|
|
|
|
it("overrides effort in subagent payload", () => {
|
|
const subPayload = buildSubagentPayload(basePayload, "mini");
|
|
expect(subPayload.effort).toBe("mini");
|
|
});
|
|
|
|
it("preserves other payload fields", () => {
|
|
const subPayload = buildSubagentPayload(basePayload, "mini");
|
|
expect(subPayload.prompt).toBe("test prompt");
|
|
expect(subPayload.bash).toBe("restricted");
|
|
expect(subPayload.push).toBe("restricted");
|
|
expect(subPayload.web).toBe("enabled");
|
|
});
|
|
|
|
it("does not mutate original payload", () => {
|
|
const subPayload = buildSubagentPayload(basePayload, "max");
|
|
expect(basePayload.effort).toBe("auto");
|
|
expect(subPayload.effort).toBe("max");
|
|
});
|
|
|
|
it("handles same effort as original", () => {
|
|
const subPayload = buildSubagentPayload(basePayload, "auto");
|
|
expect(subPayload.effort).toBe("auto");
|
|
});
|
|
});
|
|
|
|
// ─── return shape tests ─────────────────────────────────────────────────
|
|
|
|
type DelegateResult = {
|
|
success: boolean;
|
|
mode: string;
|
|
effort: string;
|
|
output: string | undefined;
|
|
error: string | undefined;
|
|
};
|
|
|
|
type BuildDelegateResultInput = {
|
|
agentResult: {
|
|
success: boolean;
|
|
output?: string;
|
|
error?: string;
|
|
};
|
|
mode: string;
|
|
effort: string;
|
|
};
|
|
|
|
function buildDelegateResult(input: BuildDelegateResultInput): DelegateResult {
|
|
return {
|
|
success: input.agentResult.success,
|
|
mode: input.mode,
|
|
effort: input.effort,
|
|
output: input.agentResult.output,
|
|
error: input.agentResult.error,
|
|
};
|
|
}
|
|
|
|
describe("delegate - return shape", () => {
|
|
it("returns success shape on successful delegation", () => {
|
|
const result = buildDelegateResult({
|
|
agentResult: { success: true, output: "agent output" },
|
|
mode: "Build",
|
|
effort: "auto",
|
|
});
|
|
expect(result.success).toBe(true);
|
|
expect(result.mode).toBe("Build");
|
|
expect(result.effort).toBe("auto");
|
|
expect(result.output).toBe("agent output");
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
|
|
it("returns failure shape on failed delegation", () => {
|
|
const result = buildDelegateResult({
|
|
agentResult: { success: false, error: "agent crashed" },
|
|
mode: "Review",
|
|
effort: "mini",
|
|
});
|
|
expect(result.success).toBe(false);
|
|
expect(result.mode).toBe("Review");
|
|
expect(result.effort).toBe("mini");
|
|
expect(result.error).toBe("agent crashed");
|
|
});
|
|
|
|
it("includes mode and effort in both success and failure", () => {
|
|
const success = buildDelegateResult({
|
|
agentResult: { success: true },
|
|
mode: "Plan",
|
|
effort: "max",
|
|
});
|
|
const failure = buildDelegateResult({
|
|
agentResult: { success: false },
|
|
mode: "Fix",
|
|
effort: "mini",
|
|
});
|
|
expect(success.mode).toBe("Plan");
|
|
expect(success.effort).toBe("max");
|
|
expect(failure.mode).toBe("Fix");
|
|
expect(failure.effort).toBe("mini");
|
|
});
|
|
});
|