@open-mercato/core 0.6.5-develop.4616.1.0cd64e1448 → 0.6.5-develop.4620.1.c20bc7e4bb
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
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { expect } from "@playwright/test";
|
|
2
|
+
import { apiRequest } from "./api.js";
|
|
3
|
+
const HEADER_PREFIX = "omop:";
|
|
4
|
+
const UNDO_PATH = "/api/audit_logs/audit-logs/actions/undo";
|
|
5
|
+
const REDO_PATH = "/api/audit_logs/audit-logs/actions/redo";
|
|
6
|
+
const ACTIONS_PATH = "/api/audit_logs/audit-logs/actions";
|
|
7
|
+
function extractOperation(response) {
|
|
8
|
+
const header = response.headers()["x-om-operation"];
|
|
9
|
+
if (!header || typeof header !== "string") return null;
|
|
10
|
+
const trimmed = header.startsWith(HEADER_PREFIX) ? header.slice(HEADER_PREFIX.length) : header;
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(decodeURIComponent(trimmed));
|
|
13
|
+
if (typeof parsed.id !== "string" || typeof parsed.commandId !== "string") return null;
|
|
14
|
+
if (typeof parsed.undoToken !== "string" || !parsed.undoToken) return null;
|
|
15
|
+
return {
|
|
16
|
+
logId: parsed.id,
|
|
17
|
+
undoToken: parsed.undoToken,
|
|
18
|
+
commandId: parsed.commandId,
|
|
19
|
+
resourceKind: parsed.resourceKind ?? null,
|
|
20
|
+
resourceId: parsed.resourceId ?? null
|
|
21
|
+
};
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function expectOperation(response, context) {
|
|
27
|
+
const op = extractOperation(response);
|
|
28
|
+
expect(op, `Expected an undo token (x-om-operation header) for ${context}, got none`).toBeTruthy();
|
|
29
|
+
return op;
|
|
30
|
+
}
|
|
31
|
+
async function undoByToken(request, token, undoToken) {
|
|
32
|
+
return apiRequest(request, "POST", UNDO_PATH, { token, data: { undoToken } });
|
|
33
|
+
}
|
|
34
|
+
async function redoByLogId(request, token, logId) {
|
|
35
|
+
return apiRequest(request, "POST", REDO_PATH, { token, data: { logId } });
|
|
36
|
+
}
|
|
37
|
+
async function undoOk(request, token, undoToken, context) {
|
|
38
|
+
const res = await undoByToken(request, token, undoToken);
|
|
39
|
+
const body = await res.json().catch(() => null);
|
|
40
|
+
expect(res.ok(), `Undo failed for ${context}: status ${res.status()} body ${JSON.stringify(body)}`).toBeTruthy();
|
|
41
|
+
expect(body?.ok, `Undo not ok for ${context}: ${JSON.stringify(body)}`).toBeTruthy();
|
|
42
|
+
return body?.logId;
|
|
43
|
+
}
|
|
44
|
+
async function redoOk(request, token, logId, context) {
|
|
45
|
+
const res = await redoByLogId(request, token, logId);
|
|
46
|
+
const body = await res.json().catch(() => null);
|
|
47
|
+
expect(res.ok(), `Redo failed for ${context}: status ${res.status()} body ${JSON.stringify(body)}`).toBeTruthy();
|
|
48
|
+
expect(body?.ok, `Redo not ok for ${context}: ${JSON.stringify(body)}`).toBeTruthy();
|
|
49
|
+
return { logId: body?.logId, undoToken: body?.undoToken ?? null };
|
|
50
|
+
}
|
|
51
|
+
async function expectTokenConsumed(request, token, undoToken, context) {
|
|
52
|
+
const res = await undoByToken(request, token, undoToken);
|
|
53
|
+
expect(res.ok(), `Expected double-undo to be rejected for ${context}, but it succeeded`).toBeFalsy();
|
|
54
|
+
}
|
|
55
|
+
async function listUndoable(request, token, params = {}) {
|
|
56
|
+
const qs = new URLSearchParams({ undoableOnly: "true", ...params }).toString();
|
|
57
|
+
const res = await apiRequest(request, "GET", `${ACTIONS_PATH}?${qs}`, { token });
|
|
58
|
+
return res.json().catch(() => null);
|
|
59
|
+
}
|
|
60
|
+
function assertFieldsEqual(actual, expected, fields, context) {
|
|
61
|
+
expect(actual, `${context}: actual entity missing`).toBeTruthy();
|
|
62
|
+
expect(expected, `${context}: expected entity missing`).toBeTruthy();
|
|
63
|
+
for (const field of fields) {
|
|
64
|
+
expect(
|
|
65
|
+
JSON.stringify(actual[field]),
|
|
66
|
+
`${context}: field "${field}" not restored (expected ${JSON.stringify(expected[field])}, got ${JSON.stringify(actual[field])})`
|
|
67
|
+
).toBe(JSON.stringify(expected[field]));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
assertFieldsEqual,
|
|
72
|
+
expectOperation,
|
|
73
|
+
expectTokenConsumed,
|
|
74
|
+
extractOperation,
|
|
75
|
+
listUndoable,
|
|
76
|
+
redoByLogId,
|
|
77
|
+
redoOk,
|
|
78
|
+
undoByToken,
|
|
79
|
+
undoOk
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=undoHarness.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/helpers/integration/undoHarness.ts"],
|
|
4
|
+
"sourcesContent": ["import { type APIRequestContext, type APIResponse, expect } from '@playwright/test'\nimport { apiRequest } from './api'\n\n/**\n * Shared harness for verifying Undo/Redo correctness against the real command bus.\n *\n * Every mutating Open Mercato API response carries the operation metadata in the\n * `x-om-operation` header (`omop:<urlencoded JSON>`) containing the `undoToken` and the\n * audit log `id` (used as `logId` for redo). These helpers extract that envelope and drive\n * the real undo/redo endpoints so tests can assert full state restoration per TC-UNDO-001.\n */\n\nconst HEADER_PREFIX = 'omop:'\nconst UNDO_PATH = '/api/audit_logs/audit-logs/actions/undo'\nconst REDO_PATH = '/api/audit_logs/audit-logs/actions/redo'\nconst ACTIONS_PATH = '/api/audit_logs/audit-logs/actions'\n\nexport type Operation = {\n logId: string\n undoToken: string\n commandId: string\n resourceKind: string | null\n resourceId: string | null\n}\n\n/** Parse the `x-om-operation` header into a structured operation, or null when absent/malformed. */\nexport function extractOperation(response: APIResponse): Operation | null {\n const header = response.headers()['x-om-operation']\n if (!header || typeof header !== 'string') return null\n const trimmed = header.startsWith(HEADER_PREFIX) ? header.slice(HEADER_PREFIX.length) : header\n try {\n const parsed = JSON.parse(decodeURIComponent(trimmed)) as Record<string, unknown>\n if (typeof parsed.id !== 'string' || typeof parsed.commandId !== 'string') return null\n if (typeof parsed.undoToken !== 'string' || !parsed.undoToken) return null\n return {\n logId: parsed.id,\n undoToken: parsed.undoToken,\n commandId: parsed.commandId,\n resourceKind: (parsed.resourceKind as string) ?? null,\n resourceId: (parsed.resourceId as string) ?? null,\n }\n } catch {\n return null\n }\n}\n\n/** Like extractOperation but fails the test if no undo token was issued. */\nexport function expectOperation(response: APIResponse, context: string): Operation {\n const op = extractOperation(response)\n expect(op, `Expected an undo token (x-om-operation header) for ${context}, got none`).toBeTruthy()\n return op as Operation\n}\n\nexport async function undoByToken(request: APIRequestContext, token: string, undoToken: string): Promise<APIResponse> {\n return apiRequest(request, 'POST', UNDO_PATH, { token, data: { undoToken } })\n}\n\nexport async function redoByLogId(request: APIRequestContext, token: string, logId: string): Promise<APIResponse> {\n return apiRequest(request, 'POST', REDO_PATH, { token, data: { logId } })\n}\n\n/** Undo and assert success; returns the resolved logId. */\nexport async function undoOk(request: APIRequestContext, token: string, undoToken: string, context: string): Promise<string> {\n const res = await undoByToken(request, token, undoToken)\n const body = (await res.json().catch(() => null)) as { ok?: boolean; logId?: string } | null\n expect(res.ok(), `Undo failed for ${context}: status ${res.status()} body ${JSON.stringify(body)}`).toBeTruthy()\n expect(body?.ok, `Undo not ok for ${context}: ${JSON.stringify(body)}`).toBeTruthy()\n return body?.logId as string\n}\n\n/** Redo and assert success; returns the new operation (new undoToken + logId). */\nexport async function redoOk(request: APIRequestContext, token: string, logId: string, context: string): Promise<{ logId: string; undoToken: string | null }> {\n const res = await redoByLogId(request, token, logId)\n const body = (await res.json().catch(() => null)) as { ok?: boolean; logId?: string; undoToken?: string } | null\n expect(res.ok(), `Redo failed for ${context}: status ${res.status()} body ${JSON.stringify(body)}`).toBeTruthy()\n expect(body?.ok, `Redo not ok for ${context}: ${JSON.stringify(body)}`).toBeTruthy()\n return { logId: body?.logId as string, undoToken: body?.undoToken ?? null }\n}\n\n/** Assert that undoing an already-consumed token is rejected (token consumption / no double-undo). */\nexport async function expectTokenConsumed(request: APIRequestContext, token: string, undoToken: string, context: string): Promise<void> {\n const res = await undoByToken(request, token, undoToken)\n expect(res.ok(), `Expected double-undo to be rejected for ${context}, but it succeeded`).toBeFalsy()\n}\n\n/** Fetch undoable actions list (for Version History assertions). */\nexport async function listUndoable(request: APIRequestContext, token: string, params: Record<string, string> = {}): Promise<unknown> {\n const qs = new URLSearchParams({ undoableOnly: 'true', ...params }).toString()\n const res = await apiRequest(request, 'GET', `${ACTIONS_PATH}?${qs}`, { token })\n return res.json().catch(() => null)\n}\n\n/**\n * Deep-equality assertion for a selected set of fields between two entity snapshots.\n * Reports the first mismatching field with context for clear bug triage.\n */\nexport function assertFieldsEqual(\n actual: Record<string, unknown> | null | undefined,\n expected: Record<string, unknown> | null | undefined,\n fields: string[],\n context: string,\n): void {\n expect(actual, `${context}: actual entity missing`).toBeTruthy()\n expect(expected, `${context}: expected entity missing`).toBeTruthy()\n for (const field of fields) {\n expect(\n JSON.stringify((actual as Record<string, unknown>)[field]),\n `${context}: field \"${field}\" not restored (expected ${JSON.stringify((expected as Record<string, unknown>)[field])}, got ${JSON.stringify((actual as Record<string, unknown>)[field])})`,\n ).toBe(JSON.stringify((expected as Record<string, unknown>)[field]))\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.5-develop.
|
|
3
|
+
"version": "0.6.5-develop.4620.1.c20bc7e4bb",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -245,16 +245,16 @@
|
|
|
245
245
|
"zod": "^4.4.3"
|
|
246
246
|
},
|
|
247
247
|
"peerDependencies": {
|
|
248
|
-
"@open-mercato/ai-assistant": "0.6.5-develop.
|
|
249
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
250
|
-
"@open-mercato/ui": "0.6.5-develop.
|
|
248
|
+
"@open-mercato/ai-assistant": "0.6.5-develop.4620.1.c20bc7e4bb",
|
|
249
|
+
"@open-mercato/shared": "0.6.5-develop.4620.1.c20bc7e4bb",
|
|
250
|
+
"@open-mercato/ui": "0.6.5-develop.4620.1.c20bc7e4bb",
|
|
251
251
|
"react": "^19.0.0",
|
|
252
252
|
"react-dom": "^19.0.0"
|
|
253
253
|
},
|
|
254
254
|
"devDependencies": {
|
|
255
|
-
"@open-mercato/ai-assistant": "0.6.5-develop.
|
|
256
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
257
|
-
"@open-mercato/ui": "0.6.5-develop.
|
|
255
|
+
"@open-mercato/ai-assistant": "0.6.5-develop.4620.1.c20bc7e4bb",
|
|
256
|
+
"@open-mercato/shared": "0.6.5-develop.4620.1.c20bc7e4bb",
|
|
257
|
+
"@open-mercato/ui": "0.6.5-develop.4620.1.c20bc7e4bb",
|
|
258
258
|
"@testing-library/dom": "^10.4.1",
|
|
259
259
|
"@testing-library/jest-dom": "^6.9.1",
|
|
260
260
|
"@testing-library/react": "^16.3.1",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { type APIRequestContext, type APIResponse, expect } from '@playwright/test'
|
|
2
|
+
import { apiRequest } from './api'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared harness for verifying Undo/Redo correctness against the real command bus.
|
|
6
|
+
*
|
|
7
|
+
* Every mutating Open Mercato API response carries the operation metadata in the
|
|
8
|
+
* `x-om-operation` header (`omop:<urlencoded JSON>`) containing the `undoToken` and the
|
|
9
|
+
* audit log `id` (used as `logId` for redo). These helpers extract that envelope and drive
|
|
10
|
+
* the real undo/redo endpoints so tests can assert full state restoration per TC-UNDO-001.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const HEADER_PREFIX = 'omop:'
|
|
14
|
+
const UNDO_PATH = '/api/audit_logs/audit-logs/actions/undo'
|
|
15
|
+
const REDO_PATH = '/api/audit_logs/audit-logs/actions/redo'
|
|
16
|
+
const ACTIONS_PATH = '/api/audit_logs/audit-logs/actions'
|
|
17
|
+
|
|
18
|
+
export type Operation = {
|
|
19
|
+
logId: string
|
|
20
|
+
undoToken: string
|
|
21
|
+
commandId: string
|
|
22
|
+
resourceKind: string | null
|
|
23
|
+
resourceId: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Parse the `x-om-operation` header into a structured operation, or null when absent/malformed. */
|
|
27
|
+
export function extractOperation(response: APIResponse): Operation | null {
|
|
28
|
+
const header = response.headers()['x-om-operation']
|
|
29
|
+
if (!header || typeof header !== 'string') return null
|
|
30
|
+
const trimmed = header.startsWith(HEADER_PREFIX) ? header.slice(HEADER_PREFIX.length) : header
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(decodeURIComponent(trimmed)) as Record<string, unknown>
|
|
33
|
+
if (typeof parsed.id !== 'string' || typeof parsed.commandId !== 'string') return null
|
|
34
|
+
if (typeof parsed.undoToken !== 'string' || !parsed.undoToken) return null
|
|
35
|
+
return {
|
|
36
|
+
logId: parsed.id,
|
|
37
|
+
undoToken: parsed.undoToken,
|
|
38
|
+
commandId: parsed.commandId,
|
|
39
|
+
resourceKind: (parsed.resourceKind as string) ?? null,
|
|
40
|
+
resourceId: (parsed.resourceId as string) ?? null,
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Like extractOperation but fails the test if no undo token was issued. */
|
|
48
|
+
export function expectOperation(response: APIResponse, context: string): Operation {
|
|
49
|
+
const op = extractOperation(response)
|
|
50
|
+
expect(op, `Expected an undo token (x-om-operation header) for ${context}, got none`).toBeTruthy()
|
|
51
|
+
return op as Operation
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function undoByToken(request: APIRequestContext, token: string, undoToken: string): Promise<APIResponse> {
|
|
55
|
+
return apiRequest(request, 'POST', UNDO_PATH, { token, data: { undoToken } })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function redoByLogId(request: APIRequestContext, token: string, logId: string): Promise<APIResponse> {
|
|
59
|
+
return apiRequest(request, 'POST', REDO_PATH, { token, data: { logId } })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Undo and assert success; returns the resolved logId. */
|
|
63
|
+
export async function undoOk(request: APIRequestContext, token: string, undoToken: string, context: string): Promise<string> {
|
|
64
|
+
const res = await undoByToken(request, token, undoToken)
|
|
65
|
+
const body = (await res.json().catch(() => null)) as { ok?: boolean; logId?: string } | null
|
|
66
|
+
expect(res.ok(), `Undo failed for ${context}: status ${res.status()} body ${JSON.stringify(body)}`).toBeTruthy()
|
|
67
|
+
expect(body?.ok, `Undo not ok for ${context}: ${JSON.stringify(body)}`).toBeTruthy()
|
|
68
|
+
return body?.logId as string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Redo and assert success; returns the new operation (new undoToken + logId). */
|
|
72
|
+
export async function redoOk(request: APIRequestContext, token: string, logId: string, context: string): Promise<{ logId: string; undoToken: string | null }> {
|
|
73
|
+
const res = await redoByLogId(request, token, logId)
|
|
74
|
+
const body = (await res.json().catch(() => null)) as { ok?: boolean; logId?: string; undoToken?: string } | null
|
|
75
|
+
expect(res.ok(), `Redo failed for ${context}: status ${res.status()} body ${JSON.stringify(body)}`).toBeTruthy()
|
|
76
|
+
expect(body?.ok, `Redo not ok for ${context}: ${JSON.stringify(body)}`).toBeTruthy()
|
|
77
|
+
return { logId: body?.logId as string, undoToken: body?.undoToken ?? null }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Assert that undoing an already-consumed token is rejected (token consumption / no double-undo). */
|
|
81
|
+
export async function expectTokenConsumed(request: APIRequestContext, token: string, undoToken: string, context: string): Promise<void> {
|
|
82
|
+
const res = await undoByToken(request, token, undoToken)
|
|
83
|
+
expect(res.ok(), `Expected double-undo to be rejected for ${context}, but it succeeded`).toBeFalsy()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Fetch undoable actions list (for Version History assertions). */
|
|
87
|
+
export async function listUndoable(request: APIRequestContext, token: string, params: Record<string, string> = {}): Promise<unknown> {
|
|
88
|
+
const qs = new URLSearchParams({ undoableOnly: 'true', ...params }).toString()
|
|
89
|
+
const res = await apiRequest(request, 'GET', `${ACTIONS_PATH}?${qs}`, { token })
|
|
90
|
+
return res.json().catch(() => null)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Deep-equality assertion for a selected set of fields between two entity snapshots.
|
|
95
|
+
* Reports the first mismatching field with context for clear bug triage.
|
|
96
|
+
*/
|
|
97
|
+
export function assertFieldsEqual(
|
|
98
|
+
actual: Record<string, unknown> | null | undefined,
|
|
99
|
+
expected: Record<string, unknown> | null | undefined,
|
|
100
|
+
fields: string[],
|
|
101
|
+
context: string,
|
|
102
|
+
): void {
|
|
103
|
+
expect(actual, `${context}: actual entity missing`).toBeTruthy()
|
|
104
|
+
expect(expected, `${context}: expected entity missing`).toBeTruthy()
|
|
105
|
+
for (const field of fields) {
|
|
106
|
+
expect(
|
|
107
|
+
JSON.stringify((actual as Record<string, unknown>)[field]),
|
|
108
|
+
`${context}: field "${field}" not restored (expected ${JSON.stringify((expected as Record<string, unknown>)[field])}, got ${JSON.stringify((actual as Record<string, unknown>)[field])})`,
|
|
109
|
+
).toBe(JSON.stringify((expected as Record<string, unknown>)[field]))
|
|
110
|
+
}
|
|
111
|
+
}
|