competitor-listing-refresh-cli.test.ts 3.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import assert from 'node:assert/strict';
  2. import test from 'node:test';
  3. import {
  4. COMPETITOR_LISTING_REFRESH_EXIT,
  5. runCompetitorListingRefreshCli,
  6. } from '../scripts/refresh-competitor-listings.js';
  7. import { ApiError } from '../src/http/api-error.js';
  8. import type { CompetitorListingRefreshRun } from '../src/modules/competitor-listing-monitor/domain.js';
  9. const runId = '11111111-1111-4111-8111-111111111111';
  10. const requestedAt = '2026-08-26T00:00:00.000Z';
  11. function run(status: CompetitorListingRefreshRun['status']): CompetitorListingRefreshRun {
  12. const terminal = ['completed', 'partial', 'failed'].includes(status);
  13. return {
  14. id: runId,
  15. workspaceId: 'demashi',
  16. platform: 'jd',
  17. trigger: 'scheduled',
  18. status,
  19. total: 3,
  20. completed: terminal ? 3 : 0,
  21. baseline: status === 'completed' ? 3 : 0,
  22. unchanged: 0,
  23. changed: 0,
  24. failed: status === 'partial' ? 1 : status === 'failed' ? 3 : 0,
  25. itemResults: [],
  26. requestedAt,
  27. startedAt: status === 'queued' ? null : '2026-08-26T00:00:01.000Z',
  28. completedAt: terminal ? '2026-08-26T00:00:02.000Z' : null,
  29. };
  30. }
  31. test('scheduled CLI waits for terminal status and maps completed, partial and failed exit codes', async (context) => {
  32. for (const scenario of [
  33. { status: 'completed' as const, exitCode: COMPETITOR_LISTING_REFRESH_EXIT.completed },
  34. { status: 'partial' as const, exitCode: COMPETITOR_LISTING_REFRESH_EXIT.partial },
  35. { status: 'failed' as const, exitCode: COMPETITOR_LISTING_REFRESH_EXIT.failed },
  36. ]) {
  37. await context.test(scenario.status, async () => {
  38. const outputs: string[] = [];
  39. const errors: string[] = [];
  40. let trigger = '';
  41. let polls = 0;
  42. const service = {
  43. async startRefresh(_workspaceId: string, _platform: 'jd', value: 'manual' | 'scheduled') {
  44. trigger = value;
  45. return run('queued');
  46. },
  47. async getRun() {
  48. polls += 1;
  49. return polls === 1 ? run('running') : run(scenario.status);
  50. },
  51. };
  52. const exitCode = await runCompetitorListingRefreshCli({
  53. service,
  54. workspaceId: 'demashi',
  55. pollMs: 1,
  56. timeoutMs: 1_000,
  57. sleep: async () => undefined,
  58. write: (line) => outputs.push(line),
  59. writeError: (line) => errors.push(line),
  60. });
  61. assert.equal(trigger, 'scheduled');
  62. assert.equal(exitCode, scenario.exitCode);
  63. assert.equal(polls, 2);
  64. const terminalLine = scenario.status === 'failed' ? errors.at(-1) : outputs.at(-1);
  65. assert.equal((JSON.parse(terminalLine ?? '{}') as { status?: string }).status, scenario.status);
  66. });
  67. }
  68. });
  69. test('scheduled CLI reports a distinct exit code when a manual refresh already owns the workspace lock', async () => {
  70. const errors: string[] = [];
  71. const service = {
  72. async startRefresh(): Promise<CompetitorListingRefreshRun> {
  73. throw new ApiError(409, 'competitor_listing_refresh_running');
  74. },
  75. async getRun(): Promise<CompetitorListingRefreshRun | null> {
  76. throw new Error('must not poll after a lock conflict');
  77. },
  78. };
  79. const exitCode = await runCompetitorListingRefreshCli({
  80. service,
  81. workspaceId: 'demashi',
  82. write: () => undefined,
  83. writeError: (line) => errors.push(line),
  84. });
  85. assert.equal(exitCode, COMPETITOR_LISTING_REFRESH_EXIT.alreadyRunning);
  86. assert.equal((JSON.parse(errors[0] ?? '{}') as { error?: string }).error, 'competitor_listing_refresh_running');
  87. });