evOo-local/packages/adapters/src/media-library.test.ts
Claude 044669edfc
Add a media library: owner folders that bots can search and import from
The deployment owner adds folders of photos and videos in the media settings. A per-folder
background job indexes them (relative path, kind, size, capture date, dimensions, duration),
re-indexes every six hours, and drops files that are gone. The owner's own runs get two tools:
search_media (words in folder and file names, kind, capture date range, newest first) and
import_media (copies one file into the conversation as an artifact). Files are never changed;
hidden entries and symlinks are skipped, and an import must resolve inside its folder.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wy1md9QE1ayJbDCwJ4vAQs
2026-10-05 08:45:30 +00:00

64 lines
2.4 KiB
TypeScript

import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
checkMediaLibraryFolder,
mediaLibraryFileType,
walkMediaLibrary,
} from "./media-library.js";
const dirs: string[] = [];
function tempDir() {
const dir = mkdtempSync(path.join(tmpdir(), "rakazo-library-unit-"));
dirs.push(dir);
return dir;
}
afterEach(() => {
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
});
async function walk(root: string) {
const out: string[] = [];
for await (const file of walkMediaLibrary(root)) out.push(file);
return out;
}
describe("media library", () => {
it("indexes photos and videos only", () => {
expect(mediaLibraryFileType("IMG_0001.JPG")).toEqual({ kind: "photo", mimeType: "image/jpeg" });
expect(mediaLibraryFileType("clip.mp4")).toEqual({ kind: "video", mimeType: "video/mp4" });
expect(mediaLibraryFileType("song.mp3")).toBeNull();
expect(mediaLibraryFileType("notes.txt")).toBeNull();
});
it("walks below the folder without hidden entries or symlinks", async () => {
const root = tempDir();
const outside = tempDir();
mkdirSync(path.join(root, "2024", "Summer"), { recursive: true });
mkdirSync(path.join(root, ".cache"));
writeFileSync(path.join(root, "2024", "Summer", "b.jpg"), "x");
writeFileSync(path.join(root, "2024", "a.png"), "x");
writeFileSync(path.join(root, "2024", "readme.txt"), "x");
writeFileSync(path.join(root, ".cache", "c.jpg"), "x");
writeFileSync(path.join(root, ".hidden.jpg"), "x");
writeFileSync(path.join(outside, "d.jpg"), "x");
symlinkSync(path.join(outside, "d.jpg"), path.join(root, "link.jpg"));
symlinkSync(outside, path.join(root, "linked-folder"));
expect(await walk(root)).toEqual(["2024/Summer/b.jpg", "2024/a.png"]);
});
it("accepts only an absolute path to an existing folder", async () => {
const root = tempDir();
writeFileSync(path.join(root, "file.jpg"), "x");
expect(await checkMediaLibraryFolder(root)).toBeNull();
expect(await checkMediaLibraryFolder("photos")).toBe("Use an absolute folder path.");
expect(await checkMediaLibraryFolder(path.join(root, "file.jpg"))).toBe(
"This is not a folder.",
);
expect(await checkMediaLibraryFolder(path.join(root, "missing"))).toBe("Folder not found.");
});
});