fix: avoid rebuilding unchanged agent runtimes on reload (#129257)

* fix: scope prepared runtime refreshes by agent (oc-92a.6)

* fix: keep scoped runtime refresh linear (oc-92a.6)

* fix: keep gateway reload hot path within lint limit
This commit is contained in:
Josh Lehman
2026-08-27 00:32:26 -07:00
committed by GitHub
parent fa16a4f80e
commit 8d82e26e5f
10 changed files with 436 additions and 58 deletions
@@ -0,0 +1,152 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
advancePreparedModelRuntimeOwnerConfig,
listConfiguredOwnerInputs,
normalizePreparedModelRuntimeInput,
ownerKey,
} from "./prepared-model-runtime.owner.js";
import type {
PreparedModelRuntimeInput,
PreparedModelRuntimeOwner,
PreparedModelRuntimeRefreshOptions,
} from "./prepared-model-runtime.types.js";
/** Whether a refresh scope must replace this owner rather than retain it. */
export function isPreparedModelRuntimeOwnerInRefreshScope(
owner: PreparedModelRuntimeOwner,
agentIds: ReadonlySet<string> | undefined,
): boolean {
if (!agentIds) {
return true;
}
// Standalone and read-only owners keep independent config identities, so only configured and
// leased run owners participate in agent-scoped retention.
if (owner.input.readOnly || (owner.provenance !== "configured" && owner.provenance !== "run")) {
return true;
}
return !owner.input.agentId || agentIds.has(owner.input.agentId);
}
/** Builds configured inputs while preserving the startup-selected default workspace. */
export function listConfiguredRefreshInputs(
config: OpenClawConfig,
options: PreparedModelRuntimeRefreshOptions,
owners: Map<string, PreparedModelRuntimeOwner>,
): PreparedModelRuntimeInput[] {
const preservedWorkspaceByAgentDir = new Map<string, Map<string, string>>();
for (const owner of owners.values()) {
const { agentDir, agentId, preserveWorkspaceDirOnRefresh, workspaceDir } = owner.input;
if (
owner.provenance !== "configured" ||
!agentId ||
!preserveWorkspaceDirOnRefresh ||
!workspaceDir
) {
continue;
}
let workspacesByDir = preservedWorkspaceByAgentDir.get(agentId);
if (!workspacesByDir) {
workspacesByDir = new Map();
preservedWorkspaceByAgentDir.set(agentId, workspacesByDir);
}
if (!workspacesByDir.has(agentDir)) {
workspacesByDir.set(agentDir, workspaceDir);
}
}
const inputs: PreparedModelRuntimeInput[] = [];
for (const rawInput of listConfiguredOwnerInputs(
config,
options.defaultWorkspaceDir,
options.allowGatewaySubagentBinding,
)) {
const input = normalizePreparedModelRuntimeInput(rawInput);
const preservedWorkspaceDir = input.agentId
? preservedWorkspaceByAgentDir.get(input.agentId)?.get(input.agentDir)
: undefined;
inputs.push(
preservedWorkspaceDir
? {
...input,
workspaceDir: preservedWorkspaceDir,
preserveWorkspaceDirOnRefresh: true,
}
: input,
);
}
return inputs;
}
/** Invalidates scoped owners and optionally advances retained owners to a new config stamp. */
export function updateOwnersForScopedRefresh(
owners: Map<string, PreparedModelRuntimeOwner>,
agentIds: ReadonlySet<string> | undefined,
staleError: Error,
options: {
retainedConfig?: OpenClawConfig;
retireStandalone?: boolean;
clearPending?: boolean;
resetPluginGeneration?: boolean;
} = {},
): void {
for (const [key, owner] of owners) {
if (!isPreparedModelRuntimeOwnerInRefreshScope(owner, agentIds)) {
if (options.retainedConfig) {
advancePreparedModelRuntimeOwnerConfig(owner, options.retainedConfig);
}
continue;
}
if (options.retireStandalone && owner.provenance === "standalone") {
owner.generation += 1;
owners.delete(key);
continue;
}
owner.generation += 1;
owner.needsRefresh = true;
owner.refreshError = staleError;
if (options.clearPending) {
owner.pending = undefined;
}
if (options.resetPluginGeneration) {
owner.pluginGeneration = undefined;
}
}
}
/** Keeps a requested scope only when every retained owner has identical prepared dependencies. */
export function resolveSafeRefreshAgentIds(
config: OpenClawConfig,
options: PreparedModelRuntimeRefreshOptions,
owners: Map<string, PreparedModelRuntimeOwner>,
): ReadonlySet<string> | undefined {
const requested = options.agentIds;
if (!requested) {
return undefined;
}
const inputs = new Map(
listConfiguredRefreshInputs(config, options, owners).flatMap((input) =>
input.agentId ? [[input.agentId, input] as const] : [],
),
);
for (const owner of owners.values()) {
if (
owner.provenance !== "configured" ||
!owner.input.agentId ||
requested.has(owner.input.agentId)
) {
continue;
}
const input = inputs.get(owner.input.agentId);
if (
!input ||
!owner.snapshot ||
owner.needsRefresh ||
owner.catalogMode !== (options.catalogMode ?? "live") ||
(options.pluginMetadataSnapshot &&
owner.snapshot.metadataSnapshot !== options.pluginMetadataSnapshot) ||
ownerKey({ ...owner.input, config: input.config }) !== ownerKey(input)
) {
return undefined;
}
}
return requested;
}
@@ -0,0 +1,136 @@
// Preserve module setup before modules that consume it.
// oxfmt-ignore
import {
getPreparedModelRuntimeMocks,
resetPreparedModelRuntimeHarness,
} from "./prepared-model-runtime.test-harness.js";
import { beforeEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { getPreparedModelRuntimeAuthStore } from "./prepared-model-runtime-auth.js";
import {
getPreparedModelRuntimeSnapshot,
refreshPreparedModelRuntimeSnapshots,
} from "./prepared-model-runtime.js";
const mocks = getPreparedModelRuntimeMocks();
describe("prepared model runtime scoped refresh", () => {
beforeEach(() => resetPreparedModelRuntimeHarness());
it("retains unaffected configured owners across an agent-scoped refresh", async () => {
mocks.configuredAgentIds = ["pro", "free"];
const initialConfig = {
agents: {
entries: {
pro: { model: "openai/gpt-5.6" },
free: { model: "openai/gpt-5.5" },
},
},
} satisfies OpenClawConfig;
const nextConfig = {
agents: {
entries: {
pro: { model: "openai/gpt-5.4" },
free: { model: "openai/gpt-5.5" },
},
},
} satisfies OpenClawConfig;
const buildCounts: number[] = [];
const freeInput = {
config: initialConfig,
agentId: "free",
agentDir: "/tmp/configured-free",
inheritedAuthDir: "/tmp/unused-agent",
workspaceDir: "/tmp/workspace-free",
};
await refreshPreparedModelRuntimeSnapshots(initialConfig, {
gatewayLifecycle: true,
onBuildStats: (stats) => buildCounts.push(stats.agentCount),
});
const retainedReader = getPreparedModelRuntimeSnapshot(freeInput)!;
const retainedAuthStore = getPreparedModelRuntimeAuthStore(retainedReader);
await refreshPreparedModelRuntimeSnapshots(nextConfig, {
gatewayLifecycle: true,
agentIds: new Set(["pro"]),
onBuildStats: (stats) => buildCounts.push(stats.agentCount),
});
const retained = getPreparedModelRuntimeSnapshot({ ...freeInput, config: nextConfig });
expect(buildCounts).toEqual([2, 1]);
expect(retained).toMatchObject({ agentId: "free", config: nextConfig });
expect(retained).not.toBe(retainedReader);
expect(retainedReader.config).toBe(initialConfig);
expect(retained?.metadataSnapshot).toBe(retainedReader.metadataSnapshot);
expect(retained?.modelCatalog).toBe(retainedReader.modelCatalog);
expect(getPreparedModelRuntimeAuthStore(retained!)).toBe(retainedAuthStore);
});
it("falls back to full refresh when an out-of-scope owner dependency changes", async () => {
mocks.configuredAgentIds = ["pro", "free"];
const initialConfig = {
agents: {
defaults: { model: "openai/gpt-5.6" },
entries: { pro: {}, free: {} },
},
} satisfies OpenClawConfig;
const nextConfig = {
agents: {
defaults: { model: "openai/gpt-5.5" },
entries: { pro: {}, free: {} },
},
} satisfies OpenClawConfig;
const buildCounts: number[] = [];
await refreshPreparedModelRuntimeSnapshots(initialConfig, {
gatewayLifecycle: true,
onBuildStats: (stats) => buildCounts.push(stats.agentCount),
});
await refreshPreparedModelRuntimeSnapshots(nextConfig, {
gatewayLifecycle: true,
agentIds: new Set(["pro"]),
onBuildStats: (stats) => buildCounts.push(stats.agentCount),
});
expect(buildCounts).toEqual([2, 2]);
});
it("builds only a newly added non-default agent", async () => {
mocks.configuredAgentIds = ["free"];
const initialConfig = {
agents: { entries: { free: { model: "openai/gpt-5.5" } } },
} satisfies OpenClawConfig;
const nextConfig = {
agents: {
entries: {
free: { model: "openai/gpt-5.5" },
pro: { model: "openai/gpt-5.6" },
},
},
} satisfies OpenClawConfig;
const buildCounts: number[] = [];
await refreshPreparedModelRuntimeSnapshots(initialConfig, {
gatewayLifecycle: true,
onBuildStats: (stats) => buildCounts.push(stats.agentCount),
});
mocks.configuredAgentIds = ["free", "pro"];
await refreshPreparedModelRuntimeSnapshots(nextConfig, {
gatewayLifecycle: true,
agentIds: new Set(["pro"]),
onBuildStats: (stats) => buildCounts.push(stats.agentCount),
});
expect(buildCounts).toEqual([1, 1]);
expect(
getPreparedModelRuntimeSnapshot({
config: nextConfig,
agentId: "pro",
agentDir: "/tmp/configured-pro",
inheritedAuthDir: "/tmp/unused-agent",
workspaceDir: "/tmp/workspace-pro",
}),
).toMatchObject({ agentId: "pro", config: nextConfig });
});
});
+46 -49
View File
@@ -10,12 +10,11 @@ import {
PreparedModelRuntimeOwnerNotPublishedError,
PreparedModelRuntimeOwnerRetention,
PreparedModelRuntimePublicationSupersededError,
advancePreparedModelRuntimeOwnerConfig,
createPreparedModelRuntimeOwner,
createPreparedModelRuntimeReplacement,
advancePreparedModelRuntimeOwnerConfig,
effectiveEnvironmentFingerprint,
hasSameLifecycleInput,
listConfiguredOwnerInputs,
normalizeOptionalDir,
normalizePreparedModelRuntimeInput,
ownerKey,
@@ -37,6 +36,12 @@ import {
notifyPreparedModelRuntimePublication,
resetPreparedModelRuntimePublicationListenersForTest,
} from "./prepared-model-runtime.publication-events.js";
import {
isPreparedModelRuntimeOwnerInRefreshScope,
listConfiguredRefreshInputs,
resolveSafeRefreshAgentIds,
updateOwnersForScopedRefresh,
} from "./prepared-model-runtime.refresh-scope.js";
import type { PreparedModelRuntimeCatalogMode } from "./prepared-model-runtime.types.js";
import { PreparedReplyDispatchPublicationOwner } from "./prepared-reply-dispatch-runtime.js";
export {
@@ -353,7 +358,11 @@ export async function prepareModelRuntimeSnapshot(
/** Invalidates every published generation before config/plugin runtime replacement. */
export function markPreparedModelRuntimeSnapshotsStale(
reason = "prepared model runtime owner is stale after config publication",
options: { waitForReplacement?: boolean; preserveReplacementWait?: boolean } = {},
options: {
waitForReplacement?: boolean;
preserveReplacementWait?: boolean;
agentIds?: ReadonlySet<string>;
} = {},
): PreparedModelRuntimeReplacementGateId | undefined {
replyDispatchPublication.clear();
if (options.waitForReplacement) {
@@ -368,19 +377,10 @@ export function markPreparedModelRuntimeSnapshotsStale(
}
refreshRequestEpoch += 1;
const staleError = new Error(reason);
for (const [key, owner] of owners) {
// Standalone owners have no publication controller to rebuild them. Retire them so the next
// standalone lifecycle boundary can activate a fresh generation after publication changes.
if (owner.provenance === "standalone") {
owner.generation += 1;
owners.delete(key);
continue;
}
owner.generation += 1;
owner.needsRefresh = true;
owner.refreshError = staleError;
owner.pluginGeneration = undefined;
}
updateOwnersForScopedRefresh(owners, options.agentIds, staleError, {
retireStandalone: true,
resetPluginGeneration: true,
});
notifyPreparedModelRuntimePublication({ phase: "invalidated" });
if (!pendingModelRuntimeReplacement) {
notifyPreparedModelRuntimePublication({ phase: "failed", error: staleError });
@@ -410,36 +410,18 @@ async function refreshPreparedModelRuntimeSnapshotsNow(
isPublicationCurrent: () => boolean,
): Promise<void> {
retainedGatewayRunOwners.clear(owners);
const { defaultWorkspaceDir: workspace, allowGatewaySubagentBinding: bindings } = options;
const catalogMode = options.catalogMode ?? "live";
gatewayLifecycleActive ||= options.gatewayLifecycle === true;
const staleError = new Error("prepared model runtime owner is stale after config publication");
for (const owner of owners.values()) {
// Invalidate every prior generation before starting any replacement. A failed reload must
// never leave an old-config snapshot available beside partially published new owners.
owner.generation += 1;
owner.needsRefresh = true;
owner.refreshError = staleError;
}
updateOwnersForScopedRefresh(owners, options.agentIds, staleError, {
retainedConfig: config,
});
const entries: Array<{ owner?: PreparedModelRuntimeOwner; input: PreparedModelRuntimeInput }> =
[];
const knownKeys = new Set<string>();
for (const rawInput of listConfiguredOwnerInputs(config, workspace, bindings)) {
let input = normalizePreparedModelRuntimeInput(rawInput);
const preservedOwner = [...owners.values()].find(
(owner) =>
owner.provenance === "configured" &&
owner.input.agentId === input.agentId &&
owner.input.agentDir === input.agentDir &&
owner.input.preserveWorkspaceDirOnRefresh &&
owner.input.workspaceDir,
);
if (preservedOwner?.input.workspaceDir) {
input = {
...input,
workspaceDir: preservedOwner.input.workspaceDir,
preserveWorkspaceDirOnRefresh: true,
};
for (const input of listConfiguredRefreshInputs(config, options, owners)) {
if (options.agentIds && input.agentId && !options.agentIds.has(input.agentId)) {
continue;
}
const key = ownerKey(input);
if (knownKeys.has(key)) {
@@ -450,6 +432,9 @@ async function refreshPreparedModelRuntimeSnapshotsNow(
entries.push({ owner, input });
}
for (const [key, owner] of owners) {
if (!isPreparedModelRuntimeOwnerInRefreshScope(owner, options.agentIds)) {
continue;
}
if (!knownKeys.has(key) && (gatewayLifecycleActive || owner.provenance === "configured")) {
owners.delete(key);
}
@@ -490,10 +475,18 @@ export function refreshPreparedModelRuntimeSnapshots(
if (options.isPublicationCurrent?.() === false) {
return Promise.resolve();
}
const requestedScopedRefresh = options.agentIds !== undefined;
const initialAgentIds =
typeof config === "function" ? undefined : resolveSafeRefreshAgentIds(config, options, owners);
const forceFullRefresh = requestedScopedRefresh && initialAgentIds === undefined;
// Stale synchronously. Queued publication must never leave the prior generation request-visible.
markPreparedModelRuntimeSnapshotsStale(undefined, { waitForReplacement: true });
markPreparedModelRuntimeSnapshotsStale(undefined, {
waitForReplacement: true,
agentIds: initialAgentIds,
});
const requestEpoch = refreshRequestEpoch;
const replacement = pendingModelRuntimeReplacement;
let publicationAgentIds = initialAgentIds;
const isPublicationCurrent = () =>
requestEpoch === refreshRequestEpoch && options.isPublicationCurrent?.() !== false;
return enqueuePreparedModelRuntimePublication(async () => {
@@ -504,7 +497,14 @@ export function refreshPreparedModelRuntimeSnapshots(
if (!isPublicationCurrent()) {
return;
}
await refreshPreparedModelRuntimeSnapshotsNow(currentConfig, options, isPublicationCurrent);
publicationAgentIds = forceFullRefresh
? undefined
: resolveSafeRefreshAgentIds(currentConfig, options, owners);
await refreshPreparedModelRuntimeSnapshotsNow(
currentConfig,
{ ...options, agentIds: publicationAgentIds },
isPublicationCurrent,
);
if (!isPublicationCurrent()) {
return;
}
@@ -528,13 +528,10 @@ export function refreshPreparedModelRuntimeSnapshots(
if (isPublicationCurrent()) {
// Candidate and queued auth builds may finish independently. A failed transaction must
// leave no owner from its partially published generation request-visible.
for (const owner of owners.values()) {
owner.generation += 1;
owner.pending = undefined;
owner.needsRefresh = true;
owner.refreshError = refreshError;
owner.pluginGeneration = undefined;
}
updateOwnersForScopedRefresh(owners, publicationAgentIds, refreshError, {
clearPending: true,
resetPluginGeneration: true,
});
}
if (isPublicationCurrent() && replacement && pendingModelRuntimeReplacement === replacement) {
pendingModelRuntimeReplacement = undefined;
@@ -117,6 +117,8 @@ export type PreparedModelRuntimeRefreshOptions = {
allowGatewaySubagentBinding?: boolean;
pluginMetadataSnapshot?: PluginMetadataSnapshot;
isPublicationCurrent?: () => boolean;
/** Restricts replacement to configured owners whose normalized agent id is present. */
agentIds?: ReadonlySet<string>;
};
export type PreparedModelRuntimeBuildStats = Readonly<{
+26 -1
View File
@@ -269,7 +269,11 @@ const hoisted = vi.hoisted(() => ({
markPreparedModelRuntimeSnapshotsStale: vi.fn(
(
_reason?: string,
_options?: { waitForReplacement?: boolean; preserveReplacementWait?: boolean },
_options?: {
waitForReplacement?: boolean;
preserveReplacementWait?: boolean;
agentIds?: ReadonlySet<string>;
},
) => Symbol("prepared-model-runtime-replacement"),
),
rejectPendingPreparedModelRuntimeReplacement: vi.fn(
@@ -1470,6 +1474,27 @@ describe("gateway hot reload model state", () => {
},
);
it("passes an agent-entry-local refresh scope through the commit and rebuild", async () => {
const logReload = { info: vi.fn(), warn: vi.fn() };
const { applyHotReload } = createReloadHandlersForTest(logReload);
const nextConfig = {} as OpenClawConfig;
await applyHotReload(
buildGatewayReloadPlan(["agents.entries.Alpha.model", "meta.lastTouchedAt"]),
nextConfig,
);
expect(hoisted.markPreparedModelRuntimeSnapshotsStale).toHaveBeenCalledWith(
"prepared model runtime owner is stale before config publication",
{ waitForReplacement: true, agentIds: new Set(["alpha"]) },
);
expect(hoisted.refreshPreparedModelRuntimeSnapshots).toHaveBeenCalledWith(nextConfig, {
allowGatewaySubagentBinding: true,
catalogMode: "static",
agentIds: new Set(["alpha"]),
});
});
it.each([
"agents.defaults.compaction.model",
"agents.defaults.compaction.maxActiveTranscriptBytes",
+8 -7
View File
@@ -4,7 +4,6 @@ import { warmCurrentProviderAuthStateOffMainThread } from "../agents/model-provi
import {
markPreparedModelRuntimeSnapshotsStale,
rejectPendingPreparedModelRuntimeReplacement,
refreshPreparedModelRuntimeSnapshots,
type PreparedModelRuntimeReplacementGateId,
} from "../agents/prepared-model-runtime.js";
import { isRestartEnabled } from "../config/commands.flags.js";
@@ -38,6 +37,7 @@ import {
type GatewayReloadHandlerParams,
type GatewayRestartTransactionResult,
} from "./server-reload-contracts.js";
import * as mrReload from "./server-reload-model-runtime-scope.js";
import { createGatewayRestartCoordinator } from "./server-reload-restart.js";
import {
assertIrreversibleReloadPlanHasRecoveryOwner,
@@ -102,6 +102,8 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams)
!isRestartRetryStopped() && (publication?.isCurrent?.() ?? true);
const state = params.getState();
const nextState = { ...state };
const modelRuntimeAgentIds = mrReload.resolveReloadAgentIds(plan.changedPaths);
const modelRuntimeRefreshScope = modelRuntimeAgentIds ? { agentIds: modelRuntimeAgentIds } : {};
resetPreparedModelRuntimeStateForHotReload();
@@ -204,7 +206,7 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams)
// retire the prior stores at the commit edge so no request can mix generations.
preparedModelRuntimeReplacementGateId = markPreparedModelRuntimeSnapshotsStale(
"prepared model runtime owner is stale before config publication",
{ waitForReplacement: true },
{ waitForReplacement: true, ...modelRuntimeRefreshScope },
);
params.setState(nextState);
// All rejecting work is complete. Publish pre-resolved lane limits at
@@ -611,11 +613,10 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams)
}
try {
const pluginMetadataSnapshot = params.getPluginMetadataSnapshot?.();
await refreshPreparedModelRuntimeSnapshots(nextConfig, {
catalogMode: "static",
allowGatewaySubagentBinding: true,
...(pluginMetadataSnapshot ? { pluginMetadataSnapshot } : {}),
await mrReload.refreshModelRuntimeAfterHotReload({
config: nextConfig,
agentIds: modelRuntimeAgentIds,
pluginMetadataSnapshot: params.getPluginMetadataSnapshot?.(),
});
} catch (err) {
scheduleRecoveryRestart("prepared model runtime reload", err);
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { resolveReloadAgentIds } from "./server-reload-model-runtime-scope.js";
describe("prepared model runtime reload scope", () => {
it("collects normalized agent ids from agent-entry-local paths", () => {
expect(
resolveReloadAgentIds(["agents.entries.Alpha.model", "agents.entries.beta.name"]),
).toEqual(new Set(["alpha", "beta"]));
});
it("ignores machine-managed metadata beside an agent-local path", () => {
expect(resolveReloadAgentIds(["agents.entries.alpha.model", "meta.lastTouchedAt"])).toEqual(
new Set(["alpha"]),
);
});
it.each([
[[]],
[["agents.entries"]],
[["agents.entries.alpha.model", "models.providers.openai.api"]],
])("falls back to full refresh for an unbounded path set: %j", (paths) => {
expect(resolveReloadAgentIds(paths)).toBeUndefined();
});
});
@@ -0,0 +1,40 @@
import { refreshPreparedModelRuntimeSnapshots } from "../agents/prepared-model-runtime.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
import { normalizeAgentId } from "../routing/session-key.js";
/** Returns affected agent ids when every meaningful reload path is agent-entry-local. */
export function resolveReloadAgentIds(
changedPaths: readonly string[],
): ReadonlySet<string> | undefined {
if (changedPaths.length === 0) {
return undefined;
}
const agentIds = new Set<string>();
for (const path of changedPaths) {
if (path === "meta" || path.startsWith("meta.")) {
continue;
}
const match = /^agents\.entries\.([^.]+)(?:\.|$)/.exec(path);
if (!match?.[1]) {
return undefined;
}
agentIds.add(normalizeAgentId(match[1]));
}
return agentIds.size > 0 ? agentIds : undefined;
}
export function refreshModelRuntimeAfterHotReload(params: {
config: OpenClawConfig;
agentIds: ReadonlySet<string> | undefined;
pluginMetadataSnapshot: PluginMetadataSnapshot | undefined;
}): Promise<void> {
return refreshPreparedModelRuntimeSnapshots(params.config, {
catalogMode: "static",
allowGatewaySubagentBinding: true,
...(params.agentIds ? { agentIds: params.agentIds } : {}),
...(params.pluginMetadataSnapshot
? { pluginMetadataSnapshot: params.pluginMetadataSnapshot }
: {}),
});
}
+1
View File
@@ -311,6 +311,7 @@ describe("unit-fast vitest lane", () => {
"src/acp/translator.error-kind.test.ts",
"src/agents/auth-profiles/oauth-refresh-error.test.ts",
"src/agents/embedded-agent-runner/model.provider-hooks.timeout.test.ts",
"src/agents/prepared-model-runtime.scoped-refresh.test.ts",
"src/agents/tools/computer-tool.context.test.ts",
"src/agents/tools/computer-tool.schema.test.ts",
"src/agents/tools/computer-tool.v2.test.ts",
+1 -1
View File
@@ -258,7 +258,7 @@ const disqualifyingPatterns = [
];
const statefulTestHelperImportPattern =
/\bfrom\s+["']([^"']*(?:test-support|\.harness|message-action-runner\.test-helpers|computer-tool\.test-helpers)(?:\.js|\.ts)?)["']/gu;
/\bfrom\s+["']([^"']*(?:test-support|\.harness|prepared-model-runtime\.test-harness|message-action-runner\.test-helpers|computer-tool\.test-helpers)(?:\.js|\.ts)?)["']/gu;
const statefulTestHelperByKey = new Map();
function importsStatefulTestHelper(cwd, file, source) {