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:
can1357
2026-08-19 03:20:50 +02:00
parent aa98ea9ed5
commit d94bdfa1bb
4 changed files with 38 additions and 3 deletions
@@ -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();