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
64 lines
2.4 KiB
TypeScript
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.");
|
|
});
|
|
});
|