@open-mercato/scheduler 0.6.8-develop.7100.1.fbf66fca35 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/scheduler/__integration__/TC-SCHED-001.spec.js +2 -2
- package/dist/modules/scheduler/__integration__/TC-SCHED-001.spec.js.map +2 -2
- package/dist/modules/scheduler/__integration__/TC-SCHED-004.spec.js +4 -9
- package/dist/modules/scheduler/__integration__/TC-SCHED-004.spec.js.map +2 -2
- package/dist/modules/scheduler/__integration__/TC-SCHED-007.spec.js +3 -3
- package/dist/modules/scheduler/__integration__/TC-SCHED-007.spec.js.map +2 -2
- package/dist/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.js +3 -3
- package/dist/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.js.map +1 -1
- package/dist/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.js +44 -107
- package/dist/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.js.map +2 -2
- package/dist/modules/scheduler/__integration__/helpers/scheduler.js +3 -13
- package/dist/modules/scheduler/__integration__/helpers/scheduler.js.map +2 -2
- package/dist/modules/scheduler/api/jobs/[id]/executions/route.js +13 -7
- package/dist/modules/scheduler/api/jobs/[id]/executions/route.js.map +2 -2
- package/dist/modules/scheduler/api/targets/route.js +12 -2
- package/dist/modules/scheduler/api/targets/route.js.map +2 -2
- package/dist/modules/scheduler/api/trigger/route.js +13 -7
- package/dist/modules/scheduler/api/trigger/route.js.map +2 -2
- package/dist/modules/scheduler/backend/config/scheduled-jobs/page.js +1 -3
- package/dist/modules/scheduler/backend/config/scheduled-jobs/page.js.map +2 -2
- package/dist/modules/scheduler/cli.js +1 -32
- package/dist/modules/scheduler/cli.js.map +2 -2
- package/dist/modules/scheduler/commands/jobs.js +2 -84
- package/dist/modules/scheduler/commands/jobs.js.map +2 -2
- package/dist/modules/scheduler/data/validators.js +3 -27
- package/dist/modules/scheduler/data/validators.js.map +2 -2
- package/dist/modules/scheduler/i18n/de.json +0 -2
- package/dist/modules/scheduler/i18n/en.json +0 -2
- package/dist/modules/scheduler/i18n/es.json +0 -2
- package/dist/modules/scheduler/i18n/ko.json +0 -2
- package/dist/modules/scheduler/i18n/pl.json +0 -2
- package/dist/modules/scheduler/services/localSchedulerService.js +6 -31
- package/dist/modules/scheduler/services/localSchedulerService.js.map +2 -2
- package/dist/modules/scheduler/workers/execute-schedule.worker.js +6 -64
- package/dist/modules/scheduler/workers/execute-schedule.worker.js.map +2 -2
- package/package.json +6 -7
- package/src/modules/scheduler/__integration__/TC-SCHED-001.spec.ts +2 -2
- package/src/modules/scheduler/__integration__/TC-SCHED-004.spec.ts +8 -14
- package/src/modules/scheduler/__integration__/TC-SCHED-007.spec.ts +3 -3
- package/src/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.ts +3 -3
- package/src/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.ts +66 -154
- package/src/modules/scheduler/__integration__/helpers/scheduler.ts +5 -24
- package/src/modules/scheduler/api/jobs/[id]/executions/route.ts +19 -9
- package/src/modules/scheduler/api/targets/__tests__/route.test.ts +4 -3
- package/src/modules/scheduler/api/targets/route.ts +17 -6
- package/src/modules/scheduler/api/trigger/route.ts +19 -9
- package/src/modules/scheduler/backend/config/scheduled-jobs/page.tsx +1 -4
- package/src/modules/scheduler/cli.ts +1 -44
- package/src/modules/scheduler/commands/jobs.ts +3 -137
- package/src/modules/scheduler/data/__tests__/validators.test.ts +4 -26
- package/src/modules/scheduler/data/validators.ts +2 -37
- package/src/modules/scheduler/i18n/de.json +0 -2
- package/src/modules/scheduler/i18n/en.json +0 -2
- package/src/modules/scheduler/i18n/es.json +0 -2
- package/src/modules/scheduler/i18n/ko.json +0 -2
- package/src/modules/scheduler/i18n/pl.json +0 -2
- package/src/modules/scheduler/services/__tests__/localSchedulerService.test.ts +0 -179
- package/src/modules/scheduler/services/localSchedulerService.ts +6 -39
- package/src/modules/scheduler/workers/__tests__/execute-schedule.worker.test.ts +3 -25
- package/src/modules/scheduler/workers/execute-schedule.worker.ts +7 -76
- package/dist/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.js +0 -72
- package/dist/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.js.map +0 -7
- package/dist/modules/scheduler/__integration__/TC-SCHED-008.spec.js +0 -66
- package/dist/modules/scheduler/__integration__/TC-SCHED-008.spec.js.map +0 -7
- package/dist/modules/scheduler/lib/safeQueueTargets.js +0 -157
- package/dist/modules/scheduler/lib/safeQueueTargets.js.map +0 -7
- package/dist/modules/scheduler/lib/scheduleAccess.js +0 -22
- package/dist/modules/scheduler/lib/scheduleAccess.js.map +0 -7
- package/dist/modules/scheduler/workers/test-echo-queue.worker.js +0 -22
- package/dist/modules/scheduler/workers/test-echo-queue.worker.js.map +0 -7
- package/src/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.ts +0 -90
- package/src/modules/scheduler/__integration__/TC-SCHED-008.spec.ts +0 -94
- package/src/modules/scheduler/commands/__tests__/jobs.target-guard.test.ts +0 -177
- package/src/modules/scheduler/data/__tests__/safe-targets.test.ts +0 -110
- package/src/modules/scheduler/lib/__tests__/safeQueueTargets.test.ts +0 -311
- package/src/modules/scheduler/lib/__tests__/scheduleAccess.test.ts +0 -164
- package/src/modules/scheduler/lib/safeQueueTargets.ts +0 -324
- package/src/modules/scheduler/lib/scheduleAccess.ts +0 -104
- package/src/modules/scheduler/workers/test-echo-queue.worker.ts +0 -25
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:scheduler] found
|
|
1
|
+
[build:scheduler] found 55 entry points
|
|
2
2
|
[build:scheduler] built successfully
|
|
@@ -11,8 +11,8 @@ async function createSchedule(request, token) {
|
|
|
11
11
|
scheduleType: "interval",
|
|
12
12
|
scheduleValue: "15m",
|
|
13
13
|
timezone: "UTC",
|
|
14
|
-
targetType: "
|
|
15
|
-
|
|
14
|
+
targetType: "queue",
|
|
15
|
+
targetQueue: "scheduler-execution",
|
|
16
16
|
targetPayload: { source: "integration-test" },
|
|
17
17
|
isEnabled: true,
|
|
18
18
|
sourceType: "user"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/scheduler/__integration__/TC-SCHED-001.spec.ts"],
|
|
4
|
-
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { apiRequest, getAuthToken } from '@open-mercato/core/modules/core/__integration__/helpers/api'\n\nconst isAsyncQueueStrategy = (process.env.QUEUE_STRATEGY || 'local') === 'async'\n\ntype CreateScheduleResponse = { id?: string }\ntype SchedulerListResponse = { items?: Array<{ id?: string }> }\ntype SchedulerErrorResponse = { error?: string; message?: string; available?: boolean; items?: unknown[] }\n\nasync function createSchedule(request: import('@playwright/test').APIRequestContext, token: string) {\n const response = await apiRequest(request, 'POST', '/api/scheduler/jobs', {\n token,\n data: {\n name: `Integration Scheduler ${Date.now()}`,\n description: 'Runtime Redis URL integration probe',\n scopeType: 'organization',\n scheduleType: 'interval',\n scheduleValue: '15m',\n timezone: 'UTC',\n targetType: '
|
|
5
|
-
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,YAAY,oBAAoB;AAEzC,MAAM,wBAAwB,QAAQ,IAAI,kBAAkB,aAAa;AAMzE,eAAe,eAAe,SAAuD,OAAe;AAClG,QAAM,WAAW,MAAM,WAAW,SAAS,QAAQ,uBAAuB;AAAA,IACxE;AAAA,IACA,MAAM;AAAA,MACJ,MAAM,yBAAyB,KAAK,IAAI,CAAC;AAAA,MACzC,aAAa;AAAA,MACb,WAAW;AAAA,MACX,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,
|
|
4
|
+
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { apiRequest, getAuthToken } from '@open-mercato/core/modules/core/__integration__/helpers/api'\n\nconst isAsyncQueueStrategy = (process.env.QUEUE_STRATEGY || 'local') === 'async'\n\ntype CreateScheduleResponse = { id?: string }\ntype SchedulerListResponse = { items?: Array<{ id?: string }> }\ntype SchedulerErrorResponse = { error?: string; message?: string; available?: boolean; items?: unknown[] }\n\nasync function createSchedule(request: import('@playwright/test').APIRequestContext, token: string) {\n const response = await apiRequest(request, 'POST', '/api/scheduler/jobs', {\n token,\n data: {\n name: `Integration Scheduler ${Date.now()}`,\n description: 'Runtime Redis URL integration probe',\n scopeType: 'organization',\n scheduleType: 'interval',\n scheduleValue: '15m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'scheduler-execution',\n targetPayload: { source: 'integration-test' },\n isEnabled: true,\n sourceType: 'user',\n },\n })\n\n expect(response.status()).toBe(201)\n const body = (await response.json()) as CreateScheduleResponse\n expect(body.id).toMatch(/^[0-9a-f-]{36}$/i)\n return String(body.id)\n}\n\ntest.describe('TC-SCHED-001: Scheduler runtime queue APIs', () => {\n test('lists created schedules and exposes execution-history endpoint semantics', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n let scheduleId: string | null = null\n\n try {\n scheduleId = await createSchedule(request, token)\n\n const listResponse = await apiRequest(request, 'GET', `/api/scheduler/jobs?id=${scheduleId}`, { token })\n expect(listResponse.status()).toBe(200)\n const listBody = (await listResponse.json()) as SchedulerListResponse\n const ids = (listBody.items ?? []).map((item) => item.id)\n expect(ids).toContain(scheduleId)\n\n const executionsResponse = await apiRequest(\n request,\n 'GET',\n `/api/scheduler/jobs/${scheduleId}/executions?pageSize=5`,\n { token },\n )\n\n if (isAsyncQueueStrategy) {\n expect(executionsResponse.status()).toBe(200)\n const executionsBody = (await executionsResponse.json()) as { items?: unknown[]; total?: number; pageSize?: number }\n expect(Array.isArray(executionsBody.items)).toBe(true)\n expect(typeof executionsBody.total).toBe('number')\n expect(executionsBody.pageSize).toBe(5)\n } else {\n expect(executionsResponse.status()).toBe(400)\n const executionsBody = (await executionsResponse.json()) as SchedulerErrorResponse\n expect(executionsBody.error ?? '').toMatch(/QUEUE_STRATEGY=async|Execution history requires/i)\n expect(Array.isArray(executionsBody.items)).toBe(true)\n }\n } finally {\n if (scheduleId) {\n await apiRequest(request, 'DELETE', '/api/scheduler/jobs', {\n token,\n data: { id: scheduleId },\n }).catch(() => null)\n }\n }\n })\n\n test('validates queue names and async-only queue job access', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n\n const invalidQueueResponse = await apiRequest(\n request,\n 'GET',\n '/api/scheduler/queue-jobs/non-existent?queue=not-a-registered-queue',\n { token },\n )\n expect(invalidQueueResponse.status()).toBe(400)\n const invalidQueueBody = (await invalidQueueResponse.json()) as SchedulerErrorResponse\n expect(invalidQueueBody.error ?? '').toMatch(/Invalid queue name/i)\n\n const validQueueResponse = await apiRequest(\n request,\n 'GET',\n '/api/scheduler/queue-jobs/non-existent?queue=scheduler-execution',\n { token },\n )\n\n if (isAsyncQueueStrategy) {\n expect(validQueueResponse.status()).toBe(404)\n const validQueueBody = (await validQueueResponse.json()) as SchedulerErrorResponse\n expect(validQueueBody.error ?? '').toMatch(/Job not found/i)\n } else {\n expect(validQueueResponse.status()).toBe(400)\n const validQueueBody = (await validQueueResponse.json()) as SchedulerErrorResponse\n expect(validQueueBody.error ?? '').toMatch(/QUEUE_STRATEGY=async|BullMQ job logs are only available/i)\n expect(validQueueBody.available).toBe(false)\n }\n })\n})\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,YAAY,oBAAoB;AAEzC,MAAM,wBAAwB,QAAQ,IAAI,kBAAkB,aAAa;AAMzE,eAAe,eAAe,SAAuD,OAAe;AAClG,QAAM,WAAW,MAAM,WAAW,SAAS,QAAQ,uBAAuB;AAAA,IACxE;AAAA,IACA,MAAM;AAAA,MACJ,MAAM,yBAAyB,KAAK,IAAI,CAAC;AAAA,MACzC,aAAa;AAAA,MACb,WAAW;AAAA,MACX,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,eAAe,EAAE,QAAQ,mBAAmB;AAAA,MAC5C,WAAW;AAAA,MACX,YAAY;AAAA,IACd;AAAA,EACF,CAAC;AAED,SAAO,SAAS,OAAO,CAAC,EAAE,KAAK,GAAG;AAClC,QAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,SAAO,KAAK,EAAE,EAAE,QAAQ,kBAAkB;AAC1C,SAAO,OAAO,KAAK,EAAE;AACvB;AAEA,KAAK,SAAS,8CAA8C,MAAM;AAChE,OAAK,4EAA4E,OAAO,EAAE,QAAQ,MAAM;AACtG,UAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AACjD,QAAI,aAA4B;AAEhC,QAAI;AACF,mBAAa,MAAM,eAAe,SAAS,KAAK;AAEhD,YAAM,eAAe,MAAM,WAAW,SAAS,OAAO,0BAA0B,UAAU,IAAI,EAAE,MAAM,CAAC;AACvG,aAAO,aAAa,OAAO,CAAC,EAAE,KAAK,GAAG;AACtC,YAAM,WAAY,MAAM,aAAa,KAAK;AAC1C,YAAM,OAAO,SAAS,SAAS,CAAC,GAAG,IAAI,CAAC,SAAS,KAAK,EAAE;AACxD,aAAO,GAAG,EAAE,UAAU,UAAU;AAEhC,YAAM,qBAAqB,MAAM;AAAA,QAC/B;AAAA,QACA;AAAA,QACA,uBAAuB,UAAU;AAAA,QACjC,EAAE,MAAM;AAAA,MACV;AAEA,UAAI,sBAAsB;AACxB,eAAO,mBAAmB,OAAO,CAAC,EAAE,KAAK,GAAG;AAC5C,cAAM,iBAAkB,MAAM,mBAAmB,KAAK;AACtD,eAAO,MAAM,QAAQ,eAAe,KAAK,CAAC,EAAE,KAAK,IAAI;AACrD,eAAO,OAAO,eAAe,KAAK,EAAE,KAAK,QAAQ;AACjD,eAAO,eAAe,QAAQ,EAAE,KAAK,CAAC;AAAA,MACxC,OAAO;AACL,eAAO,mBAAmB,OAAO,CAAC,EAAE,KAAK,GAAG;AAC5C,cAAM,iBAAkB,MAAM,mBAAmB,KAAK;AACtD,eAAO,eAAe,SAAS,EAAE,EAAE,QAAQ,kDAAkD;AAC7F,eAAO,MAAM,QAAQ,eAAe,KAAK,CAAC,EAAE,KAAK,IAAI;AAAA,MACvD;AAAA,IACF,UAAE;AACA,UAAI,YAAY;AACd,cAAM,WAAW,SAAS,UAAU,uBAAuB;AAAA,UACzD;AAAA,UACA,MAAM,EAAE,IAAI,WAAW;AAAA,QACzB,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,MACrB;AAAA,IACF;AAAA,EACF,CAAC;AAED,OAAK,yDAAyD,OAAO,EAAE,QAAQ,MAAM;AACnF,UAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AAEjD,UAAM,uBAAuB,MAAM;AAAA,MACjC;AAAA,MACA;AAAA,MACA;AAAA,MACA,EAAE,MAAM;AAAA,IACV;AACA,WAAO,qBAAqB,OAAO,CAAC,EAAE,KAAK,GAAG;AAC9C,UAAM,mBAAoB,MAAM,qBAAqB,KAAK;AAC1D,WAAO,iBAAiB,SAAS,EAAE,EAAE,QAAQ,qBAAqB;AAElE,UAAM,qBAAqB,MAAM;AAAA,MAC/B;AAAA,MACA;AAAA,MACA;AAAA,MACA,EAAE,MAAM;AAAA,IACV;AAEA,QAAI,sBAAsB;AACxB,aAAO,mBAAmB,OAAO,CAAC,EAAE,KAAK,GAAG;AAC5C,YAAM,iBAAkB,MAAM,mBAAmB,KAAK;AACtD,aAAO,eAAe,SAAS,EAAE,EAAE,QAAQ,gBAAgB;AAAA,IAC7D,OAAO;AACL,aAAO,mBAAmB,OAAO,CAAC,EAAE,KAAK,GAAG;AAC5C,YAAM,iBAAkB,MAAM,mBAAmB,KAAK;AACtD,aAAO,eAAe,SAAS,EAAE,EAAE,QAAQ,0DAA0D;AACrG,aAAO,eAAe,SAAS,EAAE,KAAK,KAAK;AAAA,IAC7C;AAAA,EACF,CAAC;AACH,CAAC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
2
|
import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
|
|
3
3
|
import { readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
|
|
4
|
-
import {
|
|
5
|
-
SCHEDULER_FORBIDDEN_INTERNAL_QUEUES,
|
|
6
|
-
SCHEDULER_TARGETS_PATH
|
|
7
|
-
} from "./helpers/scheduler.js";
|
|
4
|
+
import { SCHEDULER_EXECUTION_QUEUE, SCHEDULER_TARGETS_PATH } from "./helpers/scheduler.js";
|
|
8
5
|
function expectSortedByValue(options, label) {
|
|
9
6
|
const values = options.map((option) => option.value);
|
|
10
7
|
expect(values, `${label} should be sorted alphabetically by value`).toEqual(
|
|
@@ -18,7 +15,7 @@ function expectOptionShape(options, label) {
|
|
|
18
15
|
}
|
|
19
16
|
}
|
|
20
17
|
test.describe("TC-SCHED-004: GET /api/scheduler/targets lists queues and commands", () => {
|
|
21
|
-
test("returns
|
|
18
|
+
test("returns sorted queues (incl. scheduler-execution) and a non-empty command list", async ({ request }) => {
|
|
22
19
|
const token = await getAuthToken(request, "admin");
|
|
23
20
|
const response = await apiRequest(request, "GET", SCHEDULER_TARGETS_PATH, { token });
|
|
24
21
|
expect(response.status()).toBe(200);
|
|
@@ -26,12 +23,10 @@ test.describe("TC-SCHED-004: GET /api/scheduler/targets lists queues and command
|
|
|
26
23
|
const queues = body?.queues ?? [];
|
|
27
24
|
const commands = body?.commands ?? [];
|
|
28
25
|
expect(Array.isArray(body?.queues)).toBe(true);
|
|
26
|
+
expect(queues.length).toBeGreaterThan(0);
|
|
29
27
|
expectOptionShape(queues, "queues");
|
|
28
|
+
expect(queues.map((queue) => queue.value)).toContain(SCHEDULER_EXECUTION_QUEUE);
|
|
30
29
|
expectSortedByValue(queues, "queues");
|
|
31
|
-
const queueValues = queues.map((queue) => queue.value);
|
|
32
|
-
for (const forbidden of SCHEDULER_FORBIDDEN_INTERNAL_QUEUES) {
|
|
33
|
-
expect(queueValues, `internal queue ${forbidden} must not be a scheduler target`).not.toContain(forbidden);
|
|
34
|
-
}
|
|
35
30
|
expect(Array.isArray(body?.commands)).toBe(true);
|
|
36
31
|
expect(commands.length).toBeGreaterThan(0);
|
|
37
32
|
expectOptionShape(commands, "commands");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/scheduler/__integration__/TC-SCHED-004.spec.ts"],
|
|
4
|
-
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\nimport {
|
|
5
|
-
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,YAAY,oBAAoB;AACzC,SAAS,oBAAoB;AAC7B
|
|
4
|
+
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\nimport { SCHEDULER_EXECUTION_QUEUE, SCHEDULER_TARGETS_PATH } from './helpers/scheduler'\n\ntype TargetOption = { value: string; label: string }\ntype TargetsResponse = { queues?: TargetOption[]; commands?: TargetOption[] }\n\nfunction expectSortedByValue(options: TargetOption[], label: string) {\n const values = options.map((option) => option.value)\n expect(values, `${label} should be sorted alphabetically by value`).toEqual(\n [...values].sort((a, b) => a.localeCompare(b)),\n )\n}\n\nfunction expectOptionShape(options: TargetOption[], label: string) {\n for (const option of options) {\n expect(typeof option.value, `${label} option value should be a string`).toBe('string')\n expect(typeof option.label, `${label} option label should be a string`).toBe('string')\n }\n}\n\n/**\n * TC-SCHED-004: GET /api/scheduler/targets returns available queue names\n * (from module workers) and registered command IDs (from the command\n * registry), both sorted alphabetically by value. Read-only, no fixtures.\n */\ntest.describe('TC-SCHED-004: GET /api/scheduler/targets lists queues and commands', () => {\n test('returns sorted queues (incl. scheduler-execution) and a non-empty command list', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n\n const response = await apiRequest(request, 'GET', SCHEDULER_TARGETS_PATH, { token })\n expect(response.status()).toBe(200)\n\n const body = await readJsonSafe<TargetsResponse>(response)\n const queues = body?.queues ?? []\n const commands = body?.commands ?? []\n\n // Queues: non-empty, well-shaped, sorted, and include the known scheduler queue.\n expect(Array.isArray(body?.queues)).toBe(true)\n expect(queues.length).toBeGreaterThan(0)\n expectOptionShape(queues, 'queues')\n expect(queues.map((queue) => queue.value)).toContain(SCHEDULER_EXECUTION_QUEUE)\n expectSortedByValue(queues, 'queues')\n\n // Commands: non-empty, well-shaped, and sorted.\n expect(Array.isArray(body?.commands)).toBe(true)\n expect(commands.length).toBeGreaterThan(0)\n expectOptionShape(commands, 'commands')\n expectSortedByValue(commands, 'commands')\n })\n})\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,YAAY,oBAAoB;AACzC,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B,8BAA8B;AAKlE,SAAS,oBAAoB,SAAyB,OAAe;AACnE,QAAM,SAAS,QAAQ,IAAI,CAAC,WAAW,OAAO,KAAK;AACnD,SAAO,QAAQ,GAAG,KAAK,2CAA2C,EAAE;AAAA,IAClE,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AAAA,EAC/C;AACF;AAEA,SAAS,kBAAkB,SAAyB,OAAe;AACjE,aAAW,UAAU,SAAS;AAC5B,WAAO,OAAO,OAAO,OAAO,GAAG,KAAK,kCAAkC,EAAE,KAAK,QAAQ;AACrF,WAAO,OAAO,OAAO,OAAO,GAAG,KAAK,kCAAkC,EAAE,KAAK,QAAQ;AAAA,EACvF;AACF;AAOA,KAAK,SAAS,sEAAsE,MAAM;AACxF,OAAK,kFAAkF,OAAO,EAAE,QAAQ,MAAM;AAC5G,UAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AAEjD,UAAM,WAAW,MAAM,WAAW,SAAS,OAAO,wBAAwB,EAAE,MAAM,CAAC;AACnF,WAAO,SAAS,OAAO,CAAC,EAAE,KAAK,GAAG;AAElC,UAAM,OAAO,MAAM,aAA8B,QAAQ;AACzD,UAAM,SAAS,MAAM,UAAU,CAAC;AAChC,UAAM,WAAW,MAAM,YAAY,CAAC;AAGpC,WAAO,MAAM,QAAQ,MAAM,MAAM,CAAC,EAAE,KAAK,IAAI;AAC7C,WAAO,OAAO,MAAM,EAAE,gBAAgB,CAAC;AACvC,sBAAkB,QAAQ,QAAQ;AAClC,WAAO,OAAO,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAAE,UAAU,yBAAyB;AAC9E,wBAAoB,QAAQ,QAAQ;AAGpC,WAAO,MAAM,QAAQ,MAAM,QAAQ,CAAC,EAAE,KAAK,IAAI;AAC/C,WAAO,SAAS,MAAM,EAAE,gBAAgB,CAAC;AACzC,sBAAkB,UAAU,UAAU;AACtC,wBAAoB,UAAU,UAAU;AAAA,EAC1C,CAAC;AACH,CAAC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
2
|
import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
|
|
3
3
|
import { readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
|
|
4
|
-
import {
|
|
4
|
+
import { SCHEDULER_EXECUTION_QUEUE, SCHEDULER_JOBS_PATH, uniqueScheduleName } from "./helpers/scheduler.js";
|
|
5
5
|
const invalidCases = [
|
|
6
6
|
{ label: "cron: not a cron expression", scheduleType: "cron", scheduleValue: "not-a-cron" },
|
|
7
7
|
{ label: "cron: too few fields", scheduleType: "cron", scheduleValue: "* * *" },
|
|
@@ -23,8 +23,8 @@ test.describe("TC-SCHED-007: POST /api/scheduler/jobs validates schedule value f
|
|
|
23
23
|
scheduleType: testCase.scheduleType,
|
|
24
24
|
scheduleValue: testCase.scheduleValue,
|
|
25
25
|
timezone: "UTC",
|
|
26
|
-
targetType: "
|
|
27
|
-
|
|
26
|
+
targetType: "queue",
|
|
27
|
+
targetQueue: SCHEDULER_EXECUTION_QUEUE,
|
|
28
28
|
isEnabled: true,
|
|
29
29
|
sourceType: "user"
|
|
30
30
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/scheduler/__integration__/TC-SCHED-007.spec.ts"],
|
|
4
|
-
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\nimport {
|
|
5
|
-
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,YAAY,oBAAoB;AACzC,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB,
|
|
4
|
+
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\nimport { SCHEDULER_EXECUTION_QUEUE, SCHEDULER_JOBS_PATH, uniqueScheduleName } from './helpers/scheduler'\n\ntype ValidationError = { error?: string; details?: Array<{ path?: Array<string | number> }> }\n\n/**\n * TC-SCHED-007: POST /api/scheduler/jobs rejects invalid cron and interval\n * `scheduleValue`s with 400 and an error scoped to the `scheduleValue` path.\n *\n * Interval format is `<number><unit>` with unit in s|m|h|d and a minimum floor\n * of one minute. All cases are rejected at validation, so no records are\n * created and no teardown is required.\n */\nconst invalidCases = [\n { label: 'cron: not a cron expression', scheduleType: 'cron' as const, scheduleValue: 'not-a-cron' },\n { label: 'cron: too few fields', scheduleType: 'cron' as const, scheduleValue: '* * *' },\n { label: 'interval: unsupported unit', scheduleType: 'interval' as const, scheduleValue: '15x' },\n { label: 'interval: missing unit', scheduleType: 'interval' as const, scheduleValue: '99' },\n { label: 'interval: zero seconds', scheduleType: 'interval' as const, scheduleValue: '0s' },\n { label: 'interval: one second', scheduleType: 'interval' as const, scheduleValue: '1s' },\n { label: 'interval: below one minute', scheduleType: 'interval' as const, scheduleValue: '59s' },\n]\n\ntest.describe('TC-SCHED-007: POST /api/scheduler/jobs validates schedule value format', () => {\n for (const testCase of invalidCases) {\n test(`rejects ${testCase.label} with 400 on scheduleValue`, async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n\n const response = await apiRequest(request, 'POST', SCHEDULER_JOBS_PATH, {\n token,\n data: {\n name: uniqueScheduleName('Invalid Schedule Value'),\n scopeType: 'organization',\n scheduleType: testCase.scheduleType,\n scheduleValue: testCase.scheduleValue,\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: SCHEDULER_EXECUTION_QUEUE,\n isEnabled: true,\n sourceType: 'user',\n },\n })\n\n expect(response.status(), `${testCase.label} should be rejected with 400`).toBe(400)\n const body = await readJsonSafe<ValidationError>(response)\n const issuePaths = (body?.details ?? []).map((issue) => (issue.path ?? []).join('.'))\n expect(issuePaths, `${testCase.label} error should target scheduleValue`).toContain('scheduleValue')\n })\n }\n})\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,YAAY,oBAAoB;AACzC,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B,qBAAqB,0BAA0B;AAYnF,MAAM,eAAe;AAAA,EACnB,EAAE,OAAO,+BAA+B,cAAc,QAAiB,eAAe,aAAa;AAAA,EACnG,EAAE,OAAO,wBAAwB,cAAc,QAAiB,eAAe,QAAQ;AAAA,EACvF,EAAE,OAAO,8BAA8B,cAAc,YAAqB,eAAe,MAAM;AAAA,EAC/F,EAAE,OAAO,0BAA0B,cAAc,YAAqB,eAAe,KAAK;AAAA,EAC1F,EAAE,OAAO,0BAA0B,cAAc,YAAqB,eAAe,KAAK;AAAA,EAC1F,EAAE,OAAO,wBAAwB,cAAc,YAAqB,eAAe,KAAK;AAAA,EACxF,EAAE,OAAO,8BAA8B,cAAc,YAAqB,eAAe,MAAM;AACjG;AAEA,KAAK,SAAS,0EAA0E,MAAM;AAC5F,aAAW,YAAY,cAAc;AACnC,SAAK,WAAW,SAAS,KAAK,8BAA8B,OAAO,EAAE,QAAQ,MAAM;AACjF,YAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AAEjD,YAAM,WAAW,MAAM,WAAW,SAAS,QAAQ,qBAAqB;AAAA,QACtE;AAAA,QACA,MAAM;AAAA,UACJ,MAAM,mBAAmB,wBAAwB;AAAA,UACjD,WAAW;AAAA,UACX,cAAc,SAAS;AAAA,UACvB,eAAe,SAAS;AAAA,UACxB,UAAU;AAAA,UACV,YAAY;AAAA,UACZ,aAAa;AAAA,UACb,WAAW;AAAA,UACX,YAAY;AAAA,QACd;AAAA,MACF,CAAC;AAED,aAAO,SAAS,OAAO,GAAG,GAAG,SAAS,KAAK,8BAA8B,EAAE,KAAK,GAAG;AACnF,YAAM,OAAO,MAAM,aAA8B,QAAQ;AACzD,YAAM,cAAc,MAAM,WAAW,CAAC,GAAG,IAAI,CAAC,WAAW,MAAM,QAAQ,CAAC,GAAG,KAAK,GAAG,CAAC;AACpF,aAAO,YAAY,GAAG,SAAS,KAAK,oCAAoC,EAAE,UAAU,eAAe;AAAA,IACrG,CAAC;AAAA,EACH;AACF,CAAC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -50,7 +50,7 @@ test.describe("TC-SCHED-CRUDFORM-001: Scheduled Job CrudForm persists scope, tar
|
|
|
50
50
|
scheduleValue: "15m",
|
|
51
51
|
timezone: "UTC",
|
|
52
52
|
targetType: "queue",
|
|
53
|
-
targetQueue: "scheduler-
|
|
53
|
+
targetQueue: "scheduler-execution",
|
|
54
54
|
targetPayload: createPayload,
|
|
55
55
|
isEnabled: true,
|
|
56
56
|
sourceType: "user"
|
|
@@ -65,7 +65,7 @@ test.describe("TC-SCHED-CRUDFORM-001: Scheduled Job CrudForm persists scope, tar
|
|
|
65
65
|
scheduleValue: "15m",
|
|
66
66
|
timezone: "UTC",
|
|
67
67
|
targetType: "queue",
|
|
68
|
-
targetQueue: "scheduler-
|
|
68
|
+
targetQueue: "scheduler-execution",
|
|
69
69
|
targetCommand: null,
|
|
70
70
|
targetPayload: createPayload,
|
|
71
71
|
isEnabled: true,
|
|
@@ -93,7 +93,7 @@ test.describe("TC-SCHED-CRUDFORM-001: Scheduled Job CrudForm persists scope, tar
|
|
|
93
93
|
scheduleValue: "0 0 * * *",
|
|
94
94
|
timezone: "Europe/Warsaw",
|
|
95
95
|
targetType: "queue",
|
|
96
|
-
targetQueue: "scheduler-
|
|
96
|
+
targetQueue: "scheduler-execution",
|
|
97
97
|
targetCommand: null,
|
|
98
98
|
targetPayload: updatePayload,
|
|
99
99
|
isEnabled: false
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.ts"],
|
|
4
|
-
"sourcesContent": ["import { expect, test, type APIRequestContext } from '@playwright/test';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport {\n runCrudFormRoundTrip,\n skipIfCrudFormExtensionTestsDisabled,\n type CrudRecord,\n} from '@open-mercato/core/helpers/integration/crudFormPersistence';\n\n/**\n * TC-SCHED-CRUDFORM-001: Scheduled Job CrudForm persists scope, target + payload JSON (#2466).\n *\n * The scheduler's scheduled-job is the Tier-B \"JSON-bearing scalar\" surface: scope\n * (scopeType, server-derived org/tenant), schedule (scheduleType/scheduleValue/timezone),\n * target (targetType/targetQueue), and a nested `targetPayload` JSON object. Proves create +\n * update round-trip every value.\n *\n * Verified contract (differs from the makeCrud-default reference specs):\n * - The list GET filters by `?id=` (single uuid), not the `?ids=` other sweep modules use; the\n * explicit `readById` documents that contract and mirrors the sibling specs (the shared\n * harness default reads back via `?id=` too).\n * - Read-back is camelCase: the list `transformItem` maps every column to camelCase\n * (`scopeType`, `scheduleType`, `targetPayload`, `isEnabled`, ...), so the scalar\n * expectations use camelCase keys (not the snake_case of resources/staff).\n * - The scheduled-job declares no custom entity (no `ce.ts`), so there are no `cf_*` fields \u2014\n * the surface is scalars + nested JSON only.\n * - Scope is derived server-side from the caller's auth context and is immutable on update:\n * `scopeType: 'organization'` fills org+tenant from the admin token and survives the edit.\n * - Update is a partial PUT: omitted target fields are retained, so the update changes the\n * payload JSON while proving `targetType`/`targetQueue` survive untouched.\n * - Harness cleanup deletes via `?id=`; the scheduler delete resolves the id from the query\n * string, so the default `finally` cleanup removes the fixture.\n *\n * Self-contained: the job needs no pre-created fixtures (queue target + inline JSON), and the\n * harness deletes it in `finally`.\n *\n * Gated by `OM_INTEGRATION_CRUDFORM_EXTENSION_TESTS_DISABLED` (default off \u2192 runs).\n */\nconst SCHEDULER_JOBS_PATH = '/api/scheduler/jobs';\n\nasync function readScheduledJobById(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<CrudRecord | null> {\n const response = await apiRequest(\n request,\n 'GET',\n `${SCHEDULER_JOBS_PATH}?id=${encodeURIComponent(id)}&page=1&pageSize=100`,\n { token },\n );\n expect(response.status(), `read-back scheduler jobs failed: ${response.status()}`).toBe(200);\n const body = await readJsonSafe<{ items?: CrudRecord[] }>(response);\n return (body?.items ?? []).find((item) => item.id === id) ?? null;\n}\n\ntest.describe('TC-SCHED-CRUDFORM-001: Scheduled Job CrudForm persists scope, target + payload JSON', () => {\n test.beforeAll(() => {\n skipIfCrudFormExtensionTestsDisabled();\n });\n\n test('round-trips scalars, scope, target + nested payload JSON on create and update', async ({ request }) => {\n const token = await getAuthToken(request, 'admin');\n const stamp = Date.now();\n\n const createPayload = {\n source: 'integration-test',\n attempts: 3,\n retries: { max: 5, backoff: 'exponential' },\n tags: ['alpha', 'beta'],\n };\n const updatePayload = {\n source: 'integration-test-edited',\n attempts: 1,\n retries: { max: 0, backoff: 'fixed' },\n tags: ['gamma'],\n };\n\n await runCrudFormRoundTrip({\n request,\n token,\n collectionPath: SCHEDULER_JOBS_PATH,\n readById: (id) => readScheduledJobById(request, token, id),\n create: {\n payload: {\n name: `QA CRUDFORM Scheduled Job ${stamp}`,\n description: 'Original scheduled job description',\n scopeType: 'organization',\n scheduleType: 'interval',\n scheduleValue: '15m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'scheduler-
|
|
4
|
+
"sourcesContent": ["import { expect, test, type APIRequestContext } from '@playwright/test';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport {\n runCrudFormRoundTrip,\n skipIfCrudFormExtensionTestsDisabled,\n type CrudRecord,\n} from '@open-mercato/core/helpers/integration/crudFormPersistence';\n\n/**\n * TC-SCHED-CRUDFORM-001: Scheduled Job CrudForm persists scope, target + payload JSON (#2466).\n *\n * The scheduler's scheduled-job is the Tier-B \"JSON-bearing scalar\" surface: scope\n * (scopeType, server-derived org/tenant), schedule (scheduleType/scheduleValue/timezone),\n * target (targetType/targetQueue), and a nested `targetPayload` JSON object. Proves create +\n * update round-trip every value.\n *\n * Verified contract (differs from the makeCrud-default reference specs):\n * - The list GET filters by `?id=` (single uuid), not the `?ids=` other sweep modules use; the\n * explicit `readById` documents that contract and mirrors the sibling specs (the shared\n * harness default reads back via `?id=` too).\n * - Read-back is camelCase: the list `transformItem` maps every column to camelCase\n * (`scopeType`, `scheduleType`, `targetPayload`, `isEnabled`, ...), so the scalar\n * expectations use camelCase keys (not the snake_case of resources/staff).\n * - The scheduled-job declares no custom entity (no `ce.ts`), so there are no `cf_*` fields \u2014\n * the surface is scalars + nested JSON only.\n * - Scope is derived server-side from the caller's auth context and is immutable on update:\n * `scopeType: 'organization'` fills org+tenant from the admin token and survives the edit.\n * - Update is a partial PUT: omitted target fields are retained, so the update changes the\n * payload JSON while proving `targetType`/`targetQueue` survive untouched.\n * - Harness cleanup deletes via `?id=`; the scheduler delete resolves the id from the query\n * string, so the default `finally` cleanup removes the fixture.\n *\n * Self-contained: the job needs no pre-created fixtures (queue target + inline JSON), and the\n * harness deletes it in `finally`.\n *\n * Gated by `OM_INTEGRATION_CRUDFORM_EXTENSION_TESTS_DISABLED` (default off \u2192 runs).\n */\nconst SCHEDULER_JOBS_PATH = '/api/scheduler/jobs';\n\nasync function readScheduledJobById(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<CrudRecord | null> {\n const response = await apiRequest(\n request,\n 'GET',\n `${SCHEDULER_JOBS_PATH}?id=${encodeURIComponent(id)}&page=1&pageSize=100`,\n { token },\n );\n expect(response.status(), `read-back scheduler jobs failed: ${response.status()}`).toBe(200);\n const body = await readJsonSafe<{ items?: CrudRecord[] }>(response);\n return (body?.items ?? []).find((item) => item.id === id) ?? null;\n}\n\ntest.describe('TC-SCHED-CRUDFORM-001: Scheduled Job CrudForm persists scope, target + payload JSON', () => {\n test.beforeAll(() => {\n skipIfCrudFormExtensionTestsDisabled();\n });\n\n test('round-trips scalars, scope, target + nested payload JSON on create and update', async ({ request }) => {\n const token = await getAuthToken(request, 'admin');\n const stamp = Date.now();\n\n const createPayload = {\n source: 'integration-test',\n attempts: 3,\n retries: { max: 5, backoff: 'exponential' },\n tags: ['alpha', 'beta'],\n };\n const updatePayload = {\n source: 'integration-test-edited',\n attempts: 1,\n retries: { max: 0, backoff: 'fixed' },\n tags: ['gamma'],\n };\n\n await runCrudFormRoundTrip({\n request,\n token,\n collectionPath: SCHEDULER_JOBS_PATH,\n readById: (id) => readScheduledJobById(request, token, id),\n create: {\n payload: {\n name: `QA CRUDFORM Scheduled Job ${stamp}`,\n description: 'Original scheduled job description',\n scopeType: 'organization',\n scheduleType: 'interval',\n scheduleValue: '15m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'scheduler-execution',\n targetPayload: createPayload,\n isEnabled: true,\n sourceType: 'user',\n },\n },\n expectAfterCreate: {\n scalars: {\n name: `QA CRUDFORM Scheduled Job ${stamp}`,\n description: 'Original scheduled job description',\n scopeType: 'organization',\n scheduleType: 'interval',\n scheduleValue: '15m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'scheduler-execution',\n targetCommand: null,\n targetPayload: createPayload,\n isEnabled: true,\n sourceType: 'user',\n },\n },\n update: {\n payload: (id) => ({\n id,\n name: `QA CRUDFORM Scheduled Job ${stamp} EDITED`,\n description: 'Updated scheduled job description',\n scheduleType: 'cron',\n scheduleValue: '0 0 * * *',\n timezone: 'Europe/Warsaw',\n targetPayload: updatePayload,\n isEnabled: false,\n }),\n },\n expectAfterUpdate: {\n scalars: {\n name: `QA CRUDFORM Scheduled Job ${stamp} EDITED`,\n description: 'Updated scheduled job description',\n scopeType: 'organization',\n scheduleType: 'cron',\n scheduleValue: '0 0 * * *',\n timezone: 'Europe/Warsaw',\n targetType: 'queue',\n targetQueue: 'scheduler-execution',\n targetCommand: null,\n targetPayload: updatePayload,\n isEnabled: false,\n },\n },\n });\n });\n});\n"],
|
|
5
5
|
"mappings": "AAAA,SAAS,QAAQ,YAAoC;AACrD,SAAS,YAAY,oBAAoB;AACzC,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AA+BP,MAAM,sBAAsB;AAE5B,eAAe,qBACb,SACA,OACA,IAC4B;AAC5B,QAAM,WAAW,MAAM;AAAA,IACrB;AAAA,IACA;AAAA,IACA,GAAG,mBAAmB,OAAO,mBAAmB,EAAE,CAAC;AAAA,IACnD,EAAE,MAAM;AAAA,EACV;AACA,SAAO,SAAS,OAAO,GAAG,oCAAoC,SAAS,OAAO,CAAC,EAAE,EAAE,KAAK,GAAG;AAC3F,QAAM,OAAO,MAAM,aAAuC,QAAQ;AAClE,UAAQ,MAAM,SAAS,CAAC,GAAG,KAAK,CAAC,SAAS,KAAK,OAAO,EAAE,KAAK;AAC/D;AAEA,KAAK,SAAS,uFAAuF,MAAM;AACzG,OAAK,UAAU,MAAM;AACnB,yCAAqC;AAAA,EACvC,CAAC;AAED,OAAK,iFAAiF,OAAO,EAAE,QAAQ,MAAM;AAC3G,UAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AACjD,UAAM,QAAQ,KAAK,IAAI;AAEvB,UAAM,gBAAgB;AAAA,MACpB,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS,EAAE,KAAK,GAAG,SAAS,cAAc;AAAA,MAC1C,MAAM,CAAC,SAAS,MAAM;AAAA,IACxB;AACA,UAAM,gBAAgB;AAAA,MACpB,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS,EAAE,KAAK,GAAG,SAAS,QAAQ;AAAA,MACpC,MAAM,CAAC,OAAO;AAAA,IAChB;AAEA,UAAM,qBAAqB;AAAA,MACzB;AAAA,MACA;AAAA,MACA,gBAAgB;AAAA,MAChB,UAAU,CAAC,OAAO,qBAAqB,SAAS,OAAO,EAAE;AAAA,MACzD,QAAQ;AAAA,QACN,SAAS;AAAA,UACP,MAAM,6BAA6B,KAAK;AAAA,UACxC,aAAa;AAAA,UACb,WAAW;AAAA,UACX,cAAc;AAAA,UACd,eAAe;AAAA,UACf,UAAU;AAAA,UACV,YAAY;AAAA,UACZ,aAAa;AAAA,UACb,eAAe;AAAA,UACf,WAAW;AAAA,UACX,YAAY;AAAA,QACd;AAAA,MACF;AAAA,MACA,mBAAmB;AAAA,QACjB,SAAS;AAAA,UACP,MAAM,6BAA6B,KAAK;AAAA,UACxC,aAAa;AAAA,UACb,WAAW;AAAA,UACX,cAAc;AAAA,UACd,eAAe;AAAA,UACf,UAAU;AAAA,UACV,YAAY;AAAA,UACZ,aAAa;AAAA,UACb,eAAe;AAAA,UACf,eAAe;AAAA,UACf,WAAW;AAAA,UACX,YAAY;AAAA,QACd;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,SAAS,CAAC,QAAQ;AAAA,UAChB;AAAA,UACA,MAAM,6BAA6B,KAAK;AAAA,UACxC,aAAa;AAAA,UACb,cAAc;AAAA,UACd,eAAe;AAAA,UACf,UAAU;AAAA,UACV,eAAe;AAAA,UACf,WAAW;AAAA,QACb;AAAA,MACF;AAAA,MACA,mBAAmB;AAAA,QACjB,SAAS;AAAA,UACP,MAAM,6BAA6B,KAAK;AAAA,UACxC,aAAa;AAAA,UACb,WAAW;AAAA,UACX,cAAc;AAAA,UACd,eAAe;AAAA,UACf,UAAU;AAAA,UACV,YAAY;AAAA,UACZ,aAAa;AAAA,UACb,eAAe;AAAA,UACf,eAAe;AAAA,UACf,WAAW;AAAA,QACb;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH,CAAC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,119 +1,56 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
2
|
import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
extractOperation,
|
|
5
|
+
runCrudUndoRoundTrip,
|
|
6
|
+
skipIfUndoTestsDisabled
|
|
7
|
+
} from "@open-mercato/core/helpers/integration/undoHarness";
|
|
8
|
+
import {
|
|
9
|
+
SCHEDULER_JOBS_PATH,
|
|
10
|
+
SCHEDULER_TRIGGER_PATH,
|
|
11
|
+
createScheduleJob,
|
|
12
|
+
deleteScheduleJob
|
|
13
|
+
} from "./helpers/scheduler.js";
|
|
14
|
+
function jobCreatePayload(stamp) {
|
|
15
|
+
return {
|
|
16
|
+
name: `TC-UNDO-001 Scheduler ${stamp}`,
|
|
17
|
+
description: "TC-UNDO-001 undo/redo probe",
|
|
18
|
+
scopeType: "organization",
|
|
19
|
+
scheduleType: "interval",
|
|
20
|
+
scheduleValue: "15m",
|
|
21
|
+
timezone: "UTC",
|
|
22
|
+
targetType: "queue",
|
|
23
|
+
targetQueue: "scheduler-execution",
|
|
24
|
+
isEnabled: true,
|
|
25
|
+
sourceType: "user"
|
|
26
|
+
};
|
|
5
27
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
expect(enc, "x-om-operation header should carry an omop: payload").not.toBe("");
|
|
10
|
-
const payload = JSON.parse(decodeURIComponent(enc));
|
|
11
|
-
expect(typeof payload.undoToken, "undoToken present in operation payload").toBe("string");
|
|
12
|
-
return payload.undoToken;
|
|
13
|
-
}
|
|
14
|
-
async function getById(request, token, id) {
|
|
15
|
-
const res = await apiRequest(request, "GET", `/api/scheduler/jobs?id=${encodeURIComponent(id)}`, { token });
|
|
16
|
-
expect(res.status()).toBe(200);
|
|
17
|
-
const body = await res.json();
|
|
18
|
-
return (body.items ?? [])[0];
|
|
19
|
-
}
|
|
20
|
-
async function createJob(request, token, name) {
|
|
21
|
-
const res = await apiRequest(request, "POST", "/api/scheduler/jobs", {
|
|
22
|
-
token,
|
|
23
|
-
data: {
|
|
24
|
-
name,
|
|
25
|
-
description: "TC-UNDO-001 probe",
|
|
26
|
-
scopeType: "tenant",
|
|
27
|
-
scheduleType: "cron",
|
|
28
|
-
scheduleValue: "0 0 * * *",
|
|
29
|
-
timezone: "UTC",
|
|
30
|
-
targetType: "command",
|
|
31
|
-
targetCommand: "scheduler.test.echo",
|
|
32
|
-
isEnabled: true,
|
|
33
|
-
sourceType: "user"
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
expect(res.status(), "create returns 201").toBe(201);
|
|
37
|
-
const id = (await res.json()).id;
|
|
38
|
-
expect(id, "create returns an id").toBeTruthy();
|
|
39
|
-
return { id, res };
|
|
40
|
-
}
|
|
41
|
-
async function undo(request, token, undoToken) {
|
|
42
|
-
const res = await apiRequest(request, "POST", "/api/audit_logs/audit-logs/actions/undo", {
|
|
43
|
-
token,
|
|
44
|
-
data: { undoToken }
|
|
28
|
+
test.describe("TC-UNDO-001 scheduler.jobs undo/redo (#2504)", () => {
|
|
29
|
+
test.beforeAll(() => {
|
|
30
|
+
skipIfUndoTestsDisabled();
|
|
45
31
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
} catch {
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
test.describe("TC-UNDO-001: scheduler.jobs undo actually restores state (#2504)", () => {
|
|
59
|
-
test("UPDATE -> undo restores the prior name", async ({ request }) => {
|
|
60
|
-
let token = null;
|
|
61
|
-
let id = null;
|
|
62
|
-
try {
|
|
63
|
-
token = await getAuthToken(request, "admin");
|
|
64
|
-
const originalName = `TC-UNDO-001 Update ${Date.now()}`;
|
|
65
|
-
({ id } = await createJob(request, token, originalName));
|
|
66
|
-
await settleAuditClock();
|
|
67
|
-
const updateRes = await apiRequest(request, "PUT", "/api/scheduler/jobs", {
|
|
68
|
-
token,
|
|
69
|
-
data: { id, name: `${originalName} RENAMED` }
|
|
70
|
-
});
|
|
71
|
-
expect(updateRes.status(), "update returns 200").toBe(200);
|
|
72
|
-
expect((await getById(request, token, id))?.name).toBe(`${originalName} RENAMED`);
|
|
73
|
-
await undo(request, token, readUndoToken(updateRes));
|
|
74
|
-
const restored = await getById(request, token, id);
|
|
75
|
-
expect(restored, "job still exists after update-undo").toBeTruthy();
|
|
76
|
-
expect(restored.name, "name restored to original by undo").toBe(originalName);
|
|
77
|
-
} finally {
|
|
78
|
-
await cleanup(request, token, id);
|
|
79
|
-
}
|
|
32
|
+
test("jobs CRUD commands restore scalar state on undo/redo (I1/I2/I3/I5/I6)", async ({ request }) => {
|
|
33
|
+
const token = await getAuthToken(request, "admin");
|
|
34
|
+
await runCrudUndoRoundTrip(request, token, {
|
|
35
|
+
label: "scheduler.jobs",
|
|
36
|
+
collectionPath: SCHEDULER_JOBS_PATH,
|
|
37
|
+
field: "name",
|
|
38
|
+
createPayload: jobCreatePayload,
|
|
39
|
+
updatePayload: (id, stamp) => ({ id, name: `TC-UNDO-001 Scheduler Renamed ${stamp}` })
|
|
40
|
+
});
|
|
80
41
|
});
|
|
81
|
-
test("
|
|
82
|
-
|
|
83
|
-
let
|
|
42
|
+
test("\xA74 trigger exposes no undo token", async ({ request }) => {
|
|
43
|
+
const token = await getAuthToken(request, "admin");
|
|
44
|
+
let jobId = null;
|
|
84
45
|
try {
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
({ id } = await createJob(request, token, name));
|
|
88
|
-
await settleAuditClock();
|
|
89
|
-
const deleteRes = await apiRequest(request, "DELETE", "/api/scheduler/jobs", {
|
|
46
|
+
jobId = await createScheduleJob(request, token);
|
|
47
|
+
const triggerRes = await apiRequest(request, "POST", SCHEDULER_TRIGGER_PATH, {
|
|
90
48
|
token,
|
|
91
|
-
data: { id }
|
|
49
|
+
data: { id: jobId }
|
|
92
50
|
});
|
|
93
|
-
expect(
|
|
94
|
-
expect(await getById(request, token, id), "job is gone after delete").toBeFalsy();
|
|
95
|
-
await undo(request, token, readUndoToken(deleteRes));
|
|
96
|
-
const restored = await getById(request, token, id);
|
|
97
|
-
expect(restored, "job re-materialized by delete-undo").toBeTruthy();
|
|
98
|
-
expect(restored.name).toBe(name);
|
|
99
|
-
} finally {
|
|
100
|
-
await cleanup(request, token, id);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
test("CREATE -> undo removes the created job", async ({ request }) => {
|
|
104
|
-
let token = null;
|
|
105
|
-
let id = null;
|
|
106
|
-
try {
|
|
107
|
-
token = await getAuthToken(request, "admin");
|
|
108
|
-
const name = `TC-UNDO-001 Create ${Date.now()}`;
|
|
109
|
-
const created = await createJob(request, token, name);
|
|
110
|
-
id = created.id;
|
|
111
|
-
expect(await getById(request, token, id), "job exists after create").toBeTruthy();
|
|
112
|
-
await undo(request, token, readUndoToken(created.res));
|
|
113
|
-
expect(await getById(request, token, id), "job removed by create-undo").toBeFalsy();
|
|
114
|
-
id = null;
|
|
51
|
+
expect(extractOperation(triggerRes), "trigger response carries no undo token (\xA74)").toBeNull();
|
|
115
52
|
} finally {
|
|
116
|
-
await
|
|
53
|
+
await deleteScheduleJob(request, token, jobId);
|
|
117
54
|
}
|
|
118
55
|
});
|
|
119
56
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.ts"],
|
|
4
|
-
"sourcesContent": ["import { expect, test
|
|
5
|
-
"mappings": "AAAA,SAAS,QAAQ,
|
|
4
|
+
"sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'\nimport {\n extractOperation,\n runCrudUndoRoundTrip,\n skipIfUndoTestsDisabled,\n} from '@open-mercato/core/helpers/integration/undoHarness'\nimport {\n SCHEDULER_JOBS_PATH,\n SCHEDULER_TRIGGER_PATH,\n createScheduleJob,\n deleteScheduleJob,\n} from './helpers/scheduler'\n\n/**\n * TC-UNDO-001 (#2582, refs #2468) \u2014 scheduler.jobs undo/redo correctness.\n *\n * Regression lock for #2504: scheduler.jobs undo was a silent no-op because every\n * undo handler read `logEntry.payload` (always undefined \u2014 the command bus persists\n * the undo snapshot under `commandPayload`), so undo returned `{ ok: true }` while\n * changing nothing. Fixed in PR #2514 via `extractUndoPayload`. This spec asserts the\n * corrected behavior (full restoration), driving the real command bus through the\n * public API + the audit-log undo/redo endpoints.\n *\n * scheduler.jobs is a standard `makeCrudRoute` entity (POST/PUT/DELETE on the\n * collection, `?id=` read-back, camelCase serializer, no custom fields), so the shared\n * `runCrudUndoRoundTrip` harness covers the full round-trip in one pass:\n * - I1 update \u2192 undo restores every scalar (and bumps `updatedAt`)\n * - I2 delete \u2192 undo re-materializes the soft-deleted job\n * - I3 create \u2192 undo soft-deletes (never hard-deletes)\n * - I5 a consumed undo token is rejected on a second undo\n * - I6 redo re-applies the command's after-snapshot\n *\n * I4 (custom fields) is N/A \u2014 `scheduled_job` declares no `ce.ts`, so it has no `cf_*`.\n */\nfunction jobCreatePayload(stamp: string): Record<string, unknown> {\n return {\n name: `TC-UNDO-001 Scheduler ${stamp}`,\n description: 'TC-UNDO-001 undo/redo probe',\n scopeType: 'organization',\n scheduleType: 'interval',\n scheduleValue: '15m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'scheduler-execution',\n isEnabled: true,\n sourceType: 'user',\n }\n}\n\ntest.describe('TC-UNDO-001 scheduler.jobs undo/redo (#2504)', () => {\n test.beforeAll(() => {\n skipIfUndoTestsDisabled()\n })\n\n test('jobs CRUD commands restore scalar state on undo/redo (I1/I2/I3/I5/I6)', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n\n await runCrudUndoRoundTrip(request, token, {\n label: 'scheduler.jobs',\n collectionPath: SCHEDULER_JOBS_PATH,\n field: 'name',\n createPayload: jobCreatePayload,\n updatePayload: (id, stamp) => ({ id, name: `TC-UNDO-001 Scheduler Renamed ${stamp}` }),\n })\n })\n\n // \u00A74 \u2014 manual trigger is a fire-and-forget enqueue that never goes through the\n // command bus, so it exposes no undo affordance. The assertion holds regardless of\n // QUEUE_STRATEGY: async returns 200 (enqueued), local returns 400 (rejected) \u2014 neither\n // issues an `x-om-operation` undo token.\n test('\u00A74 trigger exposes no undo token', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n let jobId: string | null = null\n try {\n jobId = await createScheduleJob(request, token)\n const triggerRes = await apiRequest(request, 'POST', SCHEDULER_TRIGGER_PATH, {\n token,\n data: { id: jobId },\n })\n expect(extractOperation(triggerRes), 'trigger response carries no undo token (\u00A74)').toBeNull()\n } finally {\n await deleteScheduleJob(request, token, jobId)\n }\n })\n})\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,YAAY,oBAAoB;AACzC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAuBP,SAAS,iBAAiB,OAAwC;AAChE,SAAO;AAAA,IACL,MAAM,yBAAyB,KAAK;AAAA,IACpC,aAAa;AAAA,IACb,WAAW;AAAA,IACX,cAAc;AAAA,IACd,eAAe;AAAA,IACf,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,WAAW;AAAA,IACX,YAAY;AAAA,EACd;AACF;AAEA,KAAK,SAAS,gDAAgD,MAAM;AAClE,OAAK,UAAU,MAAM;AACnB,4BAAwB;AAAA,EAC1B,CAAC;AAED,OAAK,yEAAyE,OAAO,EAAE,QAAQ,MAAM;AACnG,UAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AAEjD,UAAM,qBAAqB,SAAS,OAAO;AAAA,MACzC,OAAO;AAAA,MACP,gBAAgB;AAAA,MAChB,OAAO;AAAA,MACP,eAAe;AAAA,MACf,eAAe,CAAC,IAAI,WAAW,EAAE,IAAI,MAAM,iCAAiC,KAAK,GAAG;AAAA,IACtF,CAAC;AAAA,EACH,CAAC;AAMD,OAAK,uCAAoC,OAAO,EAAE,QAAQ,MAAM;AAC9D,UAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AACjD,QAAI,QAAuB;AAC3B,QAAI;AACF,cAAQ,MAAM,kBAAkB,SAAS,KAAK;AAC9C,YAAM,aAAa,MAAM,WAAW,SAAS,QAAQ,wBAAwB;AAAA,QAC3E;AAAA,QACA,MAAM,EAAE,IAAI,MAAM;AAAA,MACpB,CAAC;AACD,aAAO,iBAAiB,UAAU,GAAG,gDAA6C,EAAE,SAAS;AAAA,IAC/F,UAAE;AACA,YAAM,kBAAkB,SAAS,OAAO,KAAK;AAAA,IAC/C;AAAA,EACF,CAAC;AACH,CAAC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,20 +5,13 @@ const SCHEDULER_JOBS_PATH = "/api/scheduler/jobs";
|
|
|
5
5
|
const SCHEDULER_TARGETS_PATH = "/api/scheduler/targets";
|
|
6
6
|
const SCHEDULER_TRIGGER_PATH = "/api/scheduler/trigger";
|
|
7
7
|
const SCHEDULER_EXECUTION_QUEUE = "scheduler-execution";
|
|
8
|
-
const SCHEDULER_TEST_QUEUE = "scheduler-test";
|
|
9
|
-
const SCHEDULER_FORBIDDEN_INTERNAL_QUEUES = [
|
|
10
|
-
SCHEDULER_EXECUTION_QUEUE,
|
|
11
|
-
"stripe-webhook",
|
|
12
|
-
"payment-gateways-webhook"
|
|
13
|
-
];
|
|
14
|
-
const SCHEDULER_TEST_COMMAND = "scheduler.test.echo";
|
|
15
8
|
let sequence = 0;
|
|
16
9
|
function uniqueScheduleName(prefix) {
|
|
17
10
|
sequence += 1;
|
|
18
11
|
return `${prefix} ${Date.now()}-${sequence}`;
|
|
19
12
|
}
|
|
20
13
|
async function createScheduleJob(request, token, overrides = {}) {
|
|
21
|
-
const targetType = overrides.targetType ?? "
|
|
14
|
+
const targetType = overrides.targetType ?? "queue";
|
|
22
15
|
const data = {
|
|
23
16
|
name: overrides.name ?? uniqueScheduleName("Integration Scheduler"),
|
|
24
17
|
description: overrides.description ?? "Created by scheduler integration tests",
|
|
@@ -32,9 +25,9 @@ async function createScheduleJob(request, token, overrides = {}) {
|
|
|
32
25
|
sourceType: overrides.sourceType ?? "user"
|
|
33
26
|
};
|
|
34
27
|
if (targetType === "command") {
|
|
35
|
-
data.targetCommand = overrides.targetCommand
|
|
28
|
+
data.targetCommand = overrides.targetCommand;
|
|
36
29
|
} else {
|
|
37
|
-
data.targetQueue = overrides.targetQueue ??
|
|
30
|
+
data.targetQueue = overrides.targetQueue ?? SCHEDULER_EXECUTION_QUEUE;
|
|
38
31
|
}
|
|
39
32
|
const response = await apiRequest(request, "POST", SCHEDULER_JOBS_PATH, { token, data });
|
|
40
33
|
const body = await readJsonSafe(response);
|
|
@@ -55,11 +48,8 @@ async function deleteScheduleJob(request, token, id) {
|
|
|
55
48
|
}
|
|
56
49
|
export {
|
|
57
50
|
SCHEDULER_EXECUTION_QUEUE,
|
|
58
|
-
SCHEDULER_FORBIDDEN_INTERNAL_QUEUES,
|
|
59
51
|
SCHEDULER_JOBS_PATH,
|
|
60
52
|
SCHEDULER_TARGETS_PATH,
|
|
61
|
-
SCHEDULER_TEST_COMMAND,
|
|
62
|
-
SCHEDULER_TEST_QUEUE,
|
|
63
53
|
SCHEDULER_TRIGGER_PATH,
|
|
64
54
|
createScheduleJob,
|
|
65
55
|
deleteScheduleJob,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/scheduler/__integration__/helpers/scheduler.ts"],
|
|
4
|
-
"sourcesContent": ["import { expect, type APIRequestContext } from '@playwright/test'\nimport { apiRequest } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\n\nexport const SCHEDULER_JOBS_PATH = '/api/scheduler/jobs'\nexport const SCHEDULER_TARGETS_PATH = '/api/scheduler/targets'\nexport const SCHEDULER_TRIGGER_PATH = '/api/scheduler/trigger'\n\n/** Queue registered by the scheduler module's execute-schedule worker
|
|
5
|
-
"mappings": "AAAA,SAAS,cAAsC;AAC/C,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAEtB,MAAM,sBAAsB;AAC5B,MAAM,yBAAyB;AAC/B,MAAM,yBAAyB;AAG/B,MAAM,4BAA4B;
|
|
4
|
+
"sourcesContent": ["import { expect, type APIRequestContext } from '@playwright/test'\nimport { apiRequest } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\n\nexport const SCHEDULER_JOBS_PATH = '/api/scheduler/jobs'\nexport const SCHEDULER_TARGETS_PATH = '/api/scheduler/targets'\nexport const SCHEDULER_TRIGGER_PATH = '/api/scheduler/trigger'\n\n/** Queue registered by the scheduler module's execute-schedule worker. */\nexport const SCHEDULER_EXECUTION_QUEUE = 'scheduler-execution'\n\nexport type SchedulerJob = {\n id: string\n name: string\n description: string | null\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId: string | null\n tenantId: string | null\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n targetType: 'queue' | 'command'\n targetQueue: string | null\n targetCommand: string | null\n isEnabled: boolean\n createdAt: string | null\n updatedAt: string | null\n}\n\nexport type CreateScheduleOverrides = {\n name?: string\n description?: string | null\n scopeType?: 'system' | 'organization' | 'tenant'\n scheduleType?: 'cron' | 'interval'\n scheduleValue?: string\n timezone?: string\n targetType?: 'queue' | 'command'\n targetQueue?: string\n targetCommand?: string\n targetPayload?: Record<string, unknown>\n isEnabled?: boolean\n sourceType?: 'user' | 'module'\n}\n\nlet sequence = 0\n\n/** Stable-ish unique label so concurrent fixtures never collide on name. */\nexport function uniqueScheduleName(prefix: string): string {\n sequence += 1\n return `${prefix} ${Date.now()}-${sequence}`\n}\n\n/**\n * Create an organization-scoped queue-target schedule via the API and return its id.\n * Org/tenant are derived server-side from the caller's auth context, so the caller\n * only needs an admin (or otherwise scheduler.jobs.manage) token.\n */\nexport async function createScheduleJob(\n request: APIRequestContext,\n token: string,\n overrides: CreateScheduleOverrides = {},\n): Promise<string> {\n const targetType = overrides.targetType ?? 'queue'\n const data: Record<string, unknown> = {\n name: overrides.name ?? uniqueScheduleName('Integration Scheduler'),\n description: overrides.description ?? 'Created by scheduler integration tests',\n scopeType: overrides.scopeType ?? 'organization',\n scheduleType: overrides.scheduleType ?? 'interval',\n scheduleValue: overrides.scheduleValue ?? '15m',\n timezone: overrides.timezone ?? 'UTC',\n targetType,\n targetPayload: overrides.targetPayload ?? { source: 'integration-test' },\n isEnabled: overrides.isEnabled ?? true,\n sourceType: overrides.sourceType ?? 'user',\n }\n if (targetType === 'command') {\n data.targetCommand = overrides.targetCommand\n } else {\n data.targetQueue = overrides.targetQueue ?? SCHEDULER_EXECUTION_QUEUE\n }\n\n const response = await apiRequest(request, 'POST', SCHEDULER_JOBS_PATH, { token, data })\n const body = await readJsonSafe<{ id?: string }>(response)\n expect(response.status(), 'POST /api/scheduler/jobs should return 201').toBe(201)\n const id = body?.id\n expect(typeof id === 'string' && /^[0-9a-f-]{36}$/i.test(id), 'create response should include a UUID id').toBe(true)\n return String(id)\n}\n\n/** Fetch a single schedule by id from the list endpoint; null when absent (e.g. soft-deleted). */\nexport async function getScheduleJobById(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<SchedulerJob | null> {\n const response = await apiRequest(request, 'GET', `${SCHEDULER_JOBS_PATH}?id=${encodeURIComponent(id)}`, { token })\n expect(response.status(), 'GET /api/scheduler/jobs?id should return 200').toBe(200)\n const body = await readJsonSafe<{ items?: SchedulerJob[] }>(response)\n return (body?.items ?? []).find((item) => item.id === id) ?? null\n}\n\n/** Best-effort cleanup; swallows errors so teardown never masks the real assertion. */\nexport async function deleteScheduleJob(\n request: APIRequestContext,\n token: string,\n id: string | null,\n): Promise<void> {\n if (!id) return\n await apiRequest(request, 'DELETE', SCHEDULER_JOBS_PATH, { token, data: { id } }).catch(() => null)\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,cAAsC;AAC/C,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAEtB,MAAM,sBAAsB;AAC5B,MAAM,yBAAyB;AAC/B,MAAM,yBAAyB;AAG/B,MAAM,4BAA4B;AAmCzC,IAAI,WAAW;AAGR,SAAS,mBAAmB,QAAwB;AACzD,cAAY;AACZ,SAAO,GAAG,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,QAAQ;AAC5C;AAOA,eAAsB,kBACpB,SACA,OACA,YAAqC,CAAC,GACrB;AACjB,QAAM,aAAa,UAAU,cAAc;AAC3C,QAAM,OAAgC;AAAA,IACpC,MAAM,UAAU,QAAQ,mBAAmB,uBAAuB;AAAA,IAClE,aAAa,UAAU,eAAe;AAAA,IACtC,WAAW,UAAU,aAAa;AAAA,IAClC,cAAc,UAAU,gBAAgB;AAAA,IACxC,eAAe,UAAU,iBAAiB;AAAA,IAC1C,UAAU,UAAU,YAAY;AAAA,IAChC;AAAA,IACA,eAAe,UAAU,iBAAiB,EAAE,QAAQ,mBAAmB;AAAA,IACvE,WAAW,UAAU,aAAa;AAAA,IAClC,YAAY,UAAU,cAAc;AAAA,EACtC;AACA,MAAI,eAAe,WAAW;AAC5B,SAAK,gBAAgB,UAAU;AAAA,EACjC,OAAO;AACL,SAAK,cAAc,UAAU,eAAe;AAAA,EAC9C;AAEA,QAAM,WAAW,MAAM,WAAW,SAAS,QAAQ,qBAAqB,EAAE,OAAO,KAAK,CAAC;AACvF,QAAM,OAAO,MAAM,aAA8B,QAAQ;AACzD,SAAO,SAAS,OAAO,GAAG,4CAA4C,EAAE,KAAK,GAAG;AAChF,QAAM,KAAK,MAAM;AACjB,SAAO,OAAO,OAAO,YAAY,mBAAmB,KAAK,EAAE,GAAG,0CAA0C,EAAE,KAAK,IAAI;AACnH,SAAO,OAAO,EAAE;AAClB;AAGA,eAAsB,mBACpB,SACA,OACA,IAC8B;AAC9B,QAAM,WAAW,MAAM,WAAW,SAAS,OAAO,GAAG,mBAAmB,OAAO,mBAAmB,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC;AAClH,SAAO,SAAS,OAAO,GAAG,8CAA8C,EAAE,KAAK,GAAG;AAClF,QAAM,OAAO,MAAM,aAAyC,QAAQ;AACpE,UAAQ,MAAM,SAAS,CAAC,GAAG,KAAK,CAAC,SAAS,KAAK,OAAO,EAAE,KAAK;AAC/D;AAGA,eAAsB,kBACpB,SACA,OACA,IACe;AACf,MAAI,CAAC,GAAI;AACT,QAAM,WAAW,SAAS,UAAU,qBAAqB,EAAE,OAAO,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,MAAM,IAAI;AACpG;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,7 +3,6 @@ import { z } from "zod";
|
|
|
3
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
5
|
import { ScheduledJob } from "../../../../data/entities.js";
|
|
6
|
-
import { resolveScheduleAccess } from "../../../../lib/scheduleAccess.js";
|
|
7
6
|
import { getRedisUrlOrThrow, parseRedisUrl } from "@open-mercato/shared/lib/redis/connection";
|
|
8
7
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
9
8
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
@@ -22,15 +21,22 @@ async function GET(req, { params }) {
|
|
|
22
21
|
const em = container.resolve("em");
|
|
23
22
|
const scheduleId = params.id;
|
|
24
23
|
try {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const findFilter = {
|
|
25
|
+
id: scheduleId,
|
|
26
|
+
deletedAt: null
|
|
27
|
+
};
|
|
28
|
+
if (auth.tenantId) {
|
|
29
|
+
findFilter.tenantId = auth.tenantId;
|
|
28
30
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
if (auth.orgId) {
|
|
32
|
+
findFilter.organizationId = auth.orgId;
|
|
33
|
+
}
|
|
34
|
+
const schedule = await em.findOne(ScheduledJob, findFilter);
|
|
35
|
+
if (!schedule) {
|
|
31
36
|
return NextResponse.json({ error: translate("scheduler.error.not_found", "Schedule not found") }, { status: 404 });
|
|
32
37
|
}
|
|
33
|
-
|
|
38
|
+
const isSuperAdmin = auth.isSuperAdmin === true;
|
|
39
|
+
if (!schedule.tenantId && !schedule.organizationId && !isSuperAdmin) {
|
|
34
40
|
return NextResponse.json({ error: translate("scheduler.error.access_denied", "Access denied") }, { status: 403 });
|
|
35
41
|
}
|
|
36
42
|
const queueStrategy = process.env.QUEUE_STRATEGY || "local";
|