qwen-code/packages/web-shell/client/test/setup.ts
Tianyuan 075c3f03e5
feat(cli): forward ask_user_question answers from SDK can_use_tool (#6655)
* feat(cli): forward ask_user_question answers from SDK can_use_tool

SDK-hosted agents could receive ask_user_question calls through the
can_use_tool callback and approve them, but the user's answers never
reached the tool: the CLI called onConfirm(ProceedOnce) with no payload,
so the tool read an empty answers map and the model never got the
decisions.

Route updatedInput.answers from the SDK's allow response into the tool
confirmation payload so the collected answers reach the tool. Reuses the
existing updatedInput channel — no new SDK API or types. Document the
pattern in the TypeScript and Python SDK READMEs.

* fix(cli): forward ask_user_question answers on teammate approval path

Address review feedback on #6655:

- handleTeammateApproval now mirrors the leader path and promotes the
  user's answers from updatedInput into the confirmation payload, so
  ask_user_question calls approved through a teammate no longer drop the
  user's choices (wenshao).
- Extract a shared buildAllowConfirmationPayload helper used by both the
  leader and teammate paths, and only promote `answers` for
  ask_user_question so a same-named field on any other tool's input can't
  leak into the payload.
- Add tests for the teammate path and the defensive guards (array
  updatedInput, array/null/empty answers, foreign answers field).

* test(web-shell): stub Range client-rect methods to fix flaky CI

CodeMirror's async measure pass (scheduled via requestAnimationFrame)
calls getClientRects()/getBoundingClientRect() on a text Range. jsdom
implements these on Element but not on Range, so the call throws
"textRange(...).getClientRects is not a function" from a rAF callback
after the test completed. Vitest surfaces it as an unhandled error and
fails the whole run with exit code 1 even though every assertion passed
(seen intermittently in useComposerCore.dom.test.tsx).

Polyfill both methods on Range.prototype in the shared test setup,
mirroring the existing ResizeObserver/scrollIntoView stubs.

* refactor(cli): use ToolNames constant and broaden permission tests

Address review suggestions on #6655:

- buildAllowConfirmationPayload now gates answers-promotion on the
  ToolNames.ASK_USER_QUESTION constant instead of a bare string literal,
  so a future rename of the tool name is a compile-time break rather than
  a silent regression.
- Add an it.each case for a non-object primitive updatedInput (string) to
  cover the `typeof updatedInput !== 'object'` guard branch.
- Assert the leader path overrides toolCall.request.args with the host's
  sanitized updatedInput before confirming.
- Add a teammate-path test for an allow response with no updatedInput,
  asserting respond is called with (ProceedOnce, undefined).

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-10 23:53:46 +00:00

104 lines
2.7 KiB
TypeScript

import { vi } from 'vitest';
(
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
const globalWithDom = globalThis as typeof globalThis & {
Element?: typeof Element;
Range?: typeof Range;
ResizeObserver?: typeof ResizeObserver;
};
function createEmptyDOMRect(): DOMRect {
if (typeof DOMRect === 'function') {
return new DOMRect(0, 0, 0, 0);
}
return {
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0,
x: 0,
y: 0,
toJSON: () => ({}),
} as DOMRect;
}
function createEmptyDOMRectList(): DOMRectList {
return {
length: 0,
item: () => null,
} as DOMRectList;
}
if (typeof globalWithDom.ResizeObserver === 'undefined') {
globalWithDom.ResizeObserver = class ResizeObserverStub {
observe() {}
unobserve() {}
disconnect() {}
} as typeof ResizeObserver;
}
if (
typeof globalWithDom.Element !== 'undefined' &&
!globalWithDom.Element.prototype.scrollIntoView
) {
globalWithDom.Element.prototype.scrollIntoView = () => {};
}
// jsdom implements getClientRects()/getBoundingClientRect() on Element but not
// on Range. CodeMirror's async measure pass (scheduled via requestAnimationFrame)
// calls them on a text Range, so without this stub it throws
// "textRange(...).getClientRects is not a function" from a rAF callback after a
// test has completed — an unhandled error that flakes the whole run even though
// every assertion passed.
if (typeof globalWithDom.Range !== 'undefined') {
const rangePrototype = globalWithDom.Range.prototype as Range & {
getBoundingClientRect?: () => DOMRect;
getClientRects?: () => DOMRectList;
};
rangePrototype.getBoundingClientRect ??= createEmptyDOMRect;
rangePrototype.getClientRects ??= createEmptyDOMRectList;
}
if (typeof navigator !== 'undefined' && !navigator.clipboard) {
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: {
writeText: vi.fn(() => Promise.resolve()),
},
});
}
if (typeof window !== 'undefined' && !window.matchMedia) {
Object.defineProperty(window, 'matchMedia', {
configurable: true,
writable: true,
value: vi.fn((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
}
if (typeof navigator !== 'undefined' && !navigator.mediaDevices) {
Object.defineProperty(navigator, 'mediaDevices', {
configurable: true,
value: {
getUserMedia: vi.fn(() =>
Promise.reject(new Error('getUserMedia is not mocked for this test')),
),
},
});
}