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
|
// 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).
|
// graphics id that survives child re-creation (the image budget keys off it).
|
||||||
let toolExecutionInstanceSeq = 0;
|
let toolExecutionInstanceSeq = 0;
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ import type { HookInputComponent } from "./components/hook-input";
|
|||||||
import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
|
import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
|
||||||
import { type PlanReviewAnnotationState, PlanReviewOverlay } from "./components/plan-review-overlay";
|
import { type PlanReviewAnnotationState, PlanReviewOverlay } from "./components/plan-review-overlay";
|
||||||
import { StatusLineComponent } from "./components/status-line";
|
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 { TranscriptContainer } from "./components/transcript-container";
|
||||||
import { WelcomeComponent, type LspServerInfo as WelcomeLspServerInfo } from "./components/welcome";
|
import { WelcomeComponent, type LspServerInfo as WelcomeLspServerInfo } from "./components/welcome";
|
||||||
import { BtwController } from "./controllers/btw-controller";
|
import { BtwController } from "./controllers/btw-controller";
|
||||||
@@ -4160,6 +4160,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|||||||
this.#stopLoadingAnimation(false);
|
this.#stopLoadingAnimation(false);
|
||||||
}
|
}
|
||||||
this.#cleanupMicAnimation();
|
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.#liveCommandController.dispose();
|
||||||
this.#cancelTodoAutoClearTimer();
|
this.#cancelTodoAutoClearTimer();
|
||||||
this.#cancelObserverUiSyncTimer();
|
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 { Agent } from "@oh-my-pi/pi-agent-core";
|
||||||
import * as ai from "@oh-my-pi/pi-ai";
|
import * as ai from "@oh-my-pi/pi-ai";
|
||||||
import { createMockModel } from "@oh-my-pi/pi-ai/providers/mock";
|
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 session: AgentSession | undefined;
|
||||||
let authStorage: AuthStorage | 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 () => {
|
afterEach(async () => {
|
||||||
|
if (previousNoTitle === undefined) delete Bun.env.PI_NO_TITLE;
|
||||||
|
else Bun.env.PI_NO_TITLE = previousNoTitle;
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
await session?.dispose();
|
await session?.dispose();
|
||||||
authStorage?.close();
|
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 { stripVTControlCharacters } from "node:util";
|
||||||
import {
|
import {
|
||||||
SPINNER_RENDER_INTERVAL_MS,
|
SPINNER_RENDER_INTERVAL_MS,
|
||||||
|
stopSharedSpinnerTicker,
|
||||||
ToolExecutionComponent,
|
ToolExecutionComponent,
|
||||||
} from "@oh-my-pi/pi-coding-agent/modes/components/tool-execution";
|
} from "@oh-my-pi/pi-coding-agent/modes/components/tool-execution";
|
||||||
import { TranscriptContainer } from "@oh-my-pi/pi-coding-agent/modes/components/transcript-container";
|
import { TranscriptContainer } from "@oh-my-pi/pi-coding-agent/modes/components/transcript-container";
|
||||||
@@ -17,6 +18,13 @@ describe("ToolExecutionComponent live preview spinners", () => {
|
|||||||
await initTheme();
|
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(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
|
|||||||
Reference in New Issue
Block a user