Fixes: 1. media.ts: wrap placeholder generation in try-catch 2. toolbar.ts: check r.ok, display error message in popover
229 lines
7.9 KiB
TypeScript
229 lines
7.9 KiB
TypeScript
/**
|
|
* MCP content lifecycle tests.
|
|
*
|
|
* Covers two contracts that callers rely on:
|
|
*
|
|
* - `content_unpublish` clears `published_at` so a missing/null timestamp
|
|
* unambiguously means the item is not currently live. Re-publishing
|
|
* assigns a fresh timestamp.
|
|
* - `schema_create_collection` applies its documented default of
|
|
* `['drafts', 'revisions']` for `supports` when the caller omits it.
|
|
* Explicit `[]` is preserved as an opt-out.
|
|
*/
|
|
|
|
import { Role } from "@emdash-cms/auth";
|
|
import type { Kysely } from "kysely";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
|
|
import type { Database } from "../../../src/database/types.js";
|
|
import {
|
|
connectMcpHarness,
|
|
extractJson,
|
|
extractText,
|
|
type McpHarness,
|
|
} from "../../utils/mcp-runtime.js";
|
|
import {
|
|
setupTestDatabaseWithCollections,
|
|
teardownTestDatabase,
|
|
setupTestDatabase,
|
|
} from "../../utils/test-db.js";
|
|
|
|
const ADMIN_ID = "user_admin";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Bug #10: unpublish publishedAt
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("MCP content_unpublish — publishedAt clearing (bug #10)", () => {
|
|
let db: Kysely<Database>;
|
|
let harness: McpHarness;
|
|
|
|
beforeEach(async () => {
|
|
db = await setupTestDatabaseWithCollections();
|
|
harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (harness) await harness.cleanup();
|
|
await teardownTestDatabase(db);
|
|
});
|
|
|
|
it("unpublish clears publishedAt so 'currently live' is unambiguous", async () => {
|
|
const created = await harness.client.callTool({
|
|
name: "content_create",
|
|
arguments: { collection: "post", data: { title: "Will publish" } },
|
|
});
|
|
const id = extractJson<{ item: { id: string } }>(created).item.id;
|
|
|
|
// Publish — populates publishedAt
|
|
const published = await harness.client.callTool({
|
|
name: "content_publish",
|
|
arguments: { collection: "post", id },
|
|
});
|
|
const publishedItem = extractJson<{ item: { publishedAt: string | null } }>(published);
|
|
expect(publishedItem.item.publishedAt).toBeTruthy();
|
|
|
|
// Unpublish — should clear publishedAt
|
|
const unpublished = await harness.client.callTool({
|
|
name: "content_unpublish",
|
|
arguments: { collection: "post", id },
|
|
});
|
|
const unpubItem = extractJson<{
|
|
item: { publishedAt: string | null; status: string };
|
|
}>(unpublished);
|
|
|
|
expect(unpubItem.item.status).toBe("draft");
|
|
// Bug #10: today, publishedAt is still the old timestamp.
|
|
expect(unpubItem.item.publishedAt).toBeNull();
|
|
});
|
|
|
|
it("content_get after unpublish reflects null publishedAt and status=draft", async () => {
|
|
const created = await harness.client.callTool({
|
|
name: "content_create",
|
|
arguments: { collection: "post", data: { title: "T" } },
|
|
});
|
|
const id = extractJson<{ item: { id: string } }>(created).item.id;
|
|
await harness.client.callTool({
|
|
name: "content_publish",
|
|
arguments: { collection: "post", id },
|
|
});
|
|
await harness.client.callTool({
|
|
name: "content_unpublish",
|
|
arguments: { collection: "post", id },
|
|
});
|
|
|
|
const got = await harness.client.callTool({
|
|
name: "content_get",
|
|
arguments: { collection: "post", id },
|
|
});
|
|
const gotItem = extractJson<{
|
|
item: { publishedAt: string | null; status: string };
|
|
}>(got);
|
|
expect(gotItem.item.status).toBe("draft");
|
|
expect(gotItem.item.publishedAt).toBeNull();
|
|
});
|
|
|
|
it("re-publish after unpublish gets a fresh publishedAt timestamp", async () => {
|
|
const created = await harness.client.callTool({
|
|
name: "content_create",
|
|
arguments: { collection: "post", data: { title: "T" } },
|
|
});
|
|
const id = extractJson<{ item: { id: string } }>(created).item.id;
|
|
|
|
const firstPub = await harness.client.callTool({
|
|
name: "content_publish",
|
|
arguments: { collection: "post", id },
|
|
});
|
|
const firstTs = extractJson<{ item: { publishedAt: string } }>(firstPub).item.publishedAt;
|
|
expect(firstTs).toBeTruthy();
|
|
|
|
await harness.client.callTool({
|
|
name: "content_unpublish",
|
|
arguments: { collection: "post", id },
|
|
});
|
|
|
|
// Wait briefly so the new timestamp is distinguishable
|
|
await new Promise((r) => setTimeout(r, 5));
|
|
|
|
const secondPub = await harness.client.callTool({
|
|
name: "content_publish",
|
|
arguments: { collection: "post", id },
|
|
});
|
|
const secondTs = extractJson<{ item: { publishedAt: string } }>(secondPub).item.publishedAt;
|
|
expect(secondTs).toBeTruthy();
|
|
// Should be a new timestamp, not the old one.
|
|
expect(secondTs).not.toBe(firstTs);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Bug #11: schema_create_collection supports default
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("MCP schema_create_collection — supports default (bug #11)", () => {
|
|
let db: Kysely<Database>;
|
|
let harness: McpHarness;
|
|
|
|
beforeEach(async () => {
|
|
db = await setupTestDatabase();
|
|
harness = await connectMcpHarness({ db, userId: ADMIN_ID, userRole: Role.ADMIN });
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (harness) await harness.cleanup();
|
|
await teardownTestDatabase(db);
|
|
});
|
|
|
|
it("creating a collection without `supports` uses documented default ['drafts', 'revisions']", async () => {
|
|
const result = await harness.client.callTool({
|
|
name: "schema_create_collection",
|
|
arguments: { slug: "article", label: "Articles" },
|
|
});
|
|
expect(result.isError, extractText(result)).toBeFalsy();
|
|
const created = extractJson<{ supports: string[] }>(result);
|
|
|
|
// Bug #11: today this is [] or null. After fix: ['drafts', 'revisions'].
|
|
expect(created.supports).toEqual(expect.arrayContaining(["drafts", "revisions"]));
|
|
});
|
|
|
|
it("explicit empty supports array is preserved (regression guard — opt-out)", async () => {
|
|
const result = await harness.client.callTool({
|
|
name: "schema_create_collection",
|
|
arguments: { slug: "minimal", label: "Minimal", supports: [] },
|
|
});
|
|
expect(result.isError, extractText(result)).toBeFalsy();
|
|
const created = extractJson<{ supports: string[] }>(result);
|
|
expect(created.supports).toEqual([]);
|
|
});
|
|
|
|
it("explicit supports list is preserved exactly (regression guard)", async () => {
|
|
const result = await harness.client.callTool({
|
|
name: "schema_create_collection",
|
|
arguments: {
|
|
slug: "blog",
|
|
label: "Blog",
|
|
supports: ["drafts", "revisions", "scheduling"],
|
|
},
|
|
});
|
|
expect(result.isError, extractText(result)).toBeFalsy();
|
|
const created = extractJson<{ supports: string[] }>(result);
|
|
expect(created.supports.toSorted()).toEqual(["drafts", "revisions", "scheduling"].toSorted());
|
|
});
|
|
|
|
it("default-supports collection accepts publish/unpublish/revision flows immediately", async () => {
|
|
// Default supports should include drafts + revisions, so the standard
|
|
// publish/unpublish lifecycle should work without further config.
|
|
await harness.client.callTool({
|
|
name: "schema_create_collection",
|
|
arguments: { slug: "story", label: "Stories" },
|
|
});
|
|
await harness.client.callTool({
|
|
name: "schema_create_field",
|
|
arguments: { collection: "story", slug: "title", label: "Title", type: "string" },
|
|
});
|
|
|
|
const created = await harness.client.callTool({
|
|
name: "content_create",
|
|
arguments: { collection: "story", data: { title: "T" } },
|
|
});
|
|
expect(created.isError, extractText(created)).toBeFalsy();
|
|
const id = extractJson<{ item: { id: string } }>(created).item.id;
|
|
|
|
// Update should create a draft revision (only meaningful if 'revisions' is in supports)
|
|
await harness.client.callTool({
|
|
name: "content_update",
|
|
arguments: { collection: "story", id, data: { title: "Updated" } },
|
|
});
|
|
|
|
const revs = await harness.client.callTool({
|
|
name: "revision_list",
|
|
arguments: { collection: "story", id },
|
|
});
|
|
// If supports doesn't include 'revisions', revision_list returns empty
|
|
// or fails. After fix: revisions exist.
|
|
expect(revs.isError, extractText(revs)).toBeFalsy();
|
|
const items = extractJson<{ items: unknown[] }>(revs).items;
|
|
expect(items.length).toBeGreaterThan(0);
|
|
});
|
|
});
|