mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix(ui): retain rejected setup turns
This commit is contained in:
@@ -19,7 +19,10 @@ describe("custodian structured wizard", () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("keeps a rejected typed answer active without showing a submitted receipt", async () => {
|
||||
it.each([
|
||||
{ accepted: false, result: "rejected" },
|
||||
{ accepted: undefined, result: "ambiguous" },
|
||||
])("keeps a $result typed answer active as a plain user turn", async ({ accepted }) => {
|
||||
const step = {
|
||||
id: "port",
|
||||
type: "text" as const,
|
||||
@@ -38,7 +41,7 @@ describe("custodian structured wizard", () => {
|
||||
sessionId: "validation-session",
|
||||
reply: "Enter port 18789.",
|
||||
action: "none",
|
||||
wizardActionAccepted: false,
|
||||
...(accepted === undefined ? {} : { wizardActionAccepted: accepted }),
|
||||
wizardInputPending: true,
|
||||
step,
|
||||
});
|
||||
@@ -63,7 +66,64 @@ describe("custodian structured wizard", () => {
|
||||
});
|
||||
expect(page.querySelector(".custodian__structured-response")).toBeNull();
|
||||
expect(page.querySelector(".custodian__wizard-step")).not.toBeNull();
|
||||
expect(page.querySelector(".chat-group.user")).toBeNull();
|
||||
expect(page.querySelector(".chat-group.user")?.textContent).toContain("banana");
|
||||
const groups = [...page.querySelectorAll(".chat-group")];
|
||||
const rejectedTurnIndex = groups.findIndex(
|
||||
(group) => group.classList.contains("user") && group.textContent?.includes("banana"),
|
||||
);
|
||||
const guidanceIndex = groups.findIndex(
|
||||
(group) =>
|
||||
group.classList.contains("assistant") && group.textContent?.includes("Enter port 18789."),
|
||||
);
|
||||
expect(rejectedTurnIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(guidanceIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(rejectedTurnIndex).toBeLessThan(guidanceIndex);
|
||||
});
|
||||
|
||||
it("keeps an ambiguously delivered sensitive answer masked as a plain user turn", async () => {
|
||||
const step = {
|
||||
id: "port",
|
||||
type: "text" as const,
|
||||
message: "Gateway port",
|
||||
sensitive: true,
|
||||
};
|
||||
const request = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
sessionId: "uncertain-session",
|
||||
reply: "Enter a port.",
|
||||
action: "none",
|
||||
sensitive: true,
|
||||
wizardInputPending: true,
|
||||
step,
|
||||
})
|
||||
.mockImplementationOnce(
|
||||
async (_method: string, _params: unknown, options: { onSent?: () => void }) => {
|
||||
options.onSent?.();
|
||||
throw new Error("connection closed after send");
|
||||
},
|
||||
);
|
||||
const { context } = createContext(request);
|
||||
const { page } = await mountPage(context);
|
||||
|
||||
const input = await waitForFast(() => {
|
||||
const element = page.querySelector<HTMLInputElement>(
|
||||
'.custodian__wizard-step input[name="wizard-text"][type="password"]',
|
||||
);
|
||||
expect(element).not.toBeNull();
|
||||
return element!;
|
||||
});
|
||||
input.value = "banana";
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
await page.updateComplete;
|
||||
page.querySelector<HTMLButtonElement>(".custodian__wizard-step .btn.primary")!.click();
|
||||
|
||||
await waitForFast(() =>
|
||||
expect(page.querySelector(".chat-group.user")?.textContent).toContain("Sensitive reply sent"),
|
||||
);
|
||||
expect(page.querySelector(".custodian__structured-response")).toBeNull();
|
||||
expect(page.querySelector(".custodian__wizard-step")).not.toBeNull();
|
||||
expect(page.innerHTML).not.toContain("banana");
|
||||
});
|
||||
|
||||
it("does not confirm an answer after its Gateway client is replaced", async () => {
|
||||
@@ -131,6 +191,7 @@ describe("custodian structured wizard", () => {
|
||||
await waitForFast(() => expect(page.textContent).toContain("Gateway connection changed"));
|
||||
expect(page.textContent).not.toContain("Answer submitted");
|
||||
expect(page.querySelector(".custodian__structured-response")).toBeNull();
|
||||
expect(page.querySelector(".chat-group.user")?.textContent).toContain("18789");
|
||||
});
|
||||
|
||||
it("keeps older-Gateway wizard answers as plain user turns", async () => {
|
||||
|
||||
@@ -244,7 +244,7 @@ export class CustodianSessionStore extends CustodianTranscriptState {
|
||||
params: SystemAgentChatParams,
|
||||
displayText: string,
|
||||
questionReply: boolean,
|
||||
appendUserMessage = true,
|
||||
userTurnProjection: "always" | "unless-accepted" = "always",
|
||||
): Promise<eventNudgeState.CustodianSendOutcome> {
|
||||
const questionState = [this.answeredQuestions, this.questionReplyUncertain] as const;
|
||||
if (questionReply) {
|
||||
@@ -252,25 +252,32 @@ export class CustodianSessionStore extends CustodianTranscriptState {
|
||||
}
|
||||
this.abandonedTurnOutcomeUnknown = false;
|
||||
this.answeredQuestions = retireCustodianQuestions(this.messages, this.answeredQuestions);
|
||||
if (appendUserMessage) {
|
||||
this.messages = [
|
||||
...this.messages,
|
||||
{
|
||||
id: this.nextMessageId++,
|
||||
role: "user",
|
||||
text: displayText,
|
||||
at: Date.now(),
|
||||
question: null,
|
||||
step: null,
|
||||
structuredResponse: null,
|
||||
},
|
||||
];
|
||||
const precedingMessage = this.messages.at(-1) ?? null;
|
||||
const userMessage: CustodianMessage = {
|
||||
id: this.nextMessageId++,
|
||||
role: "user",
|
||||
text: displayText,
|
||||
at: Date.now(),
|
||||
question: null,
|
||||
step: null,
|
||||
structuredResponse: null,
|
||||
};
|
||||
if (userTurnProjection === "always") {
|
||||
this.messages = [...this.messages, userMessage];
|
||||
}
|
||||
this.input = "";
|
||||
this.emit();
|
||||
const reply = this.requestReply(client, params);
|
||||
const replyEpoch = this.requestEpoch;
|
||||
const outcome = await reply;
|
||||
if (userTurnProjection === "unless-accepted" && outcome !== "accepted") {
|
||||
// Owner-confirmed acceptance replaces the attempted turn with a receipt. Every other
|
||||
// outcome keeps the masked display in its original transcript position.
|
||||
const precedingIndex = precedingMessage ? this.messages.indexOf(precedingMessage) : -1;
|
||||
if (precedingIndex >= 0) {
|
||||
this.messages = this.messages.toSpliced(precedingIndex + 1, 0, userMessage);
|
||||
}
|
||||
}
|
||||
if (questionReply && this.requestEpoch === replyEpoch) {
|
||||
this.questionReplyUncertain = eventNudgeState.questionUncertainty(questionState[1], outcome);
|
||||
if (outcome === "rejected") {
|
||||
|
||||
@@ -25,7 +25,7 @@ type StructuredInteractionHost = {
|
||||
client: GatewayBrowserClient,
|
||||
params: SystemAgentChatParams,
|
||||
display: string,
|
||||
appendUserMessage: boolean,
|
||||
userTurnProjection: "always" | "unless-accepted",
|
||||
) => Promise<eventNudgeState.CustodianSendOutcome>;
|
||||
};
|
||||
|
||||
@@ -76,7 +76,7 @@ export function createCustodianStructuredInteraction(host: StructuredInteraction
|
||||
params.client,
|
||||
params.request,
|
||||
params.display,
|
||||
!state.wizardActionReceiptsAvailable,
|
||||
state.wizardActionReceiptsAvailable ? "unless-accepted" : "always",
|
||||
);
|
||||
if (!state.wizardActionReceiptsAvailable) {
|
||||
return outcome;
|
||||
|
||||
Reference in New Issue
Block a user