From d152f1529ffc5f771506917e9776f6162f6eb357 Mon Sep 17 00:00:00 2001 From: xzthemeofsss Date: Sat, 17 Oct 2026 04:12:05 +0800 Subject: [PATCH] fix(app): recover failed question responses (#7098) * test: reproduce failed question response recovery * fix: show question response failures and allow retry --- .../question-response-retry/after.png | Bin 0 -> 19267 bytes .../question-response-retry/before.png | Bin 0 -> 14392 bytes .../question-prompt-pagination.spec.ts | 214 +++++++++++++++++- packages/app/src/agent-stream/view.tsx | 6 + .../question-form-card.browser.test.tsx | 1 + .../app/src/components/question-form-card.tsx | 22 +- packages/app/src/i18n/resources/ar.ts | 1 + packages/app/src/i18n/resources/en.ts | 1 + packages/app/src/i18n/resources/es.ts | 1 + packages/app/src/i18n/resources/fr.ts | 1 + packages/app/src/i18n/resources/ja.ts | 1 + packages/app/src/i18n/resources/ko.ts | 1 + packages/app/src/i18n/resources/pt-BR.ts | 1 + packages/app/src/i18n/resources/ru.ts | 1 + packages/app/src/i18n/resources/zh-CN.ts | 1 + .../test-utils/question-response-daemon.ts | 69 ++++++ 16 files changed, 314 insertions(+), 7 deletions(-) create mode 100644 docs/qa-evidence/question-response-retry/after.png create mode 100644 docs/qa-evidence/question-response-retry/before.png create mode 100644 packages/server/src/server/test-utils/question-response-daemon.ts diff --git a/docs/qa-evidence/question-response-retry/after.png b/docs/qa-evidence/question-response-retry/after.png new file mode 100644 index 0000000000000000000000000000000000000000..27dddb4bf6f1fc90cd0f927d47ace455377b591c GIT binary patch literal 19267 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 literal 0 HcmV?d00001 diff --git a/docs/qa-evidence/question-response-retry/before.png b/docs/qa-evidence/question-response-retry/before.png new file mode 100644 index 0000000000000000000000000000000000000000..651e5f54da246b9ca4830400285e46473bcb1dd0 GIT binary patch literal 14392 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

1TK4eq(@!~ROU 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 { } }); }); + +const questionDaemonMessage = z.discriminatedUnion("type", [ + z.object({ type: z.literal("ready"), port: z.number(), serverId: z.string() }), + z.object({ + type: z.literal("attempt"), + requestId: z.string(), + response: z.unknown(), + sessionId: z.string(), + }), + z.object({ + type: z.literal("resolved"), + requestId: z.string(), + response: z.unknown(), + sessionId: z.string(), + pendingRequestIds: z.array(z.string()), + }), + z.object({ type: z.literal("error"), error: z.string() }), +]); + +type QuestionResponseAttempt = Extract, { type: "attempt" }>; + +type QuestionResolution = Extract, { type: "resolved" }>; + +interface QuestionRetryControl { + attempts: QuestionResponseAttempt[]; + resolutions: QuestionResolution[]; + releaseFirstResponse(): void; +} + +const retryTest = test.extend<{ + questionClient: QuestionRetryControl; +}>({ + questionClient: async ({ page }, provide, testInfo) => { + const serverDir = path.resolve(__dirname, "../../../server"); + const child = fork(path.join(serverDir, "src/server/test-utils/question-response-daemon.ts"), { + cwd: serverDir, + execArgv: ["--import", "tsx"], + stdio: ["ignore", "pipe", "pipe", "ipc"], + }); + const output: string[] = []; + child.stdout?.on("data", (data: Buffer) => output.push(data.toString())); + child.stderr?.on("data", (data: Buffer) => output.push(data.toString())); + const ready = Promise.withResolvers<{ port: number; serverId: string }>(); + const attempts: QuestionResponseAttempt[] = []; + const resolutions: QuestionResolution[] = []; + child.on("message", (message: unknown) => { + const parsed = questionDaemonMessage.parse(message); + switch (parsed.type) { + case "ready": + ready.resolve(parsed); + break; + case "attempt": + attempts.push(parsed); + break; + case "resolved": + resolutions.push(parsed); + break; + case "error": + ready.reject(new Error(parsed.error)); + break; + } + }); + child.once("exit", (code) => ready.reject(new Error(`Question daemon exited with ${code}`))); + const timeout = setTimeout( + () => ready.reject(new Error("Question daemon startup timed out")), + 20_000, + ); + try { + const daemon = await ready.promise; + clearTimeout(timeout); + const session = await seedMockAgentWorkspace({ + port: daemon.port, + repoPrefix: "question-retry-", + title: "Question response retry", + initialPrompt: "Emit synthetic questions.", + }); + try { + await page.goto("/", { waitUntil: "commit" }); + const serverId = daemon.serverId; + const host = buildSeededHost({ + serverId, + endpoint: `127.0.0.1:${daemon.port}`, + label: "Question retry host", + nowIso: new Date().toISOString(), + }); + await page.evaluate((extraHost) => { + localStorage.setItem("@paseo:e2e-extra-hosts", JSON.stringify([extraHost])); + }, host); + await page.goto(buildAgentRoute(session.workspaceId, session.agentId, serverId), { + waitUntil: "commit", + }); + await waitForQuestionPrompt(page, 120_000); + await provide({ attempts, resolutions, releaseFirstResponse: () => child.send("release") }); + } finally { + await session.cleanup(); + } + } finally { + clearTimeout(timeout); + await killProcessTree(child); + if (testInfo.status !== testInfo.expectedStatus) { + await testInfo.attach("question-daemon", { + body: output.join(""), + contentType: "text/plain", + }); + } + } + }, +}); + +retryTest( + "failed question submission preserves answers and allows retry", + async ({ page, questionClient }, testInfo) => { + retryTest.setTimeout(180_000); + const card = page.getByTestId("question-form-card"); + await chooseQuestionOption(page, "App"); + await chooseQuestionOption(page, "Behind feature flag"); + await fillQuestionAnswer(page, { + question: SUCCESS_QUESTION, + answer: "Preserve my answers after a failed submission.", + }); + await card.getByRole("button", { name: "Submit" }).click(); + await expect(card.getByRole("progressbar")).toBeVisible(); + await expectQuestionPrimaryActionDisabled(page, "Submit"); + await expect(card.getByRole("button", { name: "Dismiss" })).toBeDisabled(); + await expect(card.getByRole("tab").first()).toBeDisabled(); + await expect(card.getByRole("textbox")).not.toBeEditable(); + questionClient.releaseFirstResponse(); + await expectQuestionPrimaryActionEnabled(page, "Submit"); + await card.screenshot({ path: testInfo.outputPath("submission-failed.png") }); + await expect(card.getByRole("alert")).toHaveText("Could not send your response. Try again."); + await expect(card.getByRole("progressbar")).toHaveCount(0); + await expect(card.getByRole("button", { name: "Submit" })).toHaveText("Submit"); + await expectQuestionDismissEnabled(page); + await openQuestion(page, { index: 1, total: TOTAL_QUESTIONS }); + await expectQuestionOptionSelected(page, "App"); + await openQuestion(page, { index: 2, total: TOTAL_QUESTIONS }); + await expectQuestionOptionSelected(page, "Behind feature flag"); + await openQuestion(page, { index: 3, total: TOTAL_QUESTIONS }); + await expect(card.getByRole("textbox")).toHaveValue( + "Preserve my answers after a failed submission.", + ); + await submitQuestionAnswers(page); + await expectVisibleAgentSurfacesIdle(page); + await expect.poll(() => questionClient.attempts.length).toBe(2); + expect(questionClient.attempts[1]).toEqual(questionClient.attempts[0]); + await expect + .poll(() => questionClient.resolutions) + .toEqual([{ ...questionClient.attempts[1], type: "resolved", pendingRequestIds: [] }]); + expect(questionClient.attempts[1].response).toMatchObject({ + behavior: "allow", + updatedInput: { + answers: { + surface: "App", + rollout: "Behind feature flag", + success: "Preserve my answers after a failed submission.", + }, + }, + }); + await page.screenshot({ path: testInfo.outputPath("submission-retried.png") }); + }, +); + +retryTest( + "failed question dismissal allows an immediate retry", + async ({ page, questionClient }, testInfo) => { + retryTest.setTimeout(180_000); + const card = page.getByTestId("question-form-card"); + questionClient.releaseFirstResponse(); + await chooseQuestionOption(page, "App"); + await chooseQuestionOption(page, "Behind feature flag"); + await fillQuestionAnswer(page, { + question: SUCCESS_QUESTION, + answer: "Start with the internal team.", + }); + await card.getByRole("button", { name: "Dismiss" }).click(); + await expect(card.getByRole("alert")).toHaveText("Could not send your response. Try again."); + await expect(card.getByRole("progressbar")).toHaveCount(0); + await expect(card.getByRole("button", { name: "Dismiss" })).toHaveText("Dismiss"); + await expectQuestionDismissEnabled(page); + await expectQuestionPrimaryActionEnabled(page, "Submit"); + await card.screenshot({ path: testInfo.outputPath("dismissal-failed.png") }); + await openQuestion(page, { index: 1, total: TOTAL_QUESTIONS }); + await expectQuestionOptionSelected(page, "App"); + await openQuestion(page, { index: 3, total: TOTAL_QUESTIONS }); + await expect(card.getByRole("textbox")).toHaveValue("Start with the internal team."); + await expect(card.getByRole("alert")).toBeVisible(); + await card.getByRole("button", { name: "Dismiss" }).click(); + await expect(card).toHaveCount(0); + await expectVisibleAgentSurfacesIdle(page); + await expect.poll(() => questionClient.attempts.length).toBe(2); + expect(questionClient.attempts[1]).toEqual(questionClient.attempts[0]); + await expect + .poll(() => questionClient.resolutions) + .toEqual([{ ...questionClient.attempts[1], type: "resolved", pendingRequestIds: [] }]); + expect(questionClient.attempts[1].response).toEqual({ + behavior: "deny", + message: "Dismissed by user", + }); + await page.screenshot({ path: testInfo.outputPath("dismissal-retried.png") }); + }, +); diff --git a/packages/app/src/agent-stream/view.tsx b/packages/app/src/agent-stream/view.tsx index cd7aa42b70..09b02cda18 100644 --- a/packages/app/src/agent-stream/view.tsx +++ b/packages/app/src/agent-stream/view.tsx @@ -1531,11 +1531,17 @@ function PermissionRequestCard({ ); if (request.kind === "question") { + const responseFailed = + permissionMutation.isError && + permissionMutation.variables?.agentId === permission.agentId && + permissionMutation.variables?.requestId === permission.request.id; + const responseError = responseFailed ? t("message.question.responseFailed") : null; return ( ); } diff --git a/packages/app/src/components/question-form-card.browser.test.tsx b/packages/app/src/components/question-form-card.browser.test.tsx index 630b001aa6..83120931d1 100644 --- a/packages/app/src/components/question-form-card.browser.test.tsx +++ b/packages/app/src/components/question-form-card.browser.test.tsx @@ -58,6 +58,7 @@ function mountCard(question: Record) { permission={buildPermission(question)} onRespond={onRespond} isResponding={false} + responseError={null} />, ), ); diff --git a/packages/app/src/components/question-form-card.tsx b/packages/app/src/components/question-form-card.tsx index 655e01d210..7014de89d1 100644 --- a/packages/app/src/components/question-form-card.tsx +++ b/packages/app/src/components/question-form-card.tsx @@ -1,3 +1,4 @@ +import { Alert } from "@/components/ui/alert"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { useState, useCallback, useMemo, useRef, type RefObject } from "react"; import { View, Text, Pressable, type PressableStateCallbackType } from "react-native"; @@ -27,6 +28,7 @@ interface QuestionFormCardProps { permission: PendingPermission; onRespond: (response: AgentPermissionResponse) => void; isResponding: boolean; + responseError: string | null; } const IS_WEB = isWeb; @@ -339,7 +341,12 @@ function QuestionOtherInput({ ); } -export function QuestionFormCard({ permission, onRespond, isResponding }: QuestionFormCardProps) { +export function QuestionFormCard({ + permission, + onRespond, + isResponding, + responseError, +}: QuestionFormCardProps) { const { theme } = useUnistyles(); const { t } = useTranslation(); const isMobile = useIsCompactFormFactor(); @@ -438,7 +445,7 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi ]); const handleDeny = useCallback(() => { - if (!questions) return; + if (!questions || isResponding) return; setRespondingAction("dismiss"); if (shouldSubmitEmptyOnDismiss(questions)) { onRespond({ @@ -454,7 +461,7 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi behavior: "deny", message: "Dismissed by user", }); - }, [questions, onRespond, otherTexts, permission.request.input, selections]); + }, [questions, isResponding, onRespond, otherTexts, permission.request.input, selections]); const handleSelectQuestion = useCallback((index: number) => { setActiveQuestionIndex(index); @@ -487,6 +494,7 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi [theme.colors.surface2, theme.colors.surface1, theme.colors.borderAccent], ); + const pendingAction = isResponding ? respondingAction : null; const primaryDisabled = isResponding || (isLastQuestion ? !allAnswered : !activeQuestionAnswered); const primaryActionLabel = isLastQuestion ? t("message.question.submit") @@ -550,7 +558,7 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi const dismissLabel = resolveDismissLabel(questions, t("common.actions.dismiss")); const selected = selections[resolvedActiveQuestionIndex] ?? new Set(); const otherText = otherTexts[resolvedActiveQuestionIndex] ?? ""; - const showTextInput = activeQuestion ? questionShowsTextInput(activeQuestion) : false; + const showTextInput = questionShowsTextInput(questions[resolvedActiveQuestionIndex]); return ( @@ -604,6 +612,8 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi ) : null} + {responseError ? : null} + - {respondingAction === "dismiss" ? ( + {pendingAction === "dismiss" ? ( ) : ( @@ -631,7 +641,7 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi accessibilityLabel={primaryActionLabel} testID="question-form-primary-action" > - {respondingAction === "submit" ? ( + {pendingAction === "submit" ? ( ) : ( diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index 5e09ac5957..c5b9cf42d6 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -369,6 +369,7 @@ export const ar: TranslationResources = { failedRetry: "فشل الإملاء. اضغط على إعادة المحاولة.", }, question: { + responseFailed: "تعذر إرسال ردك. حاول مرة أخرى.", submit: "يُقدِّم", next: "التالي", answerPlaceholder: "اكتب إجابتك...", diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 8c6a870580..76135fb6c9 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -366,6 +366,7 @@ export const en = { failedRetry: "Dictation failed. Tap retry.", }, question: { + responseFailed: "Could not send your response. Try again.", submit: "Submit", next: "Next", answerPlaceholder: "Type your answer...", diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index 62667ba9f9..d2572a4bab 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -374,6 +374,7 @@ export const es: TranslationResources = { failedRetry: "El dictado falló. Toca reintentar.", }, question: { + responseFailed: "No se pudo enviar tu respuesta. Inténtalo de nuevo.", submit: "Entregar", next: "Siguiente", answerPlaceholder: "Escribe tu respuesta...", diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index 8e70ece6da..48fd08c9ed 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -374,6 +374,7 @@ export const fr: TranslationResources = { failedRetry: "La dictée a échoué. Appuyez sur Réessayer.", }, question: { + responseFailed: "Impossible d’envoyer votre réponse. Réessayez.", submit: "Envoyer", next: "Suivant", answerPlaceholder: "Saisissez votre réponse…", diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index d7b08a9c84..b3482f5c15 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -374,6 +374,7 @@ export const ja: TranslationResources = { failedRetry: "音声入力に失敗しました。再試行をタップしてください。", }, question: { + responseFailed: "回答を送信できませんでした。もう一度お試しください。", submit: "送信", next: "次へ", answerPlaceholder: "回答を入力...", diff --git a/packages/app/src/i18n/resources/ko.ts b/packages/app/src/i18n/resources/ko.ts index c82240f0c4..e39d625253 100644 --- a/packages/app/src/i18n/resources/ko.ts +++ b/packages/app/src/i18n/resources/ko.ts @@ -370,6 +370,7 @@ export const ko: TranslationResources = { failedRetry: "받아쓰기에 실패했습니다. 다시 시도하려면 누르세요.", }, question: { + responseFailed: "응답을 보내지 못했습니다. 다시 시도해 주세요.", submit: "제출", next: "다음", answerPlaceholder: "답변을 입력하세요...", diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index 622c13e798..8c3d93499a 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -373,6 +373,7 @@ export const ptBR: TranslationResources = { failedRetry: "Falha no ditado. Toque para tentar novamente.", }, question: { + responseFailed: "Não foi possível enviar sua resposta. Tente novamente.", submit: "Enviar", next: "Próximo", answerPlaceholder: "Digite sua resposta...", diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 733596de39..df57855cb6 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -373,6 +373,7 @@ export const ru: TranslationResources = { failedRetry: "Диктовка не удалась. Нажмите «Повторить».", }, question: { + responseFailed: "Не удалось отправить ответ. Попробуйте ещё раз.", submit: "Отправить", next: "Далее", answerPlaceholder: "Введите ответ...", diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index c8627ca769..984cc0d71a 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -369,6 +369,7 @@ export const zhCN: TranslationResources = { failedRetry: "听写失败。点按重试。", }, question: { + responseFailed: "未能发送你的回复,请重试。", submit: "提交", next: "下一步", answerPlaceholder: "输入你的回答...", diff --git a/packages/server/src/server/test-utils/question-response-daemon.ts b/packages/server/src/server/test-utils/question-response-daemon.ts new file mode 100644 index 0000000000..2899dc8ba3 --- /dev/null +++ b/packages/server/src/server/test-utils/question-response-daemon.ts @@ -0,0 +1,69 @@ +import { randomUUID } from "node:crypto"; +import { + MockLoadTestAgentClient, + MockLoadTestAgentSession, +} from "../agent/providers/mock-load-test-agent.js"; +import { createTestPaseoDaemon } from "./paseo-daemon.js"; + +const firstResponse = Promise.withResolvers(); + +class RetryQuestionSession extends MockLoadTestAgentSession { + private attempts = 0; + + override async respondToPermission( + requestId: string, + response: Parameters[1], + ) { + this.attempts += 1; + process.send?.({ type: "attempt", requestId, response, sessionId: this.id }); + if (this.attempts === 1) { + await firstResponse.promise; + throw new Error("Synthetic question response failure"); + } + return super.respondToPermission(requestId, response); + } +} + +class RetryQuestionClient extends MockLoadTestAgentClient { + override async createSession(config: Parameters[0]) { + const session = new RetryQuestionSession({ config, sessionId: randomUUID() }); + session.subscribe((event) => { + if (event.type === "permission_resolved") { + process.send?.({ + type: "resolved", + requestId: event.requestId, + response: event.resolution, + sessionId: session.id, + pendingRequestIds: session.getPendingPermissions().map((request) => request.id), + }); + } + }); + return session; + } +} + +async function main(): Promise { + const daemon = await createTestPaseoDaemon({ + agentClients: { mock: new RetryQuestionClient() }, + isDev: true, + mcpEnabled: false, + corsAllowedOrigins: [`http://localhost:${process.env.E2E_METRO_PORT}`], + }); + process.on("message", (message: unknown) => { + if (message === "release") firstResponse.resolve(); + }); + const shutdown = async () => { + firstResponse.resolve(); + await daemon.close(); + process.exit(0); + }; + process.once("SIGINT", () => void shutdown()); + process.once("SIGTERM", () => void shutdown()); + process.once("disconnect", () => void shutdown()); + process.send?.({ type: "ready", port: daemon.port, serverId: daemon.daemon.getServerId() }); +} + +void main().catch((error: unknown) => { + process.send?.({ type: "error", error: error instanceof Error ? error.message : String(error) }); + process.exit(1); +});