Files
Endoleap/tests/api-smoke.mjs
T

380 lines
24 KiB
JavaScript

import assert from "node:assert/strict";
import { createServer } from "node:http";
import { spawn } from "node:child_process";
import { once } from "node:events";
import { fileURLToPath } from "node:url";
import { readdir, readFile } from "node:fs/promises";
const host = "http://127.0.0.1:3005";
const graphs = new Map();
let releaseStream = () => {};
let canvasSnapshot = { version: 1, document: { schema_version: 1, nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } } };
let mockSessionSeconds = "2592000";
let mockProfile = {
id: "user", username: "test", display_name: "Test User", email: "test@example.com",
avatar_url: "https://lh3.googleusercontent.com/test-avatar=s96-c",
};
const upstream = createServer(async (req, res) => {
res.setHeader("Content-Type", "application/json");
if (req.headers.authorization === "Bearer test-only-session" && !req.url.endsWith("/auth/logout")) res.setHeader("X-Foundry-Session-Expires-In", mockSessionSeconds);
const canonical = req.url.replace("/workspaces/creative-space/", "/workspaces/space/").replace("/materials/my-board", "/materials/foundry-generated-id");
if (req.url.includes("/canvas")) assert.equal(req.headers.accept, "application/vnd.foundry.canvas+json;version=2");
if (req.url.startsWith("/projects/endoleap/workspaces/space/assets")) {
if (req.headers.authorization !== "Bearer test-only-session") { res.statusCode = 401; res.end(JSON.stringify({error:"invalid_session"})); return; }
if (req.method === "GET") { res.setHeader("X-Asset-Variant", new URL(req.url,"http://mock").searchParams.get("variant") === "preview" ? "preview" : "original"); res.setHeader("Content-Type", req.url.endsWith("/bad") ? "image/svg+xml" : "image/png"); res.end(Buffer.from([137,80,78,71,13,10,26,10])); return; }
let bytes = Buffer.alloc(0); for await (const chunk of req) bytes = Buffer.concat([bytes, chunk]);
res.end(JSON.stringify({type:req.headers["content-type"],bytes:bytes.length,key:req.headers["idempotency-key"],multipart:bytes.includes(Buffer.from('name="image"'))})); return;
}
if (canonical.endsWith("/materials/slow-body-board/canvas")) {
res.setHeader("ETag", '\"210\"');
res.write('{"version":210,"document":');
await new Promise((resolve) => setTimeout(resolve, 15200));
res.end('{"schema_version":2,"nodes":[],"edges":[],"viewport":{"x":0,"y":0,"zoom":1}}}'); return;
}
if (canonical.endsWith("/materials/stream-board/canvas")) {
assert.equal(req.headers.authorization, "Bearer test-only-session");
res.setHeader("ETag", '\"1\"'); res.setHeader("X-Canvas-Schema-Version", "2");
res.write('{"version":1,"document":');
await new Promise((resolve) => { releaseStream = resolve; });
res.end('{"schema_version":2,"nodes":[],"edges":[],"viewport":{"x":0,"y":0,"zoom":1}}}'); return;
}
if (canonical.includes("/canvas/")) {
res.end(JSON.stringify({url:req.url,method:req.method,accept:req.headers.accept,key:req.headers["idempotency-key"],version:req.headers["if-match"]})); return;
}
if (canonical === "/projects/endoleap/workspaces/space/materials/foundry-generated-id/canvas") {
if (req.headers.authorization !== "Bearer test-only-session") { res.statusCode = 401; res.end(JSON.stringify({ error: "invalid_session" })); return; }
if (req.method === "GET") { res.setHeader("X-Canvas-Schema-Version", String(canvasSnapshot.document.schema_version)); res.setHeader("ETag", '"' + canvasSnapshot.version + '"'); res.end(JSON.stringify(canvasSnapshot)); return; }
let raw = ""; for await (const chunk of req) raw += chunk;
if (req.headers["if-match"] !== '"' + canvasSnapshot.version + '"') { res.statusCode = 412; res.end(JSON.stringify({ error: "version_conflict" })); return; }
const { document } = JSON.parse(raw);
assert.equal(document.schema_version, 1); assert(Array.isArray(document.nodes));
canvasSnapshot = { version: canvasSnapshot.version + 1, document };
res.setHeader("ETag", '"' + canvasSnapshot.version + '"'); res.end(JSON.stringify({ version: canvasSnapshot.version })); return;
}
if (req.url.endsWith("/auth/logout")) {
res.statusCode = 204;
res.end();
} else if (req.url === "/projects/endoleap/workspaces/space/materials" && req.method === "POST") {
assert.equal(req.headers.authorization, "Bearer test-only-session");
let raw = "";
for await (const chunk of req) raw += chunk;
const input = JSON.parse(raw);
assert.deepEqual(input, { kind: "board", title: "未命名图谱", content: { schema_version: 1 } });
const graph = { ...input, slug: "my-board", id: "foundry-generated-id", workspace_id: "space", version: 1 };
graphs.set(graph.id, graph);
res.statusCode = 201;
res.end(JSON.stringify({ id: graph.id, workspace_id: graph.workspace_id, kind: graph.kind, version: graph.version }));
} else if (canonical === "/projects/endoleap/workspaces/space/materials/foundry-generated-id") {
assert.equal(req.headers.authorization, "Bearer test-only-session");
if (req.method === "PATCH") {
assert.equal(req.headers["if-match"], '"1"');
let raw = "";
for await (const chunk of req) raw += chunk;
const input = JSON.parse(raw);
const current = graphs.get("foundry-generated-id");
graphs.set(current.id, { ...current, ...input, version: current.version + 1 });
res.statusCode = 204;
res.end();
} else res.end(JSON.stringify(graphs.get("foundry-generated-id")));
} else if (req.url.includes("/workspaces/space/materials")) {
res.end(JSON.stringify({ url: req.url, method: req.method }));
} else if (req.url.endsWith("/auth/login")) {
res.end(
JSON.stringify({
access_token: "test-only-session",
expires_in: 2592000,
token_type: "Bearer",
}),
);
} else if (req.url.endsWith("/profile")) {
res.statusCode =
req.headers.authorization === "Bearer test-only-session" ? 200 : 401;
if (res.statusCode === 200 && req.method === "PATCH") {
let raw = "";
for await (const chunk of req) raw += chunk;
const input = JSON.parse(raw);
mockProfile = { ...mockProfile, username: input.username, display_name: input.display_name };
}
res.end(JSON.stringify(res.statusCode === 200 ? mockProfile : { error: "invalid_session" }));
} else {
res.statusCode = 404;
res.end("{}");
}
});
await new Promise((r) => upstream.listen(0, "127.0.0.1", r));
const port = upstream.address().port;
const app = spawn(
process.execPath,
[
"node_modules/next/dist/bin/next",
"start",
"--hostname",
"127.0.0.1",
"--port",
"3005",
],
{
cwd: fileURLToPath(new URL("../", import.meta.url)),
env: { ...process.env, FOUNDRY_API_ORIGIN: "http://127.0.0.1:" + port },
stdio: ["ignore", "pipe", "pipe"],
},
);
let startup = "";
app.stdout.on("data", (x) => (startup += x));
app.stderr.on("data", (x) => (startup += x));
try {
let ready = false;
for (let n = 0; n < 100; n++) {
if (app.exitCode !== null)
throw new Error("Frontend failed to start: " + startup);
try {
if ((await fetch(host)).ok) {
ready = true;
break;
}
} catch {}
await new Promise((r) => setTimeout(r, 100));
}
assert(ready, "Frontend did not start");
for (const path of ["/", "/login", "/auth/callback", "/dashboard", "/dashboard?demo=1", "/dashboard/space/foundry-generated-id"]) {
const page = await fetch(host + path);
assert.equal(page.status, 200, path);
const html = await page.text();
assert.match(html, /<html lang="zh-CN"/);
assert.match(html, /啡跃 Endoleap · 让灵感有形/);
assert.match(html, /name="description"/);
assert.match(html, /name="viewport"/);
assert(html.includes('/brand/icons/favicon.ico'));
assert(html.includes('/brand/icons/apple-touch-icon.png'));
}
const brandRoot = new URL("../public/brand/", import.meta.url);
const assets = (await readdir(brandRoot, { recursive: true })).filter((file) => /\.(png|ico|svg)$/.test(file));
assert.equal(assets.length, 88);
for (const asset of assets) {
const response = await fetch(host + "/brand/" + asset);
assert.equal(response.status, 200, asset);
assert.deepEqual(Buffer.from(await response.arrayBuffer()), await readFile(new URL(asset, brandRoot)), asset);
}
const iconManifest = JSON.parse(await readFile(new URL("../src/lib/canvas-icon-assets.json", import.meta.url), "utf8"));
for (const file of Object.values(iconManifest.assets)) {
const path = iconManifest.root + "/" + file;
const response = await fetch(host + path);
assert.equal(response.status, 200, path);
assert.match(response.headers.get("cache-control"), /max-age=31536000.*immutable/);
assert.deepEqual(await response.json(), JSON.parse(await readFile(new URL("../public" + path, import.meta.url), "utf8")));
}
assert.equal((await fetch(host + "/missing-page")).status, 404);
const res = await fetch(host + "/api/foundry/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json", Origin: host },
body: "{}",
});
assert.equal(res.status, 200);
assert.match(res.headers.get("server-timing") || "", /upstream;dur=\d/);
const body = await res.json();
assert.equal(body.access_token, undefined);
const cookie = res.headers.get("set-cookie");
assert(cookie?.includes("HttpOnly"));
assert(cookie.includes("SameSite=Lax"));
assert(cookie.includes("Max-Age=2592000"));
const session = cookie.split(";")[0];
// Hold the upstream tail: snapshot headers and its first chunk must arrive
// before the proxy has the full JSON body (the old buffering path deadlocks).
let streamTimer;
try {
const streamed = await Promise.race([fetch(host + "/api/foundry/workspaces/space/materials/stream-board/canvas", { headers: { Cookie: session } }), new Promise((_, reject) => { streamTimer = setTimeout(() => reject(new Error("Snapshot proxy buffered the body")), 2500); })]);
assert.equal(streamed.headers.get("etag"), '\"1\"'); assert.equal(streamed.headers.get("x-canvas-schema-version"), "2");
assert.match(streamed.headers.get("server-timing"), /upstream;dur=.*proxy_headers;dur=/);
assert.ok(streamed.headers.get("set-cookie")?.includes("HttpOnly"));
const reader = streamed.body.getReader(); const first = await reader.read(); assert.ok(first.value.length > 0);
releaseStream(); let raw = new TextDecoder().decode(first.value);
while (true) { const next = await reader.read(); if (next.done) break; raw += new TextDecoder().decode(next.value); }
assert.equal(JSON.parse(raw).document.schema_version, 2);
} finally { clearTimeout(streamTimer); releaseStream(); }
const v2Base = host + "/api/foundry/workspaces/space/materials/foundry-generated-id/canvas";
const idem = "11111111-1111-4111-8111-111111111111";
for (const path of ["connections", "subgraphs", "subgraphs/migrate", "subgraphs/frame-1/members", "subgraphs/frame-1/dissolve", "transactions/tx-1/undo", "ai/runs", "ai/runs/run-1/cancel", "ai/runs/run-1/apply"]) {
const response = await fetch(v2Base + "/" + path, { method: "POST", headers: { Cookie: session, Origin: host, "Content-Type": "application/json", "Idempotency-Key": idem, "If-Match": '\"7\"' }, body: "{}" });
assert.equal(response.status, 200, path); const echo = await response.json(); assert.equal(echo.key, idem); assert.equal(echo.version, '\"7\"'); assert.equal(echo.accept, "application/vnd.foundry.canvas+json;version=2");
}
const preview = await fetch(host + "/api/foundry/workspaces/space/assets/image?variant=preview", { headers: { Cookie: session } });assert.equal(preview.status,200);assert.equal(preview.headers.get("x-asset-variant"),"preview");await preview.arrayBuffer();
const directoryResponse = await (await fetch(host + "/api/foundry/workspaces/space/materials?kind=board&sort=modified&include_subgraphs=true&limit=100&offset=100", { headers: { Cookie: session } })).json();
const directoryQuery = new URL(directoryResponse.url, "http://mock").searchParams;
assert.equal(directoryQuery.get("include_subgraphs"), "true"); assert.equal(directoryQuery.get("offset"), "100");
const events = await (await fetch(v2Base + "/ai/runs/run-1?after_seq=12&limit=100&status=active&include_descendants=true&cursor=cursor&secret=drop", { headers: { Cookie: session } })).json();
const query = new URL(events.url, "http://mock").searchParams; assert.equal(query.get("after_seq"), "12"); assert.equal(query.get("include_descendants"), "true"); assert.equal(query.get("cursor"), "cursor"); assert.equal(query.get("secret"), null);
const slowSnapshot = await fetch(host + "/api/foundry/workspaces/space/materials/slow-body-board/canvas", { headers: { Cookie: session } });
assert.ok(slowSnapshot.headers.get("x-canvas-trace-id")); assert.equal((await slowSnapshot.json()).version, 210, "A valid body can finish after the old 15-second total deadline");
const asset = await fetch(host + "/api/foundry/workspaces/space/assets/image-1", { headers: { Cookie: session } });
assert.equal(asset.status, 200); assert.equal(asset.headers.get("content-type"), "image/png"); assert.equal(asset.headers.get("x-content-type-options"), "nosniff"); assert.ok(asset.headers.get("set-cookie")?.includes("HttpOnly")); assert.equal((await asset.arrayBuffer()).byteLength, 8);
assert.equal((await fetch(host + "/api/foundry/workspaces/space/assets/image-1")).status, 401);
assert.equal((await fetch(host + "/api/foundry/workspaces/space/assets/bad", { headers: { Cookie: session } })).status, 502);
const form = new FormData(); form.set("image", new Blob([new Uint8Array([137,80,78,71])], { type: "image/png" }), "test.png");
const upload = await (await fetch(host + "/api/foundry/workspaces/space/assets", { method: "POST", headers: { Cookie: session, Origin: host, "Idempotency-Key": idem }, body: form })).json();
assert.match(upload.type, /^multipart\/form-data; boundary=/); assert.equal(upload.key, idem); assert.ok(upload.multipart);
const signedInHome = await fetch(host + "/", { headers: { Cookie: session }, redirect: "manual" });
assert.equal(signedInHome.status, 307);
assert.equal(signedInHome.headers.get("location"), "/dashboard");
assert.match(signedInHome.headers.get("cache-control") || "", /private|no-store/);
assert(!(await signedInHome.text()).includes("test-only-session"));
const dashboardHome = await fetch(host + "/dashboard", { headers: { Cookie: session } });
assert.equal(dashboardHome.status, 200);
const dashboardHtml = await dashboardHome.text();
assert.match(dashboardHtml, /aria-label="(?:工作空间侧边栏|正在加载侧边栏)"/);
assert(!dashboardHtml.includes('data-testid="rf__wrapper"'));
assert(!dashboardHtml.includes('class="landing"'));
for (const guestHeaders of [{}, { Cookie: "foundry_session=expired-session" }]) {
const guestHome = await fetch(host + "/", { headers: guestHeaders, redirect: "manual" });
assert.equal(guestHome.status, 200);
assert.equal(guestHome.headers.get("location"), null);
const html = await guestHome.text();
assert(html.includes('class="landing"'));
assert(!html.includes('aria-label="工作空间侧边栏"'));
}
assert.equal(
(
await fetch(host + "/api/foundry/profile", {
headers: { Cookie: session },
})
).status,
200,
);
assert.equal((await fetch(host + "/api/foundry/profile")).status, 401);
mockSessionSeconds = "3600";
const renewed = await fetch(host + "/api/foundry/profile", { headers: { Cookie: session } });
assert.equal(renewed.status, 200); assert.match(renewed.headers.get("set-cookie") || "", /Max-Age=3600/);
assert.match(renewed.headers.get("set-cookie") || "", /HttpOnly/);
mockSessionSeconds = "garbage";
const malformedRenewal = await fetch(host + "/api/foundry/profile", { headers: { Cookie: session } });
assert.equal(malformedRenewal.headers.get("set-cookie"), null);
mockSessionSeconds = "2592000";
const expiredSession = await fetch(host + "/api/foundry/profile", { headers: { Cookie: "foundry_session=expired" } });
assert.equal(expiredSession.status, 401); assert.equal(expiredSession.headers.get("set-cookie"), null);
const profile = await (await fetch(host + "/api/foundry/profile", { headers: { Cookie: session } })).json();
assert.equal(profile.avatar_url, "https://lh3.googleusercontent.com/test-avatar=s96-c");
const updated = await fetch(host + "/api/foundry/profile", {
method: "PATCH",
headers: { "Content-Type": "application/json", Cookie: session, Origin: host },
body: JSON.stringify({ username: "test", display_name: "Updated name" }),
});
assert.equal(updated.status, 200);
const updatedProfile = await updated.json();
assert.equal(updatedProfile.display_name, "Updated name");
assert.equal(updatedProfile.avatar_url, profile.avatar_url);
const create = await fetch(host + "/api/foundry/workspaces/space/materials", {
method: "POST",
headers: { "Content-Type": "application/json", Cookie: session, Origin: host },
body: JSON.stringify({ kind: "board", title: "未命名图谱", content: { schema_version: 1 } }),
});
assert.equal(create.status, 201);
const graph = await create.json();
assert.deepEqual(graph, { id: "foundry-generated-id", workspace_id: "space", kind: "board", version: 1 });
const opened = await fetch(host + `/api/foundry/workspaces/${graph.workspace_id}/materials/${graph.id}`, { headers: { Cookie: session } });
assert.equal(opened.status, 200);
const stored = await opened.json();
assert.equal(stored.title, "未命名图谱");
assert.equal(stored.workspace_id, "space");
assert.equal(stored.content.schema_version, 1);
const rename = await fetch(host + `/api/foundry/workspaces/${graph.workspace_id}/materials/${graph.id}`, {
method: "PATCH", headers: { "Content-Type": "application/json", Cookie: session, Origin: host, "If-Match": '"1"' },
body: JSON.stringify({ title: "改名后的图谱", description: "", content: stored.content }),
});
assert.equal(rename.status, 204);
const renamed = await (await fetch(host + `/api/foundry/workspaces/${graph.workspace_id}/materials/${graph.id}`, { headers: { Cookie: session } })).json();
assert.equal(renamed.title, "改名后的图谱");
assert.equal(renamed.version, 2);
assert.deepEqual(renamed.content, stored.content);
for (const workspace of ["space", "creative-space"]) for (const board of ["foundry-generated-id", "my-board"]) {
const path = `/api/foundry/workspaces/${workspace}/materials/${board}`;
const material = await (await fetch(host + path, { headers: { Cookie: session } })).json();
assert.equal(material.id, "foundry-generated-id"); assert.equal(material.slug, "my-board");
const snapshot = await fetch(host + path + "/canvas", { headers: { Cookie: session } });
assert.equal(snapshot.status, 200); assert.equal(snapshot.headers.get("etag"), '"1"');
assert.equal((await snapshot.json()).version, 1);
}
const canvasPath = "/api/foundry/workspaces/creative-space/materials/my-board/canvas";
const largeDocument = { schema_version: 1, nodes: Array.from({ length: 15 }, (_, index) => ({ id: "text-" + index, type: "material", position: { x: index * 300, y: 100 }, width: 260, height: 200, data: { kind: "text", title: "保存测试", text: "x".repeat(90000), fontSize: 24, rotation: 30, textStyle: { bold: true, color: "#123456" } } })), edges: [], viewport: { x: 20, y: 40, zoom: 0.75 } };
const savedCanvas = await fetch(host + canvasPath, { method: "PATCH", headers: { Cookie: session, Origin: host, "Content-Type": "application/json", "If-Match": '"1"' }, body: JSON.stringify({ document: largeDocument }) });
assert.equal(savedCanvas.status, 200); assert.equal(savedCanvas.headers.get("etag"), '"2"');
assert.equal((await savedCanvas.json()).version, 2);
const restoredCanvas = await (await fetch(host + canvasPath, { headers: { Cookie: session } })).json();
assert.deepEqual(restoredCanvas.document, largeDocument);
const conflict = await fetch(host + canvasPath, { method: "PATCH", headers: { Cookie: session, Origin: host, "Content-Type": "application/json", "If-Match": '"1"' }, body: JSON.stringify({ document: largeDocument }) });
assert.equal(conflict.status, 412); assert.equal((await conflict.json()).error, "version_conflict");
const frameDocument = structuredClone(largeDocument);
frameDocument.nodes.unshift({ id: "frame-1", type: "frame", position: { x: 0, y: 0 }, width: 640, height: 400, data: { kind: "frame", title: "参考分区", text: "", rotation: 0, labelMode: "fixed" } });
frameDocument.nodes[1].data.frameId = "frame-1";
for (const [index, mode] of ["hidden", "fixed", "scaled"].entries()) {
frameDocument.nodes[0].data.labelMode = mode;
const response = await fetch(host + canvasPath, { method: "PATCH", headers: { Cookie: session, Origin: host, "Content-Type": "application/json", "If-Match": '"' + (2 + index) + '"' }, body: JSON.stringify({ document: frameDocument }) });
assert.equal(response.status, 200);
const restored = await (await fetch(host + canvasPath, { headers: { Cookie: session } })).json();
assert.equal(restored.document.nodes[0].data.labelMode, mode);
assert.equal(restored.document.nodes[1].data.frameId, "frame-1");
}
assert.equal((await fetch(host + canvasPath)).status, 401);
assert.equal((await fetch(host + "/api/foundry/workspaces/space/search/my-board/canvas")).status, 404);
const oversizedCanvas = await fetch(host + canvasPath, { method: "PATCH", headers: { Cookie: session, "Content-Type": "application/json" }, body: JSON.stringify({ document: "x".repeat(5 * 1024 * 1024) }) });
assert.equal(oversizedCanvas.status, 413);
for (const origin of ["https://example.com", "null"])
assert.equal(
(
await fetch(host + "/api/foundry/auth/login", {
method: "POST",
headers: { Origin: origin, "Content-Type": "application/json" },
body: "{}",
})
).status,
403,
);
assert.equal((await fetch(host + "/api/foundry/unlisted")).status, 404);
assert.equal(
(await fetch(host + "/api/oauth/slack?challenge=bad")).status,
400,
);
assert.equal(res.headers.get("cache-control"), "no-store");
const secure = await fetch(host + "/api/foundry/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Forwarded-Proto": "https" },
body: "{}",
});
assert.match(secure.headers.get("set-cookie"), /; Secure/);
for (const [body, status] of [["{", 400], [JSON.stringify({ text: "x".repeat(1024 * 1024) }), 413]]) {
const invalid = await fetch(host + "/api/foundry/auth/login", {
method: "POST", headers: { "Content-Type": "application/json" }, body,
});
assert.equal(invalid.status, status);
}
assert.equal((await fetch(host + "/api/foundry/profile", { method: "PUT" })).status, 405);
const materials = await fetch(host + "/api/foundry/workspaces/space/materials?kind=board&limit=100&secret=drop", {
method: "PATCH", headers: { "Content-Type": "application/json" }, body: "{}",
});
assert.deepEqual(await materials.json(), {
url: "/projects/endoleap/workspaces/space/materials?kind=board&limit=100", method: "PATCH",
});
for (const provider of ["google", "slack"]) {
const redirect = await fetch(host + "/api/oauth/" + provider + "?challenge=" + "a".repeat(43), { redirect: "manual" });
assert.equal(redirect.status, 302);
assert.equal(redirect.headers.get("location"), `http://127.0.0.1:${port}/projects/endoleap/auth/${provider}?challenge=${"a".repeat(43)}`);
}
const logout = await fetch(host + "/api/foundry/auth/logout", {
method: "POST", headers: { "Content-Type": "application/json", Cookie: session }, body: "{}",
});
assert.equal(logout.status, 204);
assert.equal(await logout.text(), "");
assert.match(logout.headers.get("set-cookie"), /Max-Age=0/);
console.log(
"PASS: V2 commands, idempotency and query forwarding, authenticated binary assets and multipart uploads, Canvas GET/PATCH and independent versions, ETag/If-Match, 1+ MiB snapshot restore, conflict and 5 MiB limit, four ID/slug combinations, authenticated home redirect, anonymous and expired-session landing, canvas-free dashboard, 88 original brand assets, 97 hashed icon shards with immutable caching, favicon metadata, avatar and profile updates, App Router pages and metadata, body validation, method and query filtering, OAuth redirects, logout, session cookie, token removal, authenticated proxy, invalid session, cross-origin rejection, path allowlist, OAuth validation",
);
} finally {
const exited = once(app, "exit");
app.kill("SIGTERM");
await exited;
await new Promise((r) => upstream.close(r));
}