mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 05:01:23 +00:00
fix: apply missed media/runtime follow-ups from merged PRs
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { MediaAttachment } from "./types.js";
|
||||
import { selectAttachments } from "./attachments.js";
|
||||
import type { MediaAttachment } from "./types.js";
|
||||
|
||||
describe("media-understanding selectAttachments guards", () => {
|
||||
it("does not throw when attachments is undefined", () => {
|
||||
@@ -26,4 +26,21 @@ describe("media-understanding selectAttachments guards", () => {
|
||||
expect(run).not.toThrow();
|
||||
expect(run()).toEqual([]);
|
||||
});
|
||||
|
||||
it("ignores malformed attachment entries inside an array", () => {
|
||||
const run = () =>
|
||||
selectAttachments({
|
||||
capability: "audio",
|
||||
attachments: [
|
||||
null,
|
||||
{ index: 1, path: 123 },
|
||||
{ index: 2, url: true },
|
||||
{ index: 3, mime: { nope: true } },
|
||||
] as unknown as MediaAttachment[],
|
||||
policy: { prefer: "path" },
|
||||
});
|
||||
|
||||
expect(run).not.toThrow();
|
||||
expect(run()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,7 +169,7 @@ function orderAttachments(
|
||||
attachments: MediaAttachment[],
|
||||
prefer?: MediaUnderstandingAttachmentsConfig["prefer"],
|
||||
): MediaAttachment[] {
|
||||
const list = Array.isArray(attachments) ? attachments : [];
|
||||
const list = Array.isArray(attachments) ? attachments.filter(isAttachmentRecord) : [];
|
||||
if (!prefer || prefer === "first") {
|
||||
return list;
|
||||
}
|
||||
@@ -189,13 +189,36 @@ function orderAttachments(
|
||||
return list;
|
||||
}
|
||||
|
||||
function isAttachmentRecord(value: unknown): value is MediaAttachment {
|
||||
if (!value || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
const entry = value as Record<string, unknown>;
|
||||
if (typeof entry.index !== "number") {
|
||||
return false;
|
||||
}
|
||||
if (entry.path !== undefined && typeof entry.path !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (entry.url !== undefined && typeof entry.url !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (entry.mime !== undefined && typeof entry.mime !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (entry.alreadyTranscribed !== undefined && typeof entry.alreadyTranscribed !== "boolean") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function selectAttachments(params: {
|
||||
capability: MediaUnderstandingCapability;
|
||||
attachments: MediaAttachment[];
|
||||
policy?: MediaUnderstandingAttachmentsConfig;
|
||||
}): MediaAttachment[] {
|
||||
const { capability, attachments, policy } = params;
|
||||
const input = Array.isArray(attachments) ? attachments : [];
|
||||
const input = Array.isArray(attachments) ? attachments.filter(isAttachmentRecord) : [];
|
||||
const matches = input.filter((item) => {
|
||||
// Skip already-transcribed audio attachments from preflight
|
||||
if (capability === "audio" && item.alreadyTranscribed) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { MediaUnderstandingDecision } from "./types.js";
|
||||
import { formatDecisionSummary } from "./runner.entries.js";
|
||||
import type { MediaUnderstandingDecision } from "./types.js";
|
||||
|
||||
describe("media-understanding formatDecisionSummary guards", () => {
|
||||
it("does not throw when decision.attachments is undefined", () => {
|
||||
@@ -26,4 +26,26 @@ describe("media-understanding formatDecisionSummary guards", () => {
|
||||
expect(run).not.toThrow();
|
||||
expect(run()).toBe("video: skipped (0/1)");
|
||||
});
|
||||
|
||||
it("ignores non-string provider/model/reason fields", () => {
|
||||
const run = () =>
|
||||
formatDecisionSummary({
|
||||
capability: "audio",
|
||||
outcome: "failed",
|
||||
attachments: [
|
||||
{
|
||||
attachmentIndex: 0,
|
||||
chosen: {
|
||||
outcome: "failed",
|
||||
provider: { bad: true },
|
||||
model: 42,
|
||||
},
|
||||
attempts: [{ reason: { malformed: true } }],
|
||||
},
|
||||
],
|
||||
} as unknown as MediaUnderstandingDecision);
|
||||
|
||||
expect(run).not.toThrow();
|
||||
expect(run()).toBe("audio: failed (0/1)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -350,15 +350,17 @@ export function formatDecisionSummary(decision: MediaUnderstandingDecision): str
|
||||
const total = attachments.length;
|
||||
const success = attachments.filter((entry) => entry?.chosen?.outcome === "success").length;
|
||||
const chosen = attachments.find((entry) => entry?.chosen)?.chosen;
|
||||
const provider = chosen?.provider?.trim();
|
||||
const model = chosen?.model?.trim();
|
||||
const provider = typeof chosen?.provider === "string" ? chosen.provider.trim() : undefined;
|
||||
const model = typeof chosen?.model === "string" ? chosen.model.trim() : undefined;
|
||||
const modelLabel = provider ? (model ? `${provider}/${model}` : provider) : undefined;
|
||||
const reason = attachments
|
||||
.flatMap((entry) => {
|
||||
const attempts = Array.isArray(entry?.attempts) ? entry.attempts : [];
|
||||
return attempts.map((attempt) => attempt?.reason).filter(Boolean);
|
||||
return attempts
|
||||
.map((attempt) => (typeof attempt?.reason === "string" ? attempt.reason : undefined))
|
||||
.filter((value): value is string => Boolean(value));
|
||||
})
|
||||
.find(Boolean);
|
||||
.find((value) => value.trim().length > 0);
|
||||
const shortReason = reason ? reason.split(":")[0]?.trim() : undefined;
|
||||
const countLabel = total > 0 ? ` (${success}/${total})` : "";
|
||||
const viaLabel = modelLabel ? ` via ${modelLabel}` : "";
|
||||
|
||||
95
src/media-understanding/transcribe-audio.test.ts
Normal file
95
src/media-understanding/transcribe-audio.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
const {
|
||||
normalizeMediaAttachments,
|
||||
createMediaAttachmentCache,
|
||||
buildProviderRegistry,
|
||||
runCapability,
|
||||
cacheCleanup,
|
||||
} = vi.hoisted(() => {
|
||||
const normalizeMediaAttachments = vi.fn();
|
||||
const cacheCleanup = vi.fn(async () => {});
|
||||
const createMediaAttachmentCache = vi.fn(() => ({ cleanup: cacheCleanup }));
|
||||
const buildProviderRegistry = vi.fn(() => new Map());
|
||||
const runCapability = vi.fn();
|
||||
return {
|
||||
normalizeMediaAttachments,
|
||||
createMediaAttachmentCache,
|
||||
buildProviderRegistry,
|
||||
runCapability,
|
||||
cacheCleanup,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./runner.js", () => ({
|
||||
normalizeMediaAttachments,
|
||||
createMediaAttachmentCache,
|
||||
buildProviderRegistry,
|
||||
runCapability,
|
||||
}));
|
||||
|
||||
import { transcribeAudioFile } from "./transcribe-audio.js";
|
||||
|
||||
describe("transcribeAudioFile", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
cacheCleanup.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("does not force audio/wav when mime is omitted", async () => {
|
||||
normalizeMediaAttachments.mockReturnValue([{ index: 0, path: "/tmp/note.mp3" }]);
|
||||
runCapability.mockResolvedValue({
|
||||
outputs: [{ kind: "audio.transcription", text: " hello " }],
|
||||
});
|
||||
|
||||
const result = await transcribeAudioFile({
|
||||
filePath: "/tmp/note.mp3",
|
||||
cfg: {} as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(normalizeMediaAttachments).toHaveBeenCalledWith({
|
||||
MediaPath: "/tmp/note.mp3",
|
||||
MediaType: undefined,
|
||||
});
|
||||
expect(result).toEqual({ text: "hello" });
|
||||
expect(cacheCleanup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns undefined and skips cache when there are no attachments", async () => {
|
||||
normalizeMediaAttachments.mockReturnValue([]);
|
||||
|
||||
const result = await transcribeAudioFile({
|
||||
filePath: "/tmp/missing.wav",
|
||||
cfg: {} as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ text: undefined });
|
||||
expect(createMediaAttachmentCache).not.toHaveBeenCalled();
|
||||
expect(runCapability).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("always cleans up cache on errors", async () => {
|
||||
const cfg = {
|
||||
tools: { media: { audio: { timeoutSeconds: 10 } } },
|
||||
} as unknown as OpenClawConfig;
|
||||
normalizeMediaAttachments.mockReturnValue([{ index: 0, path: "/tmp/note.wav" }]);
|
||||
runCapability.mockRejectedValue(new Error("boom"));
|
||||
|
||||
await expect(
|
||||
transcribeAudioFile({
|
||||
filePath: "/tmp/note.wav",
|
||||
cfg,
|
||||
}),
|
||||
).rejects.toThrow("boom");
|
||||
|
||||
expect(runCapability).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
capability: "audio",
|
||||
cfg,
|
||||
config: cfg.tools?.media?.audio,
|
||||
}),
|
||||
);
|
||||
expect(cacheCleanup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -23,7 +23,7 @@ export async function transcribeAudioFile(params: {
|
||||
}): Promise<{ text: string | undefined }> {
|
||||
const ctx = {
|
||||
MediaPath: params.filePath,
|
||||
MediaType: params.mime ?? "audio/wav",
|
||||
MediaType: params.mime,
|
||||
};
|
||||
const attachments = normalizeMediaAttachments(ctx);
|
||||
if (attachments.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user