fix: harden MEDIA parsing and add tests
parent
f4c5f2c193
commit
9bf35d3272
|
|
@ -171,8 +171,15 @@ const mediaNote =
|
||||||
? `[media attached: ${ctx.MediaPath}${ctx.MediaType ? ` (${ctx.MediaType})` : ""}${ctx.MediaUrl ? ` | ${ctx.MediaUrl}` : ""}]`
|
? `[media attached: ${ctx.MediaPath}${ctx.MediaType ? ` (${ctx.MediaType})` : ""}${ctx.MediaUrl ? ` | ${ctx.MediaUrl}` : ""}]`
|
||||||
: undefined;
|
: undefined;
|
||||||
// For command prompts we prepend the media note so Claude et al. see it; text replies stay clean.
|
// For command prompts we prepend the media note so Claude et al. see it; text replies stay clean.
|
||||||
|
const mediaReplyHint =
|
||||||
|
mediaNote && reply?.mode === "command"
|
||||||
|
? "To send an image back, add a line like: MEDIA:https://example.com/image.jpg (no spaces). Keep caption in the text body."
|
||||||
|
: undefined;
|
||||||
const commandBody = mediaNote
|
const commandBody = mediaNote
|
||||||
? `${mediaNote}\n${prefixedBody ?? ""}`.trim()
|
? [mediaNote, mediaReplyHint, prefixedBody ?? ""]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n")
|
||||||
|
.trim()
|
||||||
: prefixedBody;
|
: prefixedBody;
|
||||||
const templatingCtx: TemplateContext = {
|
const templatingCtx: TemplateContext = {
|
||||||
...sessionCtx,
|
...sessionCtx,
|
||||||
|
|
@ -282,10 +289,36 @@ const mediaNote =
|
||||||
const rawStdout = stdout.trim();
|
const rawStdout = stdout.trim();
|
||||||
let trimmed = rawStdout;
|
let trimmed = rawStdout;
|
||||||
let mediaFromCommand: string | undefined;
|
let mediaFromCommand: string | undefined;
|
||||||
const mediaMatch = /MEDIA:\s*(.+)$/im.exec(rawStdout);
|
const mediaLine = rawStdout
|
||||||
if (mediaMatch?.[1]) {
|
.split("\n")
|
||||||
mediaFromCommand = mediaMatch[1].trim();
|
.find((line) => /^MEDIA:/i.test(line));
|
||||||
trimmed = rawStdout.replace(mediaMatch[0], "").trim();
|
if (mediaLine) {
|
||||||
|
const after = mediaLine.replace(/^MEDIA:\s*/i, "");
|
||||||
|
const parts = after.trim().split(/\s+/);
|
||||||
|
if (parts.length === 1 && parts[0]) {
|
||||||
|
mediaFromCommand = parts[0];
|
||||||
|
}
|
||||||
|
trimmed = rawStdout
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => !/^MEDIA:/i.test(line))
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
// Basic sanity: accept only URLs or existing file paths without whitespace.
|
||||||
|
const hasWhitespace = mediaFromCommand
|
||||||
|
? /\s/.test(mediaFromCommand)
|
||||||
|
: false;
|
||||||
|
const looksLikeUrl = mediaFromCommand
|
||||||
|
? /^https?:\/\//i.test(mediaFromCommand)
|
||||||
|
: false;
|
||||||
|
if (
|
||||||
|
!mediaFromCommand ||
|
||||||
|
hasWhitespace ||
|
||||||
|
(!looksLikeUrl && mediaFromCommand.length > 1024)
|
||||||
|
) {
|
||||||
|
mediaFromCommand = undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trimmed = rawStdout;
|
||||||
}
|
}
|
||||||
if (stderr?.trim()) {
|
if (stderr?.trim()) {
|
||||||
logVerbose(`Command auto-reply stderr: ${stderr.trim()}`);
|
logVerbose(`Command auto-reply stderr: ${stderr.trim()}`);
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,32 @@ describe("config and templating", () => {
|
||||||
expect(result?.mediaUrl).toBe("https://example.com/img.jpg");
|
expect(result?.mediaUrl).toBe("https://example.com/img.jpg");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ignores invalid MEDIA lines with whitespace", async () => {
|
||||||
|
const runSpy = vi.spyOn(index, "runCommandWithTimeout").mockResolvedValue({
|
||||||
|
stdout: "hello\nMEDIA: not a url with spaces\nrest\n",
|
||||||
|
stderr: "",
|
||||||
|
code: 0,
|
||||||
|
signal: null,
|
||||||
|
killed: false,
|
||||||
|
});
|
||||||
|
const cfg = {
|
||||||
|
inbound: {
|
||||||
|
reply: {
|
||||||
|
mode: "command" as const,
|
||||||
|
command: ["echo", "{{Body}}"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = await index.getReplyFromConfig(
|
||||||
|
{ Body: "hi", From: "+1", To: "+2" },
|
||||||
|
undefined,
|
||||||
|
cfg,
|
||||||
|
runSpy,
|
||||||
|
);
|
||||||
|
expect(result?.text).toBe("hello\nrest");
|
||||||
|
expect(result?.mediaUrl).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("getReplyFromConfig runs command and manages session store", async () => {
|
it("getReplyFromConfig runs command and manages session store", async () => {
|
||||||
const tmpStore = path.join(os.tmpdir(), `warelay-store-${Date.now()}.json`);
|
const tmpStore = path.join(os.tmpdir(), `warelay-store-${Date.now()}.json`);
|
||||||
vi.spyOn(crypto, "randomUUID").mockReturnValue("session-123");
|
vi.spyOn(crypto, "randomUUID").mockReturnValue("session-123");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue