sync update from LAN repo (source dhk-1 @ 3b1ec5b4)

This commit is contained in:
gozeros committed 2026-11-05 10:56:34 +08:00
1 parent da6521b6cb
commit 264e2200d6
995 files changed
+148105 -13399

No files matched your search

@@ -3,12 +3,18 @@ const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const vm = require("node:vm");
const { webcrypto } = require("node:crypto");
const BACKGROUND_PATH = path.join(__dirname, "background.js");
function createHarness() {
function createHarness(options = {}) {
let storageChangedListener;
let alarmCreateCount = 0;
let clock = options.now ?? Date.now();
let fetchHandler = options.fetch;
let cookieToken = "";
const localData = options.localData || {};
const fetchUrls = [];
const settings = {
apiBaseUrl: "http://localhost:8001",
webBaseUrl: "http://localhost:5174",
@@ -18,6 +24,7 @@ function createHarness() {
taskPollingEnabled: true,
workerId: "test-worker",
taskPollIntervalSeconds: 5,
...options.settings,
};
const chrome = {
@@ -28,7 +35,7 @@ function createHarness() {
},
onAlarm: { addListener: () => {} },
},
cookies: { get: async () => null },
cookies: { get: async () => cookieToken ? { value: cookieToken } : null },
runtime: {
onInstalled: { addListener: () => {} },
onMessage: { addListener: () => {} },
@@ -36,8 +43,8 @@ function createHarness() {
},
storage: {
local: {
get: async (defaults) => ({ ...defaults }),
set: async () => {},
get: async (defaults) => ({ ...defaults, ...localData }),
set: async (updates) => Object.assign(localData, updates),
},
session: { get: async () => ({ actSessions: {} }), set: async () => {} },
sync: {
@@ -56,13 +63,26 @@ function createHarness() {
const context = {
chrome,
console,
fetch: async () => {
throw new Error("offline test");
fetch: async (url, init) => {
fetchUrls.push(String(url));
if (fetchHandler) return fetchHandler(url, init);
return {
ok: true,
status: 200,
json: async () => ({ enabled: true }),
};
},
importScripts: () => {},
setTimeout: () => 1,
clearTimeout: () => {},
URL,
AbortController,
TextEncoder,
crypto: webcrypto,
Date: class extends Date {
constructor(...args) { super(...(args.length ? args : [clock])); }
static now() { return clock; }
},
DEER_CLIPPER_DEFAULTS: settings,
deerNormalizeBaseUrl: (value) => String(value || "").replace(/\/+$/, ""),
deerMakeWorkerId: () => "test-worker",
@@ -79,6 +99,17 @@ function createHarness() {
storageChangedListener(changes, "sync");
},
getAlarmCreateCount: () => alarmCreateCount,
getFetchUrls: () => [...fetchUrls],
getSettings: () => context.getSettings(),
setSettings: (updates) => Object.assign(settings, updates),
getLocalData: () => ({ ...localData }),
advanceTime: (ms) => { clock += ms; },
getTime: () => clock,
setFetch: (handler) => { fetchHandler = handler; },
setCookie: (token) => { cookieToken = token; },
status: (url, settings = {}, force = false) => context.browserContextStatus(url, settings, force),
discoverServiceBases: (settings, force = false) =>
context.discoverServiceBases(settings, force),
resetAlarmCreateCount: () => {
alarmCreateCount = 0;
},
@@ -128,3 +159,120 @@ test("service discovery prefers the API paired with the open zncm tab", async ()
].filter((value, index, values) => values.indexOf(value) === index),
);
});
test("automatic service discovery checks status once per ten minutes, not on every task poll", async () => {
const harness = createHarness();
const settings = await harness.getSettings();
settings.autoDiscoverServiceUrl = true;
await harness.discoverServiceBases(settings);
assert.equal(harness.getFetchUrls().length, 1);
await harness.discoverServiceBases(await harness.getSettings());
assert.equal(harness.getFetchUrls().length, 1);
await harness.discoverServiceBases(await harness.getSettings(), true);
assert.equal(harness.getFetchUrls().length, 1);
harness.advanceTime(30_000);
await harness.discoverServiceBases(await harness.getSettings(), true);
assert.equal(harness.getFetchUrls().length, 2);
});
test("unchanged service discovery refreshes its timestamp after TTL expiry", async () => {
const harness = createHarness();
await settle();
harness.setSettings({ autoDiscoverServiceUrl: true });
await harness.discoverServiceBases(await harness.getSettings());
const firstSync = harness.getLocalData().lastServiceSyncAt;
harness.advanceTime(10 * 60_000 + 1);
await harness.discoverServiceBases(await harness.getSettings());
assert.notEqual(harness.getLocalData().lastServiceSyncAt, firstSync);
assert.equal(harness.getFetchUrls().length, 2);
for (let i = 0; i < 20; i++) {
harness.advanceTime(3_000);
await harness.discoverServiceBases(await harness.getSettings());
}
assert.equal(harness.getFetchUrls().length, 2);
});
test("concurrent status callers share a request and successful results expire", async () => {
const harness = createHarness();
const url = "http://example.test";
const results = await Promise.all(Array.from({ length: 30 }, () => harness.status(url)));
assert.ok(results.every((result) => result.enabled));
assert.equal(harness.getFetchUrls().length, 1);
harness.advanceTime(59_000);
await harness.status(`${url}/`);
assert.equal(harness.getFetchUrls().length, 1);
harness.advanceTime(1_001);
await harness.status(url);
assert.equal(harness.getFetchUrls().length, 2);
});
test("disabled status respects Retry-After even for manual calls and later recovers", async () => {
const harness = createHarness({
fetch: async () => ({
ok: false,
status: 403,
headers: { get: () => "300" },
json: async () => ({ detail: { code: "browser_status_disabled" } }),
}),
});
const url = "http://example.test";
const status = await harness.status(url);
assert.equal(status.enabled, false);
assert.equal(status.statusAccessDisabled, true);
assert.equal(status.authenticationRequired, undefined);
harness.advanceTime(299_999);
await harness.status(url, {}, true);
assert.equal(harness.getFetchUrls().length, 1);
harness.setFetch(async () => ({ ok: true, status: 200, json: async () => ({ enabled: true }) }));
harness.advanceTime(1);
assert.equal((await harness.status(url)).enabled, true);
assert.equal(harness.getFetchUrls().length, 2);
});
test("network and server failures back off for five minutes", async () => {
for (const fetch of [
async () => { throw new Error("offline"); },
async () => ({ ok: false, status: 500, json: async () => ({ detail: "unavailable" }) }),
]) {
const harness = createHarness({ fetch });
for (let i = 0; i < 10; i++) assert.equal(await harness.status("http://example.test", {}, true), null);
assert.equal(harness.getFetchUrls().length, 1);
harness.advanceTime(300_000);
assert.equal(await harness.status("http://example.test"), null);
assert.equal(harness.getFetchUrls().length, 2);
}
});
test("status cache separates service addresses, login tokens and cookies", async () => {
const harness = createHarness();
await harness.status("http://one.test", { accessToken: "account-a" });
await harness.status("http://one.test", { accessToken: "account-b" });
await harness.status("http://two.test", { accessToken: "account-b" });
harness.setCookie("new-login");
await harness.status("http://two.test", { accessToken: "account-b" });
assert.equal(harness.getFetchUrls().length, 4);
await harness.status("http://two.test", { accessToken: "account-b" });
assert.equal(harness.getFetchUrls().length, 4);
});
test("discovery survives worker restarts but refreshes after account changes", async () => {
const harness = createHarness();
await settle();
harness.setSettings({ autoDiscoverServiceUrl: true, accessToken: "account-a" });
await harness.discoverServiceBases(await harness.getSettings());
const localData = harness.getLocalData();
assert.ok(!JSON.stringify(localData).includes("account-a"));
const restarted = createHarness({
localData,
now: harness.getTime(),
settings: { autoDiscoverServiceUrl: true, accessToken: "account-a" },
});
await restarted.discoverServiceBases(await restarted.getSettings());
assert.equal(restarted.getFetchUrls().length, 0);
restarted.setSettings({ accessToken: "account-b" });
await restarted.discoverServiceBases(await restarted.getSettings());
assert.equal(restarted.getFetchUrls().length, 1);
});