mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: enable permission-aware subagents
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* group-join.test.ts — Behavior of GroupJoinManager's state machine and timers.
|
||||
*
|
||||
* Uses fake timers to assert deterministic timeout behavior without flakiness.
|
||||
* The class itself is exercised directly with real records — no mocks beyond
|
||||
* a spy on the delivery callback.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { GroupJoinManager } from "../src/group-join.js";
|
||||
import type { AgentRecord } from "../src/types.js";
|
||||
|
||||
function makeRecord(id: string, overrides: Partial<AgentRecord> = {}): AgentRecord {
|
||||
return {
|
||||
id,
|
||||
type: "general-purpose",
|
||||
description: "test",
|
||||
status: "completed",
|
||||
toolUses: 0,
|
||||
startedAt: 0,
|
||||
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("GroupJoinManager", () => {
|
||||
beforeEach(() => vi.useFakeTimers());
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
it("returns 'pass' for unregistered agents and never invokes the callback", () => {
|
||||
const deliver = vi.fn();
|
||||
const mgr = new GroupJoinManager(deliver);
|
||||
expect(mgr.onAgentComplete(makeRecord("a"))).toBe("pass");
|
||||
expect(deliver).not.toHaveBeenCalled();
|
||||
expect(mgr.isGrouped("a")).toBe(false);
|
||||
});
|
||||
|
||||
it("holds the first completion and arms the join timeout", () => {
|
||||
const deliver = vi.fn();
|
||||
const mgr = new GroupJoinManager(deliver, 30_000);
|
||||
mgr.registerGroup("g", ["a", "b"]);
|
||||
|
||||
expect(mgr.isGrouped("a")).toBe(true);
|
||||
expect(mgr.onAgentComplete(makeRecord("a"))).toBe("held");
|
||||
|
||||
vi.advanceTimersByTime(29_999);
|
||||
expect(deliver).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("delivers all records (partial=false) when the final completion arrives in time", () => {
|
||||
const deliver = vi.fn();
|
||||
const mgr = new GroupJoinManager(deliver);
|
||||
mgr.registerGroup("g", ["a", "b"]);
|
||||
|
||||
mgr.onAgentComplete(makeRecord("a", { result: "A" }));
|
||||
expect(mgr.onAgentComplete(makeRecord("b", { result: "B" }))).toBe("delivered");
|
||||
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
const [records, partial] = deliver.mock.calls[0];
|
||||
expect(records.map((r: AgentRecord) => r.id).sort()).toEqual(["a", "b"]);
|
||||
expect(partial).toBe(false);
|
||||
|
||||
// Group is cleaned up — no future deliveries can fire from these ids
|
||||
expect(mgr.isGrouped("a")).toBe(false);
|
||||
expect(mgr.isGrouped("b")).toBe(false);
|
||||
});
|
||||
|
||||
it("delivers partial=true on timeout and re-arms the group for stragglers", () => {
|
||||
const deliver = vi.fn();
|
||||
const mgr = new GroupJoinManager(deliver, 30_000);
|
||||
mgr.registerGroup("g", ["a", "b", "c"]);
|
||||
|
||||
mgr.onAgentComplete(makeRecord("a"));
|
||||
vi.advanceTimersByTime(30_000);
|
||||
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
const [records, partial] = deliver.mock.calls[0];
|
||||
expect(records.map((r: AgentRecord) => r.id)).toEqual(["a"]);
|
||||
expect(partial).toBe(true);
|
||||
|
||||
// 'a' was delivered and is dropped from the group; 'b' and 'c' remain as stragglers
|
||||
expect(mgr.isGrouped("a")).toBe(false);
|
||||
expect(mgr.isGrouped("b")).toBe(true);
|
||||
expect(mgr.isGrouped("c")).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the shorter straggler timeout (15s) regardless of the configured group timeout", () => {
|
||||
const deliver = vi.fn();
|
||||
const mgr = new GroupJoinManager(deliver, 30_000);
|
||||
mgr.registerGroup("g", ["a", "b", "c"]);
|
||||
|
||||
// First batch: 'a' alone, partial-delivered after 30s
|
||||
mgr.onAgentComplete(makeRecord("a"));
|
||||
vi.advanceTimersByTime(30_000);
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Straggler 'b' arrives — fires at 15s, not 30s
|
||||
mgr.onAgentComplete(makeRecord("b"));
|
||||
vi.advanceTimersByTime(14_999);
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(deliver).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(deliver.mock.calls[1][0].map((r: AgentRecord) => r.id)).toEqual(["b"]);
|
||||
expect(deliver.mock.calls[1][1]).toBe(true);
|
||||
expect(mgr.isGrouped("c")).toBe(true); // 'c' is the remaining straggler now
|
||||
});
|
||||
|
||||
it("delivers stragglers as a complete batch (partial=false) when all complete before their timeout", () => {
|
||||
const deliver = vi.fn();
|
||||
const mgr = new GroupJoinManager(deliver, 30_000);
|
||||
mgr.registerGroup("g", ["a", "b", "c"]);
|
||||
|
||||
mgr.onAgentComplete(makeRecord("a"));
|
||||
vi.advanceTimersByTime(30_000); // partial: 'a'
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
|
||||
mgr.onAgentComplete(makeRecord("b"));
|
||||
expect(mgr.onAgentComplete(makeRecord("c"))).toBe("delivered");
|
||||
|
||||
expect(deliver).toHaveBeenCalledTimes(2);
|
||||
expect(deliver.mock.calls[1][0].map((r: AgentRecord) => r.id).sort()).toEqual(["b", "c"]);
|
||||
expect(deliver.mock.calls[1][1]).toBe(false);
|
||||
});
|
||||
|
||||
it("returns 'pass' for late completions arriving after a group is already delivered", () => {
|
||||
const deliver = vi.fn();
|
||||
const mgr = new GroupJoinManager(deliver);
|
||||
mgr.registerGroup("g", ["a", "b"]);
|
||||
|
||||
mgr.onAgentComplete(makeRecord("a"));
|
||||
mgr.onAgentComplete(makeRecord("b")); // full delivery
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
|
||||
// A duplicate/late completion must not trigger a second delivery
|
||||
expect(mgr.onAgentComplete(makeRecord("a"))).toBe("pass");
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("dispose() clears pending timers so a partial delivery never fires post-dispose", () => {
|
||||
const deliver = vi.fn();
|
||||
const mgr = new GroupJoinManager(deliver, 30_000);
|
||||
mgr.registerGroup("g", ["a", "b"]);
|
||||
|
||||
mgr.onAgentComplete(makeRecord("a")); // arms 30s timeout
|
||||
mgr.dispose();
|
||||
|
||||
vi.advanceTimersByTime(60_000);
|
||||
expect(deliver).not.toHaveBeenCalled();
|
||||
expect(mgr.isGrouped("a")).toBe(false);
|
||||
expect(mgr.isGrouped("b")).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user