@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.
@@ -1,4 +1,4 @@
1
- [build:core] found 3172 entry points
1
+ [build:core] found 3176 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 185 entry points
4
4
  [build:core:generated] built successfully
@@ -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.4616.1.0cd64e1448",
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.4616.1.0cd64e1448",
249
- "@open-mercato/shared": "0.6.5-develop.4616.1.0cd64e1448",
250
- "@open-mercato/ui": "0.6.5-develop.4616.1.0cd64e1448",
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.4616.1.0cd64e1448",
256
- "@open-mercato/shared": "0.6.5-develop.4616.1.0cd64e1448",
257
- "@open-mercato/ui": "0.6.5-develop.4616.1.0cd64e1448",
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
+ }