commit a37d3329a0342ef80938fdd45ea68d798f2648e8
parent 78074df18a6f7bddf017ea394fcea899c0f62ab4
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 1 Oct 2026 17:19:13 -0400
editor(e2e), export(e2e): where X posts appear persists beside the login source; a private site saves and every deploy refuses it; a public site built with X private has no Posts box
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
3 files changed, 193 insertions(+), 0 deletions(-)
diff --git a/editor/e2e/sites-crud.spec.ts b/editor/e2e/sites-crud.spec.ts
@@ -528,3 +528,51 @@ test("brand accent radio group + wordmark lead round-trip to site.json", async (
expect("wordmarkLead" in site).toBe(false);
}).toPass({ timeout: 10_000 });
});
+
+// Who a site is built for — `site.json` `audience` (release 17 slice XP). A
+// private site is the operator's own reading copy: saved from the form, and
+// never deployed — the deploy actions refuse it before a job exists, in words
+// naming the audience. (The ops routes call the same actions the Publish tab's
+// buttons do.)
+test("a private site saves its audience, and every deploy refuses it before any job", async ({
+ page,
+ request,
+}) => {
+ await resetData("empty");
+ await writeSite("privsite", { siteTitle: "Private Site", cloudflareProject: "never-real" });
+
+ await page.goto("/sites/privsite");
+ const audience = page.getByLabel("Audience", { exact: true });
+ await expect(audience).toHaveValue("public");
+ await audience.selectOption("private");
+ await page.getByRole("button", { name: /save site/i }).click();
+ await expect(page.getByRole("status").filter({ hasText: "Saved" })).toBeVisible();
+ await expect(async () => {
+ const site = await readJson<{ audience?: string; cloudflareProject?: string }>(
+ "test-transcripts/sites/privsite/site.json",
+ );
+ expect(site.audience).toBe("private");
+ expect(site.cloudflareProject).toBe("never-real");
+ }).toPass({ timeout: 10_000 });
+ await page.reload();
+ await expect(audience).toHaveValue("private");
+
+ const refusal =
+ 'Site "privsite" is private (audience: private): it is built for reading on this machine and is never deployed. Build it without deploying, or set its audience to public on its Settings tab.';
+ for (const action of ["build-deploy", "deploy-site"]) {
+ const res = await request.post(`/api/ops/${action}`, {
+ headers: { authorization: "Bearer test-worker-token" },
+ data: { siteId: "privsite" },
+ });
+ expect(res.status(), action).toBe(400);
+ expect(((await res.json()) as { error?: string }).error, action).toBe(refusal);
+ }
+
+ // Back to public: the key is gone from the file (public is the default).
+ await audience.selectOption("public");
+ await page.getByRole("button", { name: /save site/i }).click();
+ await expect(async () => {
+ const site = await readJson<Record<string, unknown>>("test-transcripts/sites/privsite/site.json");
+ expect("audience" in site).toBe(false);
+ }).toPass({ timeout: 10_000 });
+});
diff --git a/editor/e2e/x-session.spec.ts b/editor/e2e/x-session.spec.ts
@@ -76,3 +76,44 @@ test("the login source select persists, and Check shows a status line", async ({
await expect(inUse).toHaveText(`In use: Browser login (${spec}) (automatic)`);
expect((await readJson<{ social?: unknown }>("test-settings.json")).social).toEqual({ x: {} });
});
+
+// Where X posts appear — `social.x.visibility` (release 17 slice XP). The two X
+// choices share one settings block, and saveSettings replaces a nested block
+// whole, so each is written over the other: choosing one keeps the other.
+test("where X posts appear persists, beside the login source and without it", async ({ page }) => {
+ await resetData("empty");
+ await page.goto("/settings");
+ const visibility = page.getByLabel("Where X posts appear");
+ const source = page.getByLabel("x cookie source", { exact: true });
+ const social = async () =>
+ (await readJson<{ social?: unknown }>("test-settings.json")).social;
+
+ await expect(visibility).toHaveValue("public");
+ await expect(page.locator("[data-x-posts-visibility]")).toContainText(
+ "Sites already published change on their next build and deploy.",
+ );
+
+ await source.selectOption("profile");
+ await expect(page.getByLabel("x cookie source in use")).toHaveText("In use: Connected profile");
+
+ await visibility.selectOption("private");
+ await expect(page.getByLabel("x posts visibility saved")).toHaveText(
+ "Saved. Published sites change on their next build and deploy.",
+ );
+ expect(await social()).toEqual({ x: { cookieSource: "profile", visibility: "private" } });
+
+ await page.reload();
+ await expect(visibility).toHaveValue("private");
+
+ // The login source back to automatic keeps the visibility…
+ await source.selectOption("auto");
+ await expect(page.getByLabel("x cookie source in use")).toHaveText(
+ "In use: Connected profile (automatic)",
+ );
+ expect(await social()).toEqual({ x: { visibility: "private" } });
+
+ // …and public is the default, written as no key.
+ await visibility.selectOption("public");
+ await expect(page.getByLabel("x posts visibility saved")).toBeVisible();
+ expect(await social()).toEqual({ x: {} });
+});
diff --git a/export/e2e/x-posts-private.spec.ts b/export/e2e/x-posts-private.spec.ts
@@ -0,0 +1,104 @@
+import { expect, test, type Page } from "@playwright/test";
+import {
+ POST_CHANNEL,
+ POST_CHANNEL_SLUG,
+ POST_REPLY_ID,
+ POST_ROOT_ID,
+ postsPage,
+} from "./fixtures/data";
+import { installRoutes, openFilters } from "./helpers";
+
+// X posts are private (release 17 slice XP): with `social.x.visibility`
+// "private", a PUBLIC site's build carries no X channel and a PRIVATE site's
+// build carries all of it. The build itself — the index build's per-site posts
+// manifest and compose's posts tree — is pinned through the real code by
+// common/bin/compose-site.postsVisibility.test.ts: this suite's data is
+// route-mocked, never built. Here the two posts manifests that build writes
+// are served, and the visitor's side is checked: the Search in row's Posts box
+// and the post hits come and go with the manifest, with nothing special-cased.
+//
+// The fixture posts say "kappa" (two of them); no video does.
+
+const X_POST_SLUGS = [
+ `${POST_CHANNEL_SLUG}/${POST_ROOT_ID}`,
+ `${POST_CHANNEL_SLUG}/${POST_REPLY_ID}`,
+];
+
+const fulfillJson = (body: unknown) => ({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify(body),
+});
+
+// The site posts manifest compose writes: the X channel listed (a private
+// site's build), or no channel at all (a public site whose only posts were X
+// posts). Routed after installRoutes, so these answers win.
+async function servePostsManifest(page: Page, built: "public" | "private") {
+ await page.route("**/posts/manifest.json", (route) =>
+ route.fulfill(
+ fulfillJson({
+ version: 1,
+ channels:
+ built === "private"
+ ? [{ name: POST_CHANNEL, slug: POST_CHANNEL_SLUG, postCount: 4, platform: "twitter" }]
+ : [],
+ totalCount: built === "private" ? 4 : 0,
+ generatedAt: new Date().toISOString(),
+ }),
+ ),
+ );
+ await page.route(/\/posts\/[^/]+\/page-\d+\.json$/, (route) =>
+ route.fulfill(
+ fulfillJson(
+ postsPage().map((p) => ({
+ ...p,
+ platform: "twitter",
+ url: `https://x.com/tester/status/${p.id}`,
+ })),
+ ),
+ ),
+ );
+}
+
+const postsBox = (page: Page) =>
+ page.getByTestId("search-in-row").getByRole("checkbox", { name: "Posts", exact: true });
+
+async function search(page: Page, q: string) {
+ await page.locator('input[data-testid^="leaf-query-"]').first().fill(q);
+ await page.getByTestId("search-submit").click();
+}
+
+test.describe("X posts private", () => {
+ test.beforeEach(async ({ page }) => {
+ await installRoutes(page);
+ });
+
+ test("a public site built with X posts private has no Posts box and finds no X post", async ({
+ page,
+ }) => {
+ await servePostsManifest(page, "public");
+ await page.goto("/");
+ await openFilters(page);
+ await expect(page.getByRole("checkbox", { name: "Transcripts", exact: true })).toBeVisible();
+ await expect(postsBox(page)).toHaveCount(0);
+ await search(page, "kappa");
+ await expect(page.getByText(/^searched \d+\/\d+$/)).toBeVisible({ timeout: 15_000 });
+ await expect(page.getByTestId("results-summary")).toHaveText("Matching videos (0)");
+ await expect(page.locator(`[data-result-slug^="${POST_CHANNEL_SLUG}/"]`)).toHaveCount(0);
+ });
+
+ test("a private site's build shows the X posts", async ({ page }) => {
+ await servePostsManifest(page, "private");
+ await page.goto("/");
+ await openFilters(page);
+ await expect(postsBox(page)).toBeChecked();
+ await search(page, "kappa");
+ const cards = page.locator("[data-card-header]");
+ await expect(async () => {
+ const got = await cards.evaluateAll((els) =>
+ els.map((e) => e.getAttribute("data-result-slug") ?? ""),
+ );
+ expect(got.slice().sort()).toEqual(X_POST_SLUGS.slice().sort());
+ }).toPass({ timeout: 15_000 });
+ });
+});