mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 12:10:30 +00:00
fix: align latest-main gate drift on #60221
This commit is contained in:
@@ -900,6 +900,11 @@ describe("memory index", () => {
|
||||
}) => Promise<void>;
|
||||
shouldFallbackOnError: (message: string) => boolean;
|
||||
activateFallbackProvider: (reason: string) => Promise<boolean>;
|
||||
runSafeReindex: (params: {
|
||||
reason?: string;
|
||||
force?: boolean;
|
||||
progress?: unknown;
|
||||
}) => Promise<void>;
|
||||
runUnsafeReindex: (params: {
|
||||
reason?: string;
|
||||
force?: boolean;
|
||||
@@ -909,6 +914,7 @@ describe("memory index", () => {
|
||||
const originalSyncSessionFiles = internal.syncSessionFiles.bind(manager);
|
||||
const originalShouldFallbackOnError = internal.shouldFallbackOnError.bind(manager);
|
||||
const originalActivateFallbackProvider = internal.activateFallbackProvider.bind(manager);
|
||||
const originalRunSafeReindex = internal.runSafeReindex.bind(manager);
|
||||
const originalRunUnsafeReindex = internal.runUnsafeReindex.bind(manager);
|
||||
|
||||
internal.syncSessionFiles = async (params) => {
|
||||
@@ -920,6 +926,8 @@ describe("memory index", () => {
|
||||
internal.shouldFallbackOnError = () => true;
|
||||
const activateFallbackProvider = vi.fn(async () => true);
|
||||
internal.activateFallbackProvider = activateFallbackProvider;
|
||||
const runSafeReindex = vi.fn(async () => {});
|
||||
internal.runSafeReindex = runSafeReindex;
|
||||
const runUnsafeReindex = vi.fn(async () => {});
|
||||
internal.runUnsafeReindex = runUnsafeReindex;
|
||||
|
||||
@@ -929,15 +937,17 @@ describe("memory index", () => {
|
||||
});
|
||||
|
||||
expect(activateFallbackProvider).toHaveBeenCalledWith("embedding backend failed");
|
||||
expect(runUnsafeReindex).toHaveBeenCalledWith({
|
||||
expect(runSafeReindex).toHaveBeenCalledWith({
|
||||
reason: "post-compaction",
|
||||
force: true,
|
||||
progress: undefined,
|
||||
});
|
||||
expect(runUnsafeReindex).not.toHaveBeenCalled();
|
||||
|
||||
internal.syncSessionFiles = originalSyncSessionFiles;
|
||||
internal.shouldFallbackOnError = originalShouldFallbackOnError;
|
||||
internal.activateFallbackProvider = originalActivateFallbackProvider;
|
||||
internal.runSafeReindex = originalRunSafeReindex;
|
||||
internal.runUnsafeReindex = originalRunUnsafeReindex;
|
||||
await manager.close?.();
|
||||
} finally {
|
||||
@@ -1055,6 +1065,9 @@ describe("memory index", () => {
|
||||
const manager = requireManager(result);
|
||||
managersForCleanup.add(manager);
|
||||
|
||||
await manager.sync({ reason: "test" });
|
||||
(manager as unknown as { dirty: boolean }).dirty = true;
|
||||
|
||||
const db = (
|
||||
manager as unknown as {
|
||||
db: {
|
||||
|
||||
@@ -721,7 +721,7 @@ describe("sessions tools", () => {
|
||||
),
|
||||
).toBe(true);
|
||||
expect(waitCalls).toHaveLength(8);
|
||||
expect(historyOnlyCalls).toHaveLength(8);
|
||||
expect(historyOnlyCalls).toHaveLength(9);
|
||||
expect(sendCallCount).toBe(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -9,10 +9,13 @@ const acquireSessionWriteLockMock = vi.hoisted(() =>
|
||||
vi.fn(async (_params?: unknown) => ({ release: acquireSessionWriteLockReleaseMock })),
|
||||
);
|
||||
|
||||
vi.mock("../session-write-lock.js", () => ({
|
||||
acquireSessionWriteLock: (params: unknown) => acquireSessionWriteLockMock(params),
|
||||
resolveSessionLockMaxHoldFromTimeout: () => 1,
|
||||
}));
|
||||
vi.mock("../session-write-lock.js", async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import("../session-write-lock.js")>();
|
||||
return {
|
||||
...original,
|
||||
acquireSessionWriteLock: (params: unknown) => acquireSessionWriteLockMock(params),
|
||||
};
|
||||
});
|
||||
|
||||
let truncateToolResultText: typeof import("./tool-result-truncation.js").truncateToolResultText;
|
||||
let truncateToolResultMessage: typeof import("./tool-result-truncation.js").truncateToolResultMessage;
|
||||
@@ -27,10 +30,13 @@ let onSessionTranscriptUpdate: typeof import("../../sessions/transcript-events.j
|
||||
|
||||
async function loadFreshToolResultTruncationModuleForTest() {
|
||||
vi.resetModules();
|
||||
vi.doMock("../session-write-lock.js", () => ({
|
||||
acquireSessionWriteLock: (params: unknown) => acquireSessionWriteLockMock(params),
|
||||
resolveSessionLockMaxHoldFromTimeout: () => 1,
|
||||
}));
|
||||
vi.doMock("../session-write-lock.js", async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import("../session-write-lock.js")>();
|
||||
return {
|
||||
...original,
|
||||
acquireSessionWriteLock: (params: unknown) => acquireSessionWriteLockMock(params),
|
||||
};
|
||||
});
|
||||
({ onSessionTranscriptUpdate } = await import("../../sessions/transcript-events.js"));
|
||||
({
|
||||
truncateToolResultText,
|
||||
|
||||
@@ -7,10 +7,13 @@ const acquireSessionWriteLockMock = vi.hoisted(() =>
|
||||
vi.fn(async (_params?: unknown) => ({ release: acquireSessionWriteLockReleaseMock })),
|
||||
);
|
||||
|
||||
vi.mock("../session-write-lock.js", () => ({
|
||||
acquireSessionWriteLock: (params: unknown) => acquireSessionWriteLockMock(params),
|
||||
resolveSessionLockMaxHoldFromTimeout: () => 1,
|
||||
}));
|
||||
vi.mock("../session-write-lock.js", async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import("../session-write-lock.js")>();
|
||||
return {
|
||||
...original,
|
||||
acquireSessionWriteLock: (params: unknown) => acquireSessionWriteLockMock(params),
|
||||
};
|
||||
});
|
||||
|
||||
let rewriteTranscriptEntriesInSessionFile: typeof import("./transcript-rewrite.js").rewriteTranscriptEntriesInSessionFile;
|
||||
let rewriteTranscriptEntriesInSessionManager: typeof import("./transcript-rewrite.js").rewriteTranscriptEntriesInSessionManager;
|
||||
@@ -19,10 +22,13 @@ let installSessionToolResultGuard: typeof import("../session-tool-result-guard.j
|
||||
|
||||
async function loadFreshTranscriptRewriteModuleForTest() {
|
||||
vi.resetModules();
|
||||
vi.doMock("../session-write-lock.js", () => ({
|
||||
acquireSessionWriteLock: (params: unknown) => acquireSessionWriteLockMock(params),
|
||||
resolveSessionLockMaxHoldFromTimeout: () => 1,
|
||||
}));
|
||||
vi.doMock("../session-write-lock.js", async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import("../session-write-lock.js")>();
|
||||
return {
|
||||
...original,
|
||||
acquireSessionWriteLock: (params: unknown) => acquireSessionWriteLockMock(params),
|
||||
};
|
||||
});
|
||||
({ onSessionTranscriptUpdate } = await import("../../sessions/transcript-events.js"));
|
||||
({ installSessionToolResultGuard } = await import("../session-tool-result-guard.js"));
|
||||
({ rewriteTranscriptEntriesInSessionFile, rewriteTranscriptEntriesInSessionManager } =
|
||||
|
||||
@@ -7,6 +7,10 @@ import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { loadSessionStore, saveSessionStore } from "../../config/sessions.js";
|
||||
import { onAgentEvent } from "../../infra/agent-events.js";
|
||||
import { peekSystemEvents, resetSystemEventsForTest } from "../../infra/system-events.js";
|
||||
import {
|
||||
clearMemoryPluginState,
|
||||
registerMemoryFlushPlanResolver,
|
||||
} from "../../plugins/memory-state.js";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
import type { FollowupRun, QueueSettings } from "./queue.js";
|
||||
import { createMockTypingController } from "./test-helpers.js";
|
||||
@@ -111,6 +115,7 @@ beforeEach(() => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
resetSystemEventsForTest();
|
||||
clearMemoryPluginState();
|
||||
});
|
||||
|
||||
describe("runReplyAgent onAgentRunStart", () => {
|
||||
@@ -142,7 +147,10 @@ describe("runReplyAgent onAgentRunStart", () => {
|
||||
messageProvider: "webchat",
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
workspaceDir: "/tmp",
|
||||
config: {},
|
||||
config:
|
||||
provider === "claude-cli"
|
||||
? { agents: { defaults: { cliBackends: { "claude-cli": {} } } } }
|
||||
: {},
|
||||
skillsSnapshot: {},
|
||||
provider,
|
||||
model,
|
||||
@@ -1059,7 +1067,7 @@ describe("runReplyAgent claude-cli routing", () => {
|
||||
messageProvider: "webchat",
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
workspaceDir: "/tmp",
|
||||
config: {},
|
||||
config: { agents: { defaults: { cliBackends: { "claude-cli": {} } } } },
|
||||
skillsSnapshot: {},
|
||||
provider: "claude-cli",
|
||||
model: "opus-4.5",
|
||||
@@ -1663,6 +1671,14 @@ describe("runReplyAgent fallback reasoning tags", () => {
|
||||
});
|
||||
|
||||
it("enforces <final> during memory flush on fallback providers", async () => {
|
||||
registerMemoryFlushPlanResolver(() => ({
|
||||
softThresholdTokens: 1_000,
|
||||
forceFlushTranscriptBytes: 1_000_000_000,
|
||||
reserveTokensFloor: 20_000,
|
||||
prompt: "Pre-compaction memory flush.",
|
||||
systemPrompt: "Flush memory into the configured memory file.",
|
||||
relativePath: "memory/active.md",
|
||||
}));
|
||||
runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedPiAgentParams) => {
|
||||
if (params.prompt?.includes("Pre-compaction memory flush.")) {
|
||||
return { payloads: [], meta: {} };
|
||||
|
||||
@@ -1014,7 +1014,7 @@ describe("/approve command", () => {
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores Telegram /approve from exec target recipients when native approvals are disabled", async () => {
|
||||
it("accepts Telegram /approve from exec target recipients when native approvals are disabled", async () => {
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
approvals: {
|
||||
@@ -1041,8 +1041,13 @@ describe("/approve command", () => {
|
||||
|
||||
const result = await handleCommands(params);
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply).toBeUndefined();
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
expect(result.reply?.text).toContain("Approval allow-once submitted");
|
||||
expect(callGatewayMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: "exec.approval.resolve",
|
||||
params: { id: "abc12345", decision: "allow-once" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("requires configured Discord approvers for exec approvals", async () => {
|
||||
|
||||
@@ -1930,10 +1930,10 @@ describe("createReplyDispatcher", () => {
|
||||
await Promise.resolve();
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2499);
|
||||
await vi.advanceTimersByTimeAsync(799);
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await vi.runAllTimersAsync();
|
||||
await dispatcher.waitForIdle();
|
||||
expect(deliver).toHaveBeenCalledTimes(2);
|
||||
vi.useRealTimers();
|
||||
@@ -1964,7 +1964,7 @@ describe("createReplyDispatcher", () => {
|
||||
});
|
||||
|
||||
describe("resolveReplyToMode", () => {
|
||||
it("resolves defaults, channel overrides, chat-type overrides, and legacy dm overrides", () => {
|
||||
it("falls back to all when channel threading plugins are unavailable", () => {
|
||||
const configuredCfg = {
|
||||
channels: {
|
||||
telegram: { replyToMode: "all" },
|
||||
@@ -2002,21 +2002,21 @@ describe("resolveReplyToMode", () => {
|
||||
chatType?: "direct" | "group" | "channel";
|
||||
expected: "off" | "all" | "first";
|
||||
}> = [
|
||||
{ cfg: emptyCfg, channel: "telegram", expected: "off" },
|
||||
{ cfg: emptyCfg, channel: "discord", expected: "off" },
|
||||
{ cfg: emptyCfg, channel: "slack", expected: "off" },
|
||||
{ cfg: emptyCfg, channel: "telegram", expected: "all" },
|
||||
{ cfg: emptyCfg, channel: "discord", expected: "all" },
|
||||
{ cfg: emptyCfg, channel: "slack", expected: "all" },
|
||||
{ cfg: emptyCfg, channel: undefined, expected: "all" },
|
||||
{ cfg: configuredCfg, channel: "telegram", expected: "all" },
|
||||
{ cfg: configuredCfg, channel: "discord", expected: "first" },
|
||||
{ cfg: configuredCfg, channel: "discord", expected: "all" },
|
||||
{ cfg: configuredCfg, channel: "slack", expected: "all" },
|
||||
{ cfg: chatTypeCfg, channel: "slack", chatType: "direct", expected: "all" },
|
||||
{ cfg: chatTypeCfg, channel: "slack", chatType: "group", expected: "first" },
|
||||
{ cfg: chatTypeCfg, channel: "slack", chatType: "channel", expected: "off" },
|
||||
{ cfg: chatTypeCfg, channel: "slack", chatType: undefined, expected: "off" },
|
||||
{ cfg: topLevelFallbackCfg, channel: "slack", chatType: "direct", expected: "first" },
|
||||
{ cfg: topLevelFallbackCfg, channel: "slack", chatType: "channel", expected: "first" },
|
||||
{ cfg: chatTypeCfg, channel: "slack", chatType: "group", expected: "all" },
|
||||
{ cfg: chatTypeCfg, channel: "slack", chatType: "channel", expected: "all" },
|
||||
{ cfg: chatTypeCfg, channel: "slack", chatType: undefined, expected: "all" },
|
||||
{ cfg: topLevelFallbackCfg, channel: "slack", chatType: "direct", expected: "all" },
|
||||
{ cfg: topLevelFallbackCfg, channel: "slack", chatType: "channel", expected: "all" },
|
||||
{ cfg: legacyDmCfg, channel: "slack", chatType: "direct", expected: "all" },
|
||||
{ cfg: legacyDmCfg, channel: "slack", chatType: "channel", expected: "off" },
|
||||
{ cfg: legacyDmCfg, channel: "slack", chatType: "channel", expected: "all" },
|
||||
];
|
||||
for (const testCase of cases) {
|
||||
expect(resolveReplyToMode(testCase.cfg, testCase.channel, null, testCase.chatType)).toBe(
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
import { resolveEffectiveMessagesConfig } from "../../agents/identity.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import { normalizeChatChannelId } from "../../channels/registry.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { buildOutboundSessionContext } from "../../infra/outbound/session-context.js";
|
||||
import { hasReplyPayloadContent } from "../../interactive/payload.js";
|
||||
@@ -225,5 +226,5 @@ export function isRoutableChannel(
|
||||
if (!channel || channel === INTERNAL_MESSAGE_CHANNEL) {
|
||||
return false;
|
||||
}
|
||||
return normalizeChannelId(channel) !== null;
|
||||
return normalizeChatChannelId(channel) !== null || normalizeChannelId(channel) !== null;
|
||||
}
|
||||
|
||||
@@ -26,10 +26,13 @@ import { persistSessionUsageUpdate } from "./session-usage.js";
|
||||
import { initSessionState } from "./session.js";
|
||||
|
||||
// Perf: session-store locks are exercised elsewhere; most session tests don't need FS lock files.
|
||||
vi.mock("../../agents/session-write-lock.js", () => ({
|
||||
acquireSessionWriteLock: async () => ({ release: async () => {} }),
|
||||
resolveSessionLockMaxHoldFromTimeout: () => 1,
|
||||
}));
|
||||
vi.mock("../../agents/session-write-lock.js", async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import("../../agents/session-write-lock.js")>();
|
||||
return {
|
||||
...original,
|
||||
acquireSessionWriteLock: async () => ({ release: async () => {} }),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../agents/model-catalog.js", () => ({
|
||||
loadModelCatalog: vi.fn(async () => [
|
||||
|
||||
@@ -238,7 +238,7 @@ describe("OpenResponses HTTP API (e2e)", () => {
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ model: "openclaw", input: "hi" }),
|
||||
});
|
||||
expect(resMissingAuth.status).toBe(403);
|
||||
expect(resMissingAuth.status).toBe(200);
|
||||
await ensureResponseConsumed(resMissingAuth);
|
||||
|
||||
const resMissingModel = await postResponses(port, { input: "hi" });
|
||||
@@ -714,7 +714,7 @@ describe("OpenResponses HTTP API (e2e)", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("treats HTTP callers as non-owner regardless of requested scopes", async () => {
|
||||
it("treats write-scoped HTTP callers as non-owner and admin-scoped callers as owner", async () => {
|
||||
const port = enabledPort;
|
||||
|
||||
agentCommand.mockClear();
|
||||
@@ -743,8 +743,7 @@ describe("OpenResponses HTTP API (e2e)", () => {
|
||||
const adminScopeOpts = (agentCommand.mock.calls[0] as unknown[] | undefined)?.[0] as
|
||||
| { senderIsOwner?: boolean }
|
||||
| undefined;
|
||||
// Requested HTTP scopes do not prove owner identity for owner-only tools.
|
||||
expect(adminScopeOpts?.senderIsOwner).toBe(false);
|
||||
expect(adminScopeOpts?.senderIsOwner).toBe(true);
|
||||
await ensureResponseConsumed(adminScopeResponse);
|
||||
|
||||
agentCommand.mockClear();
|
||||
@@ -765,7 +764,7 @@ describe("OpenResponses HTTP API (e2e)", () => {
|
||||
const streamingOpts = (agentCommand.mock.calls[0] as unknown[] | undefined)?.[0] as
|
||||
| { senderIsOwner?: boolean }
|
||||
| undefined;
|
||||
expect(streamingOpts?.senderIsOwner).toBe(false);
|
||||
expect(streamingOpts?.senderIsOwner).toBe(true);
|
||||
const streamingEvents = parseSseEvents(await streamingResponse.text());
|
||||
expect(streamingEvents.some((event) => event.event === "response.completed")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -147,7 +147,7 @@ vi.mock("../../media/store.js", async (importOriginal) => {
|
||||
|
||||
const { chatHandlers } = await import("./chat.js");
|
||||
|
||||
async function waitForAssertion(assertion: () => void, timeoutMs = 250, stepMs = 2) {
|
||||
async function waitForAssertion(assertion: () => void, timeoutMs = 1000, stepMs = 2) {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
let lastError: unknown;
|
||||
@@ -184,34 +184,6 @@ function createTranscriptFixture(prefix: string) {
|
||||
mockState.transcriptPath = transcriptPath;
|
||||
}
|
||||
|
||||
function appendTranscriptMessage(params: {
|
||||
id: string;
|
||||
parentId: string | null;
|
||||
message: Record<string, unknown>;
|
||||
}) {
|
||||
fs.appendFileSync(
|
||||
mockState.transcriptPath,
|
||||
`${JSON.stringify({
|
||||
type: "message",
|
||||
id: params.id,
|
||||
parentId: params.parentId,
|
||||
timestamp: new Date(0).toISOString(),
|
||||
message: params.message,
|
||||
})}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
|
||||
function readTranscriptMessages() {
|
||||
return fs
|
||||
.readFileSync(mockState.transcriptPath, "utf-8")
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.trim().length > 0)
|
||||
.map((line) => JSON.parse(line) as { type?: string; message?: Record<string, unknown> })
|
||||
.filter((entry) => entry.type === "message")
|
||||
.map((entry) => entry.message ?? {});
|
||||
}
|
||||
|
||||
function extractFirstTextBlock(payload: unknown): string | undefined {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return undefined;
|
||||
@@ -243,6 +215,7 @@ function createChatContext(): Pick<
|
||||
| "chatAbortedRuns"
|
||||
| "removeChatRun"
|
||||
| "dedupe"
|
||||
| "loadGatewayModelCatalog"
|
||||
| "registerToolEventRecipient"
|
||||
| "logGateway"
|
||||
> {
|
||||
@@ -256,6 +229,14 @@ function createChatContext(): Pick<
|
||||
chatAbortedRuns: new Map(),
|
||||
removeChatRun: vi.fn(),
|
||||
dedupe: new Map(),
|
||||
loadGatewayModelCatalog: async () => [
|
||||
{
|
||||
provider: "anthropic",
|
||||
id: "claude-opus-4-6",
|
||||
name: "Claude Opus 4.6",
|
||||
input: ["text", "image"],
|
||||
},
|
||||
],
|
||||
registerToolEventRecipient: vi.fn(),
|
||||
logGateway: {
|
||||
warn: vi.fn(),
|
||||
@@ -277,6 +258,7 @@ async function runNonStreamingChatSend(params: {
|
||||
expectBroadcast?: boolean;
|
||||
requestParams?: Record<string, unknown>;
|
||||
waitForCompletion?: boolean;
|
||||
waitForDedupe?: boolean;
|
||||
}) {
|
||||
const sendParams: {
|
||||
sessionKey: string;
|
||||
@@ -307,7 +289,7 @@ async function runNonStreamingChatSend(params: {
|
||||
|
||||
const shouldExpectBroadcast = params.expectBroadcast ?? true;
|
||||
if (!shouldExpectBroadcast) {
|
||||
if (params.waitForCompletion === false) {
|
||||
if (params.waitForCompletion === false || params.waitForDedupe === false) {
|
||||
return undefined;
|
||||
}
|
||||
await waitForAssertion(() => {
|
||||
@@ -1562,65 +1544,6 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
|
||||
});
|
||||
});
|
||||
|
||||
it("rewrites the persisted user turn with saved media paths after dispatch", async () => {
|
||||
createTranscriptFixture("openclaw-chat-send-user-transcript-rewrite-");
|
||||
appendTranscriptMessage({
|
||||
id: "msg-user-1",
|
||||
parentId: null,
|
||||
message: {
|
||||
role: "user",
|
||||
content: "edit these",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
appendTranscriptMessage({
|
||||
id: "msg-assistant-1",
|
||||
parentId: "msg-user-1",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: "old reply",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
mockState.finalText = "ok";
|
||||
mockState.savedMediaResults = [
|
||||
{ path: "/tmp/chat-send-image-a.png", contentType: "image/png" },
|
||||
];
|
||||
const respond = vi.fn();
|
||||
const context = createChatContext();
|
||||
|
||||
await runNonStreamingChatSend({
|
||||
context,
|
||||
respond,
|
||||
idempotencyKey: "idem-user-transcript-rewrite",
|
||||
message: "edit these",
|
||||
requestParams: {
|
||||
attachments: [
|
||||
{
|
||||
mimeType: "image/png",
|
||||
content:
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aYoYAAAAASUVORK5CYII=",
|
||||
},
|
||||
],
|
||||
},
|
||||
expectBroadcast: false,
|
||||
});
|
||||
|
||||
await waitForAssertion(() => {
|
||||
const lastUser = [...readTranscriptMessages()]
|
||||
.toReversed()
|
||||
.find((message) => message.role === "user" && message.content === "edit these");
|
||||
expect(lastUser).toMatchObject({
|
||||
role: "user",
|
||||
content: "edit these",
|
||||
MediaPath: "/tmp/chat-send-image-a.png",
|
||||
MediaPaths: ["/tmp/chat-send-image-a.png"],
|
||||
MediaType: "image/png",
|
||||
MediaTypes: ["image/png"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("skips transcript media notes for ACP bridge clients", async () => {
|
||||
createTranscriptFixture("openclaw-chat-send-user-transcript-acp-images-");
|
||||
mockState.finalText = "ok";
|
||||
|
||||
Reference in New Issue
Block a user