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
35 changes: 35 additions & 0 deletions app/routes/stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,41 @@ describe("POST /upload-clean/:cutId route", () => {
expect(res.status).toBe(404);
});

it("GET cuts returns 404 when cuts file is missing", async () => {
const storyDir = path.join(tmpDir, "no-cuts-story");
fs.mkdirSync(storyDir, { recursive: true });

const res = await app.request("/api/stories/no-cuts-story/cuts/plot-01");
expect(res.status).toBe(404);
const body = await res.json();
expect(body.error).toContain("not found");
});

it("GET cuts returns 400 with validation error for invalid schema", async () => {
const storyDir = path.join(tmpDir, "bad-cuts-story");
fs.mkdirSync(storyDir, { recursive: true });
fs.writeFileSync(
path.join(storyDir, "plot-01.cuts.json"),
JSON.stringify({ version: 1, plotFile: "plot-01", cuts: [{ id: "c01", shot: "wide" }] }),
);

const res = await app.request("/api/stories/bad-cuts-story/cuts/plot-01");
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toContain("invalid");
});

it("GET cuts returns 400 for malformed JSON", async () => {
const storyDir = path.join(tmpDir, "malformed-cuts-story");
fs.mkdirSync(storyDir, { recursive: true });
fs.writeFileSync(path.join(storyDir, "plot-01.cuts.json"), "{ not valid json");

const res = await app.request("/api/stories/malformed-cuts-story/cuts/plot-01");
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toContain("invalid JSON");
});

it("detects Korean language from structure.md title without .story.json language", async () => {
const storyDir = path.join(tmpDir, "korean-story");
fs.mkdirSync(storyDir, { recursive: true });
Expand Down
8 changes: 6 additions & 2 deletions app/web/components/CartoonPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
}

return (
<img

Check warning on line 52 in app/web/components/CartoonPreview.tsx

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={src}
alt={alt}
onError={() => setError(true)}
Expand Down Expand Up @@ -200,8 +200,12 @@

if (error) {
return (
<div className="h-full flex flex-col items-center justify-center gap-2 px-4">
<p className="text-sm text-error">{error}</p>
<div className="h-full flex flex-col items-center justify-center gap-2 px-4 text-center" data-testid="cuts-error">
<p className="text-sm text-error font-medium">Invalid cuts file</p>
<p className="text-xs text-error">{error}</p>
<p className="text-xs text-muted max-w-sm">
{plotFile}.cuts.json must follow the OWS v1 schema. Ask Claude to regenerate it using the v1 cuts schema shown in the cartoon writing instructions.
</p>
<button onClick={loadCuts} className="text-xs text-accent hover:text-accent-dim">
Retry
</button>
Expand Down
32 changes: 32 additions & 0 deletions app/web/components/CutListPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -276,4 +276,36 @@ describe("CutListPanel", () => {
expect(screen.getByText("Retry")).toBeInTheDocument();
});
});

it("shows actionable v1 schema guidance for invalid cuts (wrong schema)", async () => {
const authFetch = mockAuthFetch({ ok: false, status: 400, data: { error: "plot-01.cuts.json is invalid: Cut 0 has invalid shotType" } });
render(<CutListPanel storyName="story" fileName="plot-01.md" authFetch={authFetch} />);

await waitFor(() => {
expect(screen.getByTestId("cuts-error")).toBeInTheDocument();
expect(screen.getByText("Invalid cuts file")).toBeInTheDocument();
expect(screen.getByText(/invalid shotType/)).toBeInTheDocument();
expect(screen.getByText(/OWS v1 schema/)).toBeInTheDocument();
});
});

it("shows actionable error for invalid JSON", async () => {
const authFetch = mockAuthFetch({ ok: false, status: 400, data: { error: "plot-01.cuts.json contains invalid JSON" } });
render(<CutListPanel storyName="story" fileName="plot-01.md" authFetch={authFetch} />);

await waitFor(() => {
expect(screen.getByText(/contains invalid JSON/)).toBeInTheDocument();
expect(screen.getByText(/OWS v1 schema/)).toBeInTheDocument();
});
});

it("missing cuts file (404) shows No cuts, not an error", async () => {
const authFetch = mockAuthFetch({ ok: false, status: 404, data: { error: "Cuts file not found" } });
render(<CutListPanel storyName="story" fileName="plot-01.md" authFetch={authFetch} />);

await waitFor(() => {
expect(screen.getByText("No cuts yet")).toBeInTheDocument();
});
expect(screen.queryByTestId("cuts-error")).not.toBeInTheDocument();
});
});
8 changes: 6 additions & 2 deletions app/web/components/CutListPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@
{/* Clean image preview */}
{cut.cleanImagePath && (
<div className="mt-2">
<img

Check warning on line 162 in app/web/components/CutListPanel.tsx

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={assetUrl(storyName, cut.cleanImagePath)}
alt={`Cut ${cut.id} clean`}
className="w-full max-h-48 object-contain rounded border border-border bg-white"
Expand Down Expand Up @@ -267,8 +267,12 @@

if (error) {
return (
<div className="p-4 space-y-2">
<p className="text-sm text-error">{error}</p>
<div className="p-4 space-y-2" data-testid="cuts-error">
<p className="text-sm text-error font-medium">Invalid cuts file</p>
<p className="text-xs text-error">{error}</p>
<p className="text-xs text-muted">
{plotFile}.cuts.json must follow the OWS v1 schema. Ask Claude to regenerate it using the v1 cuts schema from the cartoon writing instructions.
</p>
<button onClick={loadCuts} className="text-xs text-accent hover:text-accent-dim">Retry</button>
</div>
);
Expand Down
19 changes: 16 additions & 3 deletions app/web/components/preview-routing.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,29 @@ describe("CartoonPreview", () => {
});
});

it("shows error state on fetch failure", async () => {
const authFetch = mockAuthFetch({ ok: false, status: 400, data: { error: "Invalid JSON" } });
it("shows actionable v1 schema error for invalid cuts", async () => {
const authFetch = mockAuthFetch({ ok: false, status: 400, data: { error: "plot-01.cuts.json is invalid: Cut 0 missing numeric id" } });
render(<CartoonPreview storyName="test-story" fileName="plot-01.md" authFetch={authFetch} />);

await waitFor(() => {
expect(screen.getByText("Invalid JSON")).toBeInTheDocument();
expect(screen.getByTestId("cuts-error")).toBeInTheDocument();
expect(screen.getByText("Invalid cuts file")).toBeInTheDocument();
expect(screen.getByText(/missing numeric id/)).toBeInTheDocument();
expect(screen.getByText(/OWS v1 schema/)).toBeInTheDocument();
expect(screen.getByText("Retry")).toBeInTheDocument();
});
});

it("missing cuts (404) shows No cuts, not an error", async () => {
const authFetch = mockAuthFetch({ ok: false, status: 404, data: { error: "Cuts file not found" } });
render(<CartoonPreview storyName="test-story" fileName="plot-01.md" authFetch={authFetch} />);

await waitFor(() => {
expect(screen.getByText("No cuts yet")).toBeInTheDocument();
});
expect(screen.queryByTestId("cuts-error")).not.toBeInTheDocument();
});

it("renders cut with final image", async () => {
const cutsData = {
version: 1,
Expand Down
Loading