cabloy 5.1.201 → 5.1.203
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/.cabloy-version +1 -1
- package/CHANGELOG.md +17 -0
- package/CONTRIBUTING.md +18 -17
- package/README.md +2 -2
- package/package.json +2 -3
- package/repo-agent-governance/managed-assets.json +33 -33
- package/repo-agent-governance/policies/repository.md +1 -0
- package/repo-agent-governance/skills/cabloy-spec-execution/SKILL.md +3 -2
- package/repo-agent-governance/skills/cabloy-spec-execution/references/execution-protocol.md +1 -1
- package/repo-agent-governance/skills/cabloy-spec-execution/references/status-and-evidence.md +11 -10
- package/repo-agent-governance/skills/cabloy-spec-generation/SKILL.md +1 -1
- package/repo-agent-governance/skills/cabloy-spec-generation/references/canonical-spec-input.md +4 -2
- package/repo-agent-governance/skills/cabloy-spec-generation/references/repo-specs-document-set.md +16 -13
- package/repo-agent-governance/skills/cabloy-spec-generation/references/traceability-and-status-rules.md +18 -11
- package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +2 -2
- package/repo-agent-governance/skills/cabloy-workflow/evals/evals.json +1 -1
- package/repo-agent-governance/skills/cabloy-worktree-environment/SKILL.md +5 -5
- package/repo-agent-governance/tests/spec-audit.test.mjs +323 -0
- package/repo-agent-governance/tools/spec-audit/audit.mjs +129 -3
- package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.mjs +26 -7
- package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.test.mjs +82 -0
- package/repo-agent-governance/tools/spec-charts/spec-parser.mjs +40 -1
- package/repo-docs/ai/ai-spec-driven-development.md +1 -0
- package/repo-docs/ai/playbook-spec-execution.md +3 -2
- package/repo-docs/ai/playbook-spec-generation.md +1 -1
- package/repo-docs/assets/img/cabloy-admin-ssr-cover-en.png +0 -0
- package/repo-docs/assets/img/cabloy-admin-tabs-cover-en.png +0 -0
- package/repo-docs/assets/img/cabloy-ai-crud-cover-en.png +0 -0
- package/repo-docs/demo.md +14 -0
- package/repo-docs/frontend/scripts.md +10 -18
- package/repo-docs/fullstack/parallel-worktree-environment.md +2 -2
- package/repo-docs/index.md +2 -2
- package/repo-docs/reference/repo-scripts.md +15 -31
- package/repo-e2e/config/playwright.config.ts +14 -19
- package/repo-e2e/scripts/e2e.test.ts +17 -0
- package/repo-e2e/scripts/e2e.ts +5 -1
- package/repo-e2e/scripts/runE2e.ts +3 -15
- package/repo-e2e/scripts/runE2eArgs.test.ts +21 -17
- package/repo-e2e/scripts/runE2eArgs.ts +16 -29
- package/repo-e2e/scripts/startE2eVona.ts +20 -9
- package/repo-e2e/specs/a-commerce.spec.ts +232 -31
- package/repo-e2e/specs/cabloy-basic.spec.ts +39 -12
- package/repo-e2e/specs/home-user-account.spec.ts +9 -2
- package/scripts/upgrade.test.ts +147 -0
- package/scripts/upgrade.ts +49 -27
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +51 -5
- package/vona/packages-cli/cli-set-api/src/lib/command/bin.dev.ts +6 -1
- package/vona/packages-cli/cli-set-api/test/binDevMode.test.ts +52 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +41 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/e2eFixture.ts +52 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/e2eFixture.ts +224 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/e2eFixture.ts +694 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/e2eFixture.test.ts +434 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { IPaymentOutcomeEvent, IRefundOutcomeEvent } from 'vona-module-a-pay';
|
|
2
|
+
import type { EntityOutboxEvent, EntityWebhookInbox } from 'vona-module-a-pay';
|
|
3
|
+
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
import type { TypeOrderState } from '../entity/order.tsx';
|
|
7
|
+
import type { EntityOrderAudit, TypeOrderAuditOperation } from '../entity/orderAudit.tsx';
|
|
8
|
+
import type { EntityStockBalance } from '../entity/stockBalance.tsx';
|
|
9
|
+
import type { EntityStockReservation } from '../entity/stockReservation.tsx';
|
|
10
|
+
|
|
11
|
+
export const fixturePrefix = 'e2e-fixture-';
|
|
12
|
+
|
|
13
|
+
export function createOwnedFixtureName(userId: string | number) {
|
|
14
|
+
return `${fixturePrefix}${userId}-${randomUUID().replaceAll('-', '').slice(0, 12)}`;
|
|
15
|
+
}
|
|
16
|
+
const mockProvider = 'pay-mock:mock';
|
|
17
|
+
|
|
18
|
+
export function isOwnedFixtureName(name: string, userId: string | number) {
|
|
19
|
+
return new RegExp(`^${fixturePrefix}${userId}-[0-9a-f]{12}$`).test(name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isFixtureCustomer<T extends { name: string; anonymous?: boolean }>(
|
|
23
|
+
user: T | undefined,
|
|
24
|
+
): user is T {
|
|
25
|
+
return !!user && !user.anonymous && user.name.startsWith(fixturePrefix);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function fixtureReservationMatchesLine(
|
|
29
|
+
reservation: Pick<
|
|
30
|
+
EntityStockReservation,
|
|
31
|
+
'skuId' | 'orderLineId' | 'stockBalanceId' | 'quantity'
|
|
32
|
+
>,
|
|
33
|
+
line: { id: string | number; skuId: string | number; quantity: number },
|
|
34
|
+
balance: Pick<EntityStockBalance, 'id' | 'skuId'>,
|
|
35
|
+
) {
|
|
36
|
+
return (
|
|
37
|
+
String(reservation.orderLineId) === String(line.id) &&
|
|
38
|
+
String(reservation.skuId) === String(line.skuId) &&
|
|
39
|
+
String(reservation.stockBalanceId) === String(balance.id) &&
|
|
40
|
+
String(balance.skuId) === String(line.skuId) &&
|
|
41
|
+
reservation.quantity === line.quantity
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function fixtureAdvancedPaymentAuditMatches(
|
|
46
|
+
audits: Pick<
|
|
47
|
+
EntityOrderAudit,
|
|
48
|
+
'id' | 'orderId' | 'operation' | 'fromState' | 'toState' | 'correlationId'
|
|
49
|
+
>[],
|
|
50
|
+
orderId: string | number,
|
|
51
|
+
state: Extract<TypeOrderState, 'shipped' | 'refunded'>,
|
|
52
|
+
paymentCorrelationId: string,
|
|
53
|
+
shipmentCorrelationId?: string,
|
|
54
|
+
refundCorrelationId?: string,
|
|
55
|
+
) {
|
|
56
|
+
const expected: Array<{
|
|
57
|
+
operation: TypeOrderAuditOperation;
|
|
58
|
+
fromState: TypeOrderState;
|
|
59
|
+
toState: TypeOrderState;
|
|
60
|
+
}> = [
|
|
61
|
+
{ operation: 'paid', fromState: 'awaiting_payment', toState: 'paid' },
|
|
62
|
+
...(state === 'shipped'
|
|
63
|
+
? [{ operation: 'shipped' as const, fromState: 'paid' as const, toState: 'shipped' as const }]
|
|
64
|
+
: [
|
|
65
|
+
{
|
|
66
|
+
operation: 'refund_requested' as const,
|
|
67
|
+
fromState: 'paid' as const,
|
|
68
|
+
toState: 'refund_requested' as const,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
operation: 'refund_approved' as const,
|
|
72
|
+
fromState: 'refund_requested' as const,
|
|
73
|
+
toState: 'refund_approved' as const,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
operation: 'refunded' as const,
|
|
77
|
+
fromState: 'refund_approved' as const,
|
|
78
|
+
toState: 'refunded' as const,
|
|
79
|
+
},
|
|
80
|
+
]),
|
|
81
|
+
];
|
|
82
|
+
const steps = audits.filter(audit => audit.operation !== 'created');
|
|
83
|
+
return (
|
|
84
|
+
steps.length === expected.length &&
|
|
85
|
+
steps.every(
|
|
86
|
+
(audit, index) =>
|
|
87
|
+
String(audit.orderId) === String(orderId) &&
|
|
88
|
+
audit.operation === expected[index].operation &&
|
|
89
|
+
audit.fromState === expected[index].fromState &&
|
|
90
|
+
audit.toState === expected[index].toState &&
|
|
91
|
+
(index !== 0 || audit.correlationId === paymentCorrelationId) &&
|
|
92
|
+
(state !== 'shipped' || index !== 1 || audit.correlationId === shipmentCorrelationId) &&
|
|
93
|
+
(state !== 'refunded' || index !== 3 || audit.correlationId === refundCorrelationId),
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function fixtureEventNeedsDispatch(event: EntityOutboxEvent, now = new Date()) {
|
|
99
|
+
if (event.state === 'dispatched') {
|
|
100
|
+
if (
|
|
101
|
+
!event.dispatchedAt ||
|
|
102
|
+
event.claimToken ||
|
|
103
|
+
event.claimedAt ||
|
|
104
|
+
event.claimExpiresAt ||
|
|
105
|
+
event.errorSummary ||
|
|
106
|
+
event.attemptCount < 1
|
|
107
|
+
) {
|
|
108
|
+
throw new Error('fixture event has inconsistent dispatched state');
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (
|
|
113
|
+
event.state !== 'pending' ||
|
|
114
|
+
event.attemptCount !== 0 ||
|
|
115
|
+
event.claimToken ||
|
|
116
|
+
event.claimedAt ||
|
|
117
|
+
event.claimExpiresAt ||
|
|
118
|
+
event.dispatchedAt ||
|
|
119
|
+
event.errorSummary ||
|
|
120
|
+
(event.nextAttemptAt && event.nextAttemptAt > now)
|
|
121
|
+
) {
|
|
122
|
+
throw new Error('fixture event is not eligible for dispatch');
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function selectFixtureEvent(
|
|
128
|
+
events: EntityOutboxEvent[],
|
|
129
|
+
inboxes: EntityWebhookInbox[],
|
|
130
|
+
expected: {
|
|
131
|
+
kind: 'payment' | 'refund';
|
|
132
|
+
paymentSessionId: string | number;
|
|
133
|
+
businessReference: string;
|
|
134
|
+
amountMinor: number;
|
|
135
|
+
currency: string;
|
|
136
|
+
refundOperationId?: string | number;
|
|
137
|
+
clientName: string;
|
|
138
|
+
environment: 'sandbox' | 'live';
|
|
139
|
+
},
|
|
140
|
+
) {
|
|
141
|
+
const eventType = `${expected.kind}.outcome.v1`;
|
|
142
|
+
const candidates = events.filter(
|
|
143
|
+
event =>
|
|
144
|
+
event.eventType === eventType &&
|
|
145
|
+
String(event.paymentSessionId) === String(expected.paymentSessionId) &&
|
|
146
|
+
(expected.kind === 'payment'
|
|
147
|
+
? !event.refundOperationId
|
|
148
|
+
: String(event.refundOperationId) === String(expected.refundOperationId)),
|
|
149
|
+
);
|
|
150
|
+
if (candidates.length !== 1) throw new Error('fixture outbox event is missing or ambiguous');
|
|
151
|
+
const event = candidates[0];
|
|
152
|
+
const payload = event.payload as IPaymentOutcomeEvent | IRefundOutcomeEvent;
|
|
153
|
+
if (
|
|
154
|
+
typeof payload?.eventId !== 'string' ||
|
|
155
|
+
!payload.eventId.startsWith(`mock-${expected.kind}-`) ||
|
|
156
|
+
String(event.paymentSessionId) !== String(payload.paymentSessionId) ||
|
|
157
|
+
(expected.kind === 'payment'
|
|
158
|
+
? !!event.refundOperationId
|
|
159
|
+
: String(event.refundOperationId) !==
|
|
160
|
+
String((payload as IRefundOutcomeEvent).refundOperationId)) ||
|
|
161
|
+
payload.providerName !== mockProvider ||
|
|
162
|
+
payload.businessReference !== expected.businessReference ||
|
|
163
|
+
String(payload.paymentSessionId) !== String(expected.paymentSessionId) ||
|
|
164
|
+
payload.amountMinor !== expected.amountMinor ||
|
|
165
|
+
payload.currency !== expected.currency ||
|
|
166
|
+
!['succeeded', 'failed', 'cancelled'].includes(payload.state) ||
|
|
167
|
+
(expected.kind === 'refund' &&
|
|
168
|
+
String((payload as IRefundOutcomeEvent).refundOperationId) !==
|
|
169
|
+
String(expected.refundOperationId))
|
|
170
|
+
) {
|
|
171
|
+
throw new Error('fixture outbox payload conflicts with the mock operation');
|
|
172
|
+
}
|
|
173
|
+
const matchingInboxes = inboxes.filter(
|
|
174
|
+
inbox =>
|
|
175
|
+
inbox.providerEventId === payload.eventId &&
|
|
176
|
+
inbox.providerName === mockProvider &&
|
|
177
|
+
inbox.clientName === expected.clientName &&
|
|
178
|
+
inbox.environment === expected.environment,
|
|
179
|
+
);
|
|
180
|
+
if (matchingInboxes.length !== 1)
|
|
181
|
+
throw new Error('fixture webhook inbox is missing or ambiguous');
|
|
182
|
+
const inbox = matchingInboxes[0];
|
|
183
|
+
if (
|
|
184
|
+
inbox.state !== 'processed' ||
|
|
185
|
+
!inbox.processedAt ||
|
|
186
|
+
inbox.errorSummary ||
|
|
187
|
+
inbox.eventType !== `${expected.kind}.${payload.state}` ||
|
|
188
|
+
String(inbox.paymentSessionId) !== String(expected.paymentSessionId) ||
|
|
189
|
+
(expected.kind === 'payment'
|
|
190
|
+
? !!inbox.refundOperationId || inbox.paymentState !== payload.state
|
|
191
|
+
: String(inbox.refundOperationId) !== String(expected.refundOperationId) ||
|
|
192
|
+
inbox.refundState !== payload.state) ||
|
|
193
|
+
inbox.amountMinor !== expected.amountMinor ||
|
|
194
|
+
inbox.currency !== expected.currency ||
|
|
195
|
+
(expected.kind === 'payment' &&
|
|
196
|
+
(inbox.providerCaptureId ?? undefined) !==
|
|
197
|
+
((payload as IPaymentOutcomeEvent).providerCaptureId ?? undefined)) ||
|
|
198
|
+
(expected.kind === 'refund' &&
|
|
199
|
+
inbox.providerRefundId !== (payload as IRefundOutcomeEvent).providerRefundId)
|
|
200
|
+
) {
|
|
201
|
+
throw new Error('fixture webhook inbox is not a processed matching terminal outcome');
|
|
202
|
+
}
|
|
203
|
+
return { event, payload };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function selectPaymentFixtureEvent(
|
|
207
|
+
events: EntityOutboxEvent[],
|
|
208
|
+
inboxes: EntityWebhookInbox[],
|
|
209
|
+
expected: Omit<Parameters<typeof selectFixtureEvent>[2], 'kind' | 'refundOperationId'>,
|
|
210
|
+
) {
|
|
211
|
+
const result = selectFixtureEvent(events, inboxes, { ...expected, kind: 'payment' });
|
|
212
|
+
return { ...result, payload: result.payload as IPaymentOutcomeEvent };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function selectRefundFixtureEvent(
|
|
216
|
+
events: EntityOutboxEvent[],
|
|
217
|
+
inboxes: EntityWebhookInbox[],
|
|
218
|
+
expected: Omit<Parameters<typeof selectFixtureEvent>[2], 'kind' | 'refundOperationId'> & {
|
|
219
|
+
refundOperationId: string | number;
|
|
220
|
+
},
|
|
221
|
+
) {
|
|
222
|
+
const result = selectFixtureEvent(events, inboxes, { ...expected, kind: 'refund' });
|
|
223
|
+
return { ...result, payload: result.payload as IRefundOutcomeEvent };
|
|
224
|
+
}
|