test: cover replay scope and attachment redaction guards

This commit is contained in:
Shakker
2026-04-12 04:33:04 +01:00
committed by Shakker
parent 3cc9d53eb3
commit b1a228fc3a
4 changed files with 227 additions and 2 deletions

View File

@@ -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();

View File

@@ -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 = [

View File

@@ -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);
});
});

View File

@@ -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([
{