refactor: extract MEDIA parsing helper and tidy whitespace
parent
bafaed37fe
commit
072998a6ab
|
|
@ -25,16 +25,12 @@ import type { TwilioRequester } from "../twilio/types.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
import { logError } from "../logger.js";
|
import { logError } from "../logger.js";
|
||||||
import { ensureMediaHosted } from "../media/host.js";
|
import { ensureMediaHosted } from "../media/host.js";
|
||||||
|
import { normalizeMediaSource, splitMediaFromOutput } from "../media/parse.js";
|
||||||
|
|
||||||
type GetReplyOptions = {
|
type GetReplyOptions = {
|
||||||
onReplyStart?: () => Promise<void> | void;
|
onReplyStart?: () => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeMediaSource(src: string) {
|
|
||||||
if (src.startsWith("file://")) return src.replace("file://", "");
|
|
||||||
return src;
|
|
||||||
}
|
|
||||||
|
|
||||||
function summarizeClaudeMetadata(payload: unknown): string | undefined {
|
function summarizeClaudeMetadata(payload: unknown): string | undefined {
|
||||||
if (!payload || typeof payload !== "object") return undefined;
|
if (!payload || typeof payload !== "object") return undefined;
|
||||||
const obj = payload as Record<string, unknown>;
|
const obj = payload as Record<string, unknown>;
|
||||||
|
|
@ -293,43 +289,9 @@ const mediaNote =
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const rawStdout = stdout.trim();
|
const rawStdout = stdout.trim();
|
||||||
let trimmed = rawStdout;
|
const { text: trimmedText, mediaUrl: mediaFromCommand } =
|
||||||
let mediaFromCommand: string | undefined;
|
splitMediaFromOutput(rawStdout);
|
||||||
const mediaLine = rawStdout
|
let trimmed = trimmedText;
|
||||||
.split("\n")
|
|
||||||
.find((line) => /\bMEDIA:/i.test(line));
|
|
||||||
if (mediaLine) {
|
|
||||||
let isValidMedia = false;
|
|
||||||
const mediaMatch = mediaLine.match(/\bMEDIA:\s*([^\s]+)/i);
|
|
||||||
if (mediaMatch?.[1]) {
|
|
||||||
const candidate = normalizeMediaSource(mediaMatch[1]);
|
|
||||||
const looksLikeUrl = /^https?:\/\//i.test(candidate);
|
|
||||||
const looksLikePath =
|
|
||||||
candidate.startsWith("/") || candidate.startsWith("./");
|
|
||||||
const hasWhitespace = /\s/.test(candidate);
|
|
||||||
isValidMedia =
|
|
||||||
!hasWhitespace &&
|
|
||||||
candidate.length <= 1024 &&
|
|
||||||
(looksLikeUrl || looksLikePath);
|
|
||||||
if (isValidMedia) mediaFromCommand = candidate;
|
|
||||||
}
|
|
||||||
if (isValidMedia && mediaMatch?.[0]) {
|
|
||||||
trimmed = rawStdout
|
|
||||||
.replace(mediaMatch[0], "")
|
|
||||||
.replace(/\s{2,}/g, " ")
|
|
||||||
.replace(/\s+\n/g, "\n")
|
|
||||||
.replace(/\n{3,}/g, "\n\n")
|
|
||||||
.trim();
|
|
||||||
} else {
|
|
||||||
trimmed = rawStdout
|
|
||||||
.split("\n")
|
|
||||||
.filter((line) => line !== mediaLine)
|
|
||||||
.join("\n")
|
|
||||||
.replace(/\n\s+/g, "\n")
|
|
||||||
.replace(/\n{3,}/g, "\n\n")
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (stderr?.trim()) {
|
if (stderr?.trim()) {
|
||||||
logVerbose(`Command auto-reply stderr: ${stderr.trim()}`);
|
logVerbose(`Command auto-reply stderr: ${stderr.trim()}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ type TwilioFactoryMock = ReturnType<typeof createMockTwilio>["factory"];
|
||||||
const twilioFactory = (await import("twilio")).default as TwilioFactoryMock;
|
const twilioFactory = (await import("twilio")).default as TwilioFactoryMock;
|
||||||
|
|
||||||
import * as index from "./index.js";
|
import * as index from "./index.js";
|
||||||
|
import { splitMediaFromOutput } from "./media/parse.js";
|
||||||
|
|
||||||
const envBackup = { ...process.env } as Record<string, string | undefined>;
|
const envBackup = { ...process.env } as Record<string, string | undefined>;
|
||||||
|
|
||||||
|
|
@ -223,6 +224,14 @@ describe("config and templating", () => {
|
||||||
expect(result?.mediaUrl).toBeUndefined();
|
expect(result?.mediaUrl).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("splitMediaFromOutput strips media token and preserves text", () => {
|
||||||
|
const { text, mediaUrl } = splitMediaFromOutput(
|
||||||
|
"line1\nMEDIA:https://x/y.png\nline2",
|
||||||
|
);
|
||||||
|
expect(mediaUrl).toBe("https://x/y.png");
|
||||||
|
expect(text).toBe("line1\nline2");
|
||||||
|
});
|
||||||
|
|
||||||
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");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
// Shared helpers for parsing MEDIA tokens from command/stdout text.
|
||||||
|
|
||||||
|
export const MEDIA_LINE_RE = /\bMEDIA:/i;
|
||||||
|
export const MEDIA_TOKEN_RE = /\bMEDIA:\s*([^\s]+)/i;
|
||||||
|
|
||||||
|
export function normalizeMediaSource(src: string) {
|
||||||
|
if (src.startsWith("file://")) return src.replace("file://", "");
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitMediaFromOutput(raw: string): {
|
||||||
|
text: string;
|
||||||
|
mediaUrl?: string;
|
||||||
|
} {
|
||||||
|
const trimmedRaw = raw.trim();
|
||||||
|
let text = trimmedRaw;
|
||||||
|
let mediaUrl: string | undefined;
|
||||||
|
|
||||||
|
const mediaLine = trimmedRaw.split("\n").find((line) => MEDIA_LINE_RE.test(line));
|
||||||
|
if (!mediaLine) {
|
||||||
|
return { text: trimmedRaw };
|
||||||
|
}
|
||||||
|
|
||||||
|
let isValidMedia = false;
|
||||||
|
const mediaMatch = mediaLine.match(MEDIA_TOKEN_RE);
|
||||||
|
if (mediaMatch?.[1]) {
|
||||||
|
const candidate = normalizeMediaSource(mediaMatch[1]);
|
||||||
|
const looksLikeUrl = /^https?:\/\//i.test(candidate);
|
||||||
|
const looksLikePath = candidate.startsWith("/") || candidate.startsWith("./");
|
||||||
|
const hasWhitespace = /\s/.test(candidate);
|
||||||
|
isValidMedia =
|
||||||
|
!hasWhitespace && candidate.length <= 1024 && (looksLikeUrl || looksLikePath);
|
||||||
|
if (isValidMedia) {
|
||||||
|
mediaUrl = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValidMedia && mediaMatch?.[0]) {
|
||||||
|
text = trimmedRaw
|
||||||
|
.replace(mediaMatch[0], "")
|
||||||
|
.replace(/[ \t]{2,}/g, " ")
|
||||||
|
.replace(/[ \t]+\n/g, "\n")
|
||||||
|
.replace(/\n{2,}/g, "\n")
|
||||||
|
.trim();
|
||||||
|
} else {
|
||||||
|
text = trimmedRaw
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => line !== mediaLine)
|
||||||
|
.join("\n")
|
||||||
|
.replace(/[ \t]{2,}/g, " ")
|
||||||
|
.replace(/[ \t]+\n/g, "\n")
|
||||||
|
.replace(/\n{2,}/g, "\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text, mediaUrl };
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue