@floken-io/engine 0.0.1
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/CHANGELOG.md +39 -0
- package/LICENSE +153 -0
- package/README.md +138 -0
- package/dist/chunk-RMCROXES.js +376 -0
- package/dist/conformance.d.ts +162 -0
- package/dist/conformance.js +711 -0
- package/dist/index.d.ts +1948 -0
- package/dist/index.js +3645 -0
- package/dist/spi-BABH0Tcs.d.ts +635 -0
- package/package.json +45 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
import { ENGINE_ERROR_CODES, EngineError, deepEqual, STATE_SCHEMA_VERSION } from './chunk-RMCROXES.js';
|
|
2
|
+
|
|
3
|
+
// src/conformance/report.ts
|
|
4
|
+
async function runConformanceCases(suite, subject, specs) {
|
|
5
|
+
const cases = [];
|
|
6
|
+
for (const [name, refs, body] of specs) {
|
|
7
|
+
try {
|
|
8
|
+
await body();
|
|
9
|
+
cases.push({ name, refs, ok: true });
|
|
10
|
+
} catch (e) {
|
|
11
|
+
cases.push({ name, refs, ok: false, error: describeError(e) });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const passed = cases.filter((c) => c.ok).length;
|
|
15
|
+
const failed = cases.length - passed;
|
|
16
|
+
return {
|
|
17
|
+
suite,
|
|
18
|
+
// `exactOptionalPropertyTypes`:不能用 `subject: undefined` 占键
|
|
19
|
+
...subject === void 0 ? {} : { subject },
|
|
20
|
+
cases,
|
|
21
|
+
total: cases.length,
|
|
22
|
+
passed,
|
|
23
|
+
failed,
|
|
24
|
+
ok: failed === 0
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function formatConformanceReport(report) {
|
|
28
|
+
const head = `[${report.suite} conformance]${report.subject === void 0 ? "" : ` ${report.subject}`}`;
|
|
29
|
+
const lines = [`${report.ok ? "\u2713" : "\u2717"} ${head} \u2014 ${report.passed}/${report.total} \u901A\u8FC7`];
|
|
30
|
+
for (const c of report.cases) {
|
|
31
|
+
const refs = c.refs.length === 0 ? "" : ` (${c.refs.join(" ")})`;
|
|
32
|
+
if (c.ok) lines.push(` \u2713 ${c.name}${refs}`);
|
|
33
|
+
else lines.push(` \u2717 ${c.name}${refs}`, ` ${c.error ?? "(\u65E0\u9519\u8BEF\u4FE1\u606F)"}`);
|
|
34
|
+
}
|
|
35
|
+
return lines.join("\n");
|
|
36
|
+
}
|
|
37
|
+
function assertTrue(cond, message) {
|
|
38
|
+
if (!cond) throw new Error(message);
|
|
39
|
+
}
|
|
40
|
+
function assertDeepEqual(actual, expected, what) {
|
|
41
|
+
if (!deepEqual(actual, expected)) {
|
|
42
|
+
throw new Error(`${what}\uFF1A\u671F\u671B ${show(expected)}\uFF0C\u5B9E\u5F97 ${show(actual)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function isEngineErrorLike(e) {
|
|
46
|
+
if (e instanceof EngineError) return true;
|
|
47
|
+
return typeof e === "object" && e !== null && e.floken === true && typeof e.code === "string";
|
|
48
|
+
}
|
|
49
|
+
function assertEngineErrorValue(value, expectedCode, what) {
|
|
50
|
+
if (!isEngineErrorLike(value)) {
|
|
51
|
+
throw new Error(`${what}\uFF1A\u671F\u671B\u629B EngineError\uFF0C\u5B9E\u5F97 ${describeError(value)}`);
|
|
52
|
+
}
|
|
53
|
+
assertTrue(
|
|
54
|
+
value.code === expectedCode,
|
|
55
|
+
`${what}\uFF1A\u9519\u8BEF\u7801\u671F\u671B ${expectedCode}\uFF0C\u5B9E\u5F97 ${value.code}`
|
|
56
|
+
);
|
|
57
|
+
assertTrue(
|
|
58
|
+
value.floken === true,
|
|
59
|
+
`${what}\uFF1A\u9519\u8BEF\u7F3A floken \u5370\u8BB0 \u2014\u2014 \u5BBF\u4E3B\u81EA\u7814\u5B9E\u73B0\u987B\u7528 engine \u5BFC\u51FA\u7684\u5DE5\u5382\u629B\u9519\uFF0C\u5426\u5219\u5F15\u64CE\u65E0\u6CD5\u5F52\u4E00`
|
|
60
|
+
);
|
|
61
|
+
assertTrue(value.pkg === "engine", `${what}\uFF1Apkg \u671F\u671B 'engine'\uFF0C\u5B9E\u5F97 ${String(value.pkg)}`);
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
async function mustThrow(fn, what) {
|
|
65
|
+
try {
|
|
66
|
+
await fn();
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return e;
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`${what}\uFF1A\u671F\u671B\u629B\u9519\uFF0C\u4F46\u6B63\u5E38\u8FD4\u56DE\u4E86`);
|
|
71
|
+
}
|
|
72
|
+
async function catchEngineError(fn, expectedCode, what) {
|
|
73
|
+
return assertEngineErrorValue(await mustThrow(fn, what), expectedCode, what);
|
|
74
|
+
}
|
|
75
|
+
function describeError(e) {
|
|
76
|
+
if (isEngineErrorLike(e)) {
|
|
77
|
+
const hint = e.hint === void 0 ? "" : ` \u2014 ${e.hint}`;
|
|
78
|
+
return `${e.name}: ${e.message} [${e.code}]${hint}`;
|
|
79
|
+
}
|
|
80
|
+
if (e instanceof Error) return `${e.name}: ${e.message}`;
|
|
81
|
+
return show(e);
|
|
82
|
+
}
|
|
83
|
+
function show(value) {
|
|
84
|
+
if (value === void 0) return "undefined";
|
|
85
|
+
try {
|
|
86
|
+
const s = JSON.stringify(value);
|
|
87
|
+
return s === void 0 ? String(value) : s;
|
|
88
|
+
} catch {
|
|
89
|
+
return String(value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/conformance/store.ts
|
|
94
|
+
var TS = "2026-09-30T00:00:00.000Z";
|
|
95
|
+
var seq = 0;
|
|
96
|
+
function uniqueId(prefix) {
|
|
97
|
+
seq += 1;
|
|
98
|
+
return `${prefix}_${seq.toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
99
|
+
}
|
|
100
|
+
function makeState(instanceId, over = {}) {
|
|
101
|
+
return {
|
|
102
|
+
instanceId,
|
|
103
|
+
processId: "expense",
|
|
104
|
+
definitionVersion: 1,
|
|
105
|
+
status: over.status ?? "running",
|
|
106
|
+
rev: over.rev ?? 0,
|
|
107
|
+
stateSchema: STATE_SCHEMA_VERSION,
|
|
108
|
+
startedAt: TS,
|
|
109
|
+
updatedAt: TS,
|
|
110
|
+
...over.businessKey === void 0 ? {} : { businessKey: over.businessKey },
|
|
111
|
+
tokens: [{ id: `${instanceId}::t1`, nodeId: "Task_Approve", state: "active", assignee: "u_1" }],
|
|
112
|
+
completedNodes: [],
|
|
113
|
+
variables: over.variables ?? { amount: 100 },
|
|
114
|
+
auditTrail: [{ seq: 1, at: TS, actor: "u_1", action: "start" }]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
var conflict = ENGINE_ERROR_CODES.PERSIST_CONFLICT;
|
|
118
|
+
var exists = ENGINE_ERROR_CODES.PERSIST_ALREADY_EXISTS;
|
|
119
|
+
async function runStoreConformance(store, options = {}) {
|
|
120
|
+
const prefix = options.idPrefix ?? "floken-conf";
|
|
121
|
+
const newId = () => uniqueId(prefix);
|
|
122
|
+
return runConformanceCases("store", options.subject, [
|
|
123
|
+
// ── 读路径 ──
|
|
124
|
+
[
|
|
125
|
+
"load \u672A\u77E5 id \u8FD4\u56DE null\uFF08\u4E0D\u5F97\u629B\u9519\uFF09",
|
|
126
|
+
["\xA77.2"],
|
|
127
|
+
async () => {
|
|
128
|
+
const got = await store.load(newId());
|
|
129
|
+
assertTrue(got === null, `\u671F\u671B null\uFF0C\u5B9E\u5F97 ${show(got)}`);
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
// ── INSERT 路径 ──
|
|
133
|
+
[
|
|
134
|
+
"INSERT\uFF1AexpectedRev === 0 \u6210\u529F\uFF0C\u4E14 rev \u88AB\u5F52\u4E00\u5316\u4E3A 1\uFF08\u4E0D\u4FE1\u4EFB next.rev\uFF09",
|
|
135
|
+
["INV-1", "AC-E11"],
|
|
136
|
+
async () => {
|
|
137
|
+
const s = makeState(newId(), { rev: 999 });
|
|
138
|
+
await store.save(s, 0);
|
|
139
|
+
const got = await store.load(s.instanceId);
|
|
140
|
+
assertTrue(got !== null, "INSERT \u540E load \u8FD4\u56DE null");
|
|
141
|
+
assertDeepEqual(
|
|
142
|
+
got.rev,
|
|
143
|
+
1,
|
|
144
|
+
"INSERT \u540E\u7684 rev\uFF08\u5B58\u50A8\u5C42\u987B\u6309 expectedRev+1 \u5F52\u4E00\u5316\uFF0C\u4E0E SQL \u7684 SET rev = rev + 1 \u540C\u6784\uFF09"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
[
|
|
149
|
+
"INSERT \u540E\u5B8C\u6574\u5185\u5BB9\u53EF\u6309 id \u8BFB\u56DE\uFF08\u9664 rev \u5916\u9010\u5B57\u6BB5\u6DF1\u7B49\uFF09",
|
|
150
|
+
["\xA77.2"],
|
|
151
|
+
async () => {
|
|
152
|
+
const s = makeState(newId(), { businessKey: "BK_1" });
|
|
153
|
+
await store.save(s, 0);
|
|
154
|
+
const got = await store.load(s.instanceId);
|
|
155
|
+
assertTrue(got !== null, "INSERT \u540E load \u8FD4\u56DE null");
|
|
156
|
+
assertDeepEqual({ ...got, rev: 0 }, { ...s, rev: 0 }, "\u8BFB\u56DE\u5185\u5BB9");
|
|
157
|
+
}
|
|
158
|
+
],
|
|
159
|
+
// ── 隔离性(内存与 SQL 都必须成立,实现机制不同) ──
|
|
160
|
+
[
|
|
161
|
+
"load() \u4EA4\u51FA\u526F\u672C\uFF1A\u5BBF\u4E3B\u6539\u52A8\u624B\u91CC\u90A3\u4EFD\u4E0D\u5F97\u6CC4\u6F0F\u8FDB\u5DF2\u63D0\u4EA4\u72B6\u6001",
|
|
162
|
+
["\xA79 T5"],
|
|
163
|
+
async () => {
|
|
164
|
+
const s = makeState(newId());
|
|
165
|
+
await store.save(s, 0);
|
|
166
|
+
const first = await store.load(s.instanceId);
|
|
167
|
+
assertTrue(first !== null, "load \u8FD4\u56DE null");
|
|
168
|
+
first.status = "terminated";
|
|
169
|
+
first.variables.leak = true;
|
|
170
|
+
const second = await store.load(s.instanceId);
|
|
171
|
+
assertTrue(second !== null, "load \u8FD4\u56DE null");
|
|
172
|
+
assertTrue(
|
|
173
|
+
second.status === "running",
|
|
174
|
+
`load() \u672A\u4EA4\u51FA\u526F\u672C\uFF1A\u5BBF\u4E3B\u7684\u6539\u52A8\u6CC4\u6F0F\u8FDB\u4E86\u5E93\uFF08status \u5B9E\u5F97 ${second.status}\uFF09`
|
|
175
|
+
);
|
|
176
|
+
assertTrue(
|
|
177
|
+
second.variables.leak === void 0,
|
|
178
|
+
`load() \u672A\u505A\u6DF1\u62F7\u8D1D\uFF1A\u5D4C\u5957\u5B57\u6BB5\u88AB\u6CC4\u6F0F\uFF08variables.leak \u5B9E\u5F97 ${show(second.variables.leak)}\uFF09`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
[
|
|
183
|
+
"save() \u5B58\u5165\u526F\u672C\uFF1Asave \u4E4B\u540E\u6539 next \u4E0D\u5F97\u5F71\u54CD\u5DF2\u63D0\u4EA4\u72B6\u6001",
|
|
184
|
+
["\xA79 T5"],
|
|
185
|
+
async () => {
|
|
186
|
+
const s = makeState(newId(), { businessKey: "BK_ORIGINAL" });
|
|
187
|
+
await store.save(s, 0);
|
|
188
|
+
s.businessKey = "BK_MUTATED_AFTER_SAVE";
|
|
189
|
+
s.variables.amount = 999;
|
|
190
|
+
const got = await store.load(s.instanceId);
|
|
191
|
+
assertTrue(got !== null, "load \u8FD4\u56DE null");
|
|
192
|
+
assertTrue(
|
|
193
|
+
got.businessKey === "BK_ORIGINAL",
|
|
194
|
+
`save() \u5B58\u4E86\u5F15\u7528\u800C\u975E\u526F\u672C\uFF08businessKey \u5B9E\u5F97 ${show(got.businessKey)}\uFF09`
|
|
195
|
+
);
|
|
196
|
+
assertTrue(
|
|
197
|
+
got.variables.amount === 100,
|
|
198
|
+
`save() \u672A\u505A\u6DF1\u62F7\u8D1D\uFF1A\u5D4C\u5957\u5B57\u6BB5\u88AB\u6539\uFF08variables.amount \u5B9E\u5F97 ${show(got.variables.amount)}\uFF09`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
],
|
|
202
|
+
[
|
|
203
|
+
"load() \u8FD4\u56DE\u503C JSON \u5F80\u8FD4\u6DF1\u7B49\uFF08INV-14\uFF1A\u5E93\u5185\u53EA\u5141\u8BB8\u7EAF\u6570\u636E\uFF09",
|
|
204
|
+
["INV-14", "AC-E8"],
|
|
205
|
+
async () => {
|
|
206
|
+
const s = makeState(newId());
|
|
207
|
+
await store.save(s, 0);
|
|
208
|
+
const got = await store.load(s.instanceId);
|
|
209
|
+
assertTrue(got !== null, "load \u8FD4\u56DE null");
|
|
210
|
+
assertDeepEqual(JSON.parse(JSON.stringify(got)), got, "JSON \u5F80\u8FD4");
|
|
211
|
+
}
|
|
212
|
+
],
|
|
213
|
+
// ── INSERT 冲突 ──
|
|
214
|
+
[
|
|
215
|
+
"\u91CD\u590D INSERT \u629B PERSIST_ALREADY_EXISTS\uFF0C\u4E14\u4E0D\u8986\u76D6\u539F\u8BB0\u5F55\u3001\u4E0D\u6D88\u8D39 rev",
|
|
216
|
+
["AC-E11"],
|
|
217
|
+
async () => {
|
|
218
|
+
const first = makeState(newId(), { businessKey: "BK_1" });
|
|
219
|
+
await store.save(first, 0);
|
|
220
|
+
const second = makeState(first.instanceId, { businessKey: "BK_2" });
|
|
221
|
+
await catchEngineError(
|
|
222
|
+
() => store.save(second, 0),
|
|
223
|
+
exists,
|
|
224
|
+
"\u540C id \u4E8C\u6B21 INSERT"
|
|
225
|
+
);
|
|
226
|
+
const got = await store.load(first.instanceId);
|
|
227
|
+
assertTrue(got !== null, "load \u8FD4\u56DE null");
|
|
228
|
+
assertTrue(
|
|
229
|
+
got.businessKey === "BK_1",
|
|
230
|
+
`ALREADY_EXISTS \u4E4B\u540E\u539F\u8BB0\u5F55\u88AB\u8986\u76D6\uFF08businessKey \u5B9E\u5F97 ${show(got.businessKey)}\uFF09`
|
|
231
|
+
);
|
|
232
|
+
assertTrue(got.rev === 1, `\u5931\u8D25\u8DEF\u5F84\u4E0D\u5F97\u6D88\u8D39 rev\uFF08\u5B9E\u5F97 ${got.rev}\uFF09`);
|
|
233
|
+
}
|
|
234
|
+
],
|
|
235
|
+
// ── CAS 路径 ──
|
|
236
|
+
[
|
|
237
|
+
"CAS \u6210\u529F\uFF1AexpectedRev \u547D\u4E2D \u2192 \u5199\u5165\u751F\u6548\u4E14 rev \u6070\u597D +1",
|
|
238
|
+
["INV-1", "AC-E11"],
|
|
239
|
+
async () => {
|
|
240
|
+
const s = makeState(newId());
|
|
241
|
+
await store.save(s, 0);
|
|
242
|
+
await store.save({ ...s, businessKey: "BK_2" }, 1);
|
|
243
|
+
const got = await store.load(s.instanceId);
|
|
244
|
+
assertTrue(got !== null, "load \u8FD4\u56DE null");
|
|
245
|
+
assertTrue(got.rev === 2, `CAS \u540E rev \u5E94\u4E3A 2\uFF08\u5B9E\u5F97 ${got.rev}\uFF09`);
|
|
246
|
+
assertTrue(
|
|
247
|
+
got.businessKey === "BK_2",
|
|
248
|
+
`CAS \u5185\u5BB9\u672A\u751F\u6548\uFF08businessKey \u5B9E\u5F97 ${show(got.businessKey)}\uFF09`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
],
|
|
252
|
+
[
|
|
253
|
+
"CAS \u5931\u8D25\uFF08rev \u843D\u540E\uFF09\u629B PERSIST_CONFLICT\uFF0C\u4E14\u5E93\u5185 rev \u4E0E\u5185\u5BB9\u90FD\u4E0D\u53D8",
|
|
254
|
+
["INV-1", "AC-E11"],
|
|
255
|
+
async () => {
|
|
256
|
+
const s = makeState(newId());
|
|
257
|
+
await store.save(s, 0);
|
|
258
|
+
await store.save({ ...s, businessKey: "BK_2" }, 1);
|
|
259
|
+
const err = await catchEngineError(
|
|
260
|
+
() => store.save({ ...s, businessKey: "BK_STALE" }, 1),
|
|
261
|
+
conflict,
|
|
262
|
+
"\u843D\u540E rev \u7684 CAS"
|
|
263
|
+
);
|
|
264
|
+
assertDeepEqual(err.details?.actualRev, 2, "details.actualRev");
|
|
265
|
+
const got = await store.load(s.instanceId);
|
|
266
|
+
assertTrue(got !== null, "load \u8FD4\u56DE null");
|
|
267
|
+
assertTrue(got.rev === 2, `CONFLICT \u540E rev \u4E0D\u5F97\u53D8\uFF08\u5B9E\u5F97 ${got.rev}\uFF09`);
|
|
268
|
+
assertTrue(
|
|
269
|
+
got.businessKey === "BK_2",
|
|
270
|
+
`CONFLICT \u540E\u5185\u5BB9\u4E0D\u5F97\u53D8\uFF08businessKey \u5B9E\u5F97 ${show(got.businessKey)}\uFF09`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
],
|
|
274
|
+
[
|
|
275
|
+
"CAS \u5931\u8D25\uFF08rev \u8D85\u524D\uFF09\u540C\u6837\u629B PERSIST_CONFLICT\uFF08\u4E0D\u5F97\u5F53\u6210\u6210\u529F\uFF09",
|
|
276
|
+
["INV-1", "AC-E11"],
|
|
277
|
+
async () => {
|
|
278
|
+
const s = makeState(newId());
|
|
279
|
+
await store.save(s, 0);
|
|
280
|
+
const err = await catchEngineError(
|
|
281
|
+
() => store.save({ ...s, businessKey: "BK_AHEAD" }, 5),
|
|
282
|
+
conflict,
|
|
283
|
+
"\u8D85\u524D rev \u7684 CAS"
|
|
284
|
+
);
|
|
285
|
+
assertDeepEqual(err.details?.actualRev, 1, "details.actualRev");
|
|
286
|
+
const got = await store.load(s.instanceId);
|
|
287
|
+
assertTrue(got !== null, "load \u8FD4\u56DE null");
|
|
288
|
+
assertTrue(got.rev === 1, `CONFLICT \u540E rev \u4E0D\u5F97\u53D8\uFF08\u5B9E\u5F97 ${got.rev}\uFF09`);
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
[
|
|
292
|
+
"\u5B9E\u4F8B\u4E0D\u5B58\u5728 + expectedRev > 0 \u2192 \u629B PERSIST_CONFLICT\uFF08\u4E14\u4E0D\u5EFA\u884C\u3001details \u65E0 actualRev\uFF09",
|
|
293
|
+
["INV-1", "AC-E11"],
|
|
294
|
+
async () => {
|
|
295
|
+
const s = makeState(newId());
|
|
296
|
+
const err = await catchEngineError(
|
|
297
|
+
() => store.save(s, 3),
|
|
298
|
+
conflict,
|
|
299
|
+
"\u672A\u77E5\u5B9E\u4F8B\u7684 CAS"
|
|
300
|
+
);
|
|
301
|
+
assertTrue(
|
|
302
|
+
!("actualRev" in (err.details ?? {})),
|
|
303
|
+
`\u5B9E\u4F8B\u4E0D\u5B58\u5728\u65F6 details \u4E0D\u5E94\u5E26 actualRev\uFF08\u5B9E\u5F97 ${show(err.details)}\uFF09`
|
|
304
|
+
);
|
|
305
|
+
assertTrue(await store.load(s.instanceId) === null, "CONFLICT \u4E0D\u5F97\u987A\u5E26\u5EFA\u884C");
|
|
306
|
+
}
|
|
307
|
+
],
|
|
308
|
+
[
|
|
309
|
+
"\u5E76\u53D1\u540C rev \u7684\u4E24\u4E2A save\uFF1A\u6070\u4E00\u80DC\u4E00\u8D25\uFF08CAS \u539F\u5B50\u6027\uFF0CNFR-E5\uFF09",
|
|
310
|
+
["INV-1", "NFR-E5"],
|
|
311
|
+
async () => {
|
|
312
|
+
const s = makeState(newId());
|
|
313
|
+
await store.save(s, 0);
|
|
314
|
+
const results = await Promise.allSettled([
|
|
315
|
+
store.save({ ...s, businessKey: "A" }, 1),
|
|
316
|
+
store.save({ ...s, businessKey: "B" }, 1)
|
|
317
|
+
]);
|
|
318
|
+
const won = results.filter((r) => r.status === "fulfilled").length;
|
|
319
|
+
const lost = results.filter((r) => r.status === "rejected").length;
|
|
320
|
+
assertTrue(
|
|
321
|
+
won === 1 && lost === 1,
|
|
322
|
+
`\u540C\u4E00 rev \u7684\u5E76\u53D1 save \u5FC5\u987B\u6070\u4E00\u80DC\u4E00\u8D25\uFF08\u5B9E\u5F97 ${won} \u80DC ${lost} \u8D25\uFF09\u2014\u2014 \u5168\u80DC = \u9759\u9ED8\u8986\u76D6`
|
|
323
|
+
);
|
|
324
|
+
const rejected = results.find((r) => r.status === "rejected");
|
|
325
|
+
assertTrue(rejected !== void 0, "\u6CA1\u6709\u5931\u8D25\u7684 save");
|
|
326
|
+
assertEngineErrorValue(rejected.reason, conflict, "\u5E76\u53D1 save \u7684\u8D25\u8005");
|
|
327
|
+
const got = await store.load(s.instanceId);
|
|
328
|
+
assertTrue(got !== null, "load \u8FD4\u56DE null");
|
|
329
|
+
assertTrue(got.rev === 2, `CAS \u53EA\u5E94\u53D1\u751F\u4E00\u6B21\uFF0Crev \u5E94\u4E3A 2\uFF08\u5B9E\u5F97 ${got.rev}\uFF09`);
|
|
330
|
+
const winner = results[0]?.status === "fulfilled" ? "A" : "B";
|
|
331
|
+
assertTrue(
|
|
332
|
+
got.businessKey === winner,
|
|
333
|
+
`\u5E93\u5185\u5E94\u4E3A\u80DC\u8005\u7684\u5185\u5BB9\uFF08\u671F\u671B ${winner}\uFF0C\u5B9E\u5F97 ${show(got.businessKey)}\uFF09`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
],
|
|
337
|
+
// ── 写入前置校验 ──
|
|
338
|
+
[
|
|
339
|
+
"\u5199\u5165\u975E\u7EAF\u6570\u636E\u5FC5\u987B\u629B\u9519\uFF0C\u4E14\u4E0D\u6C61\u67D3\u5E93\uFF08AC-E8 / INV-14\uFF09",
|
|
340
|
+
["AC-E8", "INV-14"],
|
|
341
|
+
async () => {
|
|
342
|
+
const s = makeState(newId());
|
|
343
|
+
const dirty = { ...s, variables: { bad: () => 1 } };
|
|
344
|
+
await catchEngineError(() => store.save(dirty, 0), "ENGINE_STATE_SHAPE_INVALID", "\u5199\u5165\u542B\u51FD\u6570\u7684 state");
|
|
345
|
+
assertTrue(
|
|
346
|
+
await store.load(s.instanceId) === null,
|
|
347
|
+
"\u810F\u6570\u636E\u4E0D\u5F97\u5165\u5E93\uFF1A\u5199\u5165\u524D\u5C31\u8BE5\u62D2\u7EDD\uFF0C\u800C\u4E0D\u662F\u8BA9\u5E93\u91CC\u51FA\u73B0\u975E\u7EAF\u6570\u636E"
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
]
|
|
351
|
+
]);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/conformance/projection.ts
|
|
355
|
+
var TS2 = "2026-09-30T00:00:00.000Z";
|
|
356
|
+
var seq2 = 0;
|
|
357
|
+
function uniqueId2(prefix) {
|
|
358
|
+
seq2 += 1;
|
|
359
|
+
return `${prefix}_p${seq2.toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
360
|
+
}
|
|
361
|
+
function header(instanceId, rev) {
|
|
362
|
+
return {
|
|
363
|
+
instanceId,
|
|
364
|
+
processId: "expense",
|
|
365
|
+
definitionVersion: 1,
|
|
366
|
+
status: "running",
|
|
367
|
+
rev,
|
|
368
|
+
stateSchema: STATE_SCHEMA_VERSION,
|
|
369
|
+
startedAt: TS2,
|
|
370
|
+
updatedAt: TS2
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function task(args) {
|
|
374
|
+
return {
|
|
375
|
+
taskId: args.taskId,
|
|
376
|
+
instanceId: args.instanceId,
|
|
377
|
+
nodeId: args.nodeId ?? "Task_Approve",
|
|
378
|
+
assignee: args.assignee ?? "u_1",
|
|
379
|
+
status: args.status ?? "active",
|
|
380
|
+
createdAt: TS2
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function delta(instanceId, over = {}) {
|
|
384
|
+
const rev = over.rev ?? 1;
|
|
385
|
+
return {
|
|
386
|
+
rev,
|
|
387
|
+
action: over.action ?? { name: "approve", actor: "u_1", at: TS2 },
|
|
388
|
+
added: over.added ?? [],
|
|
389
|
+
removed: over.removed ?? [],
|
|
390
|
+
changed: over.changed ?? [],
|
|
391
|
+
instance: header(instanceId, rev)
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function index(tasks) {
|
|
395
|
+
return new Map(tasks.map((t) => [t.taskId, t]));
|
|
396
|
+
}
|
|
397
|
+
function assertSameTask(actual, expected, what) {
|
|
398
|
+
assertTrue(actual !== void 0, `${what}\uFF1AtaskId=${expected.taskId} \u8BFB\u4E0D\u56DE\u6765\uFF08\u53EF\u80FD\u88AB\u8BEF\u5F53\u6210 removed\uFF1F\uFF09`);
|
|
399
|
+
assertDeepEqual(
|
|
400
|
+
{
|
|
401
|
+
taskId: actual.taskId,
|
|
402
|
+
instanceId: actual.instanceId,
|
|
403
|
+
nodeId: actual.nodeId,
|
|
404
|
+
assignee: actual.assignee,
|
|
405
|
+
status: actual.status,
|
|
406
|
+
createdAt: actual.createdAt
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
taskId: expected.taskId,
|
|
410
|
+
instanceId: expected.instanceId,
|
|
411
|
+
nodeId: expected.nodeId,
|
|
412
|
+
assignee: expected.assignee,
|
|
413
|
+
status: expected.status,
|
|
414
|
+
createdAt: expected.createdAt
|
|
415
|
+
},
|
|
416
|
+
what
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
function assertRows(actual, expected, what) {
|
|
420
|
+
const got = index(actual);
|
|
421
|
+
const want = index(expected);
|
|
422
|
+
assertDeepEqual(
|
|
423
|
+
[...got.keys()].sort(),
|
|
424
|
+
[...want.keys()].sort(),
|
|
425
|
+
`${what}\uFF08\u53EF\u89C1 taskId \u96C6\u5408\uFF09`
|
|
426
|
+
);
|
|
427
|
+
for (const t of want.values()) assertSameTask(got.get(t.taskId), t, what);
|
|
428
|
+
}
|
|
429
|
+
async function runProjectionConformance(projection, readback, options = {}) {
|
|
430
|
+
const prefix = options.idPrefix ?? "floken-conf";
|
|
431
|
+
const newId = () => uniqueId2(prefix);
|
|
432
|
+
return runConformanceCases("projection", options.subject, [
|
|
433
|
+
[
|
|
434
|
+
"apply \u589E\u884C\uFF1Aadded \u6BCF\u884C\u90FD\u80FD\u6309 taskId \u8BFB\u56DE\uFF0C\u5FC5\u586B\u5B57\u6BB5\u4E00\u81F4",
|
|
435
|
+
["\xA77.2"],
|
|
436
|
+
async () => {
|
|
437
|
+
const id = newId();
|
|
438
|
+
const a = task({ instanceId: id, taskId: "T_a", assignee: "u_1" });
|
|
439
|
+
const b = task({ instanceId: id, taskId: "T_b", assignee: "u_2" });
|
|
440
|
+
await projection.apply(id, delta(id, { added: [a, b] }));
|
|
441
|
+
assertRows(await readback(id), [a, b], "apply \u540E\u7684\u5F85\u529E");
|
|
442
|
+
}
|
|
443
|
+
],
|
|
444
|
+
[
|
|
445
|
+
"apply \u5E42\u7B49\uFF1A\u540C\u4E00 delta \u8FDE\u7EED\u4E24\u6B21 \u2192 \u5F85\u529E\u96C6\u5408\u4E0D\u53D8\uFF08AC-E12\uFF09",
|
|
446
|
+
["AC-E12", "INV-15"],
|
|
447
|
+
async () => {
|
|
448
|
+
const id = newId();
|
|
449
|
+
const a = task({ instanceId: id, taskId: "T_a" });
|
|
450
|
+
const b = task({ instanceId: id, taskId: "T_b" });
|
|
451
|
+
const d = delta(id, { added: [a, b] });
|
|
452
|
+
const before = JSON.stringify(d);
|
|
453
|
+
await projection.apply(id, d);
|
|
454
|
+
const once = await readback(id);
|
|
455
|
+
await projection.apply(id, d);
|
|
456
|
+
const twice = await readback(id);
|
|
457
|
+
assertRows(once, [a, b], "\u9996\u6B21 apply");
|
|
458
|
+
assertRows(twice, [a, b], "\u91CD\u590D apply");
|
|
459
|
+
assertDeepEqual(twice.length, once.length, "\u91CD\u590D apply \u540E\u7684\u884C\u6570\uFF08\u4E0D\u5F97\u7FFB\u500D\uFF09");
|
|
460
|
+
assertTrue(JSON.stringify(d) === before, "\u6295\u5F71\u4E0D\u5F97\u4FEE\u6539\u4F20\u5165\u7684 delta\uFF08\u5E94\u89C6\u4E3A\u53EA\u8BFB\uFF09");
|
|
461
|
+
}
|
|
462
|
+
],
|
|
463
|
+
[
|
|
464
|
+
"apply \u771F\u5220\uFF1Aremoved \u91CC\u7684 taskId \u5FC5\u987B\u67E5\u4E0D\u5230\uFF08INV-15 \xB7 \u5934\u53F7\u9759\u9ED8\u9519\u8BEF\uFF09",
|
|
465
|
+
["INV-15", "AC-E12"],
|
|
466
|
+
async () => {
|
|
467
|
+
const id = newId();
|
|
468
|
+
const a = task({ instanceId: id, taskId: "T_a" });
|
|
469
|
+
const b = task({ instanceId: id, taskId: "T_b" });
|
|
470
|
+
await projection.apply(id, delta(id, { added: [a, b] }));
|
|
471
|
+
await projection.apply(id, delta(id, { rev: 2, removed: ["T_a"] }));
|
|
472
|
+
const rows = await readback(id);
|
|
473
|
+
assertTrue(
|
|
474
|
+
!index(rows).has("T_a"),
|
|
475
|
+
`removed \u91CC\u7684 taskId \u4ECD\u80FD\u8BFB\u5230 \u2014\u2014 \u6295\u5F71\u6CA1\u5B9E\u73B0\u771F\u5220\uFF08\u8BFB\u56DE ${show(rows.map((t) => t.taskId))}\uFF09`
|
|
476
|
+
);
|
|
477
|
+
assertRows(rows, [b], "\u5220\u9664\u540E\u7684\u5F85\u529E");
|
|
478
|
+
}
|
|
479
|
+
],
|
|
480
|
+
[
|
|
481
|
+
"\u540C\u4E00 delta \u5185 added \u4E0E removed \u5E76\u5B58\uFF1A\u4E24\u8005\u90FD\u751F\u6548",
|
|
482
|
+
["INV-15"],
|
|
483
|
+
async () => {
|
|
484
|
+
const id = newId();
|
|
485
|
+
const oldTask = task({ instanceId: id, taskId: "T_old" });
|
|
486
|
+
await projection.apply(id, delta(id, { added: [oldTask] }));
|
|
487
|
+
const fresh = task({ instanceId: id, taskId: "T_new", assignee: "u_9" });
|
|
488
|
+
await projection.apply(
|
|
489
|
+
id,
|
|
490
|
+
delta(id, { rev: 2, added: [fresh], removed: ["T_old"] })
|
|
491
|
+
);
|
|
492
|
+
assertRows(await readback(id), [fresh], "\u6DF7\u5408 delta \u540E\u7684\u5F85\u529E");
|
|
493
|
+
}
|
|
494
|
+
],
|
|
495
|
+
[
|
|
496
|
+
"changed \u751F\u6548\uFF1A\u72B6\u6001\u88AB\u66F4\u65B0\uFF0C\u800C\u4E0D\u662F\u88AB\u5FFD\u7565",
|
|
497
|
+
["\xA77.2", "AC-E12"],
|
|
498
|
+
async () => {
|
|
499
|
+
const id = newId();
|
|
500
|
+
const active = task({ instanceId: id, taskId: "T_a", status: "active" });
|
|
501
|
+
await projection.apply(id, delta(id, { added: [active] }));
|
|
502
|
+
const done = { ...active, status: "done" };
|
|
503
|
+
await projection.apply(id, delta(id, { rev: 2, changed: [done] }));
|
|
504
|
+
assertRows(await readback(id), [done], "changed \u540E\u7684\u5F85\u529E");
|
|
505
|
+
}
|
|
506
|
+
],
|
|
507
|
+
[
|
|
508
|
+
"\u7A7A delta\uFF1A\u4E0D\u5F97\u4EA7\u751F\u4EFB\u4F55\u526F\u4F5C\u7528",
|
|
509
|
+
["\xA76.2"],
|
|
510
|
+
async () => {
|
|
511
|
+
const id = newId();
|
|
512
|
+
const a = task({ instanceId: id, taskId: "T_a" });
|
|
513
|
+
await projection.apply(id, delta(id, { added: [a] }));
|
|
514
|
+
const before = await readback(id);
|
|
515
|
+
await projection.apply(id, delta(id, { rev: 2 }));
|
|
516
|
+
assertRows(await readback(id), before, "\u7A7A delta \u540E\u7684\u5F85\u529E");
|
|
517
|
+
}
|
|
518
|
+
],
|
|
519
|
+
[
|
|
520
|
+
"sync \u5168\u91CF\u5BF9\u8D26\uFF1A\u8BFB\u56DE\u4E0E\u4F20\u5165 tasks \u4E00\u81F4\uFF0C\u591A\u4F59\u884C\u88AB\u6E05\u7406\uFF08INV-18\uFF09",
|
|
521
|
+
["INV-18"],
|
|
522
|
+
async () => {
|
|
523
|
+
const id = newId();
|
|
524
|
+
await projection.apply(
|
|
525
|
+
id,
|
|
526
|
+
delta(id, {
|
|
527
|
+
added: [
|
|
528
|
+
task({ instanceId: id, taskId: "T_stale1" }),
|
|
529
|
+
task({ instanceId: id, taskId: "T_stale2" })
|
|
530
|
+
]
|
|
531
|
+
})
|
|
532
|
+
);
|
|
533
|
+
const truth = task({ instanceId: id, taskId: "T_truth", assignee: "u_7" });
|
|
534
|
+
await projection.sync(id, [truth]);
|
|
535
|
+
assertRows(
|
|
536
|
+
await readback(id),
|
|
537
|
+
[truth],
|
|
538
|
+
'sync \u540E\u7684\u5F85\u529E\uFF08"\u5168\u91CF\u5BF9\u8D26"= \u4EE5\u4F20\u5165\u5217\u8868\u4E3A\u51C6\uFF1A\u591A\u4F59\u884C\u5FC5\u987B\u6E05\u6389\uFF0C\u5426\u5219\u8865\u505A\u7B49\u4E8E\u6CA1\u505A\uFF09'
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
],
|
|
542
|
+
[
|
|
543
|
+
"sync \u4F20\u7A7A\u6570\u7EC4 \u2192 \u8BE5\u5B9E\u4F8B\u5F85\u529E\u6E05\u7A7A",
|
|
544
|
+
["INV-18"],
|
|
545
|
+
async () => {
|
|
546
|
+
const id = newId();
|
|
547
|
+
await projection.apply(id, delta(id, { added: [task({ instanceId: id, taskId: "T_a" })] }));
|
|
548
|
+
await projection.sync(id, []);
|
|
549
|
+
assertRows(await readback(id), [], "sync \u7A7A\u5217\u8868\u540E\u7684\u5F85\u529E");
|
|
550
|
+
}
|
|
551
|
+
],
|
|
552
|
+
[
|
|
553
|
+
"\u8DE8\u5B9E\u4F8B\u9694\u79BB\uFF1Aapply \u4E00\u4E2A\u5B9E\u4F8B\u4E0D\u5F97\u5F71\u54CD\u53E6\u4E00\u4E2A",
|
|
554
|
+
["\xA77.2"],
|
|
555
|
+
async () => {
|
|
556
|
+
const idA = newId();
|
|
557
|
+
const idB = newId();
|
|
558
|
+
const a = task({ instanceId: idA, taskId: "T_a" });
|
|
559
|
+
const b = task({ instanceId: idB, taskId: "T_b" });
|
|
560
|
+
await projection.apply(idA, delta(idA, { added: [a] }));
|
|
561
|
+
await projection.apply(idB, delta(idB, { added: [b] }));
|
|
562
|
+
assertRows(await readback(idA), [a], "\u5B9E\u4F8B A \u7684\u5F85\u529E");
|
|
563
|
+
assertRows(await readback(idB), [b], "\u5B9E\u4F8B B \u7684\u5F85\u529E");
|
|
564
|
+
}
|
|
565
|
+
]
|
|
566
|
+
]);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/conformance/definition.ts
|
|
570
|
+
var UNKNOWN_PROCESS_ID = "__floken_conf_no_such_process__";
|
|
571
|
+
async function runDefinitionConformance(source, fixtures, options = {}) {
|
|
572
|
+
const byProcess = /* @__PURE__ */ new Map();
|
|
573
|
+
for (const f of fixtures) {
|
|
574
|
+
const list = byProcess.get(f.processId);
|
|
575
|
+
if (list === void 0) byProcess.set(f.processId, [f]);
|
|
576
|
+
else list.push(f);
|
|
577
|
+
}
|
|
578
|
+
const multi = [...byProcess.values()].filter((l) => l.length >= 2);
|
|
579
|
+
function requirePair() {
|
|
580
|
+
const first = multi[0];
|
|
581
|
+
if (first === void 0) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
`fixtures \u4E0D\u5408\u6CD5\uFF1A\u987B\u542B\u540C\u4E00 processId \u7684 \u22652 \u4E2A\u7248\u672C\uFF08\u5B9E\u5F97 ${fixtures.length} \u6761 / ${byProcess.size} \u4E2A processId\uFF09`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
return first;
|
|
587
|
+
}
|
|
588
|
+
return runConformanceCases("definition", options.subject, [
|
|
589
|
+
// ── 输入自检(套件不会对自己的输入装瞎) ──
|
|
590
|
+
[
|
|
591
|
+
"fixtures \u81EA\u68C0\uFF1A\u987B\u542B\u540C\u4E00 processId \u7684 \u22652 \u4E2A\u7248\u672C\uFF0C\u4E14\u5185\u5BB9\u4E92\u4E0D\u76F8\u540C",
|
|
592
|
+
["\xA77.2"],
|
|
593
|
+
async () => {
|
|
594
|
+
const first = requirePair();
|
|
595
|
+
for (let i = 0; i < first.length; i += 1) {
|
|
596
|
+
for (let j = i + 1; j < first.length; j += 1) {
|
|
597
|
+
const a = first[i];
|
|
598
|
+
const b = first[j];
|
|
599
|
+
assertTrue(
|
|
600
|
+
a !== void 0 && b !== void 0 && !deepEqual(a.definition, b.definition),
|
|
601
|
+
`fixtures \u91CC ${a?.processId}@${a?.version} \u4E0E @${b?.version} \u5185\u5BB9\u76F8\u540C \u2014\u2014 \u7248\u672C\u95F4\u5FC5\u987B\u6709\u5DEE\u5F02\uFF0C\u5426\u5219\u300C\u5FFD\u7565 version\u300D\u65E0\u6CD5\u88AB\u89C2\u6D4B`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
],
|
|
607
|
+
// ── ① 版本精确命中 ──
|
|
608
|
+
[
|
|
609
|
+
"\u9010\u7248\u672C\u7CBE\u786E\u547D\u4E2D\uFF1AgetDefinition(pid, v) \u53D6\u56DE\u7684\u5185\u5BB9\u4E0E\u5BBF\u4E3B\u58F0\u660E\u7684\u6DF1\u7B49",
|
|
610
|
+
["\xA77.2", "AC-E10"],
|
|
611
|
+
async () => {
|
|
612
|
+
for (const f of fixtures) {
|
|
613
|
+
const got = await source.getDefinition(f.processId, f.version);
|
|
614
|
+
assertTrue(
|
|
615
|
+
got !== null,
|
|
616
|
+
`${f.processId}@${f.version} \u53D6\u56DE null\uFF08\u5BBF\u4E3B\u58F0\u660E\u4E86\u8FD9\u4E00\u683C\uFF0C\u5B9E\u73B0\u5374\u8BF4\u6CA1\u6709\uFF09`
|
|
617
|
+
);
|
|
618
|
+
assertDeepEqual(got, f.definition, `${f.processId}@${f.version} \u7684\u5185\u5BB9`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
],
|
|
622
|
+
// ── ★ 头号目标:version 必须真的被用上 ──
|
|
623
|
+
[
|
|
624
|
+
"\u2605 \u540C\u4E00 processId \u7684\u4E0D\u540C version \u8FD4\u56DE\u4E0D\u540C\u5185\u5BB9\uFF08\u9632\u300C\u5FFD\u7565 version \u53C2\u6570\u300D\u7684\u9759\u9ED8\u9519\u8BEF\uFF09",
|
|
625
|
+
["AC-E10", "INV-19"],
|
|
626
|
+
async () => {
|
|
627
|
+
const group = requirePair();
|
|
628
|
+
for (let i = 0; i < group.length; i += 1) {
|
|
629
|
+
for (let j = i + 1; j < group.length; j += 1) {
|
|
630
|
+
const a = group[i];
|
|
631
|
+
const b = group[j];
|
|
632
|
+
assertTrue(a !== void 0 && b !== void 0, "fixtures \u7F3A\u5931");
|
|
633
|
+
const ga = await source.getDefinition(a.processId, a.version);
|
|
634
|
+
const gb = await source.getDefinition(b.processId, b.version);
|
|
635
|
+
assertTrue(
|
|
636
|
+
!deepEqual(ga, gb),
|
|
637
|
+
`${a.processId}@${a.version} \u4E0E @${b.version} \u53D6\u56DE\u4E86\u76F8\u540C\u5185\u5BB9 \u2014\u2014 getDefinition \u591A\u534A\u5FFD\u7565\u4E86 version \u53C2\u6570\uFF08\u5728\u9014\u5B9E\u4F8B\u4F1A\u8DD1\u5230\u53D1\u8D77\u65F6\u4E0D\u5B58\u5728\u7684\u8282\u70B9\u4E0A\uFF09`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
],
|
|
643
|
+
// ── ② 不存在 = null(不得抛、不得回退) ──
|
|
644
|
+
[
|
|
645
|
+
`\u672A\u77E5 processId \u2192 null\uFF08\u4E0D\u5F97\u629B\u9519\uFF0C\u4E5F\u4E0D\u5F97\u8FD4\u56DE\u4EFB\u4E00\u5DF2\u6709\u7248\u672C\uFF09`,
|
|
646
|
+
["\xA77.2"],
|
|
647
|
+
async () => {
|
|
648
|
+
const got = await source.getDefinition(UNKNOWN_PROCESS_ID, 1);
|
|
649
|
+
assertTrue(
|
|
650
|
+
got === null,
|
|
651
|
+
`\u672A\u77E5 processId \u671F\u671B null\uFF0C\u5B9E\u5F97 ${show(got)} \u2014\u2014 \u8FD4\u56DE\u4EFB\u4E00\u7248\u672C\u4F1A\u8BA9\u5F15\u64CE\u62FF\u5230\u9519\u8BEF\u7684\u56FE\u7EB8`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
],
|
|
655
|
+
[
|
|
656
|
+
"\u2605 \u672A\u77E5 version \u2192 null\uFF08\u4E0D\u5F97\u56DE\u9000\u5230\u6700\u65B0\u7248 / \u4EFB\u4E00\u5DF2\u6709\u7248\u672C\uFF09",
|
|
657
|
+
["AC-E10", "INV-19"],
|
|
658
|
+
async () => {
|
|
659
|
+
const group = requirePair();
|
|
660
|
+
const versions = group.map((f) => f.version);
|
|
661
|
+
const max = Math.max(...versions);
|
|
662
|
+
const min = Math.min(...versions);
|
|
663
|
+
const probes = [max + 1, ...min > 1 ? [min - 1] : []];
|
|
664
|
+
for (const v of probes) {
|
|
665
|
+
const got = await source.getDefinition(group[0]?.processId ?? "", v);
|
|
666
|
+
assertTrue(
|
|
667
|
+
got === null,
|
|
668
|
+
`@${v} \u671F\u671B null\uFF08\u8FD9\u4E00\u7248\u4E0D\u5B58\u5728\uFF09\uFF0C\u5B9E\u5F97 ${show(got)} \u2014\u2014 \u56DE\u9000\u5230\u522B\u7684\u7248\u672C = \u5728\u9014\u5B9E\u4F8B\u88AB\u9759\u9ED8\u6362\u6210\u53E6\u4E00\u5957\u56FE\u7EB8`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
],
|
|
673
|
+
// ── ③ 取回不得有副作用 / 不得交共享可变对象 ──
|
|
674
|
+
[
|
|
675
|
+
"\u91CD\u590D\u53D6\u56DE\u540C\u4E00 (pid, v) \u5185\u5BB9\u4E0D\u53D8\uFF1B\u53D6\u8FC7\u522B\u7684\u7248\u672C\u540E\u56DE\u6765\u4ECD\u4E0D\u53D8\uFF08\u4E0D\u5F97\u4EA4\u51FA\u4F1A\u88AB\u540E\u7EED\u8C03\u7528\u8986\u76D6\u7684\u5BF9\u8C61\uFF09",
|
|
676
|
+
["AC-E10"],
|
|
677
|
+
async () => {
|
|
678
|
+
const f = fixtures[0];
|
|
679
|
+
assertTrue(f !== void 0, "fixtures \u4E3A\u7A7A");
|
|
680
|
+
const first = await source.getDefinition(f.processId, f.version);
|
|
681
|
+
assertDeepEqual(first, f.definition, `\u9996\u6B21\u53D6\u56DE ${f.processId}@${f.version}`);
|
|
682
|
+
const other = fixtures[fixtures.length - 1];
|
|
683
|
+
if (other !== void 0) await source.getDefinition(other.processId, other.version);
|
|
684
|
+
const again = await source.getDefinition(f.processId, f.version);
|
|
685
|
+
assertDeepEqual(
|
|
686
|
+
again,
|
|
687
|
+
f.definition,
|
|
688
|
+
`\u518D\u6B21\u53D6\u56DE ${f.processId}@${f.version}\uFF08\u53D6\u8FC7\u522B\u7684\u7248\u672C\u540E\uFF0C\u5185\u5BB9\u88AB\u6539\u5199\u4E86\uFF09`
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
],
|
|
692
|
+
// ── ④ 异常入参不得炸(引擎已保证 ≥1 整数,宿主只需返回 null) ──
|
|
693
|
+
[
|
|
694
|
+
"\u975E\u6CD5 version\uFF080 / -1 / \u5C0F\u6570 / NaN\uFF09\u2192 null \u4E14\u4E0D\u5F97\u629B\u9519\uFF08\u9632\u300C\u5C31\u8FD1\u53D6\u6574\u300D\uFF09",
|
|
695
|
+
["\xA77.2"],
|
|
696
|
+
async () => {
|
|
697
|
+
const f = fixtures[0];
|
|
698
|
+
assertTrue(f !== void 0, "fixtures \u4E3A\u7A7A");
|
|
699
|
+
for (const bad of [0, -1, 1.5, Number.NaN]) {
|
|
700
|
+
const got = await source.getDefinition(f.processId, bad);
|
|
701
|
+
assertTrue(
|
|
702
|
+
got === null,
|
|
703
|
+
`version=${bad} \u671F\u671B null\uFF0C\u5B9E\u5F97 ${show(got)} \u2014\u2014 \u5C31\u8FD1\u53D6\u6574\u4F1A\u8BA9 v1.5 \u9759\u9ED8\u8DD1\u6210\u522B\u7684\u7248\u672C`
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
]
|
|
708
|
+
]);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export { formatConformanceReport, runDefinitionConformance, runProjectionConformance, runStoreConformance };
|