import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; import type { Message, Collection, Attachment } from "discord.js"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function makeMessage(attachments: Attachment[] = []): Message { const col = new Map(attachments.map((a) => [a.id, a])); return { attachments: { size: attachments.length, values: () => col.values(), }, } as unknown as Message; } function makeAttachment(id: string, name: string, url: string): Attachment { return { id, name, url } as unknown as Attachment; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("processAttachments", () => { let tmpDir: string; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "disclaw-inbox-test-")); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); vi.restoreAllMocks(); }); it("returns an empty array when message has no attachments", async () => { const { processAttachments } = await import("../../src/discord/attachment-inbox"); const message = makeMessage([]); const hints = await processAttachments(message, tmpDir); expect(hints).toEqual([]); // No inbox directory created expect(fs.existsSync(path.join(tmpDir, ".disclaw-inbox"))).toBe(false); }); it("saves an attachment to .disclaw-inbox/ and returns a hint", async () => { // Mock global fetch const fileContent = "print('hello')\n"; const contentBytes = Buffer.from(fileContent, "utf-8"); // Allocate a clean ArrayBuffer (not the Node shared pool) so Buffer.from() round-trips correctly const cleanArrayBuffer = contentBytes.buffer.slice( contentBytes.byteOffset, contentBytes.byteOffset + contentBytes.byteLength ); vi.stubGlobal( "fetch", vi.fn().mockResolvedValue({ ok: true, arrayBuffer: async () => cleanArrayBuffer, }) ); const { processAttachments } = await import("../../src/discord/attachment-inbox"); const attachment = makeAttachment("att-1", "example.py", "https://cdn.discord.com/example.py"); const message = makeMessage([attachment]); const hints = await processAttachments(message, tmpDir); expect(hints).toHaveLength(1); expect(hints[0]).toMatch(/^\[Datei verfügbar: \.disclaw-inbox\//); expect(hints[0]).toContain("example.py"); // File must exist in inbox const inboxDir = path.join(tmpDir, ".disclaw-inbox"); const files = fs.readdirSync(inboxDir); expect(files).toHaveLength(1); expect(files[0]).toMatch(/example\.py$/); // File content must match const savedContent = fs.readFileSync(path.join(inboxDir, files[0]), "utf-8"); expect(savedContent).toBe(fileContent); }); it("skips an attachment when fetch fails", async () => { vi.stubGlobal( "fetch", vi.fn().mockResolvedValue({ ok: false, status: 403, }) ); const { processAttachments } = await import("../../src/discord/attachment-inbox"); const attachment = makeAttachment("att-1", "secret.txt", "https://cdn.discord.com/secret.txt"); const message = makeMessage([attachment]); const hints = await processAttachments(message, tmpDir); expect(hints).toEqual([]); }); }); describe("cleanupInbox", () => { let tmpDir: string; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "disclaw-cleanup-test-")); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); vi.restoreAllMocks(); }); it("does nothing when inbox directory does not exist", async () => { const { cleanupInbox } = await import("../../src/discord/attachment-inbox"); expect(() => cleanupInbox(tmpDir)).not.toThrow(); }); it("deletes files older than 24 hours and keeps newer files", async () => { const { cleanupInbox } = await import("../../src/discord/attachment-inbox"); const inboxDir = path.join(tmpDir, ".disclaw-inbox"); fs.mkdirSync(inboxDir); const oldFile = path.join(inboxDir, "old-file.txt"); const newFile = path.join(inboxDir, "new-file.txt"); fs.writeFileSync(oldFile, "old"); fs.writeFileSync(newFile, "new"); // Backdate old file by 25 hours const oldTime = new Date(Date.now() - 25 * 60 * 60 * 1000); fs.utimesSync(oldFile, oldTime, oldTime); cleanupInbox(tmpDir); expect(fs.existsSync(oldFile)).toBe(false); expect(fs.existsSync(newFile)).toBe(true); }); });