import test from 'node:test'; import assert from 'node:assert/strict'; import { buildScheduledBoardPostTitle, buildScheduledPlanWorkIdBase, isPlanScheduledTaskDue, mapPlanScheduledTaskRow, shouldCreatePlanForScheduleExecution, updatePlanScheduledTaskSchema, } from './plan-schedule-service.js'; test('buildScheduledBoardPostTitle prefers the first memo line when present', () => { assert.equal( buildScheduledBoardPostTitle({ work_id: '반복작업', note: ' 첫 줄 제목 \n둘째 줄 설명', }), '첫 줄 제목', ); }); test('buildScheduledBoardPostTitle falls back to normalized work id when memo is empty', () => { assert.equal( buildScheduledBoardPostTitle({ work_id: ' 작업 ID ', note: ' \n ', }), '반복작업', ); }); test('buildScheduledPlanWorkIdBase uses the schedule work id for automation registration', () => { assert.equal( buildScheduledPlanWorkIdBase({ work_id: ' 반복-정리 ', }), '반복-정리', ); }); test('buildScheduledPlanWorkIdBase keeps managed-service runs on the schedule work id with schedule pk prefix', () => { assert.equal( buildScheduledPlanWorkIdBase({ id: 10, work_id: ' 반복-정리 ', execution_mode: 'managed-service', }), 'schedule-10-반복-정리', ); assert.equal( buildScheduledPlanWorkIdBase({ id: 10, work_id: '반복-정리-service', execution_mode: 'managed-service', }), 'schedule-10-반복-정리-service', ); }); test('buildScheduledPlanWorkIdBase falls back when managed-service schedule id is missing', () => { assert.equal( buildScheduledPlanWorkIdBase({ work_id: '반복-정리-service', execution_mode: 'managed-service', }), '반복-정리-service', ); }); test('shouldCreatePlanForScheduleExecution only returns true for managed-service schedules', () => { assert.equal( shouldCreatePlanForScheduleExecution({ execution_mode: 'codex', }), false, ); assert.equal( shouldCreatePlanForScheduleExecution({ execution_mode: 'managed-service', }), true, ); }); test('updatePlanScheduledTaskSchema keeps partial updates partial', () => { assert.deepEqual(updatePlanScheduledTaskSchema.parse({ recreateManagedServiceOnNextSave: true }), { recreateManagedServiceOnNextSave: true, }); }); test('updatePlanScheduledTaskSchema accepts second-based interval updates', () => { assert.deepEqual( updatePlanScheduledTaskSchema.parse({ repeatIntervalValue: 10, repeatIntervalUnit: 'second', repeatIntervalSeconds: 10, }), { repeatIntervalValue: 10, repeatIntervalUnit: 'second', repeatIntervalSeconds: 10, }, ); }); test('interval schedule with start time waits until start time when immediate run is enabled', () => { assert.equal( isPlanScheduledTaskDue( { schedule_mode: 'interval', immediate_run_enabled: true, repeat_interval_minutes: 60, repeat_window_start_time: '09:00', created_at: '2026-04-30T07:00:00+09:00', }, new Date('2026-04-30T08:59:00+09:00'), ), false, ); assert.equal( isPlanScheduledTaskDue( { schedule_mode: 'interval', immediate_run_enabled: true, repeat_interval_minutes: 60, repeat_window_start_time: '09:00', created_at: '2026-04-30T07:00:00+09:00', }, new Date('2026-04-30T09:00:00+09:00'), ), true, ); }); test('interval schedule with start time waits one interval after the start when immediate run is disabled', () => { assert.equal( isPlanScheduledTaskDue( { schedule_mode: 'interval', immediate_run_enabled: false, repeat_interval_minutes: 60, repeat_window_start_time: '09:00', created_at: '2026-04-30T07:00:00+09:00', }, new Date('2026-04-30T09:59:00+09:00'), ), false, ); assert.equal( isPlanScheduledTaskDue( { schedule_mode: 'interval', immediate_run_enabled: false, repeat_interval_minutes: 60, repeat_window_start_time: '09:00', created_at: '2026-04-30T07:00:00+09:00', }, new Date('2026-04-30T10:00:00+09:00'), ), true, ); }); test('interval schedule supports second-based due calculation', () => { assert.equal( isPlanScheduledTaskDue( { schedule_mode: 'interval', immediate_run_enabled: false, repeat_interval_unit: 'second', repeat_interval_value: 10, repeat_interval_seconds: 10, created_at: '2026-04-30T09:00:00+09:00', }, new Date('2026-04-30T09:00:09+09:00'), ), false, ); assert.equal( isPlanScheduledTaskDue( { schedule_mode: 'interval', immediate_run_enabled: false, repeat_interval_unit: 'second', repeat_interval_value: 10, repeat_interval_seconds: 10, created_at: '2026-04-30T09:00:00+09:00', }, new Date('2026-04-30T09:00:10+09:00'), ), true, ); }); test('interval schedule uses repeat interval value and unit when stored seconds are stale', () => { assert.equal( isPlanScheduledTaskDue( { schedule_mode: 'interval', immediate_run_enabled: false, repeat_interval_unit: 'minute', repeat_interval_value: 10, repeat_interval_seconds: 3600, created_at: '2026-04-30T09:50:00+09:00', }, new Date('2026-04-30T09:59:59+09:00'), ), false, ); assert.equal( isPlanScheduledTaskDue( { schedule_mode: 'interval', immediate_run_enabled: false, repeat_interval_unit: 'minute', repeat_interval_value: 10, repeat_interval_seconds: 3600, created_at: '2026-04-30T09:50:00+09:00', }, new Date('2026-04-30T10:00:00+09:00'), ), true, ); }); test('mapPlanScheduledTaskRow normalizes stale stored repeat interval seconds', () => { const mapped = mapPlanScheduledTaskRow({ id: 2, work_id: 'stock-alert', schedule_mode: 'interval', repeat_interval_unit: 'minute', repeat_interval_value: 10, repeat_interval_seconds: 3600, repeat_interval_minutes: 60, }); assert.equal(mapped.repeatIntervalValue, 10); assert.equal(mapped.repeatIntervalUnit, 'minute'); assert.equal(mapped.repeatIntervalSeconds, 600); assert.equal(mapped.repeatIntervalMinutes, 10); });