The dump worker becomes a page cache: a hit is served from DO SQLite storage with no diigo call; a miss asks the webapp instance in the cache's wiring and etches the page. - two independent caches, one behavior (PagedCache): DiigoCache keeps every item; PublicCache keeps shared items only, dropping private rows before they are stored; served at /public/<username> (HTML page form, or JSON) - create flow on one port: /_connect relays webapp's mint form, /_mint proxies the mint, /_wire wires a private or public cache; /_publish adds a public cache later - ids are webkeys: kept out of URLs (POST bodies), shown and logged only as a hash fingerprint; saved to the password manager under the holder's petname (never sent to the server) - items keyed on diigo's link_id; username from u_name; unkeyable items and every failure path are logged; each page read logs hit/miss - tests run the real worker + DOs in the vitest pool against a fake webapp (tests/fake-webapp.js) Generated-by: opencode (GLM) Generated-by: Claude Opus 5.5
73 lines
2.5 KiB
TypeScript
73 lines
2.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { publicOnly, toRows, usernameOf } from "../src/items.js";
|
|
import type { Item } from "../src/items.js";
|
|
|
|
/** a load_user_items row: diigo's actual field names (observed
|
|
* 2026-10-07), made-up values */
|
|
function diigoItem(linkId: number, url: string): Item {
|
|
return {
|
|
link_id: linkId,
|
|
url,
|
|
title: `title ${linkId}`,
|
|
description: "",
|
|
private: false,
|
|
u_name: "alice_99",
|
|
first_by: "bob-the-first",
|
|
user_id: 4242,
|
|
real_name: "Alice A.",
|
|
tags: "",
|
|
created_at: 1700000000,
|
|
updated_at: 1700000100 + linkId,
|
|
annotations: [],
|
|
comments: [],
|
|
};
|
|
}
|
|
|
|
function makeLog() {
|
|
const entries: { level: string; args: unknown[] }[] = [];
|
|
const at = (level: string) => (...args: unknown[]) => entries.push({ level, args });
|
|
return { log: { warn: at("warn") }, entries };
|
|
}
|
|
|
|
describe("toRows", () => {
|
|
it("keys each item on diigo's link_id, so a page of distinct items stays distinct", () => {
|
|
const { log } = makeLog();
|
|
const rows = toRows([diigoItem(11, "https://a.example/"), diigoItem(12, "https://b.example/")], log);
|
|
expect(rows.map((r) => r.itemId)).toEqual(["11", "12"]);
|
|
});
|
|
|
|
it("logs, rather than silently drops or merges, items it cannot key", () => {
|
|
const { log, entries } = makeLog();
|
|
const noId = diigoItem(13, "https://c.example/");
|
|
delete noId.link_id;
|
|
const noUrl = diigoItem(14, "");
|
|
const rows = toRows([noId, noUrl, diigoItem(15, "https://d.example/")], log);
|
|
expect(rows.map((r) => r.itemId)).toEqual(["15"]);
|
|
expect(entries.map((e) => e.level)).toEqual(["warn", "warn"]);
|
|
// diagnostics name the fields present, never their values
|
|
expect(JSON.stringify(entries)).not.toContain("https://c.example/");
|
|
});
|
|
});
|
|
|
|
describe("usernameOf", () => {
|
|
it("reads the owner's username from diigo's u_name", () => {
|
|
expect(usernameOf(diigoItem(1, "https://a.example/"))).toBe("alice_99");
|
|
});
|
|
|
|
it("answers empty for an item without one", () => {
|
|
expect(usernameOf(undefined)).toBe("");
|
|
expect(usernameOf({ u_name: "has spaces" })).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("publicOnly", () => {
|
|
it("keeps shared rows and drops private ones, before anything is stored", () => {
|
|
const { log } = makeLog();
|
|
const shared = diigoItem(21, "https://shared.example/");
|
|
const secret = { ...diigoItem(22, "https://secret.example/"), private: true };
|
|
const rows = publicOnly(toRows([shared, secret], log));
|
|
expect(rows.map((r) => r.itemId)).toEqual(["21"]);
|
|
expect(JSON.stringify(rows)).not.toContain("secret.example");
|
|
});
|
|
});
|