perf(test): split reply queue seams and unit shards
This commit is contained in:
@@ -58,7 +58,7 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
|
|||||||
- No real keys required
|
- No real keys required
|
||||||
- Should be fast and stable
|
- Should be fast and stable
|
||||||
- Projects note:
|
- 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 --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`, `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.
|
- `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.
|
||||||
|
|||||||
@@ -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`: 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: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: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` 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.
|
- 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.
|
- `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.
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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<unknown>;
|
||||||
|
refreshPreparedState: () => Promise<void>;
|
||||||
|
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 };
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { importFreshModule } from "../../../test/helpers/import-fresh.ts";
|
import { importFreshModule } from "../../../test/helpers/import-fresh.ts";
|
||||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
|
||||||
|
|
||||||
vi.mock("../../agents/auth-profiles/session-override.js", () => ({
|
vi.mock("../../agents/auth-profiles/session-override.js", () => ({
|
||||||
resolveSessionAuthProfileOverride: vi.fn().mockResolvedValue(undefined),
|
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 routeReply: typeof import("./route-reply.runtime.js").routeReply;
|
||||||
let drainFormattedSystemEvents: typeof import("./session-system-events.js").drainFormattedSystemEvents;
|
let drainFormattedSystemEvents: typeof import("./session-system-events.js").drainFormattedSystemEvents;
|
||||||
let resolveTypingMode: typeof import("./typing-mode.js").resolveTypingMode;
|
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 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 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;
|
let loadScopeCounter = 0;
|
||||||
|
|
||||||
function createGatewayDrainingError(): Error {
|
function createGatewayDrainingError(): Error {
|
||||||
@@ -212,45 +202,15 @@ describe("runPreparedReply media-only handling", () => {
|
|||||||
({ routeReply } = await import("./route-reply.runtime.js"));
|
({ routeReply } = await import("./route-reply.runtime.js"));
|
||||||
({ drainFormattedSystemEvents } = await import("./session-system-events.js"));
|
({ drainFormattedSystemEvents } = await import("./session-system-events.js"));
|
||||||
({ resolveTypingMode } = await import("./typing-mode.js"));
|
({ resolveTypingMode } = await import("./typing-mode.js"));
|
||||||
({
|
({ __testing: replyRunTesting, getActiveReplyRunCount } =
|
||||||
__testing: runsTesting,
|
await import("./reply-run-registry.js"));
|
||||||
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"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
storeRuntimeLoads.mockClear();
|
storeRuntimeLoads.mockClear();
|
||||||
updateSessionStore.mockReset();
|
updateSessionStore.mockReset();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
runsTesting.resetActiveEmbeddedRuns();
|
|
||||||
replyRunTesting.resetReplyRunRegistry();
|
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 () => {
|
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 () => {
|
it("waits for the previous active run to clear before registering a new reply operation", async () => {
|
||||||
const queueSettings = await import("./queue/settings.js");
|
const queueSettings = await import("./queue/settings.js");
|
||||||
vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" });
|
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({
|
baseParams({
|
||||||
isNewSession: false,
|
isNewSession: false,
|
||||||
sessionId: "session-overlap",
|
sessionId: "session-overlap",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.resolve();
|
expect(result).toEqual({ text: "ok" });
|
||||||
expect(vi.mocked(runReplyAgent)).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
previousRun.complete();
|
|
||||||
|
|
||||||
await expect(runPromise).resolves.toEqual({ text: "ok" });
|
|
||||||
expect(vi.mocked(runReplyAgent)).toHaveBeenCalledOnce();
|
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<void>((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<string, SessionEntry> = {
|
|
||||||
"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<void>((resolve) => {
|
|
||||||
resolveAuth = resolve;
|
|
||||||
});
|
|
||||||
const sessionStore: Record<string, SessionEntry> = {
|
|
||||||
"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 () => {
|
it("uses inbound origin channel for run messageProvider", async () => {
|
||||||
await runPreparedReply(
|
await runPreparedReply(
|
||||||
baseParams({
|
baseParams({
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
|||||||
import { applySessionHints } from "./body.js";
|
import { applySessionHints } from "./body.js";
|
||||||
import type { buildCommandContext } from "./commands.js";
|
import type { buildCommandContext } from "./commands.js";
|
||||||
import type { InlineDirectives } from "./directive-handling.js";
|
import type { InlineDirectives } from "./directive-handling.js";
|
||||||
|
import { resolvePreparedReplyQueueState } from "./get-reply-run-queue.js";
|
||||||
import { buildGroupChatContext, buildGroupIntro } from "./groups.js";
|
import { buildGroupChatContext, buildGroupIntro } from "./groups.js";
|
||||||
import { buildInboundMetaSystemPrompt, buildInboundUserContextPrefix } from "./inbound-meta.js";
|
import { buildInboundMetaSystemPrompt, buildInboundUserContextPrefix } from "./inbound-meta.js";
|
||||||
import type { createModelSelectionState } from "./model-selection.js";
|
import type { createModelSelectionState } from "./model-selection.js";
|
||||||
@@ -466,36 +467,36 @@ export async function runPreparedReply(
|
|||||||
queueMode: resolvedQueue.mode,
|
queueMode: resolvedQueue.mode,
|
||||||
});
|
});
|
||||||
if (isActive && activeRunQueueAction === "run-now") {
|
if (isActive && activeRunQueueAction === "run-now") {
|
||||||
const activeSessionIdBeforeWait = activeSessionId ?? resolveActiveQueueSessionId();
|
const queueState = await resolvePreparedReplyQueueState({
|
||||||
if (resolvedQueue.mode === "interrupt" && activeSessionIdBeforeWait) {
|
activeRunQueueAction,
|
||||||
const aborted = abortEmbeddedPiRun(activeSessionIdBeforeWait);
|
activeSessionId: activeSessionId ?? resolveActiveQueueSessionId(),
|
||||||
logVerbose(
|
queueMode: resolvedQueue.mode,
|
||||||
`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,
|
|
||||||
sessionKey,
|
sessionKey,
|
||||||
storePath,
|
sessionId: sessionIdFinal,
|
||||||
isNewSession,
|
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();
|
if (queueState.kind === "reply") {
|
||||||
({ prefixedCommandBody, queuedBody } = await rebuildPromptBodies());
|
|
||||||
({ activeSessionId, isActive, isStreaming } = resolveQueueBusyState());
|
|
||||||
if (isActive) {
|
|
||||||
typing.cleanup();
|
typing.cleanup();
|
||||||
return {
|
return queueState.reply;
|
||||||
text: "⚠️ Previous run is still shutting down. Please try again in a moment.",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
({ activeSessionId, isActive, isStreaming } = queueState.busyState);
|
||||||
}
|
}
|
||||||
const authProfileIdSource = preparedSessionState.sessionEntry?.authProfileOverrideSource;
|
const authProfileIdSource = preparedSessionState.sessionEntry?.authProfileOverrideSource;
|
||||||
const followupRun = {
|
const followupRun = {
|
||||||
|
|||||||
@@ -169,12 +169,24 @@ describe("scripts/test-projects full-suite sharding", () => {
|
|||||||
includePatterns: null,
|
includePatterns: null,
|
||||||
watchMode: false,
|
watchMode: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
config: "vitest.full-core-unit-security.config.ts",
|
||||||
|
forwardedArgs: [],
|
||||||
|
includePatterns: null,
|
||||||
|
watchMode: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
config: "vitest.full-core-unit-support.config.ts",
|
config: "vitest.full-core-unit-support.config.ts",
|
||||||
forwardedArgs: [],
|
forwardedArgs: [],
|
||||||
includePatterns: null,
|
includePatterns: null,
|
||||||
watchMode: false,
|
watchMode: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
config: "vitest.full-core-contracts.config.ts",
|
||||||
|
forwardedArgs: [],
|
||||||
|
includePatterns: null,
|
||||||
|
watchMode: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
config: "vitest.full-core-runtime.config.ts",
|
config: "vitest.full-core-runtime.config.ts",
|
||||||
forwardedArgs: [],
|
forwardedArgs: [],
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
|
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
|
||||||
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
|
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 ?? [],
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
|
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
|
||||||
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
|
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 ?? [],
|
||||||
|
);
|
||||||
|
|||||||
@@ -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 ?? [],
|
||||||
|
);
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
|
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
|
||||||
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
|
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 ?? [],
|
||||||
|
);
|
||||||
|
|||||||
@@ -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 ?? [],
|
||||||
|
);
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
|
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
|
||||||
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
|
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 ?? [],
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
|
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
|
||||||
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
|
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 ?? [],
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
|
import { createProjectShardVitestConfig } from "./vitest.project-shard-config.ts";
|
||||||
import { fullSuiteVitestShards } from "./vitest.test-shards.mjs";
|
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 ?? [],
|
||||||
|
);
|
||||||
|
|||||||
+10
-2
@@ -12,17 +12,25 @@ export const fullSuiteVitestShards = [
|
|||||||
name: "core-unit-src",
|
name: "core-unit-src",
|
||||||
projects: ["vitest.unit-src.config.ts"],
|
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",
|
config: "vitest.full-core-unit-support.config.ts",
|
||||||
name: "core-unit-support",
|
name: "core-unit-support",
|
||||||
projects: [
|
projects: [
|
||||||
"vitest.unit-support.config.ts",
|
"vitest.unit-support.config.ts",
|
||||||
"vitest.boundary.config.ts",
|
"vitest.boundary.config.ts",
|
||||||
"vitest.contracts.config.ts",
|
|
||||||
"vitest.bundled.config.ts",
|
|
||||||
"vitest.tooling.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",
|
config: "vitest.full-core-runtime.config.ts",
|
||||||
name: "core-runtime",
|
name: "core-runtime",
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { createUnitVitestConfigWithOptions } from "./vitest.unit.config.ts";
|
||||||
|
|
||||||
|
export default createUnitVitestConfigWithOptions(process.env, {
|
||||||
|
name: "unit-security",
|
||||||
|
includePatterns: ["src/security/**/*.test.ts"],
|
||||||
|
});
|
||||||
@@ -3,4 +3,5 @@ import { createUnitVitestConfigWithOptions } from "./vitest.unit.config.ts";
|
|||||||
export default createUnitVitestConfigWithOptions(process.env, {
|
export default createUnitVitestConfigWithOptions(process.env, {
|
||||||
name: "unit-src",
|
name: "unit-src",
|
||||||
includePatterns: ["src/**/*.test.ts"],
|
includePatterns: ["src/**/*.test.ts"],
|
||||||
|
extraExcludePatterns: ["src/security/**"],
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user