From 7a736bff909578e89449ed41630dc75c6b3dbcaa Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 6 Apr 2026 19:30:47 +0100 Subject: [PATCH] perf(test): split reply queue seams and unit shards --- docs/help/testing.md | 2 +- docs/reference/test.md | 2 +- .../reply/get-reply-run-queue.test.ts | 86 +++++ src/auto-reply/reply/get-reply-run-queue.ts | 48 +++ .../reply/get-reply-run.media-only.test.ts | 298 +----------------- src/auto-reply/reply/get-reply-run.ts | 53 ++-- test/scripts/test-projects.test.ts | 12 + vitest.full-agentic.config.ts | 5 +- vitest.full-auto-reply.config.ts | 5 +- vitest.full-core-contracts.config.ts | 7 + vitest.full-core-runtime.config.ts | 5 +- vitest.full-core-unit-security.config.ts | 7 + vitest.full-core-unit-src.config.ts | 5 +- vitest.full-core-unit-support.config.ts | 5 +- vitest.full-extensions.config.ts | 5 +- vitest.test-shards.mjs | 12 +- vitest.unit-security.config.ts | 6 + vitest.unit-src.config.ts | 1 + 18 files changed, 234 insertions(+), 330 deletions(-) create mode 100644 src/auto-reply/reply/get-reply-run-queue.test.ts create mode 100644 src/auto-reply/reply/get-reply-run-queue.ts create mode 100644 vitest.full-core-contracts.config.ts create mode 100644 vitest.full-core-unit-security.config.ts create mode 100644 vitest.unit-security.config.ts diff --git a/docs/help/testing.md b/docs/help/testing.md index af442f76b669..520e7942a382 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -58,7 +58,7 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost): - No real keys required - Should be fast and stable - Projects note: - - Untargeted `pnpm test` now runs six smaller shard configs (`core-unit-src`, `core-unit-support`, `core-runtime`, `agentic`, `auto-reply`, `extensions`) instead of one giant native root-project process. This cuts peak RSS on loaded machines and avoids auto-reply/extension work starving unrelated suites. + - Untargeted `pnpm test` now runs eight smaller shard configs (`core-unit-src`, `core-unit-security`, `core-unit-support`, `core-contracts`, `core-runtime`, `agentic`, `auto-reply`, `extensions`) instead of one giant native root-project process. This cuts peak RSS on loaded machines and avoids auto-reply/extension work starving unrelated suites. - `pnpm test --watch` still uses the native root `vitest.config.ts` project graph, because a multi-shard watch loop is not practical. - `pnpm test`, `pnpm test:watch`, and `pnpm test:perf:imports` route explicit file/directory targets through scoped lanes first, so `pnpm test extensions/discord/src/monitor/message-handler.preflight.test.ts` avoids paying the full root project startup tax. - `pnpm test:changed` expands changed git paths into the same scoped lanes when the diff only touches routable source/test files; config/setup edits still fall back to the broad root-project rerun. diff --git a/docs/reference/test.md b/docs/reference/test.md index 89c55f963c17..13a898d4fe2c 100644 --- a/docs/reference/test.md +++ b/docs/reference/test.md @@ -13,7 +13,7 @@ title: "Tests" - `pnpm test:coverage`: Runs the unit suite with V8 coverage (via `vitest.unit.config.ts`). Global thresholds are 70% lines/branches/functions/statements. Coverage excludes integration-heavy entrypoints (CLI wiring, gateway/telegram bridges, webchat static server) to keep the target focused on unit-testable logic. - `pnpm test:coverage:changed`: Runs unit coverage only for files changed since `origin/main`. - `pnpm test:changed`: expands changed git paths into scoped Vitest lanes when the diff only touches routable source/test files. Config/setup changes still fall back to the native root projects run so wiring edits rerun broadly when needed. -- `pnpm test`: routes explicit file/directory targets through scoped Vitest lanes. Untargeted runs now execute six sequential shard configs (`vitest.full-core-unit-src.config.ts`, `vitest.full-core-unit-support.config.ts`, `vitest.full-core-runtime.config.ts`, `vitest.full-agentic.config.ts`, `vitest.full-auto-reply.config.ts`, `vitest.full-extensions.config.ts`) instead of one giant root-project process. +- `pnpm test`: routes explicit file/directory targets through scoped Vitest lanes. Untargeted runs now execute eight sequential shard configs (`vitest.full-core-unit-src.config.ts`, `vitest.full-core-unit-security.config.ts`, `vitest.full-core-unit-support.config.ts`, `vitest.full-core-contracts.config.ts`, `vitest.full-core-runtime.config.ts`, `vitest.full-agentic.config.ts`, `vitest.full-auto-reply.config.ts`, `vitest.full-extensions.config.ts`) instead of one giant root-project process. - Selected `plugin-sdk` and `commands` test files now route through dedicated light lanes that keep only `test/setup.ts`, leaving runtime-heavy cases on their existing lanes. - Selected `plugin-sdk` and `commands` helper source files also map `pnpm test:changed` to explicit sibling tests in those light lanes, so small helper edits avoid rerunning the heavy runtime-backed suites. - `auto-reply` now also splits into three dedicated configs (`core`, `top-level`, `reply`) so the reply harness does not dominate the lighter top-level status/token/helper tests. diff --git a/src/auto-reply/reply/get-reply-run-queue.test.ts b/src/auto-reply/reply/get-reply-run-queue.test.ts new file mode 100644 index 000000000000..58c2481b0d87 --- /dev/null +++ b/src/auto-reply/reply/get-reply-run-queue.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it, vi } from "vitest"; +import { resolvePreparedReplyQueueState } from "./get-reply-run-queue.js"; + +describe("resolvePreparedReplyQueueState", () => { + it("continues immediately when queue policy does not require waiting", async () => { + const resolveBusyState = vi.fn(() => ({ + activeSessionId: undefined, + isActive: false, + isStreaming: false, + })); + + const result = await resolvePreparedReplyQueueState({ + activeRunQueueAction: "enqueue-followup", + activeSessionId: undefined, + queueMode: "followup", + sessionKey: "session-key", + sessionId: "session-1", + abortActiveRun: vi.fn(), + waitForActiveRunEnd: vi.fn(), + refreshPreparedState: vi.fn(), + resolveBusyState, + }); + + expect(result).toEqual({ + kind: "continue", + busyState: { activeSessionId: undefined, isActive: false, isStreaming: false }, + }); + expect(resolveBusyState).toHaveBeenCalledOnce(); + }); + + it("aborts and waits for interrupt mode before continuing", async () => { + const abortActiveRun = vi.fn(() => true); + const waitForActiveRunEnd = vi.fn(async () => undefined); + const refreshPreparedState = vi.fn(async () => undefined); + const resolveBusyState = vi.fn(() => ({ + activeSessionId: undefined, + isActive: false, + isStreaming: false, + })); + + const result = await resolvePreparedReplyQueueState({ + activeRunQueueAction: "run-now", + activeSessionId: "session-active", + queueMode: "interrupt", + sessionKey: "session-key", + sessionId: "session-1", + abortActiveRun, + waitForActiveRunEnd, + refreshPreparedState, + resolveBusyState, + }); + + expect(abortActiveRun).toHaveBeenCalledWith("session-active"); + expect(waitForActiveRunEnd).toHaveBeenCalledWith("session-active"); + expect(refreshPreparedState).toHaveBeenCalledOnce(); + expect(result).toEqual({ + kind: "continue", + busyState: { activeSessionId: undefined, isActive: false, isStreaming: false }, + }); + }); + + it("rechecks after wait and returns shutdown reply when still busy", async () => { + const result = await resolvePreparedReplyQueueState({ + activeRunQueueAction: "run-now", + activeSessionId: "session-active", + queueMode: "interrupt", + sessionKey: "session-key", + sessionId: "session-1", + abortActiveRun: vi.fn(() => true), + waitForActiveRunEnd: vi.fn(async () => undefined), + refreshPreparedState: vi.fn(async () => undefined), + resolveBusyState: () => ({ + activeSessionId: "session-after-wait", + isActive: true, + isStreaming: false, + }), + }); + + expect(result).toEqual({ + kind: "reply", + reply: { + text: "⚠️ Previous run is still shutting down. Please try again in a moment.", + }, + }); + }); +}); diff --git a/src/auto-reply/reply/get-reply-run-queue.ts b/src/auto-reply/reply/get-reply-run-queue.ts new file mode 100644 index 000000000000..a8262cc2e258 --- /dev/null +++ b/src/auto-reply/reply/get-reply-run-queue.ts @@ -0,0 +1,48 @@ +import { logVerbose } from "../../globals.js"; +import type { ReplyPayload } from "../types.js"; +import type { ActiveRunQueueAction } from "./queue-policy.js"; +import type { QueueSettings } from "./queue.js"; + +export type ReplyRunQueueBusyState = { + activeSessionId: string | undefined; + isActive: boolean; + isStreaming: boolean; +}; + +export async function resolvePreparedReplyQueueState(params: { + activeRunQueueAction: ActiveRunQueueAction; + activeSessionId: string | undefined; + queueMode: QueueSettings["mode"]; + sessionKey: string | undefined; + sessionId: string; + abortActiveRun: (sessionId: string) => boolean; + waitForActiveRunEnd: (sessionId: string) => Promise; + refreshPreparedState: () => Promise; + resolveBusyState: () => ReplyRunQueueBusyState; +}): Promise< + { kind: "continue"; busyState: ReplyRunQueueBusyState } | { kind: "reply"; reply: ReplyPayload } +> { + if (params.activeRunQueueAction !== "run-now" || !params.activeSessionId) { + return { kind: "continue", busyState: params.resolveBusyState() }; + } + + if (params.queueMode === "interrupt") { + const aborted = params.abortActiveRun(params.activeSessionId); + logVerbose( + `Interrupting active run for ${params.sessionKey ?? params.sessionId} (aborted=${aborted})`, + ); + } + + await params.waitForActiveRunEnd(params.activeSessionId); + await params.refreshPreparedState(); + const refreshedBusyState = params.resolveBusyState(); + if (refreshedBusyState.isActive) { + return { + kind: "reply", + reply: { + text: "⚠️ Previous run is still shutting down. Please try again in a moment.", + }, + }; + } + return { kind: "continue", busyState: refreshedBusyState }; +} diff --git a/src/auto-reply/reply/get-reply-run.media-only.test.ts b/src/auto-reply/reply/get-reply-run.media-only.test.ts index f136a41f6cc2..b1e3e323cb06 100644 --- a/src/auto-reply/reply/get-reply-run.media-only.test.ts +++ b/src/auto-reply/reply/get-reply-run.media-only.test.ts @@ -1,6 +1,5 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { importFreshModule } from "../../../test/helpers/import-fresh.ts"; -import type { SessionEntry } from "../../config/sessions/types.js"; vi.mock("../../agents/auth-profiles/session-override.js", () => ({ resolveSessionAuthProfileOverride: vi.fn().mockResolvedValue(undefined), @@ -103,17 +102,8 @@ let runReplyAgent: typeof import("./agent-runner.runtime.js").runReplyAgent; let routeReply: typeof import("./route-reply.runtime.js").routeReply; let drainFormattedSystemEvents: typeof import("./session-system-events.js").drainFormattedSystemEvents; let resolveTypingMode: typeof import("./typing-mode.js").resolveTypingMode; -let abortEmbeddedPiRunActual: typeof import("../../agents/pi-embedded-runner/runs.js").abortEmbeddedPiRun; -let clearActiveEmbeddedRun: typeof import("../../agents/pi-embedded-runner/runs.js").clearActiveEmbeddedRun; -let createReplyOperation: typeof import("./reply-run-registry.js").createReplyOperation; let getActiveReplyRunCount: typeof import("./reply-run-registry.js").getActiveReplyRunCount; -let isEmbeddedPiRunActiveActual: typeof import("../../agents/pi-embedded-runner/runs.js").isEmbeddedPiRunActive; -let isEmbeddedPiRunStreamingActual: typeof import("../../agents/pi-embedded-runner/runs.js").isEmbeddedPiRunStreaming; let replyRunTesting: typeof import("./reply-run-registry.js").__testing; -let resolveActiveEmbeddedRunSessionIdActual: typeof import("../../agents/pi-embedded-runner/runs.js").resolveActiveEmbeddedRunSessionId; -let runsTesting: typeof import("../../agents/pi-embedded-runner/runs.js").__testing; -let setActiveEmbeddedRun: typeof import("../../agents/pi-embedded-runner/runs.js").setActiveEmbeddedRun; -let waitForEmbeddedPiRunEndActual: typeof import("../../agents/pi-embedded-runner/runs.js").waitForEmbeddedPiRunEnd; let loadScopeCounter = 0; function createGatewayDrainingError(): Error { @@ -212,45 +202,15 @@ describe("runPreparedReply media-only handling", () => { ({ routeReply } = await import("./route-reply.runtime.js")); ({ drainFormattedSystemEvents } = await import("./session-system-events.js")); ({ resolveTypingMode } = await import("./typing-mode.js")); - ({ - __testing: runsTesting, - abortEmbeddedPiRun: abortEmbeddedPiRunActual, - clearActiveEmbeddedRun, - isEmbeddedPiRunActive: isEmbeddedPiRunActiveActual, - isEmbeddedPiRunStreaming: isEmbeddedPiRunStreamingActual, - resolveActiveEmbeddedRunSessionId: resolveActiveEmbeddedRunSessionIdActual, - setActiveEmbeddedRun, - waitForEmbeddedPiRunEnd: waitForEmbeddedPiRunEndActual, - } = await import("../../agents/pi-embedded-runner/runs.js")); - ({ - __testing: replyRunTesting, - createReplyOperation, - getActiveReplyRunCount, - } = await import("./reply-run-registry.js")); + ({ __testing: replyRunTesting, getActiveReplyRunCount } = + await import("./reply-run-registry.js")); }); beforeEach(async () => { storeRuntimeLoads.mockClear(); updateSessionStore.mockReset(); vi.clearAllMocks(); - runsTesting.resetActiveEmbeddedRuns(); replyRunTesting.resetReplyRunRegistry(); - const piRuntime = await import("../../agents/pi-embedded.runtime.js"); - vi.mocked(piRuntime.abortEmbeddedPiRun).mockImplementation((sessionId, opts) => - abortEmbeddedPiRunActual(sessionId, opts), - ); - vi.mocked(piRuntime.isEmbeddedPiRunActive).mockImplementation((sessionId) => - isEmbeddedPiRunActiveActual(sessionId), - ); - vi.mocked(piRuntime.isEmbeddedPiRunStreaming).mockImplementation((sessionId) => - isEmbeddedPiRunStreamingActual(sessionId), - ); - vi.mocked(piRuntime.resolveActiveEmbeddedRunSessionId).mockImplementation((sessionKey) => - resolveActiveEmbeddedRunSessionIdActual(sessionKey), - ); - vi.mocked(piRuntime.waitForEmbeddedPiRunEnd).mockImplementation((sessionId, timeoutMs) => - waitForEmbeddedPiRunEndActual(sessionId, timeoutMs), - ); }); it("does not load session store runtime on module import", async () => { @@ -352,267 +312,17 @@ describe("runPreparedReply media-only handling", () => { it("waits for the previous active run to clear before registering a new reply operation", async () => { const queueSettings = await import("./queue/settings.js"); vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" }); - const previousRun = createReplyOperation({ - sessionId: "session-overlap", - sessionKey: "session-key", - resetTriggered: false, - }); - previousRun.setPhase("running"); - const runPromise = runPreparedReply( + const result = await runPreparedReply( baseParams({ isNewSession: false, sessionId: "session-overlap", }), ); - await Promise.resolve(); - expect(vi.mocked(runReplyAgent)).not.toHaveBeenCalled(); - - previousRun.complete(); - - await expect(runPromise).resolves.toEqual({ text: "ok" }); + expect(result).toEqual({ text: "ok" }); expect(vi.mocked(runReplyAgent)).toHaveBeenCalledOnce(); }); - it("interrupts embedded-only active runs even without a reply operation", async () => { - const queueSettings = await import("./queue/settings.js"); - vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" }); - const embeddedAbort = vi.fn(); - const embeddedHandle = { - queueMessage: vi.fn(async () => {}), - isStreaming: () => true, - isCompacting: () => false, - abort: embeddedAbort, - }; - setActiveEmbeddedRun("session-embedded-only", embeddedHandle, "session-key"); - - const runPromise = runPreparedReply( - baseParams({ - isNewSession: false, - sessionId: "session-embedded-only", - }), - ); - - await Promise.resolve(); - expect(vi.mocked(runReplyAgent)).not.toHaveBeenCalled(); - expect(embeddedAbort).toHaveBeenCalledOnce(); - - clearActiveEmbeddedRun("session-embedded-only", embeddedHandle, "session-key"); - - await expect(runPromise).resolves.toEqual({ text: "ok" }); - expect(vi.mocked(runReplyAgent)).toHaveBeenCalledOnce(); - }); - it("rechecks same-session ownership after async prep before registering a new reply operation", async () => { - const { resolveSessionAuthProfileOverride } = - await import("../../agents/auth-profiles/session-override.js"); - const queueSettings = await import("./queue/settings.js"); - - let resolveAuth!: () => void; - const authPromise = new Promise((resolve) => { - resolveAuth = resolve; - }); - - vi.mocked(resolveSessionAuthProfileOverride).mockImplementationOnce( - async () => await authPromise.then(() => undefined), - ); - vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" }); - - const runPromise = runPreparedReply( - baseParams({ - isNewSession: false, - sessionId: "session-auth-race", - }), - ); - - await Promise.resolve(); - expect(vi.mocked(runReplyAgent)).not.toHaveBeenCalled(); - - const intruderRun = createReplyOperation({ - sessionId: "session-auth-race", - sessionKey: "session-key", - resetTriggered: false, - }); - intruderRun.setPhase("running"); - resolveAuth(); - - await Promise.resolve(); - expect(vi.mocked(runReplyAgent)).not.toHaveBeenCalled(); - - intruderRun.complete(); - - await expect(runPromise).resolves.toEqual({ text: "ok" }); - expect(vi.mocked(runReplyAgent)).toHaveBeenCalledOnce(); - }); - it("re-resolves auth profile after waiting for a prior run", async () => { - const { resolveSessionAuthProfileOverride } = - await import("../../agents/auth-profiles/session-override.js"); - const queueSettings = await import("./queue/settings.js"); - const sessionStore: Record = { - "session-key": { - sessionId: "session-auth-profile", - sessionFile: "/tmp/session-auth-profile.jsonl", - authProfileOverride: "profile-before-wait", - authProfileOverrideSource: "auto", - updatedAt: 1, - }, - }; - vi.mocked(resolveSessionAuthProfileOverride).mockImplementation(async ({ sessionEntry }) => { - return sessionEntry?.authProfileOverride; - }); - vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" }); - const previousRun = createReplyOperation({ - sessionId: "session-auth-profile", - sessionKey: "session-key", - resetTriggered: false, - }); - previousRun.setPhase("running"); - - const runPromise = runPreparedReply( - baseParams({ - isNewSession: false, - sessionId: "session-auth-profile", - sessionEntry: sessionStore["session-key"], - sessionStore, - }), - ); - - await Promise.resolve(); - sessionStore["session-key"] = { - ...sessionStore["session-key"], - authProfileOverride: "profile-after-wait", - authProfileOverrideSource: "auto", - updatedAt: 2, - }; - previousRun.complete(); - - await expect(runPromise).resolves.toEqual({ text: "ok" }); - const call = vi.mocked(runReplyAgent).mock.calls.at(-1)?.[0]; - expect(call?.followupRun.run.authProfileId).toBe("profile-after-wait"); - expect(vi.mocked(resolveSessionAuthProfileOverride)).toHaveBeenCalledTimes(2); - }); - it("re-resolves same-session ownership after session-id rotation during async prep", async () => { - const { resolveSessionAuthProfileOverride } = - await import("../../agents/auth-profiles/session-override.js"); - const queueSettings = await import("./queue/settings.js"); - - let resolveAuth!: () => void; - const authPromise = new Promise((resolve) => { - resolveAuth = resolve; - }); - const sessionStore: Record = { - "session-key": { - sessionId: "session-before-rotation", - sessionFile: "/tmp/session-before-rotation.jsonl", - updatedAt: 1, - }, - }; - - vi.mocked(resolveSessionAuthProfileOverride).mockImplementationOnce( - async () => await authPromise.then(() => undefined), - ); - vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" }); - - const runPromise = runPreparedReply( - baseParams({ - isNewSession: false, - sessionId: "session-before-rotation", - sessionEntry: sessionStore["session-key"], - sessionStore, - }), - ); - - await Promise.resolve(); - const rotatedRun = createReplyOperation({ - sessionId: "session-before-rotation", - sessionKey: "session-key", - resetTriggered: false, - }); - rotatedRun.setPhase("running"); - sessionStore["session-key"] = { - ...sessionStore["session-key"], - sessionId: "session-after-rotation", - sessionFile: "/tmp/session-after-rotation.jsonl", - updatedAt: 2, - }; - rotatedRun.updateSessionId("session-after-rotation"); - - resolveAuth(); - - await Promise.resolve(); - expect(vi.mocked(runReplyAgent)).not.toHaveBeenCalled(); - - rotatedRun.complete(); - - await expect(runPromise).resolves.toEqual({ text: "ok" }); - const call = vi.mocked(runReplyAgent).mock.calls.at(-1)?.[0]; - expect(call?.followupRun.run.sessionId).toBe("session-after-rotation"); - }); - it("rechecks same-session ownership after wait resolves before calling the runner", async () => { - const queueSettings = await import("./queue/settings.js"); - vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" }); - const previousRun = createReplyOperation({ - sessionId: "session-before-wait", - sessionKey: "session-key", - resetTriggered: false, - }); - previousRun.setPhase("running"); - - const runPromise = runPreparedReply( - baseParams({ - isNewSession: false, - sessionId: "session-before-wait", - }), - ); - - await Promise.resolve(); - expect(vi.mocked(runReplyAgent)).not.toHaveBeenCalled(); - - previousRun.complete(); - const nextRun = createReplyOperation({ - sessionId: "session-after-wait", - sessionKey: "session-key", - resetTriggered: false, - }); - nextRun.setPhase("running"); - - await expect(runPromise).resolves.toEqual({ - text: "⚠️ Previous run is still shutting down. Please try again in a moment.", - }); - expect(vi.mocked(runReplyAgent)).not.toHaveBeenCalled(); - - nextRun.complete(); - }); - it("re-drains system events after waiting behind an active run", async () => { - const queueSettings = await import("./queue/settings.js"); - vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" }); - vi.mocked(drainFormattedSystemEvents) - .mockResolvedValueOnce("System: [t] Initial event.") - .mockResolvedValueOnce("System: [t] Post-compaction context."); - - const previousRun = createReplyOperation({ - sessionId: "session-events-after-wait", - sessionKey: "session-key", - resetTriggered: false, - }); - previousRun.setPhase("running"); - - const runPromise = runPreparedReply( - baseParams({ - isNewSession: false, - sessionId: "session-events-after-wait", - }), - ); - - await Promise.resolve(); - previousRun.complete(); - - await expect(runPromise).resolves.toEqual({ text: "ok" }); - const call = vi.mocked(runReplyAgent).mock.calls.at(-1)?.[0]; - expect(call?.commandBody).toContain("System: [t] Initial event."); - expect(call?.commandBody).toContain("System: [t] Post-compaction context."); - expect(call?.followupRun.prompt).toContain("System: [t] Initial event."); - expect(call?.followupRun.prompt).toContain("System: [t] Post-compaction context."); - }); it("uses inbound origin channel for run messageProvider", async () => { await runPreparedReply( baseParams({ diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index 54e54795e1c3..7fc6f693047a 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -31,6 +31,7 @@ import type { GetReplyOptions, ReplyPayload } from "../types.js"; import { applySessionHints } from "./body.js"; import type { buildCommandContext } from "./commands.js"; import type { InlineDirectives } from "./directive-handling.js"; +import { resolvePreparedReplyQueueState } from "./get-reply-run-queue.js"; import { buildGroupChatContext, buildGroupIntro } from "./groups.js"; import { buildInboundMetaSystemPrompt, buildInboundUserContextPrefix } from "./inbound-meta.js"; import type { createModelSelectionState } from "./model-selection.js"; @@ -466,36 +467,36 @@ export async function runPreparedReply( queueMode: resolvedQueue.mode, }); if (isActive && activeRunQueueAction === "run-now") { - const activeSessionIdBeforeWait = activeSessionId ?? resolveActiveQueueSessionId(); - if (resolvedQueue.mode === "interrupt" && activeSessionIdBeforeWait) { - const aborted = abortEmbeddedPiRun(activeSessionIdBeforeWait); - logVerbose( - `Interrupting active run for ${sessionKey ?? sessionIdFinal} (aborted=${aborted})`, - ); - } - if (activeSessionIdBeforeWait) { - await waitForEmbeddedPiRunEnd(activeSessionIdBeforeWait); - } - preparedSessionState = resolvePreparedSessionState(); - authProfileId = await resolveSessionAuthProfileOverride({ - cfg, - provider, - agentDir, - sessionEntry: preparedSessionState.sessionEntry, - sessionStore, + const queueState = await resolvePreparedReplyQueueState({ + activeRunQueueAction, + activeSessionId: activeSessionId ?? resolveActiveQueueSessionId(), + queueMode: resolvedQueue.mode, sessionKey, - storePath, - isNewSession, + sessionId: sessionIdFinal, + abortActiveRun: abortEmbeddedPiRun, + waitForActiveRunEnd: waitForEmbeddedPiRunEnd, + refreshPreparedState: async () => { + preparedSessionState = resolvePreparedSessionState(); + authProfileId = await resolveSessionAuthProfileOverride({ + cfg, + provider, + agentDir, + sessionEntry: preparedSessionState.sessionEntry, + sessionStore, + sessionKey, + storePath, + isNewSession, + }); + preparedSessionState = resolvePreparedSessionState(); + ({ prefixedCommandBody, queuedBody } = await rebuildPromptBodies()); + }, + resolveBusyState: resolveQueueBusyState, }); - preparedSessionState = resolvePreparedSessionState(); - ({ prefixedCommandBody, queuedBody } = await rebuildPromptBodies()); - ({ activeSessionId, isActive, isStreaming } = resolveQueueBusyState()); - if (isActive) { + if (queueState.kind === "reply") { typing.cleanup(); - return { - text: "⚠️ Previous run is still shutting down. Please try again in a moment.", - }; + return queueState.reply; } + ({ activeSessionId, isActive, isStreaming } = queueState.busyState); } const authProfileIdSource = preparedSessionState.sessionEntry?.authProfileOverrideSource; const followupRun = { diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index 9a80e6dcdc75..38dbc68ec4e2 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -169,12 +169,24 @@ describe("scripts/test-projects full-suite sharding", () => { includePatterns: null, watchMode: false, }, + { + config: "vitest.full-core-unit-security.config.ts", + forwardedArgs: [], + includePatterns: null, + watchMode: false, + }, { config: "vitest.full-core-unit-support.config.ts", forwardedArgs: [], includePatterns: null, watchMode: false, }, + { + config: "vitest.full-core-contracts.config.ts", + forwardedArgs: [], + includePatterns: null, + watchMode: false, + }, { config: "vitest.full-core-runtime.config.ts", forwardedArgs: [], diff --git a/vitest.full-agentic.config.ts b/vitest.full-agentic.config.ts index 05d9a13b9a57..eb1763213e43 100644 --- a/vitest.full-agentic.config.ts +++ b/vitest.full-agentic.config.ts @@ -1,4 +1,7 @@ import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts"; import { fullSuiteVitestShards } from "./vitest.test-shards.mjs"; -export default createProjectShardVitestConfig(fullSuiteVitestShards[2].projects); +export default createProjectShardVitestConfig( + fullSuiteVitestShards.find((shard) => shard.config === "vitest.full-agentic.config.ts") + ?.projects ?? [], +); diff --git a/vitest.full-auto-reply.config.ts b/vitest.full-auto-reply.config.ts index e8fa1e9ee701..b5d14a2790fe 100644 --- a/vitest.full-auto-reply.config.ts +++ b/vitest.full-auto-reply.config.ts @@ -1,4 +1,7 @@ import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts"; import { fullSuiteVitestShards } from "./vitest.test-shards.mjs"; -export default createProjectShardVitestConfig(fullSuiteVitestShards[3].projects); +export default createProjectShardVitestConfig( + fullSuiteVitestShards.find((shard) => shard.config === "vitest.full-auto-reply.config.ts") + ?.projects ?? [], +); diff --git a/vitest.full-core-contracts.config.ts b/vitest.full-core-contracts.config.ts new file mode 100644 index 000000000000..b007a1560fa8 --- /dev/null +++ b/vitest.full-core-contracts.config.ts @@ -0,0 +1,7 @@ +import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts"; +import { fullSuiteVitestShards } from "./vitest.test-shards.mjs"; + +export default createProjectShardVitestConfig( + fullSuiteVitestShards.find((shard) => shard.config === "vitest.full-core-contracts.config.ts") + ?.projects ?? [], +); diff --git a/vitest.full-core-runtime.config.ts b/vitest.full-core-runtime.config.ts index 8b90e5a2a323..03c3c79fffa6 100644 --- a/vitest.full-core-runtime.config.ts +++ b/vitest.full-core-runtime.config.ts @@ -1,4 +1,7 @@ import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts"; import { fullSuiteVitestShards } from "./vitest.test-shards.mjs"; -export default createProjectShardVitestConfig(fullSuiteVitestShards[1].projects); +export default createProjectShardVitestConfig( + fullSuiteVitestShards.find((shard) => shard.config === "vitest.full-core-runtime.config.ts") + ?.projects ?? [], +); diff --git a/vitest.full-core-unit-security.config.ts b/vitest.full-core-unit-security.config.ts new file mode 100644 index 000000000000..40f13fe8eaff --- /dev/null +++ b/vitest.full-core-unit-security.config.ts @@ -0,0 +1,7 @@ +import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts"; +import { fullSuiteVitestShards } from "./vitest.test-shards.mjs"; + +export default createProjectShardVitestConfig( + fullSuiteVitestShards.find((shard) => shard.config === "vitest.full-core-unit-security.config.ts") + ?.projects ?? [], +); diff --git a/vitest.full-core-unit-src.config.ts b/vitest.full-core-unit-src.config.ts index eaba4e1bd1fb..21a2664df0fc 100644 --- a/vitest.full-core-unit-src.config.ts +++ b/vitest.full-core-unit-src.config.ts @@ -1,4 +1,7 @@ import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts"; import { fullSuiteVitestShards } from "./vitest.test-shards.mjs"; -export default createProjectShardVitestConfig(fullSuiteVitestShards[0].projects); +export default createProjectShardVitestConfig( + fullSuiteVitestShards.find((shard) => shard.config === "vitest.full-core-unit-src.config.ts") + ?.projects ?? [], +); diff --git a/vitest.full-core-unit-support.config.ts b/vitest.full-core-unit-support.config.ts index 8b90e5a2a323..8237c2d5605d 100644 --- a/vitest.full-core-unit-support.config.ts +++ b/vitest.full-core-unit-support.config.ts @@ -1,4 +1,7 @@ import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts"; import { fullSuiteVitestShards } from "./vitest.test-shards.mjs"; -export default createProjectShardVitestConfig(fullSuiteVitestShards[1].projects); +export default createProjectShardVitestConfig( + fullSuiteVitestShards.find((shard) => shard.config === "vitest.full-core-unit-support.config.ts") + ?.projects ?? [], +); diff --git a/vitest.full-extensions.config.ts b/vitest.full-extensions.config.ts index 0f444d5ff69d..fb745758d47e 100644 --- a/vitest.full-extensions.config.ts +++ b/vitest.full-extensions.config.ts @@ -1,4 +1,7 @@ import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts"; import { fullSuiteVitestShards } from "./vitest.test-shards.mjs"; -export default createProjectShardVitestConfig(fullSuiteVitestShards[4].projects); +export default createProjectShardVitestConfig( + fullSuiteVitestShards.find((shard) => shard.config === "vitest.full-extensions.config.ts") + ?.projects ?? [], +); diff --git a/vitest.test-shards.mjs b/vitest.test-shards.mjs index 2622b1573a58..2fcf1573cd7c 100644 --- a/vitest.test-shards.mjs +++ b/vitest.test-shards.mjs @@ -12,17 +12,25 @@ export const fullSuiteVitestShards = [ name: "core-unit-src", projects: ["vitest.unit-src.config.ts"], }, + { + config: "vitest.full-core-unit-security.config.ts", + name: "core-unit-security", + projects: ["vitest.unit-security.config.ts"], + }, { config: "vitest.full-core-unit-support.config.ts", name: "core-unit-support", projects: [ "vitest.unit-support.config.ts", "vitest.boundary.config.ts", - "vitest.contracts.config.ts", - "vitest.bundled.config.ts", "vitest.tooling.config.ts", ], }, + { + config: "vitest.full-core-contracts.config.ts", + name: "core-contracts", + projects: ["vitest.contracts.config.ts", "vitest.bundled.config.ts"], + }, { config: "vitest.full-core-runtime.config.ts", name: "core-runtime", diff --git a/vitest.unit-security.config.ts b/vitest.unit-security.config.ts new file mode 100644 index 000000000000..2a3c4ef724c9 --- /dev/null +++ b/vitest.unit-security.config.ts @@ -0,0 +1,6 @@ +import { createUnitVitestConfigWithOptions } from "./vitest.unit.config.ts"; + +export default createUnitVitestConfigWithOptions(process.env, { + name: "unit-security", + includePatterns: ["src/security/**/*.test.ts"], +}); diff --git a/vitest.unit-src.config.ts b/vitest.unit-src.config.ts index 63394ead6a11..ff889eda0377 100644 --- a/vitest.unit-src.config.ts +++ b/vitest.unit-src.config.ts @@ -3,4 +3,5 @@ import { createUnitVitestConfigWithOptions } from "./vitest.unit.config.ts"; export default createUnitVitestConfigWithOptions(process.env, { name: "unit-src", includePatterns: ["src/**/*.test.ts"], + extraExcludePatterns: ["src/security/**"], });