mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:40:42 +00:00
test: cover replay scope and attachment redaction guards
This commit is contained in:
@@ -968,6 +968,33 @@ describe("sanitizeSessionHistory", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps mutable thinking turns outside exact anthropic replay", async () => {
|
||||
setNonGoogleModelApi();
|
||||
|
||||
const messages = castAgentMessages([
|
||||
makeUserMessage("read a file"),
|
||||
makeAssistantMessage([
|
||||
{
|
||||
type: "thinking",
|
||||
thinking: "I should use the read tool",
|
||||
thinkingSignature: "reasoning_text",
|
||||
},
|
||||
{ type: "toolCall", id: "tool_123", name: " read ", arguments: { path: "/tmp/test" } },
|
||||
]),
|
||||
]);
|
||||
|
||||
const result = await sanitizeGithubCopilotHistory({ messages });
|
||||
const assistant = getAssistantMessage(result);
|
||||
expect(assistant.content).toEqual([
|
||||
{
|
||||
type: "thinking",
|
||||
thinking: "I should use the read tool",
|
||||
thinkingSignature: "reasoning_text",
|
||||
},
|
||||
{ type: "toolCall", id: "tool_123", name: "read", arguments: { path: "/tmp/test" } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the earlier anthropic replay prefix stable after a later subagent turn", async () => {
|
||||
setNonGoogleModelApi();
|
||||
|
||||
|
||||
@@ -1043,6 +1043,121 @@ describe("wrapStreamFnSanitizeMalformedToolCalls", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("drops signed thinking turns when replay would expose non-content attachment payload fields", async () => {
|
||||
const attachmentContent = "SIGNED_THINKING_NESTED_ATTACHMENT";
|
||||
const messages = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "internal", thinkingSignature: "sig_1" },
|
||||
{
|
||||
type: "toolUse",
|
||||
id: "call_1",
|
||||
name: "sessions_spawn",
|
||||
input: {
|
||||
task: "inspect attachment",
|
||||
attachments: [
|
||||
{
|
||||
name: "snapshot.txt",
|
||||
mimeType: "text/plain",
|
||||
data: attachmentContent,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "retry" }],
|
||||
},
|
||||
];
|
||||
const baseFn = vi.fn((_model, _context) =>
|
||||
createFakeStream({ events: [], resultMessage: { role: "assistant", content: [] } }),
|
||||
);
|
||||
|
||||
const wrapped = wrapStreamFnSanitizeMalformedToolCalls(
|
||||
baseFn as never,
|
||||
new Set(["sessions_spawn"]),
|
||||
{ validateAnthropicTurns: true } as never,
|
||||
);
|
||||
const stream = wrapped(
|
||||
{ api: "anthropic-messages" } as never,
|
||||
{ messages } as never,
|
||||
{} as never,
|
||||
) as FakeWrappedStream | Promise<FakeWrappedStream>;
|
||||
await Promise.resolve(stream);
|
||||
|
||||
expect(baseFn).toHaveBeenCalledTimes(1);
|
||||
const seenContext = baseFn.mock.calls[0]?.[1] as { messages: unknown[] };
|
||||
expect(seenContext.messages).toEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "retry" }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps mutable thinking turns outside anthropic replay-only preservation", async () => {
|
||||
const messages = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "internal", thinkingSignature: "sig_1" },
|
||||
{ type: "toolCall", id: "call_1", name: " read ", arguments: {} },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "retry" }],
|
||||
},
|
||||
];
|
||||
const baseFn = vi.fn((_model, _context) =>
|
||||
createFakeStream({ events: [], resultMessage: { role: "assistant", content: [] } }),
|
||||
);
|
||||
|
||||
const wrapped = wrapStreamFnSanitizeMalformedToolCalls(
|
||||
baseFn as never,
|
||||
new Set(["read"]),
|
||||
{ validateAnthropicTurns: true } as never,
|
||||
);
|
||||
const stream = wrapped(
|
||||
{ api: "openai-completions" } as never,
|
||||
{ messages } as never,
|
||||
{} as never,
|
||||
) as FakeWrappedStream | Promise<FakeWrappedStream>;
|
||||
await Promise.resolve(stream);
|
||||
|
||||
expect(baseFn).toHaveBeenCalledTimes(1);
|
||||
const seenContext = baseFn.mock.calls[0]?.[1] as { messages: unknown[] };
|
||||
expect(seenContext.messages).toEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "internal", thinkingSignature: "sig_1" },
|
||||
{ type: "toolCall", id: "call_1", name: "read", arguments: {} },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "call_1",
|
||||
toolName: "read",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "[openclaw] missing tool result in session history; inserted synthetic error result for transcript repair.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
timestamp: expect.any(Number),
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "retry" }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves sessions_spawn attachment payloads on replay", async () => {
|
||||
const attachmentContent = "INLINE_ATTACHMENT_PAYLOAD";
|
||||
const messages = [
|
||||
|
||||
@@ -74,4 +74,50 @@ describe("sanitizeToolCallInputs redacts sessions_spawn attachments", () => {
|
||||
).toBe("__OPENCLAW_REDACTED__");
|
||||
expect(JSON.stringify(out)).not.toContain(secret);
|
||||
});
|
||||
|
||||
it("replaces non-content attachment payload fields with a minimal redacted stub", () => {
|
||||
const secret = "NESTED_ATTACHMENT_SECRET"; // pragma: allowlist secret
|
||||
const input = castAgentMessages([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "toolUse",
|
||||
id: "call_3",
|
||||
name: "sessions_spawn",
|
||||
input: {
|
||||
task: "do thing",
|
||||
attachments: [
|
||||
{
|
||||
name: "payload.json",
|
||||
mimeType: "application/json",
|
||||
encoding: "utf8",
|
||||
data: secret,
|
||||
nested: { secret },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const out = sanitizeToolCallInputs(input);
|
||||
const msg = out[0] as { content?: unknown[] };
|
||||
const tool = (msg.content?.[0] ?? null) as {
|
||||
input?: { attachments?: unknown[] };
|
||||
arguments?: { attachments?: unknown[] };
|
||||
} | null;
|
||||
const attachment =
|
||||
(tool?.input?.attachments?.[0] ?? tool?.arguments?.attachments?.[0] ?? null) as
|
||||
| Record<string, unknown>
|
||||
| null;
|
||||
expect(attachment).toEqual({
|
||||
name: "payload.json",
|
||||
mimeType: "application/json",
|
||||
encoding: "utf8",
|
||||
content: "__OPENCLAW_REDACTED__",
|
||||
});
|
||||
expect(JSON.stringify(out)).not.toContain(secret);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -405,7 +405,7 @@ describe("sanitizeToolCallInputs", () => {
|
||||
|
||||
const out = sanitizeToolCallInputs(input, {
|
||||
allowedToolNames: ["read"],
|
||||
preserveImmutableThinkingTurns: true,
|
||||
allowProviderOwnedThinkingReplay: true,
|
||||
});
|
||||
|
||||
expect(out).toEqual([]);
|
||||
@@ -437,13 +437,50 @@ describe("sanitizeToolCallInputs", () => {
|
||||
|
||||
const out = sanitizeToolCallInputs(input, {
|
||||
allowedToolNames: ["sessions_spawn"],
|
||||
preserveImmutableThinkingTurns: true,
|
||||
allowProviderOwnedThinkingReplay: true,
|
||||
});
|
||||
|
||||
expect(out).toEqual([]);
|
||||
expect(JSON.stringify(out)).not.toContain(secret);
|
||||
});
|
||||
|
||||
it("keeps signed-thinking assistant turns when sessions_spawn attachments are already redacted", () => {
|
||||
const input = castAgentMessages([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "thinking",
|
||||
thinking: "Let me replay the helper turn.",
|
||||
thinkingSignature: "sig_spawn_safe",
|
||||
},
|
||||
{
|
||||
type: "toolUse",
|
||||
id: "call_spawn",
|
||||
name: "sessions_spawn",
|
||||
input: {
|
||||
task: "inspect attachment",
|
||||
attachments: [
|
||||
{
|
||||
name: "snapshot.txt",
|
||||
mimeType: "text/plain",
|
||||
content: "__OPENCLAW_REDACTED__",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const out = sanitizeToolCallInputs(input, {
|
||||
allowedToolNames: ["sessions_spawn"],
|
||||
allowProviderOwnedThinkingReplay: true,
|
||||
});
|
||||
|
||||
expect(out).toEqual(input);
|
||||
});
|
||||
|
||||
it("keeps generic thinking turns mutable when immutable preservation is disabled", () => {
|
||||
const input = castAgentMessages([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user