Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ deviating from agreed scope or product behavior.
Target **9/10+ for minimalness, elegance, and correctness**: the smallest complete
solution, clear ownership, and no known material defects. Prefer existing patterns
and subtraction. No opportunistic refactors, speculative abstractions, or new
features disguised as fixes. If the fix keeps growing, revisit the cause and scope.
features disguised as fixes. Before expanding into another shared subsystem or
adding alternate-adapter support, show the human the scope change and smallest
complete alternative. Require a current caller or explicit approval for adapter
parity. Review necessity separately from correctness; passing tests do not justify
scope growth. Split at real ownership boundaries, not by deleting safety coverage.

Keep files cohesive and group modules and tests by owner. Treat size as a review
signal, not a quota. Extract stable boundaries only when they simplify the
Expand Down
8 changes: 7 additions & 1 deletion dev/agent-observer.test.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from "vitest";
import { test, expect, vi, beforeEach, afterEach } from "vitest";
import {
finalizeEvent,
generateSecretKey,
Expand All @@ -7,6 +7,12 @@ import {
} from "nostr-tools";
import { decodeAgentObserver } from "./agent-observer.mjs";

// Keep frame construction and validation in the same second at the ±300s boundary.
beforeEach(() => {
vi.spyOn(Date, "now").mockReturnValue(1700000000999);
});
afterEach(() => vi.restoreAllMocks());

const owner = generateSecretKey(),
agent = generateSecretKey(),
stranger = generateSecretKey();
Expand Down
10 changes: 10 additions & 0 deletions docs/browser-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,13 @@ focused visible dwell, non-reading opening/composer focus, individual markers,
encrypted publication/readback, reload, cancellation and local manual-unread.
The reload control holds network content so verified disk-restore wiring is required.
This is not a deployed-relay, native signer or cross-device integration test.

## Fixture server isolation

Concurrent Vite fixture servers must own separate optimizer caches. Use
`tests/browser/vite-server.mjs` for new fixtures; its `close()` releases the owned
cache. The existing emoji and conversation fixtures retain their explicitly owned
temporary caches. Do not share Vite's default `node_modules/.vite`: another server
can invalidate dependency imports and leave a blank fixture with a 504
`Outdated Optimize Dep`. Keep import failures visible; retries or longer UI waits
do not repair module loading.
4 changes: 2 additions & 2 deletions tests/browser/agents.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test";
import { createServer } from "vite";
import { createServer } from "./vite-server.mjs";
import react from "@vitejs/plugin-react";
import { fileURLToPath } from "node:url";

Expand All @@ -16,8 +16,8 @@ test("My agents reads the existing library with exact linked keys and session-sa
});
const errors = [];
page.on("pageerror", (error) => errors.push(String(error)));
await server.listen();
try {
await server.listen();
await page.goto(
`http://127.0.0.1:${server.httpServer.address().port}/tests/fixtures/agents.html`,
);
Expand Down
4 changes: 2 additions & 2 deletions tests/browser/avatar-loading.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from "@playwright/test";
import { createServer } from "vite";
import { createServer } from "./vite-server.mjs";
import react from "@vitejs/plugin-react";
import { fileURLToPath } from "node:url";

Expand All @@ -14,8 +14,8 @@ test("shared avatars defer offscreen artwork, omit the referrer and recover from
logLevel: "error",
server: { host: "127.0.0.1", port: 0, strictPort: false },
});
await server.listen();
try {
await server.listen();
const requests = [];
await page.route("https://images.example/avatar.png", async (route) => {
requests.push(route.request().headers());
Expand Down
59 changes: 37 additions & 22 deletions tests/browser/image-scroll.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test";
import { createServer } from "vite";
import { createServer } from "./vite-server.mjs";
import react from "@vitejs/plugin-react";
import { fileURLToPath } from "node:url";
import { settle, anchor, expectAnchor } from "./timeline.mjs";
Expand Down Expand Up @@ -54,7 +54,6 @@ async function fixtureServer() {
test("delayed and failed images preserve bottom and reading anchors across remounts", async ({
page,
}, testInfo) => {
const server = await fixtureServer();
const pending = new Set();
const requests = new Map();
let held = true;
Expand Down Expand Up @@ -102,8 +101,9 @@ test("delayed and failed images preserve bottom and reading anchors across remou
),
)
.toBe(true);
await server.listen();
const server = await fixtureServer();
try {
await server.listen();
await page.goto(
`http://127.0.0.1:${server.httpServer.address().port}/tests/fixtures/image-scroll.html`,
);
Expand Down Expand Up @@ -180,9 +180,12 @@ test("delayed and failed images preserve bottom and reading anchors across remou
expect(box.height).toBeGreaterThan(0);
}
} finally {
await release();
await page.unrouteAll({ behavior: "wait" });
await server.close();
try {
await release();
await page.unrouteAll({ behavior: "wait" });
} finally {
await server.close();
}
}
});

Expand Down Expand Up @@ -314,12 +317,12 @@ async function routeOriginals(page, requests) {
test("blurhash visibility, decode swap, failure and retired source lifetimes", async ({
page,
}, testInfo) => {
const server = await fixtureServer();
const requests = [];
await holdDecodes(page);
await routeOriginals(page, requests);
await server.listen();
const server = await fixtureServer();
try {
await server.listen();
await page.goto(
`http://127.0.0.1:${server.httpServer.address().port}/tests/fixtures/attachment-image.html`,
);
Expand Down Expand Up @@ -402,19 +405,22 @@ test("blurhash visibility, decode swap, failure and retired source lifetimes", a
contentType: "application/json",
});
} finally {
await page.unrouteAll({ behavior: "wait" });
await server.close();
try {
await page.unrouteAll({ behavior: "wait" });
} finally {
await server.close();
}
}
});

test("original ready first cannot regress on late visibility; missing and invalid hashes still load", async ({
page,
}) => {
const server = await fixtureServer();
await holdDecodes(page, true);
await routeOriginals(page, []);
await server.listen();
const server = await fixtureServer();
try {
await server.listen();
await page.goto(
`http://127.0.0.1:${server.httpServer.address().port}/tests/fixtures/attachment-image.html`,
);
Expand All @@ -433,19 +439,22 @@ test("original ready first cannot regress on late visibility; missing and invali
await shown(page);
}
} finally {
await page.unrouteAll({ behavior: "wait" });
await server.close();
try {
await page.unrouteAll({ behavior: "wait" });
} finally {
await server.close();
}
}
});

test("original decode rejection retains blur and the next source still recovers", async ({
page,
}) => {
const server = await fixtureServer();
await holdDecodes(page);
await routeOriginals(page, []);
await server.listen();
const server = await fixtureServer();
try {
await server.listen();
await page.goto(
`http://127.0.0.1:${server.httpServer.address().port}/tests/fixtures/attachment-image.html`,
);
Expand All @@ -463,25 +472,28 @@ test("original decode rejection retains blur and the next source still recovers"
await shown(page);
await expect(frame(page).locator("canvas")).toHaveCount(0);
} finally {
await page.unrouteAll({ behavior: "wait" });
await server.close();
try {
await page.unrouteAll({ behavior: "wait" });
} finally {
await server.close();
}
}
});

for (const unavailable of ["canvas", "visibility"]) {
test(`unavailable ${unavailable} keeps the placeholder and original loading`, async ({
page,
}) => {
const server = await fixtureServer();
await holdDecodes(page);
await page.addInitScript((unavailable) => {
if (unavailable === "canvas")
HTMLCanvasElement.prototype.getContext = () => null;
else window.IntersectionObserver = undefined;
}, unavailable);
await routeOriginals(page, []);
await server.listen();
const server = await fixtureServer();
try {
await server.listen();
await page.goto(
`http://127.0.0.1:${server.httpServer.address().port}/tests/fixtures/attachment-image.html`,
);
Expand All @@ -496,8 +508,11 @@ for (const unavailable of ["canvas", "visibility"]) {
await shown(page);
await expect(frame(page).locator("canvas")).toHaveCount(0);
} finally {
await page.unrouteAll({ behavior: "wait" });
await server.close();
try {
await page.unrouteAll({ behavior: "wait" });
} finally {
await server.close();
}
}
});
}
4 changes: 2 additions & 2 deletions tests/browser/mention-edit.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test";
import { createServer } from "vite";
import { createServer } from "./vite-server.mjs";
import react from "@vitejs/plugin-react";
import { fileURLToPath } from "node:url";
test("editing selected name plus pasting same name cannot transfer notification to pasted prose", async ({
Expand All @@ -13,8 +13,8 @@ test("editing selected name plus pasting same name cannot transfer notification
logLevel: "error",
server: { host: "127.0.0.1", port: 0 },
});
await server.listen();
try {
await server.listen();
await page.goto(
`http://127.0.0.1:${server.httpServer.address().port}/tests/fixtures/mentions.html`,
);
Expand Down
4 changes: 2 additions & 2 deletions tests/browser/mentions.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test";
import { createServer } from "vite";
import { createServer } from "./vite-server.mjs";
import react from "@vitejs/plugin-react";
import { fileURLToPath } from "node:url";

Expand All @@ -16,8 +16,8 @@ test("actual composer selects namesakes by exact key, publishes channel/reply ta
});
const errors = [];
page.on("pageerror", (error) => errors.push(String(error)));
await server.listen();
try {
await server.listen();
await page.goto(
`http://127.0.0.1:${server.httpServer.address().port}/tests/fixtures/mentions.html`,
);
Expand Down
4 changes: 2 additions & 2 deletions tests/browser/messages.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test";
import { createServer } from "vite";
import { createServer } from "./vite-server.mjs";
import react from "@vitejs/plugin-react";
import { fileURLToPath } from "node:url";

Expand All @@ -18,8 +18,8 @@ test("shared thread UI auto-loads, follows live replies, retries and isolates re
});
const errors = [];
page.on("pageerror", (error) => errors.push(String(error)));
await server.listen();
try {
await server.listen();
const address = server.httpServer.address();
await page.goto(
`http://127.0.0.1:${address.port}/tests/fixtures/messages.html`,
Expand Down
6 changes: 3 additions & 3 deletions tests/browser/profiles.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test";
import { createServer } from "vite";
import { createServer } from "./vite-server.mjs";
import react from "@vitejs/plugin-react";
import { fileURLToPath } from "node:url";

Expand All @@ -16,8 +16,8 @@ test("profile plumbing: exact avatar/mention targets, thread enrichment, lifecyc
});
const errors = [];
page.on("pageerror", (error) => errors.push(String(error)));
await server.listen();
try {
await server.listen();
await page.goto(
`http://127.0.0.1:${server.httpServer.address().port}/tests/fixtures/profiles.html`,
);
Expand Down Expand Up @@ -170,8 +170,8 @@ test("contextual panel callbacks retire with opening, channel, contribution and
logLevel: "error",
server: { host: "127.0.0.1", port: 0, strictPort: false },
});
await server.listen();
try {
await server.listen();
await page.goto(
`http://127.0.0.1:${server.httpServer.address().port}/tests/fixtures/profiles.html?context-probe`,
);
Expand Down
2 changes: 1 addition & 1 deletion tests/browser/terminal-renderer.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test";
import { createServer } from "vite";
import { createServer } from "./vite-server.mjs";
import react from "@vitejs/plugin-react";
import { fileURLToPath } from "node:url";

Expand Down
2 changes: 1 addition & 1 deletion tests/browser/typeahead.spec.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test";
import { createServer } from "vite";
import { createServer } from "./vite-server.mjs";
import react from "@vitejs/plugin-react";
import { fileURLToPath } from "node:url";

Expand Down
24 changes: 24 additions & 0 deletions tests/browser/vite-server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createServer as createViteServer } from "vite";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

// Parallel fixture servers must not invalidate each other's optimized imports.
export async function createServer(config) {
const cacheDir = await mkdtemp(join(tmpdir(), "buzz-fixture-vite-"));
try {
const server = await createViteServer({ ...config, cacheDir });
const close = server.close.bind(server);
server.close = async () => {
try {
await close();
} finally {
await rm(cacheDir, { recursive: true, force: true });
}
};
return server;
} catch (error) {
await rm(cacheDir, { recursive: true, force: true });
throw error;
}
}
Loading
Loading