diff --git a/app/web/components/StoriesPage.tsx b/app/web/components/StoriesPage.tsx index 228ad1d..cc892f8 100644 --- a/app/web/components/StoriesPage.tsx +++ b/app/web/components/StoriesPage.tsx @@ -302,7 +302,7 @@ export function StoriesPage({ token, authFetch }: StoriesPageProps) { setPublishProgress(""); }, 3000); } - }, [authFetch]); + }, [authFetch, storyContentTypes, walletAddress]); const handleDestroySession = useCallback((name: string) => { if (name.startsWith("_new_")) { diff --git a/app/web/components/publish-callback.test.tsx b/app/web/components/publish-callback.test.tsx new file mode 100644 index 0000000..7aa99fa --- /dev/null +++ b/app/web/components/publish-callback.test.tsx @@ -0,0 +1,82 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { render, screen, cleanup, fireEvent, act } from "@testing-library/react"; +import { useState, useCallback } from "react"; +import { getContentTypeForPublish } from "../lib/publish-helpers"; + +afterEach(cleanup); + +function TestPublishComponent({ authFetch }: { authFetch: (url: string, opts?: RequestInit) => void }) { + const [storyContentTypes, setStoryContentTypes] = useState>({}); + + const handlePublish = useCallback((storyName: string, storylineId: number | undefined) => { + const ct = getContentTypeForPublish(storyContentTypes, storyName, storylineId); + const payload = { storyName, ...(ct ? { contentType: ct } : {}) }; + authFetch("/api/publish/file", { + method: "POST", + body: JSON.stringify(payload), + }); + }, [authFetch, storyContentTypes]); + + return ( +
+ + + +
+ ); +} + +describe("StoriesPage.handlePublish dependency array (source guard)", () => { + it("production handlePublish includes storyContentTypes and walletAddress in deps", async () => { + const fs = await import("fs"); + const path = await import("path"); + const source = fs.readFileSync( + path.resolve(__dirname, "StoriesPage.tsx"), + "utf-8", + ); + + const handlePublishMatch = source.match( + /const handlePublish = useCallback\([\s\S]*?\}, \[([^\]]+)\]\)/, + ); + expect(handlePublishMatch).toBeTruthy(); + const deps = handlePublishMatch![1]; + expect(deps).toContain("storyContentTypes"); + expect(deps).toContain("walletAddress"); + }); +}); + +describe("publish callback boundary (stale closure regression)", () => { + it("cartoon genesis includes contentType after metadata update", () => { + const authFetch = vi.fn(); + render(); + + fireEvent.click(screen.getByTestId("publish-genesis")); + expect(authFetch).toHaveBeenCalledTimes(1); + const firstPayload = JSON.parse(authFetch.mock.calls[0][1].body); + expect(firstPayload.contentType).toBeUndefined(); + + act(() => { fireEvent.click(screen.getByTestId("set-cartoon")); }); + + fireEvent.click(screen.getByTestId("publish-genesis")); + expect(authFetch).toHaveBeenCalledTimes(2); + const secondPayload = JSON.parse(authFetch.mock.calls[1][1].body); + expect(secondPayload.contentType).toBe("cartoon"); + }); + + it("cartoon plot omits contentType even after metadata update", () => { + const authFetch = vi.fn(); + render(); + + act(() => { fireEvent.click(screen.getByTestId("set-cartoon")); }); + fireEvent.click(screen.getByTestId("publish-plot")); + + const payload = JSON.parse(authFetch.mock.calls[0][1].body); + expect(payload.contentType).toBeUndefined(); + }); +}); diff --git a/app/web/lib/publish-helpers.test.ts b/app/web/lib/publish-helpers.test.ts index 2ffca99..9ad3288 100644 --- a/app/web/lib/publish-helpers.test.ts +++ b/app/web/lib/publish-helpers.test.ts @@ -21,4 +21,19 @@ describe("getContentTypeForPublish", () => { it("returns undefined for unknown story", () => { expect(getContentTypeForPublish({}, "unknown", undefined)).toBeUndefined(); }); + + it("returns cartoon after metadata update (simulates stale closure fix)", () => { + let storyContentTypes: Record = {}; + + const buildPayload = (storyName: string, storylineId: number | undefined) => { + const ct = getContentTypeForPublish(storyContentTypes, storyName, storylineId); + return ct ? { contentType: ct } : {}; + }; + + expect(buildPayload("my-cartoon", undefined)).toEqual({}); + + storyContentTypes = { "my-cartoon": "cartoon" }; + + expect(buildPayload("my-cartoon", undefined)).toEqual({ contentType: "cartoon" }); + }); });