@open-mercato/scheduler 0.6.5-develop.5187.1.82e5532561 → 0.6.5-develop.5200.1.871eca3402
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.
|
@@ -1,123 +1,56 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
2
|
import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
};
|
|
6
27
|
}
|
|
7
|
-
|
|
8
|
-
const header = res.headers()["x-om-operation"] ?? "";
|
|
9
|
-
const enc = header.startsWith("omop:") ? header.slice(5) : "";
|
|
10
|
-
expect(enc, "x-om-operation header should carry an omop: payload").not.toBe("");
|
|
11
|
-
const payload = JSON.parse(decodeURIComponent(enc));
|
|
12
|
-
expect(typeof payload.undoToken, "undoToken present in operation payload").toBe("string");
|
|
13
|
-
return payload.undoToken;
|
|
14
|
-
}
|
|
15
|
-
async function getById(request, token, id) {
|
|
16
|
-
const res = await apiRequest(request, "GET", `/api/scheduler/jobs?id=${encodeURIComponent(id)}`, { token });
|
|
17
|
-
expect(res.status()).toBe(200);
|
|
18
|
-
const body = await res.json();
|
|
19
|
-
return (body.items ?? [])[0];
|
|
20
|
-
}
|
|
21
|
-
async function createJob(request, token, name) {
|
|
22
|
-
const res = await apiRequest(request, "POST", "/api/scheduler/jobs", {
|
|
23
|
-
token,
|
|
24
|
-
data: {
|
|
25
|
-
name,
|
|
26
|
-
description: "TC-UNDO-001 probe",
|
|
27
|
-
scopeType: "tenant",
|
|
28
|
-
scheduleType: "cron",
|
|
29
|
-
scheduleValue: "0 0 * * *",
|
|
30
|
-
timezone: "UTC",
|
|
31
|
-
targetType: "queue",
|
|
32
|
-
targetQueue: "default",
|
|
33
|
-
isEnabled: true,
|
|
34
|
-
sourceType: "user"
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
expect(res.status(), "create returns 201").toBe(201);
|
|
38
|
-
const id = (await res.json()).id;
|
|
39
|
-
expect(id, "create returns an id").toBeTruthy();
|
|
40
|
-
return { id, res };
|
|
41
|
-
}
|
|
42
|
-
async function undo(request, token, undoToken) {
|
|
43
|
-
const res = await apiRequest(request, "POST", "/api/audit_logs/audit-logs/actions/undo", {
|
|
44
|
-
token,
|
|
45
|
-
data: { undoToken }
|
|
46
|
-
});
|
|
47
|
-
const raw = await res.text();
|
|
48
|
-
expect(res.status(), `undo returns 200 (body: ${raw})`).toBe(200);
|
|
49
|
-
const body = JSON.parse(raw);
|
|
50
|
-
expect(body.ok, "undo body is { ok: true }").toBe(true);
|
|
51
|
-
}
|
|
52
|
-
async function cleanup(request, token, id) {
|
|
53
|
-
if (!token || !id) return;
|
|
54
|
-
try {
|
|
55
|
-
await apiRequest(request, "DELETE", "/api/scheduler/jobs", { token, data: { id } });
|
|
56
|
-
} catch {
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
test.describe("TC-UNDO-001: scheduler.jobs undo actually restores state (#2504)", () => {
|
|
28
|
+
test.describe("TC-UNDO-001 scheduler.jobs undo/redo (#2504)", () => {
|
|
60
29
|
test.beforeAll(() => {
|
|
61
30
|
skipIfUndoTestsDisabled();
|
|
62
31
|
});
|
|
63
|
-
test("
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
token,
|
|
73
|
-
data: { id, name: `${originalName} RENAMED` }
|
|
74
|
-
});
|
|
75
|
-
expect(updateRes.status(), "update returns 200").toBe(200);
|
|
76
|
-
expect((await getById(request, token, id))?.name).toBe(`${originalName} RENAMED`);
|
|
77
|
-
await undo(request, token, readUndoToken(updateRes));
|
|
78
|
-
const restored = await getById(request, token, id);
|
|
79
|
-
expect(restored, "job still exists after update-undo").toBeTruthy();
|
|
80
|
-
expect(restored.name, "name restored to original by undo").toBe(originalName);
|
|
81
|
-
} finally {
|
|
82
|
-
await cleanup(request, token, id);
|
|
83
|
-
}
|
|
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
|
+
});
|
|
84
41
|
});
|
|
85
|
-
test("
|
|
86
|
-
|
|
87
|
-
let
|
|
42
|
+
test("\xA74 trigger exposes no undo token", async ({ request }) => {
|
|
43
|
+
const token = await getAuthToken(request, "admin");
|
|
44
|
+
let jobId = null;
|
|
88
45
|
try {
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
({ id } = await createJob(request, token, name));
|
|
92
|
-
await settleAuditClock();
|
|
93
|
-
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, {
|
|
94
48
|
token,
|
|
95
|
-
data: { id }
|
|
49
|
+
data: { id: jobId }
|
|
96
50
|
});
|
|
97
|
-
expect(
|
|
98
|
-
expect(await getById(request, token, id), "job is gone after delete").toBeFalsy();
|
|
99
|
-
await undo(request, token, readUndoToken(deleteRes));
|
|
100
|
-
const restored = await getById(request, token, id);
|
|
101
|
-
expect(restored, "job re-materialized by delete-undo").toBeTruthy();
|
|
102
|
-
expect(restored.name).toBe(name);
|
|
103
|
-
} finally {
|
|
104
|
-
await cleanup(request, token, id);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
test("CREATE -> undo removes the created job", async ({ request }) => {
|
|
108
|
-
let token = null;
|
|
109
|
-
let id = null;
|
|
110
|
-
try {
|
|
111
|
-
token = await getAuthToken(request, "admin");
|
|
112
|
-
const name = `TC-UNDO-001 Create ${Date.now()}`;
|
|
113
|
-
const created = await createJob(request, token, name);
|
|
114
|
-
id = created.id;
|
|
115
|
-
expect(await getById(request, token, id), "job exists after create").toBeTruthy();
|
|
116
|
-
await undo(request, token, readUndoToken(created.res));
|
|
117
|
-
expect(await getById(request, token, id), "job removed by create-undo").toBeFalsy();
|
|
118
|
-
id = null;
|
|
51
|
+
expect(extractOperation(triggerRes), "trigger response carries no undo token (\xA74)").toBeNull();
|
|
119
52
|
} finally {
|
|
120
|
-
await
|
|
53
|
+
await deleteScheduleJob(request, token, jobId);
|
|
121
54
|
}
|
|
122
55
|
});
|
|
123
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/scheduler",
|
|
3
|
-
"version": "0.6.5-develop.
|
|
3
|
+
"version": "0.6.5-develop.5200.1.871eca3402",
|
|
4
4
|
"description": "Database-managed scheduled jobs with admin UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -58,13 +58,13 @@
|
|
|
58
58
|
"./*/*/*/*/*/*.json": "./src/*/*/*/*/*/*.json"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@open-mercato/events": "0.6.5-develop.
|
|
62
|
-
"@open-mercato/queue": "0.6.5-develop.
|
|
61
|
+
"@open-mercato/events": "0.6.5-develop.5200.1.871eca3402",
|
|
62
|
+
"@open-mercato/queue": "0.6.5-develop.5200.1.871eca3402",
|
|
63
63
|
"cron-parser": "^5.5.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
66
|
"@mikro-orm/core": "^7.0.14",
|
|
67
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
67
|
+
"@open-mercato/shared": "0.6.5-develop.5200.1.871eca3402",
|
|
68
68
|
"bullmq": "^5.0.0",
|
|
69
69
|
"ioredis": "^5.0.0",
|
|
70
70
|
"zod": ">=3.23.0"
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
81
|
+
"@open-mercato/shared": "0.6.5-develop.5200.1.871eca3402",
|
|
82
82
|
"@types/jest": "^30.0.0",
|
|
83
83
|
"@types/node": "^25.9.2",
|
|
84
84
|
"jest": "^30.4.2",
|
|
@@ -1,179 +1,86 @@
|
|
|
1
|
-
import { expect, test
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
2
|
import { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'
|
|
3
|
-
import {
|
|
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'
|
|
4
14
|
|
|
5
15
|
/**
|
|
6
|
-
* TC-UNDO-001
|
|
16
|
+
* TC-UNDO-001 (#2582, refs #2468) — scheduler.jobs undo/redo correctness.
|
|
7
17
|
*
|
|
8
|
-
*
|
|
9
|
-
* (always undefined — the command bus persists
|
|
10
|
-
* `commandPayload`), so undo returned `{ ok: true }` while
|
|
11
|
-
* nothing.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - DELETE -> undo => soft-deleted job is re-materialized
|
|
18
|
+
* Regression lock for #2504: scheduler.jobs undo was a silent no-op because every
|
|
19
|
+
* undo handler read `logEntry.payload` (always undefined — the command bus persists
|
|
20
|
+
* the undo snapshot under `commandPayload`), so undo returned `{ ok: true }` while
|
|
21
|
+
* changing nothing. Fixed in PR #2514 via `extractUndoPayload`. This spec asserts the
|
|
22
|
+
* corrected behavior (full restoration), driving the real command bus through the
|
|
23
|
+
* public API + the audit-log undo/redo endpoints.
|
|
15
24
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* -
|
|
20
|
-
* -
|
|
21
|
-
* -
|
|
25
|
+
* scheduler.jobs is a standard `makeCrudRoute` entity (POST/PUT/DELETE on the
|
|
26
|
+
* collection, `?id=` read-back, camelCase serializer, no custom fields), so the shared
|
|
27
|
+
* `runCrudUndoRoundTrip` harness covers the full round-trip in one pass:
|
|
28
|
+
* - I1 update → undo restores every scalar (and bumps `updatedAt`)
|
|
29
|
+
* - I2 delete → undo re-materializes the soft-deleted job
|
|
30
|
+
* - I3 create → undo soft-deletes (never hard-deletes)
|
|
31
|
+
* - I5 a consumed undo token is rejected on a second undo
|
|
32
|
+
* - I6 redo re-applies the command's after-snapshot
|
|
33
|
+
*
|
|
34
|
+
* I4 (custom fields) is N/A — `scheduled_job` declares no `ce.ts`, so it has no `cf_*`.
|
|
22
35
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// the wrong log. A short settle guarantees the operation under undo is strictly
|
|
36
|
-
// the most recent, keeping the round-trip deterministic.
|
|
37
|
-
async function settleAuditClock(): Promise<void> {
|
|
38
|
-
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function readUndoToken(res: APIResponse): string {
|
|
42
|
-
const header = res.headers()['x-om-operation'] ?? ''
|
|
43
|
-
const enc = header.startsWith('omop:') ? header.slice(5) : ''
|
|
44
|
-
expect(enc, 'x-om-operation header should carry an omop: payload').not.toBe('')
|
|
45
|
-
const payload = JSON.parse(decodeURIComponent(enc)) as { undoToken?: string }
|
|
46
|
-
expect(typeof payload.undoToken, 'undoToken present in operation payload').toBe('string')
|
|
47
|
-
return payload.undoToken as string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function getById(
|
|
51
|
-
request: APIRequestContext,
|
|
52
|
-
token: string,
|
|
53
|
-
id: string,
|
|
54
|
-
): Promise<ScheduleRow | undefined> {
|
|
55
|
-
const res = await apiRequest(request, 'GET', `/api/scheduler/jobs?id=${encodeURIComponent(id)}`, { token })
|
|
56
|
-
expect(res.status()).toBe(200)
|
|
57
|
-
const body = (await res.json()) as { items?: ScheduleRow[] }
|
|
58
|
-
return (body.items ?? [])[0]
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function createJob(request: APIRequestContext, token: string, name: string): Promise<{ id: string; res: APIResponse }> {
|
|
62
|
-
const res = await apiRequest(request, 'POST', '/api/scheduler/jobs', {
|
|
63
|
-
token,
|
|
64
|
-
data: {
|
|
65
|
-
name,
|
|
66
|
-
description: 'TC-UNDO-001 probe',
|
|
67
|
-
scopeType: 'tenant',
|
|
68
|
-
scheduleType: 'cron',
|
|
69
|
-
scheduleValue: '0 0 * * *',
|
|
70
|
-
timezone: 'UTC',
|
|
71
|
-
targetType: 'queue',
|
|
72
|
-
targetQueue: 'default',
|
|
73
|
-
isEnabled: true,
|
|
74
|
-
sourceType: 'user',
|
|
75
|
-
},
|
|
76
|
-
})
|
|
77
|
-
expect(res.status(), 'create returns 201').toBe(201)
|
|
78
|
-
const id = ((await res.json()) as { id: string }).id
|
|
79
|
-
expect(id, 'create returns an id').toBeTruthy()
|
|
80
|
-
return { id, res }
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function undo(request: APIRequestContext, token: string, undoToken: string): Promise<void> {
|
|
84
|
-
const res = await apiRequest(request, 'POST', '/api/audit_logs/audit-logs/actions/undo', {
|
|
85
|
-
token,
|
|
86
|
-
data: { undoToken },
|
|
87
|
-
})
|
|
88
|
-
const raw = await res.text()
|
|
89
|
-
expect(res.status(), `undo returns 200 (body: ${raw})`).toBe(200)
|
|
90
|
-
const body = JSON.parse(raw) as { ok?: boolean }
|
|
91
|
-
expect(body.ok, 'undo body is { ok: true }').toBe(true)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function cleanup(request: APIRequestContext, token: string | null, id: string | null): Promise<void> {
|
|
95
|
-
if (!token || !id) return
|
|
96
|
-
try {
|
|
97
|
-
await apiRequest(request, 'DELETE', '/api/scheduler/jobs', { token, data: { id } })
|
|
98
|
-
} catch {
|
|
99
|
-
/* ignore */
|
|
36
|
+
function jobCreatePayload(stamp: string): Record<string, unknown> {
|
|
37
|
+
return {
|
|
38
|
+
name: `TC-UNDO-001 Scheduler ${stamp}`,
|
|
39
|
+
description: 'TC-UNDO-001 undo/redo probe',
|
|
40
|
+
scopeType: 'organization',
|
|
41
|
+
scheduleType: 'interval',
|
|
42
|
+
scheduleValue: '15m',
|
|
43
|
+
timezone: 'UTC',
|
|
44
|
+
targetType: 'queue',
|
|
45
|
+
targetQueue: 'scheduler-execution',
|
|
46
|
+
isEnabled: true,
|
|
47
|
+
sourceType: 'user',
|
|
100
48
|
}
|
|
101
49
|
}
|
|
102
50
|
|
|
103
|
-
test.describe('TC-UNDO-001
|
|
51
|
+
test.describe('TC-UNDO-001 scheduler.jobs undo/redo (#2504)', () => {
|
|
104
52
|
test.beforeAll(() => {
|
|
105
53
|
skipIfUndoTestsDisabled()
|
|
106
54
|
})
|
|
107
55
|
|
|
108
|
-
test('
|
|
109
|
-
|
|
110
|
-
let id: string | null = null
|
|
111
|
-
try {
|
|
112
|
-
token = await getAuthToken(request, 'admin')
|
|
113
|
-
const originalName = `TC-UNDO-001 Update ${Date.now()}`
|
|
114
|
-
;({ id } = await createJob(request, token, originalName))
|
|
115
|
-
await settleAuditClock()
|
|
116
|
-
|
|
117
|
-
const updateRes = await apiRequest(request, 'PUT', '/api/scheduler/jobs', {
|
|
118
|
-
token,
|
|
119
|
-
data: { id, name: `${originalName} RENAMED` },
|
|
120
|
-
})
|
|
121
|
-
expect(updateRes.status(), 'update returns 200').toBe(200)
|
|
122
|
-
expect((await getById(request, token, id))?.name).toBe(`${originalName} RENAMED`)
|
|
56
|
+
test('jobs CRUD commands restore scalar state on undo/redo (I1/I2/I3/I5/I6)', async ({ request }) => {
|
|
57
|
+
const token = await getAuthToken(request, 'admin')
|
|
123
58
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
59
|
+
await runCrudUndoRoundTrip(request, token, {
|
|
60
|
+
label: 'scheduler.jobs',
|
|
61
|
+
collectionPath: SCHEDULER_JOBS_PATH,
|
|
62
|
+
field: 'name',
|
|
63
|
+
createPayload: jobCreatePayload,
|
|
64
|
+
updatePayload: (id, stamp) => ({ id, name: `TC-UNDO-001 Scheduler Renamed ${stamp}` }),
|
|
65
|
+
})
|
|
132
66
|
})
|
|
133
67
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
68
|
+
// §4 — manual trigger is a fire-and-forget enqueue that never goes through the
|
|
69
|
+
// command bus, so it exposes no undo affordance. The assertion holds regardless of
|
|
70
|
+
// QUEUE_STRATEGY: async returns 200 (enqueued), local returns 400 (rejected) — neither
|
|
71
|
+
// issues an `x-om-operation` undo token.
|
|
72
|
+
test('§4 trigger exposes no undo token', async ({ request }) => {
|
|
73
|
+
const token = await getAuthToken(request, 'admin')
|
|
74
|
+
let jobId: string | null = null
|
|
137
75
|
try {
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
;({ id } = await createJob(request, token, name))
|
|
141
|
-
await settleAuditClock()
|
|
142
|
-
|
|
143
|
-
const deleteRes = await apiRequest(request, 'DELETE', '/api/scheduler/jobs', {
|
|
76
|
+
jobId = await createScheduleJob(request, token)
|
|
77
|
+
const triggerRes = await apiRequest(request, 'POST', SCHEDULER_TRIGGER_PATH, {
|
|
144
78
|
token,
|
|
145
|
-
data: { id },
|
|
79
|
+
data: { id: jobId },
|
|
146
80
|
})
|
|
147
|
-
expect(
|
|
148
|
-
expect(await getById(request, token, id), 'job is gone after delete').toBeFalsy()
|
|
149
|
-
|
|
150
|
-
await undo(request, token, readUndoToken(deleteRes))
|
|
151
|
-
|
|
152
|
-
const restored = await getById(request, token, id)
|
|
153
|
-
expect(restored, 'job re-materialized by delete-undo').toBeTruthy()
|
|
154
|
-
expect(restored!.name).toBe(name)
|
|
155
|
-
} finally {
|
|
156
|
-
await cleanup(request, token, id)
|
|
157
|
-
}
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
test('CREATE -> undo removes the created job', async ({ request }) => {
|
|
161
|
-
let token: string | null = null
|
|
162
|
-
let id: string | null = null
|
|
163
|
-
try {
|
|
164
|
-
token = await getAuthToken(request, 'admin')
|
|
165
|
-
const name = `TC-UNDO-001 Create ${Date.now()}`
|
|
166
|
-
const created = await createJob(request, token, name)
|
|
167
|
-
id = created.id
|
|
168
|
-
|
|
169
|
-
expect(await getById(request, token, id), 'job exists after create').toBeTruthy()
|
|
170
|
-
|
|
171
|
-
await undo(request, token, readUndoToken(created.res))
|
|
172
|
-
|
|
173
|
-
expect(await getById(request, token, id), 'job removed by create-undo').toBeFalsy()
|
|
174
|
-
id = null // already undone; nothing to clean up
|
|
81
|
+
expect(extractOperation(triggerRes), 'trigger response carries no undo token (§4)').toBeNull()
|
|
175
82
|
} finally {
|
|
176
|
-
await
|
|
83
|
+
await deleteScheduleJob(request, token, jobId)
|
|
177
84
|
}
|
|
178
85
|
})
|
|
179
86
|
})
|