mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
bcea4b2e27
Add streamingBehavior to InputEvent so extensions can distinguish idle prompts from mid-stream steers and queued follow-ups. - Add streamingBehavior field to InputEvent type - Thread it through ExtensionRunner.emitInput() and AgentSession.prompt() - Add streaming-aware input gate example with tests - Document in extensions.md
85 lines
2.9 KiB
TypeScript
85 lines
2.9 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import inputTransformStreaming from "../examples/extensions/input-transform-streaming.ts";
|
|
import type {
|
|
ExecResult,
|
|
ExtensionAPI,
|
|
ExtensionContext,
|
|
InputEvent,
|
|
InputEventResult,
|
|
} from "../src/core/extensions/index.ts";
|
|
|
|
type InputHandler = (event: InputEvent, ctx: ExtensionContext) => Promise<InputEventResult | undefined>;
|
|
|
|
function setup(execResult: ExecResult) {
|
|
let handler: InputHandler | undefined;
|
|
|
|
const exec = vi.fn<ExtensionAPI["exec"]>().mockResolvedValue(execResult);
|
|
|
|
const api = {
|
|
on: (event: string, h: InputHandler) => {
|
|
if (event === "input") handler = h;
|
|
},
|
|
exec,
|
|
} as unknown as ExtensionAPI;
|
|
|
|
inputTransformStreaming(api);
|
|
|
|
const ctx = {} as ExtensionContext;
|
|
|
|
function emit(text: string, streamingBehavior?: "steer" | "followUp") {
|
|
return handler!({ type: "input", text, source: "interactive", streamingBehavior }, ctx);
|
|
}
|
|
|
|
return { emit, exec };
|
|
}
|
|
|
|
describe("input-transform-streaming example", () => {
|
|
const diffOutput = " src/index.ts | 5 ++---\n 1 file changed, 2 insertions(+), 3 deletions(-)";
|
|
const gitSuccess: ExecResult = { stdout: diffOutput, stderr: "", code: 0, killed: false };
|
|
const gitEmpty: ExecResult = { stdout: "", stderr: "", code: 0, killed: false };
|
|
const gitFail: ExecResult = { stdout: "", stderr: "not a git repo", code: 128, killed: false };
|
|
|
|
it("skips exec during steering", async () => {
|
|
const { emit, exec } = setup(gitSuccess);
|
|
const result = await emit("what changes did I make?", "steer");
|
|
expect(result).toEqual({ action: "continue" });
|
|
expect(exec).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("transforms when idle and text matches trigger", async () => {
|
|
const { emit, exec } = setup(gitSuccess);
|
|
const result = await emit("review my changes");
|
|
expect(exec).toHaveBeenCalledWith("git", ["diff", "--stat"]);
|
|
expect(result).toMatchObject({ action: "transform" });
|
|
const text = (result as { text: string }).text;
|
|
expect(text).toContain("review my changes");
|
|
expect(text).toContain("src/index.ts");
|
|
});
|
|
|
|
it("transforms when queued as follow-up", async () => {
|
|
const { emit, exec } = setup(gitSuccess);
|
|
const result = await emit("show me the diff", "followUp");
|
|
expect(exec).toHaveBeenCalled();
|
|
expect(result).toMatchObject({ action: "transform" });
|
|
});
|
|
|
|
it("continues when text does not match trigger", async () => {
|
|
const { emit, exec } = setup(gitSuccess);
|
|
const result = await emit("explain this function");
|
|
expect(result).toEqual({ action: "continue" });
|
|
expect(exec).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("continues when git diff is empty", async () => {
|
|
const { emit } = setup(gitEmpty);
|
|
const result = await emit("any changes?");
|
|
expect(result).toEqual({ action: "continue" });
|
|
});
|
|
|
|
it("continues when git fails", async () => {
|
|
const { emit } = setup(gitFail);
|
|
const result = await emit("show modified files");
|
|
expect(result).toEqual({ action: "continue" });
|
|
});
|
|
});
|