88 lines
4.5 KiB
TypeScript
88 lines
4.5 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { calculateSun } from '../src/astronomy/sunCalculator.ts';
|
|
import { observer } from '../src/config/observer.ts';
|
|
import { dailySchedule, publishImage, publishCarousel } from '../server/threads/publisher.ts';
|
|
|
|
test('schedule uses the Korean date and existing SunCalc sunrise', () => {
|
|
const now = new Date('2026-11-01T18:00:00Z');
|
|
const schedule = dailySchedule(now);
|
|
assert.equal(schedule.date, '2026-11-02');
|
|
assert.equal(schedule.sunrise.getTime(), calculateSun(now, observer).sunrise!.getTime());
|
|
assert.equal(+schedule.sunrise - +schedule.prepareAt, 300_000);
|
|
assert.equal(+schedule.captureAt - +schedule.sunrise, 300_000);
|
|
});
|
|
|
|
test('publishing waits for ready media and persists intent before sending', async () => {
|
|
const events: string[] = [];
|
|
const replies = [{ id: 'container' }, { status: 'IN_PROGRESS' }, { status: 'FINISHED' }, { id: 'post' }];
|
|
const result = await publishImage('secret', 'https://example.com/image.jpg', 'caption', async () => { events.push('persist'); },
|
|
async (url, init) => {
|
|
events.push(String(url).includes('threads_publish') ? 'publish' : 'request');
|
|
assert.equal(new Headers(init?.headers).get('Authorization'), 'Bearer secret');
|
|
return new Response(JSON.stringify(replies.shift()));
|
|
}, async () => {});
|
|
assert.equal(result, 'post');
|
|
assert.deepEqual(events.slice(-2), ['persist', 'publish']);
|
|
});
|
|
|
|
test('failed media never reaches publish', async () => {
|
|
const replies = [{ id: 'container' }, { status: 'ERROR' }];
|
|
await assert.rejects(publishImage('secret', 'https://example.com/image.jpg', 'caption', async () => {
|
|
assert.fail('must not publish');
|
|
}, async () => new Response(JSON.stringify(replies.shift())), async () => {}), /ERROR/);
|
|
});
|
|
|
|
test('another publisher owning the daily record prevents the publish request', async () => {
|
|
let requests = 0;
|
|
const replies = [{ id: 'container' }, { status: 'FINISHED' }];
|
|
await assert.rejects(publishImage('secret', 'https://example.com/image.jpg', 'caption', async () => {
|
|
throw new Error('EEXIST');
|
|
}, async () => { requests++; return new Response(JSON.stringify(replies.shift())); }, async () => {}), /EEXIST/);
|
|
assert.equal(requests, 2);
|
|
});
|
|
|
|
test('ambiguous publish response is never automatically retried', async () => {
|
|
let requests = 0;
|
|
let claimed = false;
|
|
const replies = [{ id: 'container' }, { status: 'FINISHED' }];
|
|
await assert.rejects(publishImage('secret', 'https://example.com/image.jpg', 'caption', async () => {
|
|
claimed = true;
|
|
}, async () => {
|
|
requests++;
|
|
if (requests === 3) throw new Error('Connection lost');
|
|
return new Response(JSON.stringify(replies.shift()));
|
|
}, async () => {}), /Connection lost/);
|
|
assert.equal(claimed, true);
|
|
assert.equal(requests, 3);
|
|
});
|
|
|
|
test('carousel preserves photo order, waits for children and publishes only the parent', async () => {
|
|
const posts: URLSearchParams[] = [];
|
|
const replies = [{ id: 'back' }, { status: 'FINISHED' }, { id: 'front' },
|
|
{ status: 'IN_PROGRESS' }, { status: 'FINISHED' }, { id: 'album' }, { status: 'FINISHED' }, { id: 'post' }];
|
|
let claimed = '';
|
|
const result = await publishCarousel('secret', ['https://example.com/back.jpg', 'https://example.com/front.jpg'], 'caption',
|
|
async id => { claimed = id; }, async (_url, init) => {
|
|
if (init?.method === 'POST') posts.push(init.body as URLSearchParams);
|
|
return new Response(JSON.stringify(replies.shift()));
|
|
}, async () => {});
|
|
assert.equal(result, 'post');
|
|
assert.equal(posts[0].get('image_url'), 'https://example.com/back.jpg');
|
|
assert.equal(posts[1].get('image_url'), 'https://example.com/front.jpg');
|
|
assert.equal(posts[0].get('is_carousel_item'), 'true');
|
|
assert.equal(posts[1].get('is_carousel_item'), 'true');
|
|
assert.equal(posts[2].get('media_type'), 'CAROUSEL');
|
|
assert.equal(posts[2].get('children'), 'back,front');
|
|
assert.equal(posts[2].get('text'), 'caption');
|
|
assert.equal(claimed, 'album');
|
|
assert.equal(posts[3].get('creation_id'), 'album');
|
|
assert.equal(posts.length, 4);
|
|
});
|
|
|
|
test('a failed second image prevents a partial carousel publication', async () => {
|
|
const replies = [{ id: 'back' }, { status: 'FINISHED' }, { id: 'front' }, { status: 'ERROR' }];
|
|
await assert.rejects(publishCarousel('secret', ['https://example.com/back.jpg', 'https://example.com/front.jpg'], 'caption',
|
|
async () => assert.fail('must not publish'), async () => new Response(JSON.stringify(replies.shift())), async () => {}), /ERROR/);
|
|
});
|