mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(test): route agent directory runs to owning shard (#122514)
* fix(test): route agent directories to owner projects * fix(test): preserve invalid signal diagnostics
This commit is contained in:
committed by
GitHub
parent
af3550df73
commit
94c28e093d
+54
-35
@@ -5,8 +5,10 @@ import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { constants as osConstants } from "node:os";
|
||||
import path from "node:path";
|
||||
import type { Readable, Writable } from "node:stream";
|
||||
import { embeddedAgentVitestProjectOwners } from "../test/vitest/vitest.agents-paths.mjs";
|
||||
import {
|
||||
agentVitestProjectOwners,
|
||||
embeddedAgentVitestProjectOwners,
|
||||
} from "../test/vitest/vitest.agents-paths.mjs";
|
||||
import { toolingIsolatedTestFiles } from "../test/vitest/vitest.tooling-isolated-paths.mjs";
|
||||
import { isUiTestTarget } from "../test/vitest/vitest.ui-paths.mjs";
|
||||
import { boundaryTestFiles } from "../test/vitest/vitest.unit-paths.mjs";
|
||||
@@ -35,6 +37,15 @@ type WatchdogStream = {
|
||||
on(event: string, listener: (...args: unknown[]) => void): unknown;
|
||||
off(event: string, listener: (...args: unknown[]) => void): unknown;
|
||||
};
|
||||
type NodeSignal = keyof typeof osConstants.signals;
|
||||
type VitestOutputStream = {
|
||||
setEncoding(encoding: "utf8"): unknown;
|
||||
on(event: "data", listener: (chunk: string) => void): unknown;
|
||||
on(event: "end", listener: () => void): unknown;
|
||||
};
|
||||
type VitestOutputTarget = {
|
||||
write(chunk: string): unknown;
|
||||
};
|
||||
|
||||
const ANSI_CSI_PREFIX = `${String.fromCharCode(27)}[`;
|
||||
const ANSI_CSI_SUFFIX_RE = /^[0-?]*[ -/]*[@-~]/u;
|
||||
@@ -117,7 +128,10 @@ const VITEST_OPTIONS_WITH_VALUE = new Set([
|
||||
"--retry",
|
||||
"--root",
|
||||
"-r",
|
||||
"--sequence.shuffle.seed",
|
||||
"--sequence",
|
||||
"--sequence.hooks",
|
||||
"--sequence.seed",
|
||||
"--sequence.setupFiles",
|
||||
"--shard",
|
||||
"--silent",
|
||||
"--slowTestThreshold",
|
||||
@@ -138,7 +152,6 @@ const VITEST_DOTTED_OPTIONS_WITH_VALUE_PREFIXES = [
|
||||
"--experimental.",
|
||||
"--outputFile.",
|
||||
"--retry.",
|
||||
"--sequence.",
|
||||
"--typecheck.",
|
||||
];
|
||||
const UNBOUNDED_CONFIG_ONLY_OPTIONS = [
|
||||
@@ -178,16 +191,17 @@ function isErrorWithCode(error: unknown, code: string): error is NodeJS.ErrnoExc
|
||||
return error instanceof Error && "code" in error && error.code === code;
|
||||
}
|
||||
|
||||
function isNodeSignal(signal: string): signal is NodeJS.Signals {
|
||||
function isNodeSignal(signal: string): signal is NodeSignal {
|
||||
return Object.hasOwn(osConstants.signals, signal);
|
||||
}
|
||||
|
||||
function normalizeNodeSignal(signal: string | null): NodeJS.Signals | null {
|
||||
function normalizeNodeSignal(signal: string | null): NodeSignal | null {
|
||||
if (!signal) {
|
||||
return null;
|
||||
}
|
||||
const unknownSignalMessage = `child process exited with unknown signal: ${signal}`;
|
||||
if (!isNodeSignal(signal)) {
|
||||
throw new Error(`child process exited with unknown signal: ${signal}`);
|
||||
throw new Error(unknownSignalMessage);
|
||||
}
|
||||
return signal;
|
||||
}
|
||||
@@ -667,6 +681,18 @@ function isDelegableBroadProjectRouterTarget(arg: string, cwd: string): boolean
|
||||
);
|
||||
}
|
||||
|
||||
function isPathAtOrUnder(value: string, root: string): boolean {
|
||||
return value === root || value.startsWith(`${root}/`);
|
||||
}
|
||||
|
||||
function isOwnedAgentDirectoryTarget(arg: string, cwd: string, fsImpl: VitestPathFs): boolean {
|
||||
const relative = toRepoRelativeArg(arg, cwd).replace(/\/+$/u, "");
|
||||
return (
|
||||
isPathAtOrUnder(relative, agentVitestProjectOwners.all.root) &&
|
||||
isExplicitDirectoryTargetArg(arg, cwd, fsImpl)
|
||||
);
|
||||
}
|
||||
|
||||
function isExplicitProjectRouterTargetArg(
|
||||
arg: string,
|
||||
cwd = process.cwd(),
|
||||
@@ -683,7 +709,7 @@ function isExplicitProjectRouterTargetArg(
|
||||
}
|
||||
const filePath = path.isAbsolute(arg) ? arg : path.resolve(cwd, arg);
|
||||
return fsImpl.existsSync(filePath)
|
||||
? isDelegableBroadProjectRouterTarget(arg, cwd)
|
||||
? isDelegableBroadProjectRouterTarget(arg, cwd) || isOwnedAgentDirectoryTarget(arg, cwd, fsImpl)
|
||||
: path.extname(arg) === "" &&
|
||||
/^(?:src|test|extensions|ui|packages|apps)\//u.test(toRepoRelativeArg(arg, cwd));
|
||||
}
|
||||
@@ -810,42 +836,36 @@ function hasExplicitDisabledRunFlag(argv: string[]): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasSeparateVitestOptionValueArg(argv: string[]): boolean {
|
||||
for (const arg of argv) {
|
||||
if (arg === "--") {
|
||||
return false;
|
||||
}
|
||||
if (optionConsumesNextArg(arg)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function stripRunSubcommand(argv: string[]): string[] {
|
||||
const stripped: string[] = [];
|
||||
function resolveDelegatedVitestArgs(argv: string[]): string[] {
|
||||
const positionalArgs: string[] = [];
|
||||
const optionArgs: string[] = [];
|
||||
let canRemoveRunSubcommand = true;
|
||||
let passthrough = false;
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === undefined) {
|
||||
break;
|
||||
}
|
||||
if (arg === "--") {
|
||||
stripped.push(arg);
|
||||
passthrough = true;
|
||||
canRemoveRunSubcommand = false;
|
||||
continue;
|
||||
}
|
||||
if (canRemoveRunSubcommand && optionConsumesNextArg(arg)) {
|
||||
stripped.push(arg);
|
||||
if (passthrough) {
|
||||
optionArgs.push(arg);
|
||||
continue;
|
||||
}
|
||||
if (optionConsumesNextArg(arg)) {
|
||||
optionArgs.push(arg);
|
||||
const optionValue = argv[index + 1];
|
||||
if (optionValue !== undefined) {
|
||||
optionArgs.push(optionValue);
|
||||
index += 1;
|
||||
stripped.push(optionValue);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (canRemoveRunSubcommand && arg.startsWith("-")) {
|
||||
stripped.push(arg);
|
||||
if (arg.startsWith("-")) {
|
||||
optionArgs.push(arg);
|
||||
continue;
|
||||
}
|
||||
if (canRemoveRunSubcommand && arg === "run") {
|
||||
@@ -853,9 +873,9 @@ function stripRunSubcommand(argv: string[]): string[] {
|
||||
continue;
|
||||
}
|
||||
canRemoveRunSubcommand = false;
|
||||
stripped.push(arg);
|
||||
positionalArgs.push(arg);
|
||||
}
|
||||
return stripped;
|
||||
return optionArgs.length > 0 ? [...positionalArgs, "--", ...optionArgs] : positionalArgs;
|
||||
}
|
||||
|
||||
function hasNonRunVitestSubcommand(argv: string[]): boolean {
|
||||
@@ -893,12 +913,11 @@ export function resolveTestProjectsDelegationArgs(
|
||||
resolveExplicitVitestMode(argv) === "watch" ||
|
||||
hasNonRunVitestSubcommand(argv) ||
|
||||
hasExplicitDisabledRunFlag(argv) ||
|
||||
hasSeparateVitestOptionValueArg(argv) ||
|
||||
collectExplicitProjectRouterTargetArgs(argv, cwd).length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return stripRunSubcommand(argv);
|
||||
return resolveDelegatedVitestArgs(argv);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1138,8 +1157,8 @@ export function installVitestNoOutputWatchdog(params: {
|
||||
* Forwards child output while optionally suppressing complete stderr lines.
|
||||
*/
|
||||
function forwardVitestOutput(
|
||||
stream: Readable | null,
|
||||
target: Writable,
|
||||
stream: VitestOutputStream | null,
|
||||
target: VitestOutputTarget,
|
||||
shouldSuppressLine: (line: string) => boolean = () => false,
|
||||
): void {
|
||||
if (!stream) {
|
||||
@@ -1185,7 +1204,7 @@ export function spawnWatchedVitestProcess({
|
||||
label?: string;
|
||||
onNoOutputTimeout?: () => void;
|
||||
}) {
|
||||
let forwardedSignal: NodeJS.Signals | null = null;
|
||||
let forwardedSignal: NodeSignal | null = null;
|
||||
const child = spawnVitestProcess({
|
||||
pnpmArgs,
|
||||
spawnParams,
|
||||
|
||||
@@ -1233,6 +1233,9 @@ function resolveExactSourceDirectoryTestTargets(targetArg: string, cwd: string)
|
||||
if (!isExactSourceDirectoryTarget(relative)) {
|
||||
return null;
|
||||
}
|
||||
if (isCanonicalAgentOwnerDirectoryTarget(targetArg, cwd)) {
|
||||
return [targetArg];
|
||||
}
|
||||
const prefix = `${relative}/`;
|
||||
const lightTargets = uniqueOrdered([
|
||||
...getUnitFastTestFiles(),
|
||||
@@ -1242,6 +1245,20 @@ function resolveExactSourceDirectoryTestTargets(targetArg: string, cwd: string)
|
||||
return lightTargets.length > 0 ? [...lightTargets, targetArg] : null;
|
||||
}
|
||||
|
||||
function isCanonicalAgentOwnerDirectoryTarget(targetArg: string, cwd: string) {
|
||||
if (!isExistingDirectoryTarget(targetArg, cwd)) {
|
||||
return false;
|
||||
}
|
||||
const kind = classifyTarget(targetArg, cwd);
|
||||
if (kind === agentVitestProjectOwners.all.kind) {
|
||||
return false;
|
||||
}
|
||||
const relative = toRepoRelativeTarget(targetArg, cwd).replace(/\/+$/u, "");
|
||||
return Object.values(agentVitestProjectOwners).some(
|
||||
(owner) => owner.kind === kind && isPathAtOrUnder(relative, owner.root),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds explicit test path targets that do not match any known project plan.
|
||||
*/
|
||||
@@ -3650,6 +3667,7 @@ export function buildVitestRunPlans(
|
||||
const useCliTargetArgs =
|
||||
kind === "e2e" ||
|
||||
kind === "packageDocker" ||
|
||||
grouped.every((targetArg) => isCanonicalAgentOwnerDirectoryTarget(targetArg, cwd)) ||
|
||||
(kind === "default" &&
|
||||
grouped.every((targetArg) => isFileLikeTarget(toRepoRelativeTarget(targetArg, cwd))));
|
||||
const useWholeConfigTarget = grouped.some((targetArg) =>
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
import type { AnyAgentTool } from "./agent-tools.types.js";
|
||||
import { isApplyPatchAllowedForModel } from "./apply-patch-model-policy.js";
|
||||
import type { AuthProfileStore } from "./auth-profiles/types.js";
|
||||
import { resolveProcessToolScopeKey } from "./bash-process-scope.js";
|
||||
import type { ExecToolDefaults } from "./bash-tools.exec-types.js";
|
||||
import type { ProcessToolDefaults } from "./bash-tools.process.js";
|
||||
import { listChannelAgentTools } from "./channel-tools.js";
|
||||
@@ -110,29 +111,6 @@ import { wrapToolWithGatewayCallerIdentity } from "./tools/gateway-caller-contex
|
||||
|
||||
const MEMORY_FLUSH_ALLOWED_TOOL_NAMES = new Set(["read", "write"]);
|
||||
|
||||
/** Resolve the process-tool isolation key for exec/process session state. */
|
||||
export function resolveProcessToolScopeKey(params: {
|
||||
scopeKey?: string;
|
||||
sessionKey?: string;
|
||||
sessionId?: string;
|
||||
agentId?: string;
|
||||
}): string | undefined {
|
||||
const explicitScopeKey = params.scopeKey?.trim();
|
||||
if (explicitScopeKey) {
|
||||
return explicitScopeKey;
|
||||
}
|
||||
const sessionKey = params.sessionKey?.trim();
|
||||
if (sessionKey) {
|
||||
return sessionKey;
|
||||
}
|
||||
const sessionId = params.sessionId?.trim();
|
||||
if (sessionId) {
|
||||
return sessionId;
|
||||
}
|
||||
const agentId = params.agentId?.trim();
|
||||
return agentId ? `agent:${agentId}` : undefined;
|
||||
}
|
||||
|
||||
function applyModelProviderToolPolicy(
|
||||
toolsInput: AnyAgentTool[],
|
||||
params?: {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveProcessToolScopeKey } from "./bash-process-scope.js";
|
||||
|
||||
describe("resolveProcessToolScopeKey", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "explicit scope before session identifiers",
|
||||
params: {
|
||||
scopeKey: " scope:explicit ",
|
||||
sessionKey: "session-key",
|
||||
sessionId: "session-id",
|
||||
agentId: "main",
|
||||
},
|
||||
expected: "scope:explicit",
|
||||
},
|
||||
{
|
||||
name: "session key before session and agent ids",
|
||||
params: {
|
||||
scopeKey: " ",
|
||||
sessionKey: " session-key ",
|
||||
sessionId: "session-id",
|
||||
agentId: "main",
|
||||
},
|
||||
expected: "session-key",
|
||||
},
|
||||
{
|
||||
name: "session id before agent id",
|
||||
params: { sessionKey: "\t", sessionId: " session-id ", agentId: "main" },
|
||||
expected: "session-id",
|
||||
},
|
||||
{
|
||||
name: "agent id fallback",
|
||||
params: { sessionId: "\n", agentId: " main " },
|
||||
expected: "agent:main",
|
||||
},
|
||||
{
|
||||
name: "blank inputs",
|
||||
params: { scopeKey: " ", sessionKey: "\t", sessionId: "\n", agentId: " " },
|
||||
expected: undefined,
|
||||
},
|
||||
])("uses $name", ({ params, expected }) => {
|
||||
expect(resolveProcessToolScopeKey(params)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/** Resolve the process-tool isolation key for exec/process session state. */
|
||||
export function resolveProcessToolScopeKey(params: {
|
||||
scopeKey?: string;
|
||||
sessionKey?: string;
|
||||
sessionId?: string;
|
||||
agentId?: string;
|
||||
}): string | undefined {
|
||||
const explicitScopeKey = params.scopeKey?.trim();
|
||||
if (explicitScopeKey) {
|
||||
return explicitScopeKey;
|
||||
}
|
||||
const sessionKey = params.sessionKey?.trim();
|
||||
if (sessionKey) {
|
||||
return sessionKey;
|
||||
}
|
||||
const sessionId = params.sessionId?.trim();
|
||||
if (sessionId) {
|
||||
return sessionId;
|
||||
}
|
||||
const agentId = params.agentId?.trim();
|
||||
return agentId ? `agent:${agentId}` : undefined;
|
||||
}
|
||||
@@ -875,17 +875,6 @@ export async function loadCompactHooksHarness(): Promise<{
|
||||
|
||||
vi.doMock("../agent-tools.js", () => ({
|
||||
createOpenClawCodingTools: createOpenClawCodingToolsMock,
|
||||
resolveProcessToolScopeKey: ({
|
||||
scopeKey,
|
||||
sessionKey,
|
||||
sessionId,
|
||||
agentId,
|
||||
}: {
|
||||
scopeKey?: string;
|
||||
sessionKey?: string;
|
||||
sessionId?: string;
|
||||
agentId?: string;
|
||||
}) => scopeKey ?? sessionKey ?? sessionId ?? (agentId ? `agent:${agentId}` : undefined),
|
||||
}));
|
||||
|
||||
vi.doMock("./replay-history.js", () => ({
|
||||
|
||||
@@ -27,8 +27,9 @@ import { isReasoningTagProvider } from "../../utils/provider-utils.js";
|
||||
import { createBundleLspToolRuntime } from "../agent-bundle-lsp-runtime.js";
|
||||
import { createBundleMcpToolRuntime } from "../agent-bundle-mcp-tools.js";
|
||||
import { resolveSessionAgentIds } from "../agent-scope.js";
|
||||
import { createOpenClawCodingTools, resolveProcessToolScopeKey } from "../agent-tools.js";
|
||||
import { createOpenClawCodingTools } from "../agent-tools.js";
|
||||
import { listActiveProcessSessionReferences } from "../bash-process-references.js";
|
||||
import { resolveProcessToolScopeKey } from "../bash-process-scope.js";
|
||||
import {
|
||||
makeBootstrapWarn,
|
||||
resolveBootstrapContextForRun,
|
||||
|
||||
@@ -12,12 +12,6 @@ import type { AgentMessage } from "../../runtime/index.js";
|
||||
import { hasNonzeroUsage, normalizeUsage, type NormalizedUsage } from "../../usage.js";
|
||||
import type { PromptCacheChange } from "../prompt-cache-observability.js";
|
||||
import type { EmbeddedRunAttemptResult } from "./types.js";
|
||||
export {
|
||||
assembleHarnessContextEngine as assembleAttemptContextEngine,
|
||||
bootstrapHarnessContextEngine as runAttemptContextEngineBootstrap,
|
||||
finalizeHarnessContextEngineTurn as finalizeAttemptContextEngineTurn,
|
||||
} from "../../harness/context-engine-lifecycle.js";
|
||||
|
||||
export type AttemptContextEngine = ContextEngine;
|
||||
|
||||
type AttemptBootstrapContext<TBootstrapFile = unknown, TContextFile = unknown> = {
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { createCacheTrace } from "../../cache-trace.js";
|
||||
import { countActiveToolExecutions } from "../../embedded-agent-subscribe.handlers.tools.js";
|
||||
import { isSignalTimeoutReason } from "../../failover-error.js";
|
||||
import { runAgentEndSideEffects } from "../../harness/agent-end-side-effects.js";
|
||||
import { finalizeHarnessContextEngineTurn } from "../../harness/context-engine-lifecycle.js";
|
||||
import { runAgentCleanupStep } from "../../run-cleanup-timeout.js";
|
||||
import type { AgentMessage } from "../../runtime/index.js";
|
||||
import type { AgentSession, SessionManager } from "../../sessions/index.js";
|
||||
@@ -28,10 +29,7 @@ import { runContextEngineMaintenance } from "../context-engine-maintenance.js";
|
||||
import { log } from "../logger.js";
|
||||
import { markActiveEmbeddedRunAbandoned, type EmbeddedAgentQueueHandle } from "../runs.js";
|
||||
import { buildEmbeddedAgentEndContext } from "./agent-end-context.js";
|
||||
import {
|
||||
finalizeAttemptContextEngineTurn,
|
||||
type buildContextEnginePromptCacheInfo,
|
||||
} from "./attempt-context-engine-helpers.js";
|
||||
import type { buildContextEnginePromptCacheInfo } from "./attempt-context-engine-helpers.js";
|
||||
import { buildAfterTurnRuntimeContextFromUsage } from "./attempt-prompt-helpers.js";
|
||||
import { shouldPersistCompletedBootstrapTurn } from "./attempt-thread-helpers.js";
|
||||
import {
|
||||
@@ -253,7 +251,7 @@ export async function completeEmbeddedAttemptAfterTurn(
|
||||
sessionManager?: SessionManager;
|
||||
withSessionManagerRewriteLock: WithOwnedTranscriptWrite;
|
||||
}) => {
|
||||
await finalizeAttemptContextEngineTurn({
|
||||
await finalizeHarnessContextEngineTurn({
|
||||
contextEngine: activeContextEngine,
|
||||
promptError: Boolean(state.promptError),
|
||||
aborted: lifecycleState.aborted,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import type { createPreparedEmbeddedAgentSettingsManager } from "../../agent-project-settings.js";
|
||||
import type { createCacheTrace } from "../../cache-trace.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../defaults.js";
|
||||
import { assembleHarnessContextEngine } from "../../harness/context-engine-lifecycle.js";
|
||||
import type { AgentRuntimePlan } from "../../runtime-plan/types.js";
|
||||
import type { AgentMessage } from "../../runtime/index.js";
|
||||
import type { AgentSession, SessionManager } from "../../sessions/index.js";
|
||||
@@ -32,10 +33,7 @@ import { resolveTranscriptPolicy, type TranscriptPolicy } from "../../transcript
|
||||
import { getHistoryLimitFromSessionKey, limitHistoryTurns } from "../history.js";
|
||||
import { log } from "../logger.js";
|
||||
import { sanitizeSessionHistory, validateReplayTurns } from "../replay-history.js";
|
||||
import {
|
||||
assembleAttemptContextEngine,
|
||||
type AttemptContextEngine,
|
||||
} from "./attempt-context-engine-helpers.js";
|
||||
import type { AttemptContextEngine } from "./attempt-context-engine-helpers.js";
|
||||
import type { resolveOrphanRepairPlan } from "./attempt-orphan-repair.js";
|
||||
import { prependSystemPromptAddition } from "./attempt-prompt-helpers.js";
|
||||
import { isRunnerToolCallBlockType } from "./attempt-tool-call-block-type.js";
|
||||
@@ -575,7 +573,7 @@ export async function prepareEmbeddedAttemptHistory(input: {
|
||||
});
|
||||
const messageBudget = Math.max(1, promptBudget - renderedPromptTokens);
|
||||
const transcriptReadFence = attempt.userTurnTranscriptRecorder?.getAdmissionReceipt();
|
||||
const assembled = await assembleAttemptContextEngine({
|
||||
const assembled = await assembleHarnessContextEngine({
|
||||
contextEngine: input.activeContextEngine,
|
||||
sessionId: attempt.sessionId,
|
||||
sessionKey: attempt.sessionKey,
|
||||
|
||||
@@ -21,8 +21,8 @@ import { isCronSessionKey, isSubagentSessionKey } from "../../../routing/session
|
||||
import { shouldPreserveUserFacingSessionStateForInputProvenance } from "../../../sessions/input-provenance.js";
|
||||
import { joinPresentTextSegments } from "../../../shared/text/join-segments.js";
|
||||
import { truncateUtf16Safe } from "../../../utils.js";
|
||||
import { resolveProcessToolScopeKey } from "../../agent-tools.js";
|
||||
import { listActiveProcessSessionReferences } from "../../bash-process-references.js";
|
||||
import { resolveProcessToolScopeKey } from "../../bash-process-scope.js";
|
||||
import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js";
|
||||
import { wrapPluginSystemContextSection } from "../../hook-system-context-boundary.js";
|
||||
import {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "../../agent-settings.js";
|
||||
import { toToolDefinitions } from "../../agent-tool-definition-adapter.js";
|
||||
import { resolveUserTimezone } from "../../date-time.js";
|
||||
import { bootstrapHarnessContextEngine } from "../../harness/context-engine-lifecycle.js";
|
||||
import { relocateCurrentRuntimeContextCarrierToTail } from "../../internal-runtime-context.js";
|
||||
import type { AgentMessage } from "../../runtime/index.js";
|
||||
import { guardSessionManager } from "../../session-tool-result-guard-wrapper.js";
|
||||
@@ -36,10 +37,7 @@ import { log } from "../logger.js";
|
||||
import { createEmbeddedAgentResourceLoader } from "../resource-loader.js";
|
||||
import { applySystemPromptToSession } from "../system-prompt.js";
|
||||
import { prepareEmbeddedAttemptClientTools } from "./attempt-client-tools.js";
|
||||
import {
|
||||
type AttemptContextEngine,
|
||||
runAttemptContextEngineBootstrap,
|
||||
} from "./attempt-context-engine-helpers.js";
|
||||
import type { AttemptContextEngine } from "./attempt-context-engine-helpers.js";
|
||||
import { resolveAttemptTranscriptPolicy } from "./attempt-history.js";
|
||||
import { normalizeMessagesForLlmBoundary } from "./attempt-llm-boundary.js";
|
||||
import {
|
||||
@@ -484,7 +482,7 @@ export async function prepareEmbeddedAttemptSessionManager(input: {
|
||||
input.onSessionManagerCreated(sessionManager);
|
||||
|
||||
await input.withOwnedTranscriptWrite(async () => {
|
||||
await runAttemptContextEngineBootstrap({
|
||||
await bootstrapHarnessContextEngine({
|
||||
hadSessionFile: transcriptState.hasBootstrapTranscriptState,
|
||||
contextEngine: input.activeContextEngine,
|
||||
sessionId: attempt.sessionId,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import { expect, vi, type Mock } from "vitest";
|
||||
import { vi, type Mock } from "vitest";
|
||||
import type {
|
||||
AssembleResult,
|
||||
BootstrapResult,
|
||||
@@ -667,17 +667,6 @@ vi.mock("../../cache-trace.js", () => ({
|
||||
vi.mock("../../agent-tools.js", () => ({
|
||||
createOpenClawCodingTools: (options?: { workspaceDir?: string; spawnWorkspaceDir?: string }) =>
|
||||
hoisted.createOpenClawCodingToolsMock(options),
|
||||
resolveProcessToolScopeKey: ({
|
||||
scopeKey,
|
||||
sessionKey,
|
||||
sessionId,
|
||||
agentId,
|
||||
}: {
|
||||
scopeKey?: string;
|
||||
sessionKey?: string;
|
||||
sessionId?: string;
|
||||
agentId?: string;
|
||||
}) => scopeKey ?? sessionKey ?? sessionId ?? (agentId ? `agent:${agentId}` : undefined),
|
||||
resolveToolLoopDetectionConfig: () => undefined,
|
||||
}));
|
||||
|
||||
@@ -1238,10 +1227,6 @@ export function createContextEngineBootstrapAndAssemble() {
|
||||
};
|
||||
}
|
||||
|
||||
export function expectCalledWithSessionKey(mock: ReturnType<typeof vi.fn>, sessionKey: string) {
|
||||
expect(mock).toHaveBeenCalledWith(expect.objectContaining({ sessionKey }));
|
||||
}
|
||||
|
||||
const testModel = {
|
||||
api: "openai-completions",
|
||||
provider: "openai",
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
} from "../../../plugins/provider-runtime.js";
|
||||
import { normalizeMessageChannel } from "../../../utils/message-channel.js";
|
||||
import { isReasoningTagProvider } from "../../../utils/provider-utils.js";
|
||||
import { resolveProcessToolScopeKey } from "../../agent-tools.js";
|
||||
import { listActiveProcessSessionReferences } from "../../bash-process-references.js";
|
||||
import { resolveProcessToolScopeKey } from "../../bash-process-scope.js";
|
||||
import {
|
||||
buildBootstrapPromptWarningNotice,
|
||||
buildBootstrapTruncationReportMeta,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AssistantMessage } from "../../../llm/types.js";
|
||||
import { findLatestUncompactedAttemptUsageSnapshot } from "./attempt-context-engine-helpers.js";
|
||||
import type { AgentMessage } from "../../runtime/index.js";
|
||||
import {
|
||||
buildContextEnginePromptCacheInfo,
|
||||
buildLoopPromptCacheInfo,
|
||||
findLatestUncompactedAttemptUsageSnapshot,
|
||||
resolvePromptCacheTouchTimestamp,
|
||||
} from "./attempt-context-engine-helpers.js";
|
||||
|
||||
const ASSISTANT_WITH_USAGE = {
|
||||
role: "assistant",
|
||||
@@ -41,3 +47,122 @@ describe("findLatestUncompactedAttemptUsageSnapshot", () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("context-engine prompt cache metadata", () => {
|
||||
const seedMessage = { role: "user", content: "seed", timestamp: 1 } as AgentMessage;
|
||||
|
||||
it("builds retention, last-call usage, and cache-touch metadata", () => {
|
||||
expect(
|
||||
buildContextEnginePromptCacheInfo({
|
||||
retention: "short",
|
||||
lastCallUsage: { input: 10, output: 5, cacheRead: 40, cacheWrite: 2, total: 57 },
|
||||
lastCacheTouchAt: 123,
|
||||
}),
|
||||
).toEqual({
|
||||
retention: "short",
|
||||
lastCallUsage: { input: 10, output: 5, cacheRead: 40, cacheWrite: 2, total: 57 },
|
||||
lastCacheTouchAt: 123,
|
||||
});
|
||||
});
|
||||
|
||||
it("omits metadata when no cache data is available", () => {
|
||||
expect(buildContextEnginePromptCacheInfo({})).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not reuse a prior turn's usage when the current attempt has no assistant", () => {
|
||||
const priorAssistant = {
|
||||
role: "assistant",
|
||||
content: "prior turn",
|
||||
timestamp: 2,
|
||||
usage: { input: 99, output: 7, cacheRead: 1234, total: 1340 },
|
||||
} as unknown as AgentMessage;
|
||||
|
||||
expect(
|
||||
buildLoopPromptCacheInfo({
|
||||
messagesSnapshot: [seedMessage, priorAssistant],
|
||||
prePromptMessageCount: 2,
|
||||
retention: "short",
|
||||
}),
|
||||
).toEqual({ retention: "short" });
|
||||
});
|
||||
|
||||
it("derives live loop metadata from the current attempt assistant", () => {
|
||||
const assistant = {
|
||||
role: "assistant",
|
||||
content: "tool use",
|
||||
timestamp: "2026-04-16T16:49:59.536Z",
|
||||
usage: { input: 1, output: 2, cacheRead: 39036, cacheWrite: 59934, total: 98973 },
|
||||
} as unknown as AgentMessage;
|
||||
|
||||
const promptCache = buildLoopPromptCacheInfo({
|
||||
messagesSnapshot: [seedMessage, assistant],
|
||||
prePromptMessageCount: 1,
|
||||
retention: "short",
|
||||
fallbackLastCacheTouchAt: 123,
|
||||
});
|
||||
expect(promptCache?.retention).toBe("short");
|
||||
expect(promptCache?.lastCallUsage).toMatchObject({
|
||||
cacheRead: 39036,
|
||||
cacheWrite: 59934,
|
||||
total: 98973,
|
||||
});
|
||||
expect(promptCache?.lastCacheTouchAt).toBe(Date.parse("2026-04-16T16:49:59.536Z"));
|
||||
});
|
||||
|
||||
it("keeps the latest nonzero usage when an aborted assistant reports zeros", () => {
|
||||
const completedAssistant = {
|
||||
role: "assistant",
|
||||
content: "tool use",
|
||||
timestamp: "2026-04-16T16:49:59.536Z",
|
||||
usage: { input: 38_333, output: 66, cacheRead: 120_320, total: 158_719 },
|
||||
} as unknown as AgentMessage;
|
||||
const abortedAssistant = {
|
||||
role: "assistant",
|
||||
content: "",
|
||||
timestamp: "2026-04-16T16:50:00.000Z",
|
||||
stopReason: "aborted",
|
||||
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
} as unknown as AgentMessage;
|
||||
|
||||
const promptCache = buildLoopPromptCacheInfo({
|
||||
messagesSnapshot: [seedMessage, completedAssistant, abortedAssistant],
|
||||
prePromptMessageCount: 1,
|
||||
retention: "short",
|
||||
});
|
||||
expect(promptCache?.lastCallUsage).toMatchObject({
|
||||
input: 38_333,
|
||||
cacheRead: 120_320,
|
||||
total: 158_719,
|
||||
});
|
||||
expect(promptCache?.lastCacheTouchAt).toBe(Date.parse("2026-04-16T16:49:59.536Z"));
|
||||
});
|
||||
|
||||
it("falls back to the persisted cache touch when loop usage has no cache metrics", () => {
|
||||
const assistant = {
|
||||
role: "assistant",
|
||||
content: "tool use",
|
||||
timestamp: "2026-04-16T16:49:59.536Z",
|
||||
usage: { input: 1, output: 2, total: 3 },
|
||||
} as unknown as AgentMessage;
|
||||
|
||||
const promptCache = buildLoopPromptCacheInfo({
|
||||
messagesSnapshot: [seedMessage, assistant],
|
||||
prePromptMessageCount: 1,
|
||||
retention: "short",
|
||||
fallbackLastCacheTouchAt: 123,
|
||||
});
|
||||
expect(promptCache?.retention).toBe("short");
|
||||
expect(promptCache?.lastCallUsage?.total).toBe(3);
|
||||
expect(promptCache?.lastCacheTouchAt).toBe(123);
|
||||
});
|
||||
|
||||
it("derives a live cache touch timestamp for final afterTurn usage snapshots", () => {
|
||||
expect(
|
||||
resolvePromptCacheTouchTimestamp({
|
||||
lastCallUsage: { input: 1, output: 2, cacheRead: 39036, cacheWrite: 0, total: 39039 },
|
||||
assistantTimestamp: "2026-04-16T17:04:46.974Z",
|
||||
fallbackLastCacheTouchAt: 123,
|
||||
}),
|
||||
).toBe(Date.parse("2026-04-16T17:04:46.974Z"));
|
||||
});
|
||||
});
|
||||
|
||||
+2
-432
@@ -11,29 +11,16 @@ import {
|
||||
createSessionEntryWithTranscript,
|
||||
} from "../../../config/sessions/session-accessor.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.js";
|
||||
import { buildMemorySystemPromptAddition } from "../../../context-engine/delegate.js";
|
||||
import {
|
||||
clearMemoryPluginState,
|
||||
registerTestMemoryPromptBuilder,
|
||||
} from "../../../plugins/memory-state.test-fixtures.js";
|
||||
import { clearMemoryPluginState } from "../../../plugins/memory-state.test-fixtures.js";
|
||||
import { createUserTurnTranscriptRecorder } from "../../../sessions/user-turn-transcript.js";
|
||||
import { projectAgentRunAttemptTerminal } from "../../agent-run-terminal-outcome.js";
|
||||
import { makeAgentAssistantMessage } from "../../test-helpers/agent-message-fixtures.js";
|
||||
import {
|
||||
type AttemptContextEngine,
|
||||
buildLoopPromptCacheInfo,
|
||||
assembleAttemptContextEngine,
|
||||
buildContextEnginePromptCacheInfo,
|
||||
finalizeAttemptContextEngineTurn,
|
||||
resolvePromptCacheTouchTimestamp,
|
||||
runAttemptContextEngineBootstrap,
|
||||
} from "./attempt-context-engine-helpers.js";
|
||||
import type { AttemptContextEngine } from "./attempt-context-engine-helpers.js";
|
||||
import {
|
||||
cleanupTempPaths,
|
||||
createDefaultEmbeddedSession,
|
||||
createContextEngineBootstrapAndAssemble,
|
||||
createContextEngineAttemptRunner,
|
||||
expectCalledWithSessionKey,
|
||||
getHoisted,
|
||||
preloadRunEmbeddedAttemptForTests,
|
||||
resetEmbeddedAttemptHarness,
|
||||
@@ -43,14 +30,12 @@ import type { MidTurnPrecheckRequest } from "./midturn-precheck.js";
|
||||
|
||||
const hoisted = getHoisted();
|
||||
const embeddedSessionId = "embedded-session";
|
||||
const sessionFile = "/tmp/session.jsonl";
|
||||
const seedMessage = { role: "user", content: "seed", timestamp: 1 } as AgentMessage;
|
||||
const doneMessage = { role: "assistant", content: "done", timestamp: 2 } as unknown as AgentMessage;
|
||||
|
||||
beforeAll(async () => {
|
||||
await preloadRunEmbeddedAttemptForTests();
|
||||
});
|
||||
type AfterTurnPromptCacheCall = { runtimeContext?: { promptCache?: Record<string, unknown> } };
|
||||
type TrajectoryEvent = { type?: string; data?: Record<string, unknown> };
|
||||
type ToolResultGuardInstallParams = {
|
||||
midTurnPrecheck?: {
|
||||
@@ -156,67 +141,6 @@ function createTestContextEngine(params: Partial<AttemptContextEngine>): Attempt
|
||||
} as AttemptContextEngine;
|
||||
}
|
||||
|
||||
async function runBootstrap(
|
||||
sessionKey: string,
|
||||
contextEngine: AttemptContextEngine,
|
||||
overrides: Partial<Parameters<typeof runAttemptContextEngineBootstrap>[0]> = {},
|
||||
) {
|
||||
// Shared bootstrap harness keeps session identifiers stable across context
|
||||
// engine implementations.
|
||||
await runAttemptContextEngineBootstrap({
|
||||
hadSessionFile: true,
|
||||
contextEngine,
|
||||
sessionId: embeddedSessionId,
|
||||
sessionKey,
|
||||
sessionFile,
|
||||
sessionManager: hoisted.sessionManager,
|
||||
runtimeContext: {},
|
||||
runMaintenance: hoisted.runContextEngineMaintenanceMock,
|
||||
warn: () => {},
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
async function runAssemble(
|
||||
sessionKey: string,
|
||||
contextEngine: AttemptContextEngine,
|
||||
overrides: Partial<Parameters<typeof assembleAttemptContextEngine>[0]> = {},
|
||||
) {
|
||||
return await assembleAttemptContextEngine({
|
||||
contextEngine,
|
||||
sessionId: embeddedSessionId,
|
||||
sessionKey,
|
||||
messages: [seedMessage],
|
||||
tokenBudget: 2048,
|
||||
modelId: "gpt-test",
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
async function finalizeTurn(
|
||||
sessionKey: string,
|
||||
contextEngine: AttemptContextEngine,
|
||||
overrides: Partial<Parameters<typeof finalizeAttemptContextEngineTurn>[0]> = {},
|
||||
) {
|
||||
await finalizeAttemptContextEngineTurn({
|
||||
contextEngine,
|
||||
promptError: false,
|
||||
aborted: false,
|
||||
yieldAborted: false,
|
||||
sessionIdUsed: embeddedSessionId,
|
||||
sessionKey,
|
||||
sessionFile,
|
||||
messagesSnapshot: [doneMessage],
|
||||
prePromptMessageCount: 0,
|
||||
tokenBudget: 2048,
|
||||
runtimeContext: {},
|
||||
runMaintenance: hoisted.runContextEngineMaintenanceMock,
|
||||
sessionManager: hoisted.sessionManager,
|
||||
warn: () => {},
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
|
||||
const sessionKey = "agent:main:guildchat:channel:test-ctx-engine";
|
||||
const tempPaths: string[] = [];
|
||||
@@ -2678,24 +2602,6 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
|
||||
expect(events.slice(0, afterTurnIndex)).toContain("flush");
|
||||
});
|
||||
|
||||
it("forwards sessionKey to bootstrap, assemble, and afterTurn", async () => {
|
||||
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
|
||||
const afterTurn = vi.fn(async (_params: { sessionKey?: string }) => {});
|
||||
const contextEngine = createTestContextEngine({
|
||||
bootstrap,
|
||||
assemble,
|
||||
afterTurn,
|
||||
});
|
||||
|
||||
await runBootstrap(sessionKey, contextEngine);
|
||||
await runAssemble(sessionKey, contextEngine);
|
||||
await finalizeTurn(sessionKey, contextEngine);
|
||||
|
||||
expectCalledWithSessionKey(bootstrap, sessionKey);
|
||||
expectCalledWithSessionKey(assemble, sessionKey);
|
||||
expectCalledWithSessionKey(afterTurn, sessionKey);
|
||||
});
|
||||
|
||||
it("uses SQLite transcript messages for bootstrap without treating the marker as a file", async () => {
|
||||
const storeDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ctx-engine-sqlite-"));
|
||||
tempPaths.push(storeDir);
|
||||
@@ -2753,101 +2659,6 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
|
||||
expect(bootstrap).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("forwards modelId to assemble", async () => {
|
||||
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
|
||||
const contextEngine = createTestContextEngine({ bootstrap, assemble });
|
||||
|
||||
await runBootstrap(sessionKey, contextEngine);
|
||||
await runAssemble(sessionKey, contextEngine);
|
||||
|
||||
expect(mockParams(assemble as MockCallSource, 0, "assemble params").model).toBe("gpt-test");
|
||||
});
|
||||
|
||||
it("forwards availableTools and citationsMode to assemble", async () => {
|
||||
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
|
||||
const contextEngine = createTestContextEngine({ bootstrap, assemble });
|
||||
|
||||
await runBootstrap(sessionKey, contextEngine);
|
||||
await runAssemble(sessionKey, contextEngine, {
|
||||
availableTools: new Set(["memory_search", "wiki_search"]),
|
||||
citationsMode: "on",
|
||||
});
|
||||
|
||||
expectFields(mockParams(assemble as MockCallSource, 0, "assemble params"), {
|
||||
availableTools: new Set(["memory_search", "wiki_search"]),
|
||||
citationsMode: "on",
|
||||
});
|
||||
});
|
||||
|
||||
it("lets non-legacy engines opt into the active memory prompt helper", async () => {
|
||||
registerTestMemoryPromptBuilder(({ availableTools, citationsMode }) => {
|
||||
if (!availableTools.has("memory_search")) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
"## Memory Recall",
|
||||
`tools=${[...availableTools].toSorted().join(",")}`,
|
||||
`citations=${citationsMode ?? "auto"}`,
|
||||
"",
|
||||
];
|
||||
});
|
||||
|
||||
const contextEngine = createTestContextEngine({
|
||||
assemble: async ({ messages, availableTools, citationsMode }) => ({
|
||||
messages,
|
||||
estimatedTokens: messages.length,
|
||||
systemPromptAddition: buildMemorySystemPromptAddition({
|
||||
availableTools: availableTools ?? new Set(),
|
||||
citationsMode,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await runAssemble(sessionKey, contextEngine, {
|
||||
availableTools: new Set(["wiki_search", "memory_search"]),
|
||||
citationsMode: "on",
|
||||
});
|
||||
|
||||
const assembled = requireRecord(result, "assembled context");
|
||||
expect(assembled.estimatedTokens).toBe(1);
|
||||
expect(assembled.systemPromptAddition).toBe(
|
||||
"## Memory Recall\ntools=memory_search,wiki_search\ncitations=on",
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards sessionKey to ingestBatch when afterTurn is absent", async () => {
|
||||
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
|
||||
const ingestBatch = vi.fn(
|
||||
async (_params: { sessionKey?: string; messages: AgentMessage[] }) => ({ ingestedCount: 1 }),
|
||||
);
|
||||
|
||||
await finalizeTurn(sessionKey, createTestContextEngine({ bootstrap, assemble, ingestBatch }), {
|
||||
messagesSnapshot: [seedMessage, doneMessage],
|
||||
prePromptMessageCount: 1,
|
||||
});
|
||||
|
||||
expectCalledWithSessionKey(ingestBatch, sessionKey);
|
||||
});
|
||||
|
||||
it("forwards sessionKey to per-message ingest when ingestBatch is absent", async () => {
|
||||
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
|
||||
const ingest = vi.fn(async (_params: { sessionKey?: string; message: AgentMessage }) => ({
|
||||
ingested: true,
|
||||
}));
|
||||
|
||||
await finalizeTurn(sessionKey, createTestContextEngine({ bootstrap, assemble, ingest }), {
|
||||
messagesSnapshot: [seedMessage, doneMessage],
|
||||
prePromptMessageCount: 1,
|
||||
});
|
||||
|
||||
expect(ingest).toHaveBeenCalledTimes(1);
|
||||
expect(ingest).toHaveBeenCalledWith({
|
||||
message: doneMessage,
|
||||
sessionId: embeddedSessionId,
|
||||
sessionKey,
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards silentExpected to the embedded subscription", async () => {
|
||||
await createContextEngineAttemptRunner({
|
||||
contextEngine: createContextEngineBootstrapAndAssemble(),
|
||||
@@ -2904,247 +2715,6 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
|
||||
expect(result.didDeliverSourceReplyViaMessageTool).toBe(true);
|
||||
});
|
||||
|
||||
it("skips maintenance when afterTurn fails", async () => {
|
||||
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
|
||||
const afterTurn = vi.fn(async () => {
|
||||
throw new Error("afterTurn failed");
|
||||
});
|
||||
|
||||
await finalizeTurn(sessionKey, createTestContextEngine({ bootstrap, assemble, afterTurn }));
|
||||
|
||||
expectCalledWithSessionKey(afterTurn, sessionKey);
|
||||
expect(
|
||||
hoisted.runContextEngineMaintenanceMock.mock.calls.some(
|
||||
([params]) => requireRecord(params, "maintenance params").reason === "turn",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("runs startup maintenance for existing sessions even without bootstrap()", async () => {
|
||||
const { assemble } = createContextEngineBootstrapAndAssemble();
|
||||
|
||||
await runBootstrap(
|
||||
sessionKey,
|
||||
createTestContextEngine({
|
||||
assemble,
|
||||
maintain: async () => ({
|
||||
changed: false,
|
||||
bytesFreed: 0,
|
||||
rewrittenEntries: 0,
|
||||
reason: "test maintenance",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
hoisted.runContextEngineMaintenanceMock.mock.calls.some(
|
||||
([params]) => requireRecord(params, "maintenance params").reason === "bootstrap",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("builds prompt-cache retention, last-call usage, and cache-touch metadata", () => {
|
||||
expect(
|
||||
buildContextEnginePromptCacheInfo({
|
||||
retention: "short",
|
||||
lastCallUsage: {
|
||||
input: 10,
|
||||
output: 5,
|
||||
cacheRead: 40,
|
||||
cacheWrite: 2,
|
||||
total: 57,
|
||||
},
|
||||
lastCacheTouchAt: 123,
|
||||
}),
|
||||
).toEqual({
|
||||
retention: "short",
|
||||
lastCallUsage: {
|
||||
input: 10,
|
||||
output: 5,
|
||||
cacheRead: 40,
|
||||
cacheWrite: 2,
|
||||
total: 57,
|
||||
},
|
||||
lastCacheTouchAt: 123,
|
||||
});
|
||||
});
|
||||
|
||||
it("omits prompt-cache metadata when no cache data is available", () => {
|
||||
expect(buildContextEnginePromptCacheInfo({})).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not reuse a prior turn's usage when the current attempt has no assistant", () => {
|
||||
const priorAssistant = {
|
||||
role: "assistant",
|
||||
content: "prior turn",
|
||||
timestamp: 2,
|
||||
usage: {
|
||||
input: 99,
|
||||
output: 7,
|
||||
cacheRead: 1234,
|
||||
total: 1340,
|
||||
},
|
||||
} as unknown as AgentMessage;
|
||||
const promptCache = buildLoopPromptCacheInfo({
|
||||
messagesSnapshot: [seedMessage, priorAssistant],
|
||||
prePromptMessageCount: 2,
|
||||
retention: "short",
|
||||
});
|
||||
|
||||
expect(promptCache).toEqual({ retention: "short" });
|
||||
});
|
||||
|
||||
it("derives live loop prompt-cache info from the current attempt assistant", () => {
|
||||
const toolUseAssistant = {
|
||||
role: "assistant",
|
||||
content: "tool use",
|
||||
timestamp: "2026-04-16T16:49:59.536Z",
|
||||
usage: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
cacheRead: 39036,
|
||||
cacheWrite: 59934,
|
||||
total: 98973,
|
||||
},
|
||||
} as unknown as AgentMessage;
|
||||
|
||||
const promptCache = buildLoopPromptCacheInfo({
|
||||
messagesSnapshot: [seedMessage, toolUseAssistant],
|
||||
prePromptMessageCount: 1,
|
||||
retention: "short",
|
||||
fallbackLastCacheTouchAt: 123,
|
||||
});
|
||||
expect(promptCache?.retention).toBe("short");
|
||||
expect(promptCache?.lastCallUsage?.cacheRead).toBe(39036);
|
||||
expect(promptCache?.lastCallUsage?.cacheWrite).toBe(59934);
|
||||
expect(promptCache?.lastCallUsage?.total).toBe(98973);
|
||||
expect(promptCache?.lastCacheTouchAt).toBe(Date.parse("2026-04-16T16:49:59.536Z"));
|
||||
});
|
||||
|
||||
it("keeps the latest nonzero usage when an aborted assistant reports zeros", () => {
|
||||
const completedAssistant = {
|
||||
role: "assistant",
|
||||
content: "tool use",
|
||||
timestamp: "2026-04-16T16:49:59.536Z",
|
||||
usage: { input: 38_333, output: 66, cacheRead: 120_320, total: 158_719 },
|
||||
} as unknown as AgentMessage;
|
||||
const abortedAssistant = {
|
||||
role: "assistant",
|
||||
content: "",
|
||||
timestamp: "2026-04-16T16:50:00.000Z",
|
||||
stopReason: "aborted",
|
||||
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
} as unknown as AgentMessage;
|
||||
|
||||
const promptCache = buildLoopPromptCacheInfo({
|
||||
messagesSnapshot: [seedMessage, completedAssistant, abortedAssistant],
|
||||
prePromptMessageCount: 1,
|
||||
retention: "short",
|
||||
});
|
||||
|
||||
expect(promptCache?.lastCallUsage).toMatchObject({
|
||||
input: 38_333,
|
||||
cacheRead: 120_320,
|
||||
total: 158_719,
|
||||
});
|
||||
expect(promptCache?.lastCacheTouchAt).toBe(Date.parse("2026-04-16T16:49:59.536Z"));
|
||||
});
|
||||
|
||||
it("falls back to the persisted cache touch when loop usage has no cache metrics", () => {
|
||||
const toolUseAssistant = {
|
||||
role: "assistant",
|
||||
content: "tool use",
|
||||
timestamp: "2026-04-16T16:49:59.536Z",
|
||||
usage: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
total: 3,
|
||||
},
|
||||
} as unknown as AgentMessage;
|
||||
|
||||
const promptCache = buildLoopPromptCacheInfo({
|
||||
messagesSnapshot: [seedMessage, toolUseAssistant],
|
||||
prePromptMessageCount: 1,
|
||||
retention: "short",
|
||||
fallbackLastCacheTouchAt: 123,
|
||||
});
|
||||
expect(promptCache?.retention).toBe("short");
|
||||
expect(promptCache?.lastCallUsage?.total).toBe(3);
|
||||
expect(promptCache?.lastCacheTouchAt).toBe(123);
|
||||
});
|
||||
|
||||
it("derives a live cache touch timestamp for final afterTurn usage snapshots", () => {
|
||||
const lastCallUsage = {
|
||||
input: 1,
|
||||
output: 2,
|
||||
cacheRead: 39036,
|
||||
cacheWrite: 0,
|
||||
total: 39039,
|
||||
};
|
||||
|
||||
expect(
|
||||
resolvePromptCacheTouchTimestamp({
|
||||
lastCallUsage,
|
||||
assistantTimestamp: "2026-04-16T17:04:46.974Z",
|
||||
fallbackLastCacheTouchAt: 123,
|
||||
}),
|
||||
).toBe(Date.parse("2026-04-16T17:04:46.974Z"));
|
||||
});
|
||||
|
||||
it("threads prompt-cache break observations into afterTurn", async () => {
|
||||
const afterTurn = vi.fn(async (_params: AfterTurnPromptCacheCall) => {});
|
||||
|
||||
await finalizeTurn(sessionKey, createTestContextEngine({ afterTurn }), {
|
||||
runtimeContext: {
|
||||
promptCache: {
|
||||
observation: {
|
||||
broke: true,
|
||||
previousCacheRead: 5000,
|
||||
cacheRead: 2000,
|
||||
changes: [{ code: "systemPrompt", detail: "system prompt digest changed" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const afterTurnCall = afterTurn.mock.calls.at(0)?.[0];
|
||||
const runtimeContext = afterTurnCall?.runtimeContext;
|
||||
const observation = runtimeContext?.promptCache?.observation as
|
||||
| { broke?: boolean; previousCacheRead?: number; cacheRead?: number; changes?: unknown[] }
|
||||
| undefined;
|
||||
|
||||
const observationRecord = requireRecord(observation, "prompt cache observation");
|
||||
expectFields(observationRecord, {
|
||||
broke: true,
|
||||
previousCacheRead: 5000,
|
||||
cacheRead: 2000,
|
||||
});
|
||||
expect(
|
||||
requireRecords(observationRecord.changes, "prompt cache observation changes").some(
|
||||
(change) => change.code === "systemPrompt",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("skips maintenance when ingestBatch fails", async () => {
|
||||
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
|
||||
const ingestBatch = vi.fn(async () => {
|
||||
throw new Error("ingestBatch failed");
|
||||
});
|
||||
|
||||
await finalizeTurn(sessionKey, createTestContextEngine({ bootstrap, assemble, ingestBatch }), {
|
||||
messagesSnapshot: [seedMessage, doneMessage],
|
||||
prePromptMessageCount: 1,
|
||||
});
|
||||
|
||||
expectCalledWithSessionKey(ingestBatch, sessionKey);
|
||||
expect(
|
||||
hoisted.runContextEngineMaintenanceMock.mock.calls.some(
|
||||
([params]) => requireRecord(params, "maintenance params").reason === "turn",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("disposes the session even when teardown cleanup throws", async () => {
|
||||
const disposeMock = vi.fn();
|
||||
const flushMock = vi.fn(async () => {
|
||||
|
||||
+2
-2
@@ -4,10 +4,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { filterHeartbeatTranscriptArtifacts } from "../../../auto-reply/heartbeat-filter.js";
|
||||
import { HEARTBEAT_PROMPT } from "../../../auto-reply/heartbeat.js";
|
||||
import type { BootstrapContextRunKind } from "../../bootstrap-mode.js";
|
||||
import { assembleHarnessContextEngine } from "../../harness/context-engine-lifecycle.js";
|
||||
import { limitHistoryTurns } from "../history.js";
|
||||
import { buildEmbeddedMessageActionDiscoveryInput } from "../message-action-discovery-input.js";
|
||||
import {
|
||||
assembleAttemptContextEngine,
|
||||
type AttemptContextEngine,
|
||||
resolveAttemptBootstrapContext,
|
||||
} from "./attempt-context-engine-helpers.js";
|
||||
@@ -232,7 +232,7 @@ describe("embedded attempt context injection", () => {
|
||||
HEARTBEAT_PROMPT,
|
||||
);
|
||||
const limited = limitHistoryTurns(heartbeatFiltered, 1);
|
||||
await assembleAttemptContextEngine({
|
||||
await assembleHarnessContextEngine({
|
||||
contextEngine: {
|
||||
info: { id: "test", name: "Test", version: "0.0.1" },
|
||||
ingest: async () => ({ ingested: true }),
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
resolveCompactionSuccessorTranscript,
|
||||
type ContextEngineSessionTarget,
|
||||
} from "../../../context-engine/types.js";
|
||||
import { resolveProcessToolScopeKey } from "../../agent-tools.js";
|
||||
import { listActiveProcessSessionReferences } from "../../bash-process-references.js";
|
||||
import { resolveProcessToolScopeKey } from "../../bash-process-scope.js";
|
||||
import { buildEmbeddedCompactionRuntimeContext } from "../compaction-runtime-context.js";
|
||||
import {
|
||||
compactContextEngineWithSafetyTimeout,
|
||||
|
||||
@@ -86,6 +86,52 @@ function uniqueConfiguredProofEngineId() {
|
||||
}
|
||||
|
||||
describe("harness context engine lifecycle", () => {
|
||||
it("forwards session keys across bootstrap, assemble, and afterTurn hooks", async () => {
|
||||
const bootstrap = vi.fn(async () => ({ bootstrapped: true }));
|
||||
const assemble = vi.fn(async (params: Parameters<ContextEngine["assemble"]>[0]) => ({
|
||||
messages: params.messages,
|
||||
estimatedTokens: 0,
|
||||
}));
|
||||
const afterTurn = vi.fn(async () => {});
|
||||
const contextEngine = createContextEngine({ bootstrap, assemble, afterTurn });
|
||||
|
||||
await bootstrapHarnessContextEngine({
|
||||
hadSessionFile: true,
|
||||
contextEngine,
|
||||
sessionId: sessionParams.sessionId,
|
||||
sessionKey: sessionParams.sessionKey,
|
||||
sessionFile: sessionParams.sessionFile,
|
||||
runMaintenance: async () => undefined,
|
||||
warn: () => {},
|
||||
});
|
||||
await assembleHarnessContextEngine({
|
||||
contextEngine,
|
||||
sessionId: sessionParams.sessionId,
|
||||
sessionKey: sessionParams.sessionKey,
|
||||
messages: [textMessage("user", "ask", 1)],
|
||||
modelId: "gpt-test",
|
||||
});
|
||||
await finalizeHarnessContextEngineTurn({
|
||||
contextEngine,
|
||||
promptError: false,
|
||||
aborted: false,
|
||||
yieldAborted: false,
|
||||
sessionIdUsed: sessionParams.sessionIdUsed,
|
||||
sessionKey: sessionParams.sessionKey,
|
||||
sessionFile: sessionParams.sessionFile,
|
||||
messagesSnapshot: [textMessage("assistant", "done", 2)],
|
||||
prePromptMessageCount: 0,
|
||||
runMaintenance: async () => undefined,
|
||||
warn: () => {},
|
||||
});
|
||||
|
||||
for (const hook of [bootstrap, assemble, afterTurn]) {
|
||||
expect(hook).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionKey: sessionParams.sessionKey }),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("scopes async memory preparation to non-legacy assembly with sandbox context", async () => {
|
||||
const prepare = vi.fn(async ({ sandboxed }) => [
|
||||
"## Prepared Memory",
|
||||
@@ -212,7 +258,15 @@ describe("harness context engine lifecycle", () => {
|
||||
const bootstrapRuntimeContext = {
|
||||
transcriptStorage: { kind: "sqlite" as const },
|
||||
sessionTarget,
|
||||
};
|
||||
promptCache: {
|
||||
observation: {
|
||||
broke: true,
|
||||
previousCacheRead: 5000,
|
||||
cacheRead: 2000,
|
||||
changes: [{ code: "systemPrompt", detail: "system prompt digest changed" }],
|
||||
},
|
||||
},
|
||||
} satisfies ContextEngineRuntimeContext;
|
||||
const engine = createContextEngine({
|
||||
info: {
|
||||
id: engineId,
|
||||
@@ -238,6 +292,7 @@ describe("harness context engine lifecycle", () => {
|
||||
afterTurn: vi.fn(async (params) => {
|
||||
captured.push({
|
||||
hook: "afterTurn",
|
||||
runtimeContext: params.runtimeContext,
|
||||
runtimeSettings: params.runtimeSettings,
|
||||
sessionTarget: params.sessionTarget,
|
||||
});
|
||||
@@ -282,6 +337,7 @@ describe("harness context engine lifecycle", () => {
|
||||
sessionKey: sessionParams.sessionKey,
|
||||
messages: [textMessage("user", "visible ask", 1)],
|
||||
tokenBudget: 2048,
|
||||
runtimeContext: bootstrapRuntimeContext,
|
||||
providerId: "openai",
|
||||
requestedModelId: "openai/gpt-5.5",
|
||||
modelId: "anthropic/claude-sonnet-4-6",
|
||||
@@ -305,6 +361,7 @@ describe("harness context engine lifecycle", () => {
|
||||
],
|
||||
prePromptMessageCount: 2,
|
||||
tokenBudget: 2048,
|
||||
runtimeContext: bootstrapRuntimeContext,
|
||||
providerId: "openai",
|
||||
requestedModelId: "openai/gpt-5.5",
|
||||
modelId: "anthropic/claude-sonnet-4-6",
|
||||
@@ -349,6 +406,9 @@ describe("harness context engine lifecycle", () => {
|
||||
expect(captured.find((entry) => entry.hook === "afterTurn")?.sessionTarget).toEqual(
|
||||
sessionTarget,
|
||||
);
|
||||
expect(captured.find((entry) => entry.hook === "afterTurn")?.runtimeContext).toEqual(
|
||||
bootstrapRuntimeContext,
|
||||
);
|
||||
expect(captured.find((entry) => entry.hook === "maintain")?.sessionTarget).toEqual(
|
||||
sessionTarget,
|
||||
);
|
||||
@@ -555,10 +615,11 @@ describe("harness context engine lifecycle", () => {
|
||||
const ingestBatchCalls = (ingestBatch as unknown as { mock: { calls: unknown[][] } }).mock
|
||||
.calls;
|
||||
const ingestBatchParams = ingestBatchCalls[0]?.[0] as
|
||||
| { isHeartbeat?: boolean; messages?: AgentMessage[] }
|
||||
| { isHeartbeat?: boolean; messages?: AgentMessage[]; sessionKey?: string }
|
||||
| undefined;
|
||||
expect(ingestBatchParams?.messages).toEqual([turnUser, turnAssistant]);
|
||||
expect(ingestBatchParams?.isHeartbeat).toBe(true);
|
||||
expect(ingestBatchParams?.sessionKey).toBe(sessionParams.sessionKey);
|
||||
});
|
||||
|
||||
it("forwards heartbeat state to per-message ingest fallbacks", async () => {
|
||||
@@ -586,11 +647,77 @@ describe("harness context engine lifecycle", () => {
|
||||
const ingestCalls = (ingest as unknown as { mock: { calls: unknown[][] } }).mock.calls;
|
||||
expect(ingestCalls).toHaveLength(2);
|
||||
for (const call of ingestCalls) {
|
||||
const ingestParams = call[0] as { isHeartbeat?: boolean };
|
||||
const ingestParams = call[0] as { isHeartbeat?: boolean; sessionKey?: string };
|
||||
expect(ingestParams.isHeartbeat).toBe(true);
|
||||
expect(ingestParams.sessionKey).toBe(sessionParams.sessionKey);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["afterTurn", "ingestBatch"] as const)(
|
||||
"skips turn maintenance when %s fails",
|
||||
async (failingHook) => {
|
||||
const runMaintenance = vi.fn(async () => undefined);
|
||||
const contextEngine = createContextEngine({
|
||||
afterTurn:
|
||||
failingHook === "afterTurn"
|
||||
? vi.fn(async () => {
|
||||
throw new Error("afterTurn failed");
|
||||
})
|
||||
: undefined,
|
||||
ingestBatch:
|
||||
failingHook === "ingestBatch"
|
||||
? vi.fn(async () => {
|
||||
throw new Error("ingestBatch failed");
|
||||
})
|
||||
: undefined,
|
||||
});
|
||||
|
||||
await finalizeHarnessContextEngineTurn({
|
||||
contextEngine,
|
||||
promptError: false,
|
||||
aborted: false,
|
||||
yieldAborted: false,
|
||||
sessionIdUsed: sessionParams.sessionIdUsed,
|
||||
sessionKey: sessionParams.sessionKey,
|
||||
sessionFile: sessionParams.sessionFile,
|
||||
messagesSnapshot: [textMessage("assistant", "done", 1)],
|
||||
prePromptMessageCount: 0,
|
||||
runMaintenance,
|
||||
warn: () => {},
|
||||
});
|
||||
|
||||
expect(runMaintenance).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("runs bootstrap maintenance for existing sessions without bootstrap()", async () => {
|
||||
const runMaintenance = vi.fn(async () => undefined);
|
||||
|
||||
await bootstrapHarnessContextEngine({
|
||||
hadSessionFile: true,
|
||||
contextEngine: createContextEngine({
|
||||
bootstrap: undefined,
|
||||
maintain: vi.fn(async () => ({
|
||||
changed: false,
|
||||
bytesFreed: 0,
|
||||
rewrittenEntries: 0,
|
||||
})),
|
||||
}),
|
||||
sessionId: sessionParams.sessionId,
|
||||
sessionKey: sessionParams.sessionKey,
|
||||
sessionFile: sessionParams.sessionFile,
|
||||
runMaintenance,
|
||||
warn: () => {},
|
||||
});
|
||||
|
||||
expect(runMaintenance).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
reason: "bootstrap",
|
||||
sessionKey: sessionParams.sessionKey,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ promptError: true, aborted: false, yieldAborted: false },
|
||||
{ promptError: false, aborted: true, yieldAborted: false },
|
||||
|
||||
@@ -348,11 +348,11 @@ describe("scripts/run-vitest", () => {
|
||||
[["run", file], [file]],
|
||||
[
|
||||
["run", file, "--reporter=verbose"],
|
||||
[file, "--reporter=verbose"],
|
||||
[file, "--", "--reporter=verbose"],
|
||||
],
|
||||
[
|
||||
["--reporter=verbose", "run", file],
|
||||
["--reporter=verbose", file],
|
||||
[file, "--", "--reporter=verbose"],
|
||||
],
|
||||
[
|
||||
["run", file, "--", "--watch"],
|
||||
@@ -373,6 +373,7 @@ describe("scripts/run-vitest", () => {
|
||||
expect(resolveTestProjectsDelegationArgs([file])).toEqual([file]);
|
||||
expect(resolveTestProjectsDelegationArgs(["run", file, "--reporter=verbose"])).toEqual([
|
||||
file,
|
||||
"--",
|
||||
"--reporter=verbose",
|
||||
]);
|
||||
});
|
||||
@@ -381,7 +382,7 @@ describe("scripts/run-vitest", () => {
|
||||
expect(resolveTestProjectsDelegationArgs(["test/scripts"])).toEqual(["test/scripts"]);
|
||||
expect(
|
||||
resolveTestProjectsDelegationArgs(["run", "test/scripts", "--reporter=verbose"]),
|
||||
).toEqual(["test/scripts", "--reporter=verbose"]);
|
||||
).toEqual(["test/scripts", "--", "--reporter=verbose"]);
|
||||
expect(resolveTestProjectsDelegationArgs(["test/scripts/*.test.ts"])).toEqual([
|
||||
"test/scripts/*.test.ts",
|
||||
]);
|
||||
@@ -392,6 +393,15 @@ describe("scripts/run-vitest", () => {
|
||||
expect(resolveTestProjectsDelegationArgs([prefix])).toEqual([prefix]);
|
||||
});
|
||||
|
||||
it("delegates owned agent directories with separate Vitest option values", () => {
|
||||
const directory = "src/agents/embedded-agent-runner/run";
|
||||
|
||||
expect(resolveTestProjectsDelegationArgs([directory])).toEqual([directory]);
|
||||
expect(
|
||||
resolveTestProjectsDelegationArgs([directory, "--sequence.shuffle", "--sequence.seed", "3"]),
|
||||
).toEqual([directory, "--", "--sequence.shuffle", "--sequence.seed", "3"]);
|
||||
});
|
||||
|
||||
it("delegates mixed filters when an explicit file target is present", () => {
|
||||
expect(
|
||||
resolveTestProjectsDelegationArgs(["src/agents", "test/scripts/run-vitest.test.ts"]),
|
||||
@@ -420,15 +430,29 @@ describe("scripts/run-vitest", () => {
|
||||
["--run=false", "test/scripts/run-vitest.test.ts"],
|
||||
["--no-run", "test/scripts/run-vitest.test.ts"],
|
||||
["--run", "false", "test/scripts/run-vitest.test.ts"],
|
||||
["--diff", "scripts/run-vitest.mjs"],
|
||||
["--testNamePattern", "run", "test/scripts/run-vitest.test.ts"],
|
||||
["run", "test/scripts/run-vitest.test.ts", "-t", "src"],
|
||||
];
|
||||
for (const argv of directArgvCases) {
|
||||
expect(resolveTestProjectsDelegationArgs(argv)).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
["--diff", "scripts/run-vitest.mjs", "test/scripts/run-vitest.test.ts"],
|
||||
["test/scripts/run-vitest.test.ts", "--", "--diff", "scripts/run-vitest.mjs"],
|
||||
],
|
||||
[
|
||||
["--testNamePattern", "run", "test/scripts/run-vitest.test.ts"],
|
||||
["test/scripts/run-vitest.test.ts", "--", "--testNamePattern", "run"],
|
||||
],
|
||||
[
|
||||
["run", "test/scripts/run-vitest.test.ts", "-t", "src"],
|
||||
["test/scripts/run-vitest.test.ts", "--", "-t", "src"],
|
||||
],
|
||||
])("keeps option value %j out of project target classification", (argv, expected) => {
|
||||
expect(resolveTestProjectsDelegationArgs(argv)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("reports missing explicit test files before Vitest can silently ignore them", () => {
|
||||
const fsImpl = {
|
||||
existsSync: (filePath: string) =>
|
||||
|
||||
@@ -1048,21 +1048,29 @@ describe("scripts/test-projects changed-target routing", () => {
|
||||
["src/agents/runtime-plan", "test/vitest/vitest.agents-support.config.ts"],
|
||||
["src/agents/tools", "test/vitest/vitest.agents-tools.config.ts"],
|
||||
])("routes focused agent directory %s to its owning shard", (directory, config) => {
|
||||
const plans = buildVitestRunPlans([directory]);
|
||||
|
||||
expect(plans).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect(buildVitestRunPlans([directory])).toEqual([
|
||||
{
|
||||
config,
|
||||
forwardedArgs: [],
|
||||
includePatterns: [`${directory}/**/*.test.ts`],
|
||||
forwardedArgs: [directory],
|
||||
includePatterns: null,
|
||||
watchMode: false,
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(plans.map((plan) => plan.config)).not.toContain(
|
||||
"test/vitest/vitest.agents-core.config.ts",
|
||||
);
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps shuffle options on the single owning embedded-run shard", () => {
|
||||
const directory = "src/agents/embedded-agent-runner/run";
|
||||
|
||||
expect(
|
||||
buildVitestRunPlans([directory, "--", "--sequence.shuffle", "--sequence.seed", "3"]),
|
||||
).toEqual([
|
||||
{
|
||||
config: "test/vitest/vitest.agents-embedded-agent-run.config.ts",
|
||||
forwardedArgs: ["--sequence.shuffle", "--sequence.seed", "3", directory],
|
||||
includePatterns: null,
|
||||
watchMode: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("splits the embedded-agent parent directory across every isolated harness", () => {
|
||||
|
||||
Reference in New Issue
Block a user