test: hardened new spinner and title-latch suites against full-suite pollution
- Exported stopSharedSpinnerTicker() and wired it into InteractiveMode.stop(): a live block missed by per-component stopAnimation kept the shared 80ms interval alive as a lingering event-loop handle; the spinner suite uses it to observe a freshly armed ticker instead of one leaked by earlier files - Title-disposal tests now save/clear/restore PI_NO_TITLE (main() in ACP/RPC mode sets it process-wide), matching the prewarm and orphan-submit precedent
This commit is contained in:
@@ -317,6 +317,19 @@ function unregisterSpinnerBlock(block: ToolExecutionComponent): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop the shared spinner ticker and drop every registered live block.
|
||||
* Called on interactive-mode teardown so a stray live block cannot keep the
|
||||
* process-wide 80ms interval alive past shutdown (lingering event-loop
|
||||
* handles pin the process; cf. `postmortem.quit`). Test files that assert on
|
||||
* ticker arming also use this to start from a clean slate. */
|
||||
export function stopSharedSpinnerTicker(): void {
|
||||
liveSpinnerBlocks.clear();
|
||||
if (sharedSpinnerTimer) {
|
||||
clearInterval(sharedSpinnerTimer);
|
||||
sharedSpinnerTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Stable per-instance counter so each tool execution's inline images get a
|
||||
// graphics id that survives child re-creation (the image budget keys off it).
|
||||
let toolExecutionInstanceSeq = 0;
|
||||
|
||||
@@ -166,7 +166,7 @@ import type { HookInputComponent } from "./components/hook-input";
|
||||
import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
|
||||
import { type PlanReviewAnnotationState, PlanReviewOverlay } from "./components/plan-review-overlay";
|
||||
import { StatusLineComponent } from "./components/status-line";
|
||||
import type { ToolExecutionHandle } from "./components/tool-execution";
|
||||
import { stopSharedSpinnerTicker, type ToolExecutionHandle } from "./components/tool-execution";
|
||||
import { TranscriptContainer } from "./components/transcript-container";
|
||||
import { WelcomeComponent, type LspServerInfo as WelcomeLspServerInfo } from "./components/welcome";
|
||||
import { BtwController } from "./controllers/btw-controller";
|
||||
@@ -4160,6 +4160,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
this.#stopLoadingAnimation(false);
|
||||
}
|
||||
this.#cleanupMicAnimation();
|
||||
// Stop the shared tool-spinner ticker: a live block missed by per-component
|
||||
// stopAnimation would otherwise keep an 80ms interval pinning the process.
|
||||
stopSharedSpinnerTicker();
|
||||
this.#liveCommandController.dispose();
|
||||
this.#cancelTodoAutoClearTimer();
|
||||
this.#cancelObserverUiSyncTimer();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, describe, expect, it, vi } from "bun:test";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
||||
import { Agent } from "@oh-my-pi/pi-agent-core";
|
||||
import * as ai from "@oh-my-pi/pi-ai";
|
||||
import { createMockModel } from "@oh-my-pi/pi-ai/providers/mock";
|
||||
@@ -12,8 +12,19 @@ import { createAssistantMessage } from "./helpers/agent-session-setup";
|
||||
|
||||
let session: AgentSession | undefined;
|
||||
let authStorage: AuthStorage | undefined;
|
||||
// Earlier full-suite files that boot main() in ACP/RPC mode set PI_NO_TITLE=1
|
||||
// process-wide (main.ts) and never restore it; maybeStartTitleGeneration gates
|
||||
// on it, so these tests would silently skip titling and time out.
|
||||
let previousNoTitle: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
previousNoTitle = Bun.env.PI_NO_TITLE;
|
||||
delete Bun.env.PI_NO_TITLE;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (previousNoTitle === undefined) delete Bun.env.PI_NO_TITLE;
|
||||
else Bun.env.PI_NO_TITLE = previousNoTitle;
|
||||
vi.restoreAllMocks();
|
||||
await session?.dispose();
|
||||
authStorage?.close();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "bun:test";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "bun:test";
|
||||
import { stripVTControlCharacters } from "node:util";
|
||||
import {
|
||||
SPINNER_RENDER_INTERVAL_MS,
|
||||
stopSharedSpinnerTicker,
|
||||
ToolExecutionComponent,
|
||||
} from "@oh-my-pi/pi-coding-agent/modes/components/tool-execution";
|
||||
import { TranscriptContainer } from "@oh-my-pi/pi-coding-agent/modes/components/transcript-container";
|
||||
@@ -17,6 +18,13 @@ describe("ToolExecutionComponent live preview spinners", () => {
|
||||
await initTheme();
|
||||
});
|
||||
|
||||
// Earlier test files may leak live blocks (components never stopAnimation'd),
|
||||
// which keeps the shared ticker armed on a REAL interval and makes these
|
||||
// fake-timer assertions observe a pre-existing timer instead of a fresh one.
|
||||
beforeEach(() => {
|
||||
stopSharedSpinnerTicker();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
|
||||
Reference in New Issue
Block a user