Back to mini-nostr-saf
SOURCE / PINNED RELEASE

Made of little things.

mini-nostr-saf

Release
72c0506a4a1b…
Author-recorded commit
cec3876ccbdc…
License
LICENSE
Author’s source reference
nostr://npub1whs094x2p7qshzqn77uwm0kx0826f8xdad5ppqet7w9jr6ay5scs99c7wj/wss%3A%2F%2Fgit.napplet.soy%2F/n-599618f6842

Archive hash verified: e3147c802508002b…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

tests/relay-picks.test.mjs
/**
 * 回归测试:每日推荐抓取永不悬挂、永不抛错。
 *
 * 背景(2026-10-07 用户报障):napplet 里“每日推荐”长期显示“正在获取今日推荐…”、
 * 不断刷新。根因是 fetchRelayPicks 直接 await host 的 resource.bytes():
 * host 悬挂(不解决也不拒绝)时 loading 状态永远卡住;SDK import 写在 try
 * 之外,抛错会导致未处理的 rejection,同样卡住 loading。
 * 修复:抓取器可注入 + 每次抓取硬超时 + 全链路 try/catch + UI 错误态与重试。
 */
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { runInNewContext } from "node:vm";
import ts from "typescript";

const root = dirname(dirname(fileURLToPath(import.meta.url)));

async function loadRelayPicks() {
  const source = await readFile(join(root, "src/relay-picks.ts"), "utf8");
  const compiled = ts.transpileModule(source, {
    compilerOptions: {
      module: ts.ModuleKind.CommonJS,
      target: ts.ScriptTarget.ES2022,
    },
  }).outputText;
  const module = { exports: {} };
  runInNewContext(compiled, {
    module,
    exports: module.exports,
    AbortController,
    setTimeout,
    clearTimeout,
    JSON,
    URL,
  });
  return module.exports;
}

const validDoc = {
  date: "2026-10-07",
  pool_size: 941,
  picks: [{ url: "wss://example.com", name: "example", description: "" }],
};

const blobOf = (text) =>
  new Blob([text], { type: "application/json" });

/** 断言被测 promise 在时限内解决,否则判悬挂失败。 */
async function mustSettle(promise, ms, label) {
  let timer;
  const timeout = new Promise((_, reject) => {
    timer = setTimeout(
      () => reject(new Error(`${label}: hung for ${ms}ms`)),
      ms,
    );
  });
  try {
    return await Promise.race([promise, timeout]);
  } finally {
    clearTimeout(timer);
  }
}

test("fetchRelayPicks 从带日期的 URL 取到合法文档即返回", async () => {
  const { fetchRelayPicks } = await loadRelayPicks();
  const seen = [];
  const fetcher = async (url) => {
    seen.push(url);
    return blobOf(JSON.stringify(validDoc));
  };
  const result = await mustSettle(fetchRelayPicks(fetcher, 1000), 5000, "valid");
  assert.deepEqual(result, validDoc);
  assert.ok(seen[0].includes("?date="), "优先尝试带日期的 URL");
});

test("fetchRelayPicks 日期 URL 失败时回退到无参 URL", async () => {
  const { fetchRelayPicks, RELAY_PICKS_URL } = await loadRelayPicks();
  const seen = [];
  const fetcher = async (url) => {
    seen.push(url);
    if (url.includes("?date=")) {
      throw Object.assign(new Error("blocked"), { code: "blocked-by-policy" });
    }
    return blobOf(JSON.stringify(validDoc));
  };
  const result = await mustSettle(fetchRelayPicks(fetcher, 1000), 5000, "fallback");
  assert.deepEqual(result, validDoc);
  assert.equal(seen[1], RELAY_PICKS_URL);
});

test("fetchRelayPicks 抓取器悬挂时返回 null 而不是卡住", async () => {
  const { fetchRelayPicks } = await loadRelayPicks();
  // 永不解决、忽略 signal:模拟 host 的 resource.bytes 悬挂。
  const hanging = () => new Promise(() => {});
  const result = await mustSettle(fetchRelayPicks(hanging, 100), 5000, "hang");
  assert.equal(result, null);
});

test("fetchRelayPicks 抓取器持续抛错时返回 null 而不是抛错", async () => {
  const { fetchRelayPicks } = await loadRelayPicks();
  const failing = async () => {
    throw new Error("network-error");
  };
  const result = await mustSettle(fetchRelayPicks(failing, 200), 5000, "reject");
  assert.equal(result, null);
});

test("fetchRelayPicks 拿到非法 JSON 时返回 null", async () => {
  const { fetchRelayPicks } = await loadRelayPicks();
  const bad = async () => blobOf("not json{{{");
  const result = await mustSettle(fetchRelayPicks(bad, 200), 5000, "bad json");
  assert.equal(result, null);
});

test("isRelayPicks 校验文档形状", async () => {
  const { isRelayPicks } = await loadRelayPicks();
  assert.equal(isRelayPicks(validDoc), true);
  assert.equal(isRelayPicks(null), false);
  assert.equal(isRelayPicks({ date: "x", picks: [{ name: "缺 url" }] }), false);
  assert.equal(isRelayPicks({ date: "x", picks: "不是数组" }), false);
});