import { assertEquals, assertRejects } from "@std/assert"; import { decodeCursor, encodeCursor, firstSnippet, searchNotes, } from "../src/search/opensearch.ts"; import { installFetchStub, jsonResponse } from "./_helpers/fetch_stub.ts"; const withOpenSearchEnv = (fn: () => Promise | void) => { return async () => { const previousUrl = Deno.env.get("OPENSEARCH_URL"); const previousIndex = Deno.env.get("OPENSEARCH_INDEX"); Deno.env.set("OPENSEARCH_URL", "http://opensearch.test"); Deno.env.set("OPENSEARCH_INDEX", "notes"); try { await fn(); } finally { if (previousUrl === undefined) Deno.env.delete("OPENSEARCH_URL"); else Deno.env.set("OPENSEARCH_URL", previousUrl); if (previousIndex === undefined) Deno.env.delete("OPENSEARCH_INDEX"); else Deno.env.set("OPENSEARCH_INDEX", previousIndex); } }; }; Deno.test("encodeCursor and decodeCursor round-trip", () => { const cursor = encodeCursor(3.14, "abc123"); assertEquals(decodeCursor(cursor), [3.14, "abc123"]); }); Deno.test("decodeCursor returns undefined on garbage input", () => { assertEquals(decodeCursor("not-base64-at-all!!!"), undefined); assertEquals(decodeCursor(btoa("not-json")), undefined); assertEquals(decodeCursor(btoa(JSON.stringify([1]))), undefined); assertEquals( decodeCursor(btoa(JSON.stringify(["wrong", "type"]))), undefined, ); }); Deno.test("firstSnippet prefers highlight.content > title > source content > title fallback", () => { assertEquals( firstSnippet( { content: ["hl-content"], title: ["hl-title"] }, { title: "src-title", content: "src-content" }, ), "hl-content", ); assertEquals( firstSnippet({ title: ["hl-title"] }, { title: "src-title", content: "src-content", }), "hl-title", ); const longContent = "a".repeat(500); assertEquals( firstSnippet(undefined, { content: longContent, title: "ignored" }).length, 200, ); assertEquals(firstSnippet(undefined, { title: "only-title" }), "only-title"); }); Deno.test( "searchNotes returns empty when OPENSEARCH_URL is unset", async () => { const previousUrl = Deno.env.get("OPENSEARCH_URL"); Deno.env.delete("OPENSEARCH_URL"); try { const stub = installFetchStub(() => jsonResponse({ should: "not be called" }) ); try { const result = await searchNotes({ q: "hello", discoverableOnly: true, limit: 10, }); assertEquals(result, { results: [] }); assertEquals(stub.calls.length, 0); } finally { stub.restore(); } } finally { if (previousUrl !== undefined) { Deno.env.set("OPENSEARCH_URL", previousUrl); } } }, ); Deno.test( "searchNotes builds multi_match body with filters and cursor", withOpenSearchEnv(async () => { const stub = installFetchStub(() => jsonResponse({ hits: { hits: [ { _source: { did: "did:plc:a", rkey: "rk-a", title: "T", content: "c", }, _score: 1.5, highlight: { content: ["snippet"] }, }, { _source: { did: "did:plc:b", rkey: "rk-b", title: "U", content: "d", }, _score: 1.0, }, ], }, }) ); try { const result = await searchNotes({ q: "hello world", discoverableOnly: true, did: "did:plc:a", limit: 2, cursor: encodeCursor(2.0, "rk-prev"), }); assertEquals(result.results.length, 2); assertEquals(result.results[0].snippet, "snippet"); assertEquals(result.results[1].snippet, "d"); assertEquals(result.cursor, encodeCursor(1.0, "rk-b")); assertEquals(stub.calls.length, 1); const sent = JSON.parse(stub.calls[0].body) as Record; assertEquals(sent.size, 2); assertEquals(sent.search_after, [2.0, "rk-prev"]); const query = sent.query as { bool: { must: unknown[]; filter: unknown[] }; }; const multi = (query.bool.must[0] as { multi_match: Record }) .multi_match; assertEquals(multi.query, "hello world"); assertEquals(multi.fields, ["title^2", "content"]); assertEquals(multi.fuzziness, "AUTO"); assertEquals(query.bool.filter, [ { term: { discoverable: true } }, { term: { did: "did:plc:a" } }, ]); } finally { stub.restore(); } }), ); Deno.test( "searchNotes returns cursor only when the page is full", withOpenSearchEnv(async () => { const stub = installFetchStub(() => jsonResponse({ hits: { hits: [ { _source: { did: "did:plc:a", rkey: "rk", title: "T" }, _score: 1, }, ], }, }) ); try { const result = await searchNotes({ q: "hi", discoverableOnly: false, limit: 5, }); assertEquals(result.results.length, 1); assertEquals(result.cursor, undefined); } finally { stub.restore(); } }), ); Deno.test( "searchNotes throws on non-ok response", withOpenSearchEnv(async () => { const stub = installFetchStub(() => new Response("boom", { status: 500 })); try { await assertRejects(() => searchNotes({ q: "hi", discoverableOnly: false, limit: 5, }) ); } finally { stub.restore(); } }), );