mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-17 12:30:44 +00:00
* Add embed rendering for Control UI assistant output * Add changelog entry for embed rendering * Harden canvas path resolution and stage isolation * Secure assistant media route and preserve UI avatar override * Fix chat media and history regressions * Harden embed iframe URL handling * Fix embed follow-up review regressions * Restore offloaded chat attachment persistence * Harden hook and media routing * Fix embed review follow-ups * feat(ui): add configurable embed sandbox mode * fix(gateway): harden assistant media and auth rotation * fix(gateway): restore websocket pairing handshake flows * fix(gateway): restore ws hello policy details * Restore dropped control UI shell wiring * Fix control UI reconnect cleanup regressions * fix(gateway): restore media root and auth getter compatibility * feat(ui): rename public canvas tag to embed * fix(ui): address remaining media and gateway review issues * fix(ui): address remaining embed and attachment review findings * fix(ui): restore stop control and tool card inputs * fix(ui): address history and attachment review findings * fix(ui): restore prompt contribution wiring * fix(ui): address latest history and directive reviews * fix(ui): forward password auth for assistant media * fix(ui): suppress silent transcript tokens with media * feat(ui): add granular embed sandbox modes * fix(ui): preserve relative media directives in history * docs(ui): document embed sandbox modes * fix(gateway): restrict canvas history hoisting to tool entries * fix(gateway): tighten embed follow-up review fixes * fix(ci): repair merged branch type drift * fix(prompt): restore stable runtime prompt rendering * fix(ui): harden local attachment preview checks * fix(prompt): restore channel-aware approval guidance * fix(gateway): enforce auth rotation and media cleanup * feat(ui): gate external embed urls behind config * fix(ci): repair rebased branch drift * fix(ci): resolve remaining branch check failures
107 lines
3.7 KiB
TypeScript
107 lines
3.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { splitMediaFromOutput } from "./parse.js";
|
|
|
|
describe("splitMediaFromOutput", () => {
|
|
function expectParsedMediaOutputCase(
|
|
input: string,
|
|
expected: {
|
|
mediaUrls?: string[];
|
|
text?: string;
|
|
audioAsVoice?: boolean;
|
|
},
|
|
) {
|
|
const result = splitMediaFromOutput(input);
|
|
expect(result.text).toBe(expected.text ?? "");
|
|
if ("audioAsVoice" in expected) {
|
|
expect(result.audioAsVoice).toBe(expected.audioAsVoice);
|
|
} else {
|
|
expect(result.audioAsVoice).toBeUndefined();
|
|
}
|
|
if ("mediaUrls" in expected) {
|
|
expect(result.mediaUrls).toEqual(expected.mediaUrls);
|
|
expect(result.mediaUrl).toBe(expected.mediaUrls?.[0]);
|
|
} else {
|
|
expect(result.mediaUrls).toBeUndefined();
|
|
expect(result.mediaUrl).toBeUndefined();
|
|
}
|
|
}
|
|
|
|
function expectStableAudioAsVoiceDetectionCase(input: string) {
|
|
for (const output of [splitMediaFromOutput(input), splitMediaFromOutput(input)]) {
|
|
expect(output.audioAsVoice).toBe(true);
|
|
}
|
|
}
|
|
|
|
function expectAcceptedMediaPathCase(expectedPath: string, input: string) {
|
|
expectParsedMediaOutputCase(input, { mediaUrls: [expectedPath] });
|
|
}
|
|
|
|
function expectRejectedMediaPathCase(input: string) {
|
|
expectParsedMediaOutputCase(input, { mediaUrls: undefined });
|
|
}
|
|
|
|
it.each([
|
|
["/Users/pete/My File.png", "MEDIA:/Users/pete/My File.png"],
|
|
["/Users/pete/My File.png", 'MEDIA:"/Users/pete/My File.png"'],
|
|
["./screenshots/image.png", "MEDIA:./screenshots/image.png"],
|
|
["media/inbound/image.png", "MEDIA:media/inbound/image.png"],
|
|
["./screenshot.png", " MEDIA:./screenshot.png"],
|
|
["C:\\Users\\pete\\Pictures\\snap.png", "MEDIA:C:\\Users\\pete\\Pictures\\snap.png"],
|
|
["/tmp/tts-fAJy8C/voice-1770246885083.opus", "MEDIA:/tmp/tts-fAJy8C/voice-1770246885083.opus"],
|
|
["image.png", "MEDIA:image.png"],
|
|
] as const)("accepts supported media path variant: %s", (expectedPath, input) => {
|
|
expectAcceptedMediaPathCase(expectedPath, input);
|
|
});
|
|
|
|
it.each([
|
|
"MEDIA:../../../etc/passwd",
|
|
"MEDIA:../../.env",
|
|
"MEDIA:~/.ssh/id_rsa",
|
|
"MEDIA:~/Pictures/My File.png",
|
|
"MEDIA:./foo/../../../etc/shadow",
|
|
] as const)("rejects traversal and home-dir path: %s", (input) => {
|
|
expectRejectedMediaPathCase(input);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "detects audio_as_voice tag and strips it",
|
|
input: "Hello [[audio_as_voice]] world",
|
|
expected: { audioAsVoice: true, text: "Hello world" },
|
|
},
|
|
{
|
|
name: "keeps MEDIA mentions in prose",
|
|
input: "The MEDIA: tag fails to deliver",
|
|
expected: { mediaUrls: undefined, text: "The MEDIA: tag fails to deliver" },
|
|
},
|
|
{
|
|
name: "rejects bare words without file extensions",
|
|
input: "MEDIA:screenshot",
|
|
expected: { mediaUrls: undefined, text: "MEDIA:screenshot" },
|
|
},
|
|
{
|
|
name: "keeps audio_as_voice detection stable across calls",
|
|
input: "Hello [[audio_as_voice]]",
|
|
expected: { audioAsVoice: true, text: "Hello" },
|
|
assertStable: true,
|
|
},
|
|
] as const)("$name", ({ input, expected, assertStable }) => {
|
|
expectParsedMediaOutputCase(input, expected);
|
|
if (assertStable) {
|
|
expectStableAudioAsVoiceDetectionCase(input);
|
|
}
|
|
});
|
|
|
|
it("returns ordered text and media segments while ignoring fenced MEDIA lines", () => {
|
|
const result = splitMediaFromOutput(
|
|
"Before\nMEDIA:https://example.com/a.png\n```text\nMEDIA:https://example.com/ignored.png\n```\nAfter",
|
|
);
|
|
|
|
expect(result.segments).toEqual([
|
|
{ type: "text", text: "Before" },
|
|
{ type: "media", url: "https://example.com/a.png" },
|
|
{ type: "text", text: "```text\nMEDIA:https://example.com/ignored.png\n```\nAfter" },
|
|
]);
|
|
});
|
|
});
|