Tests: narrow bootstrap routing coverage

This commit is contained in:
Gustavo Madeira Santana
2026-04-17 12:17:19 -04:00
parent 79cd5ed368
commit c66703300a
3 changed files with 107 additions and 76 deletions

View File

@@ -0,0 +1,71 @@
import type { BootstrapMode } from "../../bootstrap-mode.js";
import { resolveBootstrapMode } from "../../bootstrap-mode.js";
import { buildAgentUserPromptPrefix } from "../../system-prompt.js";
export type AttemptBootstrapRoutingInput = {
workspaceBootstrapPending: boolean;
bootstrapContextRunKind?: "default" | "heartbeat" | "cron";
trigger?: string;
sessionKey?: string;
isPrimaryRun: boolean;
isCanonicalWorkspace?: boolean;
effectiveWorkspace: string;
resolvedWorkspace: string;
hasBootstrapFileAccess: boolean;
};
export type AttemptBootstrapRouting = {
bootstrapMode: BootstrapMode;
shouldStripBootstrapFromContext: boolean;
userPromptPrefixText?: string;
};
export type AttemptWorkspaceBootstrapRoutingInput = Omit<
AttemptBootstrapRoutingInput,
"workspaceBootstrapPending"
> & {
isWorkspaceBootstrapPending: (workspaceDir: string) => Promise<boolean>;
};
export function shouldStripBootstrapFromEmbeddedContext(_params: {
bootstrapMode: BootstrapMode;
}): boolean {
return true;
}
export function resolveAttemptBootstrapRouting(
params: AttemptBootstrapRoutingInput,
): AttemptBootstrapRouting {
const bootstrapMode = resolveBootstrapMode({
bootstrapPending: params.workspaceBootstrapPending,
runKind: params.bootstrapContextRunKind ?? "default",
isInteractiveUserFacing: params.trigger === "user" || params.trigger === "manual",
isPrimaryRun: params.isPrimaryRun,
isCanonicalWorkspace:
(params.isCanonicalWorkspace ?? true) &&
params.effectiveWorkspace === params.resolvedWorkspace,
hasBootstrapFileAccess: params.hasBootstrapFileAccess,
});
return {
bootstrapMode,
shouldStripBootstrapFromContext: shouldStripBootstrapFromEmbeddedContext({
bootstrapMode,
}),
userPromptPrefixText: buildAgentUserPromptPrefix({
bootstrapMode,
}),
};
}
export async function resolveAttemptWorkspaceBootstrapRouting(
params: AttemptWorkspaceBootstrapRoutingInput,
): Promise<AttemptBootstrapRouting> {
const workspaceBootstrapPending = await params.isWorkspaceBootstrapPending(
params.resolvedWorkspace,
);
return resolveAttemptBootstrapRouting({
...params,
workspaceBootstrapPending,
});
}

View File

@@ -1,60 +1,28 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
cleanupTempPaths,
createContextEngineAttemptRunner,
getHoisted,
resetEmbeddedAttemptHarness,
} from "./attempt.spawn-workspace.test-support.js";
const hoisted = getHoisted();
import { describe, expect, it, vi } from "vitest";
import { resolveAttemptWorkspaceBootstrapRouting } from "./attempt-bootstrap-routing.js";
describe("runEmbeddedAttempt bootstrap routing", () => {
const tempPaths: string[] = [];
beforeEach(() => {
resetEmbeddedAttemptHarness();
});
afterEach(async () => {
await cleanupTempPaths(tempPaths);
});
it("resolves bootstrap pending from the canonical workspace instead of a copied sandbox", async () => {
const sandboxWorkspace = "/tmp/openclaw-sandbox-copy";
let capturedPrompt = "";
hoisted.resolveSandboxContextMock.mockResolvedValue({
enabled: true,
workspaceAccess: "ro",
workspaceDir: sandboxWorkspace,
});
hoisted.isWorkspaceBootstrapPendingMock.mockImplementation(async (workspaceDir: string) => {
const canonicalWorkspace = "/tmp/openclaw-canonical-workspace";
const isWorkspaceBootstrapPending = vi.fn(async (workspaceDir: string) => {
return workspaceDir === sandboxWorkspace;
});
await createContextEngineAttemptRunner({
sessionKey: "agent:main:bootstrap-canonical-workspace",
tempPaths,
contextEngine: {
assemble: async ({ messages }) => ({
messages,
estimatedTokens: 1,
}),
},
attemptOverrides: {
disableTools: true,
},
sessionPrompt: async (session, prompt) => {
capturedPrompt = prompt;
session.messages = [
...session.messages,
{ role: "assistant", content: "done", timestamp: 2 } as never,
];
},
const routing = await resolveAttemptWorkspaceBootstrapRouting({
isWorkspaceBootstrapPending,
trigger: "user",
isPrimaryRun: true,
isCanonicalWorkspace: true,
effectiveWorkspace: sandboxWorkspace,
resolvedWorkspace: canonicalWorkspace,
hasBootstrapFileAccess: true,
});
expect(hoisted.isWorkspaceBootstrapPendingMock).toHaveBeenCalledTimes(1);
expect(hoisted.isWorkspaceBootstrapPendingMock).not.toHaveBeenCalledWith(sandboxWorkspace);
expect(capturedPrompt).not.toContain("[Bootstrap pending]");
expect(isWorkspaceBootstrapPending).toHaveBeenCalledOnce();
expect(isWorkspaceBootstrapPending).toHaveBeenCalledWith(canonicalWorkspace);
expect(isWorkspaceBootstrapPending).not.toHaveBeenCalledWith(sandboxWorkspace);
expect(routing.bootstrapMode).toBe("none");
expect(routing.userPromptPrefixText).toBeUndefined();
});
});

View File

@@ -1,6 +1,6 @@
import path from "node:path";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import {
createAgentSession,
@@ -52,7 +52,6 @@ import {
resolveBootstrapContextForRun,
resolveContextInjectionMode,
} from "../../bootstrap-files.js";
import { resolveBootstrapMode } from "../../bootstrap-mode.js";
import { createCacheTrace } from "../../cache-trace.js";
import {
listChannelSupportedActions,
@@ -114,7 +113,6 @@ import {
import { resolveSystemPromptOverride } from "../../system-prompt-override.js";
import { buildSystemPromptParams } from "../../system-prompt-params.js";
import { buildSystemPromptReport } from "../../system-prompt-report.js";
import { buildAgentUserPromptPrefix } from "../../system-prompt.js";
import { resolveAgentTimeoutMs } from "../../timeout.js";
import { UNKNOWN_TOOL_THRESHOLD } from "../../tool-loop-detection.js";
import {
@@ -182,6 +180,11 @@ import { splitSdkTools } from "../tool-split.js";
import { mapThinkingLevel } from "../utils.js";
import { flushPendingToolResultsAfterIdle } from "../wait-for-idle-before-flush.js";
export { buildContextEnginePromptCacheInfo } from "./attempt.context-engine-helpers.js";
import {
resolveAttemptWorkspaceBootstrapRouting,
shouldStripBootstrapFromEmbeddedContext,
} from "./attempt-bootstrap-routing.js";
export { shouldStripBootstrapFromEmbeddedContext } from "./attempt-bootstrap-routing.js";
import { configureEmbeddedAttemptHttpRuntime } from "./attempt-http-runtime.js";
import {
assembleAttemptContextEngine,
@@ -318,12 +321,6 @@ export function resolveUnknownToolGuardThreshold(loopDetection?: {
return UNKNOWN_TOOL_THRESHOLD;
}
export function shouldStripBootstrapFromEmbeddedContext(_params: {
bootstrapMode: "full" | "limited" | "none";
}): boolean {
return true;
}
export function isPrimaryBootstrapRun(sessionKey?: string): boolean {
return !isSubagentSessionKey(sessionKey) && !isAcpSessionKey(sessionKey);
}
@@ -338,8 +335,7 @@ export function remapInjectedContextFilesToWorkspace(params: {
}
return params.files.map((file) => {
const relative = path.relative(params.sourceWorkspaceDir, file.path);
const canRemap =
relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
const canRemap = relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
return canRemap
? {
...file,
@@ -484,8 +480,6 @@ export async function runEmbeddedAttempt(
const sessionLabel = params.sessionKey ?? params.sessionId;
const contextInjectionMode = resolveContextInjectionMode(params.config);
// Bootstrap lifecycle is owned by the canonical workspace, not a copied sandbox view.
const workspaceBootstrapPending = await isWorkspaceBootstrapPending(resolvedWorkspace);
const agentDir = params.agentDir ?? resolveOpenClawAgentDir();
const toolsRaw = params.disableTools
? []
@@ -555,20 +549,20 @@ export async function runEmbeddedAttempt(
return allTools;
})();
const toolsEnabled = supportsModelTools(params.model);
const bootstrapRunKind = params.bootstrapContextRunKind ?? "default";
const bootstrapHasFileAccess = toolsEnabled && toolsRaw.some((tool) => tool.name === "read");
const bootstrapMode = resolveBootstrapMode({
bootstrapPending: workspaceBootstrapPending,
runKind: bootstrapRunKind,
isInteractiveUserFacing: params.trigger === "user" || params.trigger === "manual",
const bootstrapRouting = await resolveAttemptWorkspaceBootstrapRouting({
isWorkspaceBootstrapPending,
bootstrapContextRunKind: params.bootstrapContextRunKind,
trigger: params.trigger,
sessionKey: params.sessionKey,
isPrimaryRun: isPrimaryBootstrapRun(params.sessionKey),
isCanonicalWorkspace:
(params.isCanonicalWorkspace ?? true) && effectiveWorkspace === resolvedWorkspace,
isCanonicalWorkspace: params.isCanonicalWorkspace,
effectiveWorkspace,
resolvedWorkspace,
hasBootstrapFileAccess: bootstrapHasFileAccess,
});
const shouldStripBootstrapFromContext = shouldStripBootstrapFromEmbeddedContext({
bootstrapMode,
});
const bootstrapMode = bootstrapRouting.bootstrapMode;
const shouldStripBootstrapFromContext = bootstrapRouting.shouldStripBootstrapFromContext;
const {
bootstrapFiles: hookAdjustedBootstrapFiles,
contextFiles: resolvedContextFiles,
@@ -576,7 +570,7 @@ export async function runEmbeddedAttempt(
} = await resolveAttemptBootstrapContext({
contextInjectionMode,
bootstrapContextMode: params.bootstrapContextMode,
bootstrapContextRunKind: bootstrapRunKind,
bootstrapContextRunKind: params.bootstrapContextRunKind ?? "default",
bootstrapMode,
sessionFile: params.sessionFile,
hasCompletedBootstrapTurn,
@@ -927,9 +921,7 @@ export async function runEmbeddedAttempt(
});
const systemPromptOverride = createSystemPromptOverride(appendPrompt);
let systemPromptText = systemPromptOverride();
const userPromptPrefixText = buildAgentUserPromptPrefix({
bootstrapMode,
});
const userPromptPrefixText = bootstrapRouting.userPromptPrefixText;
let sessionManager: ReturnType<typeof guardSessionManager> | undefined;
let session: Awaited<ReturnType<typeof createAgentSession>>["session"] | undefined;