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);
|
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(
|
const streamPromise = consumeMethodOutcome(
|
||||||
{
|
{
|
||||||
kind: "stream",
|
kind: "stream",
|
||||||
label: "t",
|
label: "t",
|
||||||
start: (write) => {
|
start: (write) => {
|
||||||
write({ hi: true });
|
write({ hi: true });
|
||||||
queueMicrotask(() => process.emit("SIGINT"));
|
|
||||||
return () => {};
|
return () => {};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
const added = process
|
||||||
|
.listeners("SIGINT")
|
||||||
|
.filter((listener) => !listenersBefore.has(listener));
|
||||||
|
expect(added).toHaveLength(1);
|
||||||
|
for (const listener of added) listener("SIGINT");
|
||||||
await streamPromise;
|
await streamPromise;
|
||||||
expect(stdout).toContain('"hi":true');
|
expect(stdout).toContain('"hi":true');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -507,6 +507,9 @@ function renderApp(servers: Record<string, TuiServer>) {
|
||||||
async function mount(servers: Record<string, TuiServer>) {
|
async function mount(servers: Record<string, TuiServer>) {
|
||||||
const r = renderApp(servers);
|
const r = renderApp(servers);
|
||||||
await tick();
|
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");
|
r.stdin.write("x");
|
||||||
await tick();
|
await tick();
|
||||||
return r;
|
return r;
|
||||||
|
|
@ -532,6 +535,11 @@ const ENTER = "\r";
|
||||||
*/
|
*/
|
||||||
async function press(r: RenderResult, keys: string[]) {
|
async function press(r: RenderResult, keys: string[]) {
|
||||||
for (const k of keys) {
|
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);
|
r.stdin.write(k);
|
||||||
await tick();
|
await tick();
|
||||||
await tick();
|
await tick();
|
||||||
|
|
@ -552,9 +560,29 @@ async function press(r: RenderResult, keys: string[]) {
|
||||||
*/
|
*/
|
||||||
const POLL_TRIES = 100;
|
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) {
|
async function waitUntil(predicate: () => boolean, tries = POLL_TRIES) {
|
||||||
for (let i = 0; i < tries; i++) {
|
for (let i = 0; i < tries; i++) {
|
||||||
if (predicate()) return;
|
if (predicate()) {
|
||||||
|
await settleInputHandlers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
await tick();
|
await tick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -626,6 +654,23 @@ afterEach(() => {
|
||||||
while (mounted.length) mounted.pop()?.unmount();
|
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)", () => {
|
describe("App (foundation)", () => {
|
||||||
it("renders the server list with the MCP Servers header", async () => {
|
it("renders the server list with the MCP Servers header", async () => {
|
||||||
const r = renderApp(stdioServer());
|
const r = renderApp(stdioServer());
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue