feat(coding-agent): added /loop count/duration arg parsing and budgeting
- Added optional `/loop` `count|duration` command arguments and wired `command.args` into loop handling. - Implemented `loop-limit` parsing and runtime types/helpers for iteration and duration budget limits with validation. - Updated interactive mode to enforce loop limits per iteration, check duration expiry, and clear budget state on disable. - Added loop-limit parse/runtime tests and fixed `/loop` arg errors plus macOS `MallocStackLogging` environment leakage.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, test, vi } from "bun:test";
|
||||
import {
|
||||
consumeLoopLimitIteration,
|
||||
createLoopLimitRuntime,
|
||||
isLoopDurationExpired,
|
||||
parseLoopLimitArgs,
|
||||
} from "@oh-my-pi/pi-coding-agent/modes/loop-limit";
|
||||
import type { BuiltinSlashCommandRuntime } from "@oh-my-pi/pi-coding-agent/slash-commands/builtin-registry";
|
||||
import { executeBuiltinSlashCommand } from "@oh-my-pi/pi-coding-agent/slash-commands/builtin-registry";
|
||||
|
||||
describe("/loop slash command", () => {
|
||||
test("accepts an optional limit argument", async () => {
|
||||
const handleLoopCommand = vi.fn(async (_args?: string) => {});
|
||||
const runtime = {
|
||||
ctx: {
|
||||
handleLoopCommand,
|
||||
editor: { setText: vi.fn() },
|
||||
},
|
||||
handleBackgroundCommand: vi.fn(),
|
||||
} as unknown as BuiltinSlashCommandRuntime;
|
||||
const result = await executeBuiltinSlashCommand("/loop 10min", runtime);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(handleLoopCommand).toHaveBeenCalledWith("10min");
|
||||
});
|
||||
});
|
||||
|
||||
describe("loop limit parsing", () => {
|
||||
test("parses a bare positive integer as an iteration limit", () => {
|
||||
expect(parseLoopLimitArgs("10")).toEqual({ kind: "iterations", iterations: 10 });
|
||||
});
|
||||
|
||||
test("parses minute duration aliases", () => {
|
||||
expect(parseLoopLimitArgs("10m")).toEqual({ kind: "duration", durationMs: 600_000 });
|
||||
expect(parseLoopLimitArgs("10min")).toEqual({ kind: "duration", durationMs: 600_000 });
|
||||
expect(parseLoopLimitArgs("10 minutes")).toEqual({ kind: "duration", durationMs: 600_000 });
|
||||
});
|
||||
|
||||
test("rejects zero, negative, and unknown limits", () => {
|
||||
expect(parseLoopLimitArgs("0")).toBe("Loop count must be a positive integer.");
|
||||
expect(parseLoopLimitArgs("-1")).toContain("Usage: /loop");
|
||||
expect(parseLoopLimitArgs("10fortnights")).toBe("Loop duration unit must be seconds, minutes, or hours.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("loop limit runtime", () => {
|
||||
test("allows exactly the configured number of auto-submitted iterations", () => {
|
||||
const config = parseLoopLimitArgs("3");
|
||||
expect(config).toEqual({ kind: "iterations", iterations: 3 });
|
||||
if (!config || typeof config === "string") throw new Error("expected parsed config");
|
||||
|
||||
const limit = createLoopLimitRuntime(config);
|
||||
expect(consumeLoopLimitIteration(limit)).toBe(true);
|
||||
expect(consumeLoopLimitIteration(limit)).toBe(true);
|
||||
expect(consumeLoopLimitIteration(limit)).toBe(true);
|
||||
expect(consumeLoopLimitIteration(limit)).toBe(false);
|
||||
expect(limit).toEqual({ kind: "iterations", initial: 3, remaining: 0 });
|
||||
});
|
||||
|
||||
test("stops duration-limited loops at the configured deadline", () => {
|
||||
const config = parseLoopLimitArgs("10m");
|
||||
expect(config).toEqual({ kind: "duration", durationMs: 600_000 });
|
||||
if (!config || typeof config === "string") throw new Error("expected parsed config");
|
||||
|
||||
const limit = createLoopLimitRuntime(config, 1_000);
|
||||
expect(consumeLoopLimitIteration(limit, 600_999)).toBe(true);
|
||||
expect(isLoopDurationExpired(limit, 600_999)).toBe(false);
|
||||
expect(consumeLoopLimitIteration(limit, 601_000)).toBe(false);
|
||||
expect(isLoopDurationExpired(limit, 601_000)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user