feat: add deterministic agent publishing contract
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it } from 'node:test'
|
||||
|
||||
import {
|
||||
MAX_COVER_BYTES,
|
||||
decodeCoverImage,
|
||||
parsePostBody,
|
||||
parsePostPatch,
|
||||
postResponse,
|
||||
} from '../src/app/api/agent/posts/post-contract.ts'
|
||||
|
||||
const content = {
|
||||
root: {
|
||||
type: 'root',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
direction: 'rtl',
|
||||
children: [{ type: 'paragraph', version: 1, children: [{ type: 'text', version: 1, text: 'محتوى' }] }],
|
||||
},
|
||||
}
|
||||
|
||||
const validBody = {
|
||||
title: 'دليل عربي',
|
||||
slug: 'arabic-ai-guide',
|
||||
dek: 'وصف عملي',
|
||||
content,
|
||||
category: 'أدوات',
|
||||
access: 'free',
|
||||
readingMinutes: 7,
|
||||
publish: true,
|
||||
}
|
||||
|
||||
describe('parsePostBody', () => {
|
||||
it('accepts the exact publishing contract', () => {
|
||||
const parsed = parsePostBody(validBody)
|
||||
assert.equal(parsed.slug, 'arabic-ai-guide')
|
||||
assert.equal(parsed.publish, true)
|
||||
})
|
||||
|
||||
it('rejects direct status and level writes', () => {
|
||||
assert.throws(() => parsePostBody({ ...validBody, status: 'published' }), /unsupported field: status/)
|
||||
assert.throws(() => parsePostBody({ ...validBody, level: 'متقدم' }), /unsupported field: level/)
|
||||
})
|
||||
|
||||
it('requires a non-empty VIP teaser', () => {
|
||||
assert.throws(() => parsePostBody({ ...validBody, access: 'vip', teaser: ' ' }), /VIP teaser/)
|
||||
})
|
||||
|
||||
it('rejects invalid access, category, slug, and publish values', () => {
|
||||
assert.throws(() => parsePostBody({ ...validBody, access: 'paid' }), /access/)
|
||||
assert.throws(() => parsePostBody({ ...validBody, category: 'عام' }), /category/)
|
||||
assert.throws(() => parsePostBody({ ...validBody, slug: 'عنوان عربي' }), /slug/)
|
||||
assert.throws(() => parsePostBody({ ...validBody, publish: 'true' }), /publish/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parsePostPatch', () => {
|
||||
it('accepts publish-only draft promotion', () => {
|
||||
assert.deepEqual(parsePostPatch({ publish: true }, 'arabic-ai-guide'), { publish: true })
|
||||
})
|
||||
|
||||
it('accepts a full republish only for the path slug', () => {
|
||||
assert.equal(parsePostPatch(validBody, 'arabic-ai-guide').slug, 'arabic-ai-guide')
|
||||
assert.throws(() => parsePostPatch(validBody, 'different-slug'), /slug must match/)
|
||||
})
|
||||
|
||||
it('rejects status writes and empty updates', () => {
|
||||
assert.throws(() => parsePostPatch({ status: 'published' }, 'arabic-ai-guide'), /unsupported field: status/)
|
||||
assert.throws(() => parsePostPatch({}, 'arabic-ai-guide'), /at least one field/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('decodeCoverImage', () => {
|
||||
it('accepts a small JPEG and normalizes its filename', () => {
|
||||
const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0xdb, 0xff, 0xd9])
|
||||
const file = decodeCoverImage({ dataBase64: jpeg.toString('base64'), filename: '../cover.jpg' })
|
||||
assert.equal(file.mimetype, 'image/jpeg')
|
||||
assert.equal(file.name, 'cover.jpg')
|
||||
assert.deepEqual(file.data, jpeg)
|
||||
})
|
||||
|
||||
it('rejects non-JPEG data and oversized files', () => {
|
||||
assert.throws(() => decodeCoverImage({ dataBase64: Buffer.from('png').toString('base64') }), /JPEG/)
|
||||
const oversized = Buffer.alloc(MAX_COVER_BYTES + 1, 0)
|
||||
oversized[0] = 0xff
|
||||
oversized[1] = 0xd8
|
||||
oversized[oversized.length - 2] = 0xff
|
||||
oversized[oversized.length - 1] = 0xd9
|
||||
assert.throws(() => decodeCoverImage({ dataBase64: oversized.toString('base64') }), /5MB/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('postResponse', () => {
|
||||
it('returns the minimal deterministic response and canonical URL', () => {
|
||||
assert.deepEqual(
|
||||
postResponse({
|
||||
id: 42,
|
||||
title: 'دليل عربي',
|
||||
slug: 'arabic-ai-guide',
|
||||
status: 'published',
|
||||
access: 'vip',
|
||||
category: 'أدوات',
|
||||
publishedAt: '2026-10-09T12:00:00.000Z',
|
||||
}),
|
||||
{
|
||||
id: 42,
|
||||
title: 'دليل عربي',
|
||||
slug: 'arabic-ai-guide',
|
||||
status: 'published',
|
||||
access: 'vip',
|
||||
category: 'أدوات',
|
||||
publishedAt: '2026-10-09T12:00:00.000Z',
|
||||
url: 'https://anwarpy.ai/posts/arabic-ai-guide',
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user