openclaw / src /agents /embedded-agent-runner /run /attempt-normalization.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
4c76b0d verified
Raw History Blame Contribute Delete
15 kB
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createDeferred } from "../../../../test/helpers/promise.js";
import { getAgentEventLifecycleGeneration } from "../../../infra/agent-events.js";
import { normalizeEmbeddedRunAttempt } from "./attempt-normalization.js";
import { applyEmbeddedAttemptSessionIdentity } from "./attempt-session-identity.js";
import { loadAttemptSessionEntryAfterQuotaMaintenance } from "./attempt-transcript-helpers.js";
import { createEmbeddedRunContextRecoveryState } from "./context-recovery-state.js";
import { createEmbeddedRunLaneController } from "./lane-controller.js";
import {
assertAgentHarnessRunAdmission,
buildContextEngineCompactionSessionTarget,
resetNoRealConversationTokenSnapshot,
} from "./session-bootstrap.js";
import { createEmbeddedRunSessionPromptState } from "./session-prompt-state.js";
const sessionAccessorMocks = vi.hoisted(() => ({
listSessionEntriesReadOnly: vi.fn(() => []),
loadSessionEntry: vi.fn(),
patchSessionEntryCore:
vi.fn<typeof import("../../../config/sessions/session-accessor.js").patchSessionEntryCore>(),
updateSessionEntry: vi.fn(async () => undefined),
}));
vi.mock("../../../config/sessions/session-accessor.js", () => ({
findTranscriptEvent: vi.fn(async () => undefined),
...sessionAccessorMocks,
loadSessionEntryReadOnly: sessionAccessorMocks.loadSessionEntry,
}));
beforeEach(() => {
sessionAccessorMocks.listSessionEntriesReadOnly.mockReset().mockReturnValue([]);
sessionAccessorMocks.loadSessionEntry.mockReset();
sessionAccessorMocks.patchSessionEntryCore.mockReset().mockResolvedValue(null);
sessionAccessorMocks.updateSessionEntry.mockReset().mockResolvedValue(undefined);
});
it.each([0, 2])(
"retains compaction facts when parent Stop arrives during persistence (%s ingress records)",
async (recordedCompactionCount) => {
const persistence = createDeferred();
const controller = new AbortController();
const generation = getAgentEventLifecycleGeneration();
const params = {
abortSignal: controller.signal,
prompt: "Stop while persisting",
runId: "normalization-stop",
sessionId: "normalization-stop",
sessionFile: "agent:main:normalization-stop",
timeoutMs: 30_000,
workspaceDir: "/tmp",
};
const laneController = createEmbeddedRunLaneController({
getParams: () => params,
getLifecycleGeneration: () => generation,
initialQueuedLifecycleGeneration: generation,
globalLane: "normalization-stop-global",
sessionLane: "normalization-stop-session",
setParams: vi.fn(),
setLifecycleGeneration: vi.fn(),
});
const cancelled = new Error("cancelled while user persistence was pending");
const contextRecoveryState = createEmbeddedRunContextRecoveryState();
contextRecoveryState.autoCompactionCount = recordedCompactionCount;
contextRecoveryState.lastCompactionTokensAfter = recordedCompactionCount > 0 ? 60 : undefined;
// Cancellation exits before model normalization; only the completed-attempt boundary is live.
const normalization = normalizeEmbeddedRunAttempt({
runInput: {
runParams: params,
laneController,
},
preparedRuntime: { snapshot: () => ({}) },
recordedCompactionCount,
dispatchedAttempt: {
rawAttempt: { compactionCount: 2, compactionTokensAfter: 40.9 },
},
sessionPromptState: {
activePrompt: { persisted: true },
waitForCurrentUserMessagePersistence: () => persistence.promise,
},
contextRecoveryState,
} as never);
controller.abort(cancelled);
persistence.resolve();
await expect(normalization).rejects.toBe(cancelled);
expect(contextRecoveryState).toMatchObject({
autoCompactionCount: 2,
lastCompactionTokensAfter: recordedCompactionCount > 0 ? 60 : 40,
});
},
);
describe("buildContextEngineCompactionSessionTarget", () => {
it("leaves the key absent when a marker has no stored mapping", () => {
expect(
buildContextEngineCompactionSessionTarget({
sessionFile: "sqlite:main:marker-session:/tmp/sessions.json",
sessionId: "stale-outer-session",
}),
).toEqual({
agentId: "main",
sessionId: "marker-session",
storePath: "/tmp/sessions.json",
});
});
it("uses the configured default agent without inventing a session key", () => {
expect(
buildContextEngineCompactionSessionTarget({
config: {
agents: { list: [{ id: "main" }, { id: "worker", default: true }] },
session: { store: "/tmp/{agentId}/sessions.json" },
},
sessionFile: "compat-session",
sessionId: "compat-session",
}),
).toEqual({
agentId: "worker",
sessionId: "compat-session",
storePath: "/tmp/worker/sessions.json",
});
});
it("uses the persisted fixed-store owner for a bare compaction key", () => {
expect(
buildContextEngineCompactionSessionTarget({
config: {
agents: {
ownership: "explicit",
defaults: { sessionStore: { agentId: "ops" } },
entries: { ops: {}, research: {} },
},
session: { store: "/tmp/shared-sessions.json" },
},
sessionFile: "global",
sessionId: "ops-session",
sessionKey: "global",
}),
).toMatchObject({
agentId: "ops",
sessionKey: "global",
storePath: "/tmp/shared-sessions.json",
});
});
it("rejects a partial target that conflicts with the fixed-store owner", () => {
expect(() =>
buildContextEngineCompactionSessionTarget({
config: {
agents: {
ownership: "explicit",
defaults: { sessionStore: { agentId: "ops" } },
entries: { ops: {}, research: {} },
},
session: { store: "/tmp/shared-sessions.json" },
},
sessionFile: "global",
sessionId: "ops-session",
sessionKey: "global",
sessionTarget: {
agentId: "research",
sessionId: "ops-session",
sessionKey: "global",
},
}),
).toThrow(/belongs to "ops"/u);
});
it("preserves an adopted session id without inventing a session key", () => {
expect(
buildContextEngineCompactionSessionTarget({
sessionFile: "",
sessionId: "previous-session",
sessionTarget: {
agentId: "main",
sessionId: "adopted-session",
storePath: "/tmp/sessions.json",
},
}),
).toEqual({
agentId: "main",
sessionId: "adopted-session",
storePath: "/tmp/sessions.json",
});
});
});
describe("fixed-store session bootstrap", () => {
const config = {
agents: {
ownership: "explicit" as const,
defaults: { sessionStore: { agentId: "ops" } },
entries: { ops: {}, research: {} },
},
session: { store: "/tmp/shared-sessions.json" },
};
it.each([false, true])(
"keeps the prepared reset target and its commit owner (closed=%s)",
async (closeBeforeCommit) => {
const sessionTarget = {
agentId: "ops",
sessionId: "ops-session",
sessionKey: "global",
storePath: "/tmp/explicit-openclaw-agent.sqlite",
};
const callerError = new Error("reset owner closed while waiting for commit");
let closed = false;
const assertActive = () => {
if (closed) {
throw callerError;
}
};
sessionAccessorMocks.patchSessionEntryCore.mockImplementationOnce(
async (_scope, _update, options) => {
closed = closeBeforeCommit;
options?.assertCommitAllowed?.();
return null;
},
);
const reset = resetNoRealConversationTokenSnapshot({ sessionTarget, assertActive });
if (closeBeforeCommit) {
await expect(reset).rejects.toBe(callerError);
} else {
await reset;
}
expect(sessionAccessorMocks.patchSessionEntryCore).toHaveBeenCalledWith(
sessionTarget,
expect.any(Function),
expect.objectContaining({ skipMaintenance: true, assertCommitAllowed: assertActive }),
);
},
);
it("carries the persisted owner into harness admission", () => {
assertAgentHarnessRunAdmission({
config,
sessionId: "ops-session",
sessionKey: "global",
} as never);
expect(sessionAccessorMocks.loadSessionEntry).toHaveBeenCalledWith(
expect.objectContaining({
agentId: "ops",
sessionKey: "global",
storePath: "/tmp/shared-sessions.json",
}),
);
});
it("carries the resolved owner into quota-maintenance reads", async () => {
sessionAccessorMocks.loadSessionEntry.mockReturnValueOnce({
sessionId: "ops-session",
updatedAt: 1,
});
await loadAttemptSessionEntryAfterQuotaMaintenance({
agentId: "ops",
sessionKey: "global",
storePath: "/tmp/shared-sessions.json",
});
expect(sessionAccessorMocks.loadSessionEntry).toHaveBeenCalledWith({
agentId: "ops",
sessionKey: "global",
storePath: "/tmp/shared-sessions.json",
});
});
});
describe("createEmbeddedRunSessionPromptState", () => {
it("keeps the admitted writer fence private across context-engine target adoption", async () => {
await using state = await createEmbeddedRunSessionPromptState({
runParams: {
agentId: "main",
prompt: "hello",
runId: "run-b",
sessionFile: "agent:main:main",
sessionId: "session-before",
sessionKey: "agent:main:main",
sessionTarget: {
agentId: "main",
expectedLifecycleRevision: "revision-a",
expectedWriterRunId: "run-b",
sessionId: "session-before",
sessionKey: "agent:main:main",
storePath: "/tmp/sessions.json",
},
timeoutMs: 30_000,
workspaceDir: "/tmp",
} as never,
lifecycleGeneration: "generation-a",
onInterrupt: () => {},
resolvedSessionKey: "agent:main:main",
sessionAgentId: "main",
});
state.sessionTarget = {
agentId: "main",
sessionId: "session-after",
sessionKey: "agent:main:main",
storePath: "/tmp/sessions.json",
};
expect(state.sessionTarget).not.toHaveProperty("expectedWriterRunId");
expect(state.sessionWriterFence).toEqual({
expectedLifecycleRevision: "revision-a",
expectedWriterRunId: "run-b",
});
});
});
function promptState(storePath = "/tmp/sessions.json") {
return {
sessionId: "session-before",
sessionFile: "agent:main:main",
sessionTarget: {
agentId: "main",
sessionId: "session-before",
sessionKey: "agent:main:main",
storePath,
},
adoptSessionId: vi.fn(),
};
}
describe("applyEmbeddedAttemptSessionIdentity", () => {
it("rejects a legacy successor file that cannot map to SQLite", () => {
const state = promptState();
expect(() =>
applyEmbeddedAttemptSessionIdentity({
sessionPromptState: state,
sessionIdUsed: "session-after",
sessionFileUsed: "/tmp/session-after.jsonl",
}),
).toThrow("successor files are unsupported");
expect(state.adoptSessionId).not.toHaveBeenCalled();
expect(state.sessionTarget).toMatchObject({ sessionId: "session-before" });
});
it("resolves a legacy SQLite marker successor", () => {
const state = promptState();
applyEmbeddedAttemptSessionIdentity({
sessionPromptState: state,
sessionIdUsed: "session-after",
sessionFileUsed: "sqlite:main:session-after:/tmp/sessions.json",
});
expect(state.sessionTarget).toMatchObject({
agentId: "main",
sessionId: "session-after",
sessionKey: "agent:main:main",
storePath: "/tmp/sessions.json",
});
});
it("rebinds a legacy SQLite marker successor over the retained active entry", () => {
sessionAccessorMocks.loadSessionEntry.mockReturnValue({
sessionId: "session-before",
updatedAt: 1,
});
const state = promptState();
applyEmbeddedAttemptSessionIdentity({
sessionPromptState: state,
sessionIdUsed: "session-after",
sessionFileUsed: "sqlite:main:session-after:/tmp/sessions.json",
});
expect(state.sessionTarget).toEqual({
agentId: "main",
sessionId: "session-after",
sessionKey: "agent:main:main",
storePath: "/tmp/sessions.json",
});
});
it("rejects a legacy marker successor already mapped to another key", () => {
sessionAccessorMocks.loadSessionEntry.mockReturnValue({
sessionId: "session-before",
updatedAt: 1,
});
sessionAccessorMocks.listSessionEntriesReadOnly.mockReturnValue([
{
sessionKey: "agent:main:other",
entry: { sessionId: "session-after", updatedAt: 2 },
},
] as never);
const state = promptState();
expect(() =>
applyEmbeddedAttemptSessionIdentity({
sessionPromptState: state,
sessionIdUsed: "session-after",
sessionFileUsed: "sqlite:main:session-after:/tmp/sessions.json",
}),
).toThrow("successor target changed the active session binding");
});
it("rejects a legacy SQLite marker outside the active store", () => {
const state = promptState();
expect(() =>
applyEmbeddedAttemptSessionIdentity({
sessionPromptState: state,
sessionIdUsed: "session-after",
sessionFileUsed: "sqlite:main:session-after:/tmp/other-sessions.json",
}),
).toThrow("successor target changed the active session binding");
});
it.each(["sqlite:other:session-after:/tmp/sessions.json", "agent:other:main"])(
"rejects a cross-agent legacy successor identity: %s",
(sessionFileUsed) => {
const state = promptState();
expect(() =>
applyEmbeddedAttemptSessionIdentity({
sessionPromptState: state,
sessionIdUsed: "session-after",
sessionFileUsed,
}),
).toThrow(/successor (identity is inconsistent|files are unsupported)/u);
},
);
it("retargets an id-only successor without discarding its SQLite identity", () => {
const state = promptState();
applyEmbeddedAttemptSessionIdentity({
sessionPromptState: state,
sessionIdUsed: "session-after",
});
expect(state.sessionTarget).toMatchObject({ sessionId: "session-after" });
});
it("refreshes a legacy marker for an id-only successor", () => {
const state = promptState();
state.sessionFile = "sqlite:main:session-before:/tmp/sessions.json";
applyEmbeddedAttemptSessionIdentity({
sessionPromptState: state,
sessionIdUsed: "session-after",
});
expect(state.sessionFile).toBe("sqlite:main:session-after:/tmp/sessions.json");
expect(state.sessionTarget).toMatchObject({ sessionId: "session-after" });
});
});