Merge branch 'v2/main' into v2/fix/1860-mrtr-results-panel
This commit is contained in:
commit
7cd8d3e4c7
2 changed files with 58 additions and 2 deletions
|
|
@ -258,18 +258,29 @@ describe("runMethod", () => {
|
|||
);
|
||||
expect(stdout.trim().split("\n")).toHaveLength(2);
|
||||
|
||||
// Invoke only the SIGINT handler consumeMethodOutcome registers, rather
|
||||
// than broadcasting `process.emit("SIGINT")` process-wide: `atomically`
|
||||
// (loaded via core's store-io) pulls in `when-exit`, whose module-level
|
||||
// `process.once("SIGINT")` handler re-raises the signal with
|
||||
// `process.kill(process.pid, "SIGINT")` — on Windows an unconditional
|
||||
// TerminateProcess that kills the vitest worker fork mid-run (#1941).
|
||||
const listenersBefore = new Set(process.listeners("SIGINT"));
|
||||
const streamPromise = consumeMethodOutcome(
|
||||
{
|
||||
kind: "stream",
|
||||
label: "t",
|
||||
start: (write) => {
|
||||
write({ hi: true });
|
||||
queueMicrotask(() => process.emit("SIGINT"));
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
{},
|
||||
);
|
||||
const added = process
|
||||
.listeners("SIGINT")
|
||||
.filter((listener) => !listenersBefore.has(listener));
|
||||
expect(added).toHaveLength(1);
|
||||
for (const listener of added) listener("SIGINT");
|
||||
await streamPromise;
|
||||
expect(stdout).toContain('"hi":true');
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -507,6 +507,9 @@ function renderApp(servers: Record<string, TuiServer>) {
|
|||
async function mount(servers: Record<string, TuiServer>) {
|
||||
const r = renderApp(servers);
|
||||
await tick();
|
||||
// The mount commit's effect flush can still be queued behind this tick, and
|
||||
// this write is the one that absorbs the dropped first keypress.
|
||||
await settleInputHandlers();
|
||||
r.stdin.write("x");
|
||||
await tick();
|
||||
return r;
|
||||
|
|
@ -532,6 +535,11 @@ const ENTER = "\r";
|
|||
*/
|
||||
async function press(r: RenderResult, keys: string[]) {
|
||||
for (const k of keys) {
|
||||
// Same hazard `waitUntil` guards against, at the other end: a caller can
|
||||
// reach here on a turn that still has React's passive-effect flush queued
|
||||
// (e.g. straight after a plain `tick`, or after an earlier key committed a
|
||||
// render), so settle before every write rather than only after a poll.
|
||||
await settleInputHandlers();
|
||||
r.stdin.write(k);
|
||||
await tick();
|
||||
await tick();
|
||||
|
|
@ -552,9 +560,29 @@ async function press(r: RenderResult, keys: string[]) {
|
|||
*/
|
||||
const POLL_TRIES = 100;
|
||||
|
||||
/**
|
||||
* One check-phase turn, queued BEHIND React's already-scheduled passive-effect
|
||||
* flush. A frame observed by a poll predicate is written during React's
|
||||
* COMMIT, but ink re-arms its useInput listeners in the passive-effect flush
|
||||
* React schedules (via setImmediate in Node) during that same commit. Node's
|
||||
* event loop runs the timers phase before the check phase, so a 25ms poll
|
||||
* tick can observe the new frame and let the test write the next keypress
|
||||
* BEFORE that flush has run — the key is then dispatched to the previous
|
||||
* commit's stale useInput closures (where e.g. pendingStepUp is still null)
|
||||
* and silently swallowed (#1942). Yielding one setImmediate turn after the
|
||||
* predicate passes sequences the next stdin write after the flush (FIFO
|
||||
* within the check queue), so "frame visible" once again implies "input
|
||||
* handlers armed".
|
||||
*/
|
||||
const settleInputHandlers = () =>
|
||||
new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
async function waitUntil(predicate: () => boolean, tries = POLL_TRIES) {
|
||||
for (let i = 0; i < tries; i++) {
|
||||
if (predicate()) return;
|
||||
if (predicate()) {
|
||||
await settleInputHandlers();
|
||||
return;
|
||||
}
|
||||
await tick();
|
||||
}
|
||||
}
|
||||
|
|
@ -626,6 +654,23 @@ afterEach(() => {
|
|||
while (mounted.length) mounted.pop()?.unmount();
|
||||
});
|
||||
|
||||
// Pins the synchronization contract the OAuth step-up assertions depend on
|
||||
// (#1942). The `setImmediate` sentinel below stands in for React's pending
|
||||
// passive-effect flush — the turn where ink re-arms `useInput`. If `waitUntil`
|
||||
// ever returns without yielding a check-phase turn, the sentinel has not run
|
||||
// and this fails, instead of the regression resurfacing as a differently-named
|
||||
// flaky OAuth test under coverage instrumentation.
|
||||
describe("test helpers", () => {
|
||||
it("waitUntil settles input handlers before resolving", async () => {
|
||||
let flushed = false;
|
||||
setImmediate(() => {
|
||||
flushed = true;
|
||||
});
|
||||
await waitUntil(() => true);
|
||||
expect(flushed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("App (foundation)", () => {
|
||||
it("renders the server list with the MCP Servers header", async () => {
|
||||
const r = renderApp(stdioServer());
|
||||
|
|
|
|||
Loading…
Reference in a new issue