Files
magpie/internal/gui/tests/sync-usage-error.test.cjs
yetone 10f7b4aa3f davsync, gui: a WebDAV server whose HEAD says Content-Length: 0 no longer fails every shared usage day as "kept 0 of 6869 bytes", and a usage share that failed is its own red line under the sync status (#1259, mzcinf on 坚果云)
After each PUT, check read the file's size from a HEAD and called anything
shorter a cut write. On 坚果云 every usage write failed that way with 0
bytes kept, three tries each; the 0 can only have come from the HEAD's
Content-Length, since a 6869-byte body that the PUT sends with its
Content-Length (not chunked) isn't cut by a dropping network three times
running. A HEAD that says short is now asked again of PROPFIND (Depth 0)
getcontentlength, the size WebDAV clients of 坚果云 read. The write counts
as kept when that size is whole. When PROPFIND gives no size, the HEAD's
stands. A real cut write is still told as cut, with the bytes PROPFIND
says were kept. The extra request is made only after a short HEAD, so
坚果云's request limit isn't spent on servers whose HEAD is right. The
backup's own PUT goes through the same check, so a 坚果云 user whose
setup changes would have hit the same error on the main sync.

Settings > Sync put "Couldn't share usage: …" in the same grey line as
"Synced · host", so it read as part of a sync that went fine. It is now a
line of its own under the status, in red, wrapped whole at 440px. The CLI
already printed it on its own line.

Verified: TestNutstoreHeadSaysNoLength and TestNutstoreBackupWrite use a
fake 坚果云: HEAD Content-Length: 0, real getcontentlength on PROPFIND,
409 for a read in a missing folder, a chunked PUT refused. Whole writes
sync with no usage error, and each write goes once with one HEAD and one
PROPFIND. A tunnel that keeps half is still told as cut, with the half it kept.
On the old dav.go all three fail ("the WebDAV server kept 0 of 536 bytes
of magpie.magpie-backup", "kept 0 of 6869"). TestDAVShortWrite and
TestUsageSharedShortWrite/TestUsageSyncShortWrite pass unchanged.
sync-usage-error.test.cjs runs in Chromium and WebKit, en/zh/ja/de, at
900 and 440px, and fails on the old app.js ("the status line carries no
usage error: 14:46 已同步 · dav.jianguoyun.com · 用量未能共享:…"). The
sync-*, s3-sync, usage-computers, gui-ja, gui-de, gui-zh-tw, i18n-keys
and click-scroll tests pass, 78/78. go test -tags nogui -count=1
./internal/davsync ./internal/gui pass under a temp HOME. go vet passes
on darwin, GOOS=linux and GOOS=windows. Not tried against a real 坚果云
account: that its HEAD answers 0 is inferred from the error, and that its
PROPFIND gives the real size is how its other WebDAV clients read sizes,
neither seen here.
2026-10-08 14:52:11 +08:00

115 lines
6.8 KiB
JavaScript

// Run with Node's test runner and Playwright on the module path; see README.md.
// #1259: a sync whose usage couldn't be shared read "已同步 · dav.jianguoyun.com
// · 用量未能共享:…" in one grey line, as if the whole sync went fine. The
// usage failure is now a line of its own under the status, in red, wrapped
// whole at 440px; the status line names only the sync. Without a usage
// error there is no such line, and a sync that failed says only that. In
// English, Chinese, Japanese and German, Chromium and WebKit, at 900 and
// 440px. The API is faked; nothing is synced.
const assert = require("node:assert/strict");
const fs = require("node:fs/promises");
const path = require("node:path");
const { test } = require("node:test");
const { chromium, webkit } = require("playwright");
const assets = path.resolve(__dirname, "../assets");
const CUT = "the WebDAV server kept 0 of 6869 bytes of 5f0c-2026-08-29.magpie-usage: the write was cut short — sync again, and if it keeps happening the network to the server is dropping long uploads";
function serve(lang, sync) {
const settings = {
theme: "light", lang, tray: "panel", version: "test", dir: "/tmp/magpie",
gateway: "http://127.0.0.1:3425", visionModels: [], imageGenModels: [],
fx: { rate: 7.2, stale: false },
};
return async (route) => {
const request = route.request();
const pathname = new URL(request.url()).pathname;
const json = (data) => route.fulfill({ json: data });
if (pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:false};` });
if (pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
if (pathname === "/api/state") return json({ agents: [], profiles: [], settings });
if (pathname === "/api/settings") return json(settings);
if (pathname === "/api/usage/quotas") return json([]);
if (pathname === "/api/groups") return json({ groups: [], models: [] });
if (pathname === "/api/plugins") return json({ plugins: [] });
if (pathname === "/api/davsync") return json(sync);
if (pathname.startsWith("/api/")) return json({});
const file = path.join(assets, pathname === "/" ? "index.html" : pathname);
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
return route.fulfill({ body: await fs.readFile(file), contentType });
};
}
const words = {
en: { usage: "Couldn't share usage: ", sync: "Couldn't sync: " },
zh: { usage: "用量未能共享:", sync: "同步失败:" },
ja: { usage: "使用量を共有できませんでした:", sync: null },
de: { usage: "Nutzung konnte nicht geteilt werden: ", sync: null },
};
const base = { on: true, kind: "webdav", url: "https://dav.jianguoyun.com/dav/", user: "me", passwordSet: true, passphraseSet: true, keys: true, agents: true, library: true, usage: true };
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
for (const lang of Object.keys(words)) {
for (const width of [900, 440]) {
const w = words[lang];
test(`${engine} ${lang} ${width}px: usage that couldn't be shared is its own red line`, async (t) => {
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
t.after(() => browser.close());
for (const c of [
{ name: "usage failed", sync: { ...base, last: new Date(Date.now() - 60000).toISOString(), usageError: CUT } },
{ name: "all shared", sync: { ...base, last: new Date(Date.now() - 60000).toISOString() } },
{ name: "sync failed", sync: { ...base, error: "the WebDAV server refused the user name or password (HTTP 401)", usageError: CUT } },
]) {
const page = await (await browser.newContext({ viewport: { width, height: 800 }, reducedMotion: "reduce" })).newPage();
const errors = [];
page.on("pageerror", (e) => errors.push(e.message));
await page.route("**/*", serve(lang, c.sync));
page.setDefaultTimeout(5000);
await page.goto("http://magpie.test/");
await page.locator("#prefs").click();
await page.locator("#setTab-sync").click();
const first = page.locator("#syncList .row.pref").first();
await first.waitFor({ state: "visible" });
const status = first.locator(".who > .sub").first();
const line = first.locator(".sync-usage-err");
const said = await status.textContent();
assert.ok(!said.includes("6869"), `${c.name}: the status line carries no usage error: ${said}`);
if (c.name === "usage failed") {
assert.equal(await line.count(), 1, "the usage failure has a line");
assert.equal(await line.textContent(), w.usage + CUT);
const [color, statusColor, red] = await page.evaluate(() => {
const l = document.querySelector("#syncList .sync-usage-err");
const s = l.parentElement.querySelector(".sub");
const probe = document.createElement("i");
probe.style.color = "var(--red)";
document.body.append(probe);
const r = getComputedStyle(probe).color;
probe.remove();
return [getComputedStyle(l).color, getComputedStyle(s).color, r];
});
assert.equal(color, red, "the usage line is red");
assert.notEqual(statusColor, red, "the status beside it isn't");
// shown whole: wrapped, not cut off, and inside the window
const fit = await line.evaluate((e) => ({ sw: e.scrollWidth, cw: e.clientWidth, right: e.getBoundingClientRect().right, h: e.getBoundingClientRect().height }));
assert.ok(fit.sw <= fit.cw + 1, `the line isn't cut off: ${JSON.stringify(fit)}`);
assert.ok(fit.right <= width, `the line is inside the window: ${JSON.stringify(fit)}`);
assert.ok(fit.h > 0);
const over = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
assert.ok(over <= 0, `no sideways scroll: ${over}`);
if (process.env.ARTIFACT_DIR) {
await fs.mkdir(process.env.ARTIFACT_DIR, { recursive: true });
await first.screenshot({ path: path.join(process.env.ARTIFACT_DIR, `${engine}-${lang}-${width}-sync-usage-error.png`) });
}
} else {
assert.equal(await line.count(), 0, `${c.name}: no usage line`);
if (c.name === "sync failed" && w.sync) assert.ok(said.startsWith(w.sync), said);
}
assert.deepEqual(errors, []);
await page.close();
}
});
}
}
}