import assert from 'node:assert/strict'; import test from 'node:test'; import { COMPETITOR_LISTING_REFRESH_EXIT, runCompetitorListingRefreshCli, } from '../scripts/refresh-competitor-listings.js'; import { ApiError } from '../src/http/api-error.js'; import type { CompetitorListingRefreshRun } from '../src/modules/competitor-listing-monitor/domain.js'; const runId = '11111111-1111-4111-8111-111111111111'; const requestedAt = '2026-08-26T00:00:00.000Z'; function run(status: CompetitorListingRefreshRun['status']): CompetitorListingRefreshRun { const terminal = ['completed', 'partial', 'failed'].includes(status); return { id: runId, workspaceId: 'demashi', platform: 'jd', trigger: 'scheduled', status, total: 3, completed: terminal ? 3 : 0, baseline: status === 'completed' ? 3 : 0, unchanged: 0, changed: 0, failed: status === 'partial' ? 1 : status === 'failed' ? 3 : 0, itemResults: [], requestedAt, startedAt: status === 'queued' ? null : '2026-08-26T00:00:01.000Z', completedAt: terminal ? '2026-08-26T00:00:02.000Z' : null, }; } test('scheduled CLI waits for terminal status and maps completed, partial and failed exit codes', async (context) => { for (const scenario of [ { status: 'completed' as const, exitCode: COMPETITOR_LISTING_REFRESH_EXIT.completed }, { status: 'partial' as const, exitCode: COMPETITOR_LISTING_REFRESH_EXIT.partial }, { status: 'failed' as const, exitCode: COMPETITOR_LISTING_REFRESH_EXIT.failed }, ]) { await context.test(scenario.status, async () => { const outputs: string[] = []; const errors: string[] = []; let trigger = ''; let polls = 0; const service = { async startRefresh(_workspaceId: string, _platform: 'jd', value: 'manual' | 'scheduled') { trigger = value; return run('queued'); }, async getRun() { polls += 1; return polls === 1 ? run('running') : run(scenario.status); }, }; const exitCode = await runCompetitorListingRefreshCli({ service, workspaceId: 'demashi', pollMs: 1, timeoutMs: 1_000, sleep: async () => undefined, write: (line) => outputs.push(line), writeError: (line) => errors.push(line), }); assert.equal(trigger, 'scheduled'); assert.equal(exitCode, scenario.exitCode); assert.equal(polls, 2); const terminalLine = scenario.status === 'failed' ? errors.at(-1) : outputs.at(-1); assert.equal((JSON.parse(terminalLine ?? '{}') as { status?: string }).status, scenario.status); }); } }); test('scheduled CLI reports a distinct exit code when a manual refresh already owns the workspace lock', async () => { const errors: string[] = []; const service = { async startRefresh(): Promise { throw new ApiError(409, 'competitor_listing_refresh_running'); }, async getRun(): Promise { throw new Error('must not poll after a lock conflict'); }, }; const exitCode = await runCompetitorListingRefreshCli({ service, workspaceId: 'demashi', write: () => undefined, writeError: (line) => errors.push(line), }); assert.equal(exitCode, COMPETITOR_LISTING_REFRESH_EXIT.alreadyRunning); assert.equal((JSON.parse(errors[0] ?? '{}') as { error?: string }).error, 'competitor_listing_refresh_running'); });