fix(app): advance multi-question prompts step by step (#1462)

This commit is contained in:
Mohamed Boudra
2026-06-11 03:45:32 +00:00
committed by GitHub
parent 6db285b1d9
commit 2828dce65f
5 changed files with 270 additions and 37 deletions
@@ -11,6 +11,20 @@ import {
MockLoadTestAgentClient,
} from "./mock-load-test-agent.js";
type PermissionRequestedEvent = Extract<AgentStreamEvent, { type: "permission_requested" }>;
function expectSinglePermissionRequest(events: AgentStreamEvent[]): PermissionRequestedEvent {
const permissions = events.filter(
(event): event is PermissionRequestedEvent => event.type === "permission_requested",
);
expect(permissions).toHaveLength(1);
const [permission] = permissions;
if (!permission) {
throw new Error("permission request missing");
}
return permission;
}
describe("MockLoadTestAgentClient", () => {
afterEach(() => {
vi.useRealTimers();
@@ -131,6 +145,60 @@ describe("MockLoadTestAgentClient", () => {
expect(events).toHaveLength(eventCountAfterInterrupt);
});
test("emits the free-write question scenario selected by prompt", async () => {
vi.useFakeTimers();
const client = new MockLoadTestAgentClient();
const session = await client.createSession({
provider: "mock",
cwd: process.cwd(),
model: "ten-second-stream",
});
const events: AgentStreamEvent[] = [];
const unsubscribe = session.subscribe((event) => events.push(event));
const resultPromise = session.run("Emit synthetic questions: two free-write questions.");
await vi.advanceTimersByTimeAsync(0);
const permission = expectSinglePermissionRequest(events);
expect(permission.request).toMatchObject({
provider: "mock",
name: "MockQuestions",
kind: "question",
input: {
questions: [
{
question: "What is the GitHub private repo URL to push to?",
header: "repoUrl",
options: [],
multiSelect: false,
},
{
question: "What should the first commit message be?",
header: "commitMessage",
options: [],
multiSelect: false,
},
],
},
});
await session.respondToPermission(permission.request.id, {
behavior: "allow",
updatedInput: {
answers: {
repoUrl: "git@github.com:user/private-repo.git",
commitMessage: "Initialize private repo",
},
},
});
await expect(resultPromise).resolves.toMatchObject({
sessionId: session.id,
finalText: "Synthetic questions resolved",
canceled: false,
});
unsubscribe();
});
test("agent manager coalesces adjacent assistant tokens into fewer messages", async () => {
vi.useFakeTimers();
const workdir = mkdtempSync(join(tmpdir(), "paseo-mock-load-test-"));
@@ -127,12 +127,80 @@ interface AgentStreamStressRequest {
coalesced: boolean;
}
interface MockQuestionOption {
label: string;
description?: string;
}
interface MockQuestionPromptQuestion {
question: string;
header: string;
options: MockQuestionOption[];
multiSelect: boolean;
allowOther?: boolean;
allowEmpty?: boolean;
placeholder?: string;
dismissLabel?: string;
}
interface MockQuestionPromptRequest {
questions: MockQuestionPromptQuestion[];
}
function shouldEmitPlanApprovalPrompt(prompt: AgentPromptInput): boolean {
return /emit\s+(?:a\s+)?synthetic\s+plan\s+approval/i.test(promptToText(prompt));
}
function shouldEmitQuestionPrompt(prompt: AgentPromptInput): boolean {
return /emit\s+(?:a\s+)?synthetic\s+questions?/i.test(promptToText(prompt));
function parseMockQuestionPrompt(prompt: AgentPromptInput): MockQuestionPromptRequest | null {
const text = promptToText(prompt);
if (!/emit\s+(?:a\s+)?synthetic\s+questions?/i.test(text)) {
return null;
}
if (/free[-\s]?write|freeform|text[-\s]?only/i.test(text)) {
return {
questions: [
{
question: "What is the GitHub private repo URL to push to?",
header: "repoUrl",
options: [],
multiSelect: false,
placeholder: "git@github.com:user/repo.git",
},
{
question: "What should the first commit message be?",
header: "commitMessage",
options: [],
multiSelect: false,
placeholder: "Initial commit",
},
],
};
}
return {
questions: [
{
question: "Which surface should this apply to?",
header: "surface",
options: [{ label: "App" }, { label: "Desktop" }],
multiSelect: false,
},
{
question: "Which rollout should we use?",
header: "rollout",
options: [{ label: "Immediately" }, { label: "Behind feature flag" }],
multiSelect: false,
},
{
question: "What success criteria should we use?",
header: "success",
options: [],
multiSelect: false,
placeholder: "Describe success...",
},
],
};
}
function resolveModelProfile(modelId: string | null | undefined): {
@@ -536,10 +604,11 @@ export class MockLoadTestAgentSession implements AgentSession {
const largePayload = parseLargeAgentStreamPayloadPrompt(prompt);
const stress = parseAgentStreamStressPrompt(prompt);
const questionPrompt = parseMockQuestionPrompt(prompt);
if (shouldEmitPlanApprovalPrompt(prompt)) {
this.schedulePlanApprovalTurn(turn);
} else if (shouldEmitQuestionPrompt(prompt)) {
this.scheduleQuestionPromptTurn(turn);
} else if (questionPrompt) {
this.scheduleQuestionPromptTurn(turn, questionPrompt);
} else if (largePayload) {
this.scheduleLargePayloadTurn(turn, largePayload);
} else if (stress) {
@@ -712,9 +781,12 @@ export class MockLoadTestAgentSession implements AgentSession {
turn.timer.unref?.();
}
private scheduleQuestionPromptTurn(turn: ActiveTurn): void {
private scheduleQuestionPromptTurn(
turn: ActiveTurn,
questionPrompt: MockQuestionPromptRequest,
): void {
turn.timer = setTimeout(() => {
this.emitQuestionPromptTurn(turn);
this.emitQuestionPromptTurn(turn, questionPrompt);
}, 0);
turn.timer.unref?.();
}
@@ -771,7 +843,10 @@ export class MockLoadTestAgentSession implements AgentSession {
});
}
private emitQuestionPromptTurn(turn: ActiveTurn): void {
private emitQuestionPromptTurn(
turn: ActiveTurn,
questionPrompt: MockQuestionPromptRequest,
): void {
if (this.activeTurn !== turn) {
return;
}
@@ -790,27 +865,7 @@ export class MockLoadTestAgentSession implements AgentSession {
kind: "question",
title: "Questions",
input: {
questions: [
{
question: "Which surface should this apply to?",
header: "surface",
options: [{ label: "App" }, { label: "Desktop" }],
multiSelect: false,
},
{
question: "Which rollout should we use?",
header: "rollout",
options: [{ label: "Immediately" }, { label: "Behind feature flag" }],
multiSelect: false,
},
{
question: "What success criteria should we use?",
header: "success",
options: [],
multiSelect: false,
placeholder: "Describe success...",
},
],
questions: questionPrompt.questions,
},
metadata: {
source: "mock_questions",