Emdash source with visual editor image upload fix
Fixes: 1. media.ts: wrap placeholder generation in try-catch 2. toolbar.ts: check r.ok, display error message in popover
This commit is contained in:
228
packages/core/tests/integration/mcp/lifecycle.test.ts
Normal file
228
packages/core/tests/integration/mcp/lifecycle.test.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user