@vmz/test 0.0.2 → 0.0.4
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/README.md +16 -11
- package/dist/browser.d.ts +1 -2
- package/dist/browser.js +2 -3
- package/dist/compile.d.ts +1 -1
- package/dist/compile.js +79 -4
- package/dist/deployment.d.ts +1 -1
- package/dist/deployment.js +2 -2
- package/dist/discover.d.ts +1 -1
- package/dist/discover.js +1 -1
- package/dist/logic.d.ts +0 -1
- package/dist/logic.js +14 -4
- package/dist/protocol.d.ts +2 -3
- package/dist/protocol.js +2 -3
- package/dist/resume.d.ts +1 -1
- package/dist/resume.js +2 -2
- package/dist/ssr.d.ts +1 -1
- package/dist/ssr.js +2 -2
- package/package.json +3 -4
- package/src/browser.ts +0 -602
- package/src/compile.ts +0 -509
- package/src/deployment.ts +0 -204
- package/src/discover.ts +0 -74
- package/src/index.ts +0 -37
- package/src/logic.ts +0 -427
- package/src/protocol.ts +0 -128
- package/src/resume.ts +0 -275
- package/src/run.ts +0 -124
- package/src/ssr.ts +0 -265
package/src/logic.ts
DELETED
|
@@ -1,427 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Logic-mode host — headless document (linkedom), same Direct __vmzCreate as production.
|
|
3
|
-
* Design: 规划设计/vmz/16 — not Browser Host / Playwright.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import { pathToFileURL } from 'node:url';
|
|
9
|
-
import { createRequire } from 'node:module';
|
|
10
|
-
import { resolveChunkArtifacts } from './compile.js';
|
|
11
|
-
|
|
12
|
-
const require = createRequire(import.meta.url);
|
|
13
|
-
|
|
14
|
-
function loadLinkedom(): { parseHTML: (html: string) => { window: any } } {
|
|
15
|
-
try {
|
|
16
|
-
return require('linkedom');
|
|
17
|
-
} catch {
|
|
18
|
-
throw new Error('linkedom not found (add dependency on @vmz/test)');
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function installHeadlessDocument(): Document {
|
|
23
|
-
const { parseHTML } = loadLinkedom();
|
|
24
|
-
const { window } = parseHTML('<!DOCTYPE html><html><body><div id="app"></div></body></html>');
|
|
25
|
-
(globalThis as any).window = window;
|
|
26
|
-
(globalThis as any).document = window.document;
|
|
27
|
-
(globalThis as any).HTMLElement = window.HTMLElement;
|
|
28
|
-
(globalThis as any).Node = window.Node;
|
|
29
|
-
(globalThis as any).DocumentFragment = window.DocumentFragment;
|
|
30
|
-
(globalThis as any).Text = window.Text;
|
|
31
|
-
(globalThis as any).Comment = window.Comment;
|
|
32
|
-
return window.document;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type LogicHost = {
|
|
36
|
-
document: Document;
|
|
37
|
-
app: Element;
|
|
38
|
-
dom: any;
|
|
39
|
-
Component: any;
|
|
40
|
-
inst: any;
|
|
41
|
-
lastPrecision: Record<string, unknown> | null;
|
|
42
|
-
mount: (props?: object) => Promise<void>;
|
|
43
|
-
click: (selector?: string) => void;
|
|
44
|
-
write: (field: string, value: unknown) => void;
|
|
45
|
-
flush: () => Promise<void>;
|
|
46
|
-
destroy: () => void;
|
|
47
|
-
precisionReset: () => void;
|
|
48
|
-
precisionSnapshot: () => void;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export async function createLogicHost(opts: { outDir: string; chunkId: string; components?: Record<string, string> }): Promise<LogicHost> {
|
|
52
|
-
const arts = resolveChunkArtifacts(opts.outDir, opts.chunkId);
|
|
53
|
-
if (!arts.clientPath) {
|
|
54
|
-
throw new Error(`missing ${opts.chunkId}.client.js under ${opts.outDir}`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const document = installHeadlessDocument();
|
|
58
|
-
const app = document.getElementById('app');
|
|
59
|
-
if (!app) throw new Error('headless #app missing');
|
|
60
|
-
|
|
61
|
-
const dom = await import(pathToFileURL(path.join(opts.outDir, 'vmz-dom.js')).href);
|
|
62
|
-
const Component = (await import(pathToFileURL(arts.clientPath).href)).default;
|
|
63
|
-
|
|
64
|
-
if (!Component?.__vmzDirect || typeof Component.__vmzCreate !== 'function') {
|
|
65
|
-
throw new Error('logic host requires Direct __vmzCreate (rebuild with current compiler)');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (opts.components && typeof dom.registerComponents === 'function') {
|
|
69
|
-
const map: Record<string, any> = {};
|
|
70
|
-
for (const [name, chunk] of Object.entries(opts.components)) {
|
|
71
|
-
const cArts = resolveChunkArtifacts(opts.outDir, chunk);
|
|
72
|
-
if (!cArts.clientPath) throw new Error(`registerComponents: missing ${chunk}.client.js`);
|
|
73
|
-
map[name] = (await import(pathToFileURL(cArts.clientPath).href)).default;
|
|
74
|
-
}
|
|
75
|
-
dom.registerComponents(map);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (typeof dom.__vmzPrecisionEnable === 'function') {
|
|
79
|
-
dom.__vmzPrecisionEnable(true);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const host: LogicHost = {
|
|
83
|
-
document,
|
|
84
|
-
app,
|
|
85
|
-
dom,
|
|
86
|
-
Component,
|
|
87
|
-
inst: null,
|
|
88
|
-
lastPrecision: null,
|
|
89
|
-
async mount(props = {}) {
|
|
90
|
-
let createHits = 0;
|
|
91
|
-
const orig = host.Component.__vmzCreate;
|
|
92
|
-
host.Component.__vmzCreate = function (this: unknown, api: unknown) {
|
|
93
|
-
createHits += 1;
|
|
94
|
-
return orig.call(this, api);
|
|
95
|
-
};
|
|
96
|
-
host.inst = await host.dom.mount(host.Component, host.app, props);
|
|
97
|
-
host.Component.__vmzCreate = orig;
|
|
98
|
-
if (createHits !== 1) {
|
|
99
|
-
throw new Error(`mount must call __vmzCreate once, got ${createHits}`);
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
click(selector = 'button') {
|
|
103
|
-
if (!host.inst) throw new Error('click before mount');
|
|
104
|
-
const el = host.app.querySelector(selector);
|
|
105
|
-
if (!el) throw new Error(`click: no element for ${JSON.stringify(selector)}`);
|
|
106
|
-
(el as HTMLElement).click();
|
|
107
|
-
},
|
|
108
|
-
write(field, value) {
|
|
109
|
-
if (!host.inst) throw new Error('write before mount');
|
|
110
|
-
host.inst[field] = value;
|
|
111
|
-
},
|
|
112
|
-
async flush() {
|
|
113
|
-
if (!host.inst) throw new Error('flush before mount');
|
|
114
|
-
await host.dom.flushPending(host.inst);
|
|
115
|
-
},
|
|
116
|
-
destroy() {
|
|
117
|
-
if (!host.inst) throw new Error('destroy before mount');
|
|
118
|
-
host.dom.destroy(host.inst);
|
|
119
|
-
},
|
|
120
|
-
precisionReset() {
|
|
121
|
-
if (typeof host.dom.__vmzPrecisionReset === 'function') host.dom.__vmzPrecisionReset();
|
|
122
|
-
},
|
|
123
|
-
precisionSnapshot() {
|
|
124
|
-
if (typeof host.dom.__vmzPrecisionSnapshot === 'function') {
|
|
125
|
-
host.lastPrecision = host.dom.__vmzPrecisionSnapshot();
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
return host;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
type Diag = { severity: string; message: string; [k: string]: unknown };
|
|
134
|
-
|
|
135
|
-
export type LogicResult = {
|
|
136
|
-
status: 'passed' | 'failed' | 'error';
|
|
137
|
-
diagnostics: Diag[];
|
|
138
|
-
planId: string | null;
|
|
139
|
-
programId: string | null;
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
function runOneAssertion(a: Record<string, unknown>, host: LogicHost, fail: (message: string, extra?: Record<string, unknown>) => void) {
|
|
143
|
-
const kind = String(a.kind || '');
|
|
144
|
-
const expect = a.expect && typeof a.expect === 'object' ? (a.expect as Record<string, unknown>) : {};
|
|
145
|
-
|
|
146
|
-
if (kind === 'text' || (kind === 'assert' && String(a.assertion || 'text') === 'text')) {
|
|
147
|
-
const text = host.app.textContent ?? '';
|
|
148
|
-
if (expect.equals != null && text !== String(expect.equals)) {
|
|
149
|
-
fail(`text equals want ${JSON.stringify(expect.equals)}, got ${JSON.stringify(text)}`);
|
|
150
|
-
}
|
|
151
|
-
if (expect.contains != null && !text.includes(String(expect.contains))) {
|
|
152
|
-
fail(`text contains want ${JSON.stringify(expect.contains)}, got ${JSON.stringify(text)}`);
|
|
153
|
-
}
|
|
154
|
-
if (expect.notContains != null && text.includes(String(expect.notContains))) {
|
|
155
|
-
fail(`text notContains want absent ${JSON.stringify(expect.notContains)}, got ${JSON.stringify(text)}`);
|
|
156
|
-
}
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (kind === 'state') {
|
|
161
|
-
if (!host.inst) {
|
|
162
|
-
fail('state assertion before mount');
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
for (const [k, v] of Object.entries(expect)) {
|
|
166
|
-
if (host.inst[k] !== v) {
|
|
167
|
-
fail(`state.${k} want ${JSON.stringify(v)}, got ${JSON.stringify(host.inst[k])}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (kind === 'precision') {
|
|
174
|
-
host.precisionSnapshot();
|
|
175
|
-
const snap = host.lastPrecision || {};
|
|
176
|
-
if (expect.minWrites != null && Number(snap.writes || 0) < Number(expect.minWrites)) {
|
|
177
|
-
fail(`precision.writes want >= ${expect.minWrites}, got ${snap.writes}`);
|
|
178
|
-
}
|
|
179
|
-
if (expect.maxWrites != null && Number(snap.writes || 0) > Number(expect.maxWrites)) {
|
|
180
|
-
fail(`precision.writes want <= ${expect.maxWrites}, got ${snap.writes}`);
|
|
181
|
-
}
|
|
182
|
-
if (expect.maxBindingEvals != null && Number(snap.bindingEvals || 0) > Number(expect.maxBindingEvals)) {
|
|
183
|
-
fail(`precision.bindingEvals want <= ${expect.maxBindingEvals}, got ${snap.bindingEvals}`);
|
|
184
|
-
}
|
|
185
|
-
if (expect.maxPatchExecs != null && Number(snap.patchExecs || 0) > Number(expect.maxPatchExecs)) {
|
|
186
|
-
fail(`precision.patchExecs want <= ${expect.maxPatchExecs}, got ${snap.patchExecs}`);
|
|
187
|
-
}
|
|
188
|
-
if (expect.patchesIncludeDep != null) {
|
|
189
|
-
const dep = String(expect.patchesIncludeDep);
|
|
190
|
-
const map = (snap.patchesByDep as Record<string, number>) || {};
|
|
191
|
-
if (!map[dep]) {
|
|
192
|
-
fail(`precision.patchesByDep missing ${dep}: ${JSON.stringify(map)}`);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (expect.writesIncludeRoot != null) {
|
|
196
|
-
const rootKey = String(expect.writesIncludeRoot);
|
|
197
|
-
const map = (snap.writesByRoot as Record<string, number>) || {};
|
|
198
|
-
if (!map[rootKey]) {
|
|
199
|
-
fail(`precision.writesByRoot missing ${rootKey}: ${JSON.stringify(map)}`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
if (expect.patchesIncludeBinding != null) {
|
|
203
|
-
const bid = String(expect.patchesIncludeBinding);
|
|
204
|
-
const map = (snap.patchesByBinding as Record<string, number>) || {};
|
|
205
|
-
if (!map[bid]) {
|
|
206
|
-
fail(`precision.patchesByBinding missing ${bid}: ${JSON.stringify(map)}`);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (expect.bindingEvalsIncludeBinding != null) {
|
|
210
|
-
const bid = String(expect.bindingEvalsIncludeBinding);
|
|
211
|
-
const map = (snap.bindingEvalsByBinding as Record<string, number>) || {};
|
|
212
|
-
if (!map[bid]) {
|
|
213
|
-
fail(`precision.bindingEvalsByBinding missing ${bid}: ${JSON.stringify(map)}`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (expect.domCreates === 0 || expect.domCreates === false) {
|
|
217
|
-
if (Number(snap.domCreates || 0) !== 0) {
|
|
218
|
-
fail(`precision.domCreates want 0 after action window, got ${snap.domCreates}`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (kind === 'destroyed') {
|
|
225
|
-
if (!host.inst) {
|
|
226
|
-
fail('destroyed assertion before mount');
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
const want = expect.value !== false;
|
|
230
|
-
if (Boolean(host.inst.__vmzDestroyed) !== want) {
|
|
231
|
-
fail(`__vmzDestroyed want ${want}, got ${host.inst.__vmzDestroyed}`);
|
|
232
|
-
}
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (kind === 'exists') {
|
|
237
|
-
const sel = String(expect.selector || '');
|
|
238
|
-
if (!sel || !host.app.querySelector(sel)) {
|
|
239
|
-
fail(`exists: missing ${JSON.stringify(sel)}`);
|
|
240
|
-
}
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (kind === 'domKeys') {
|
|
245
|
-
const attr = String(expect.attr || 'data-vmz-key');
|
|
246
|
-
const keys = [...host.app.querySelectorAll(`[${attr}]`)].map((el) => el.getAttribute(attr));
|
|
247
|
-
if (Array.isArray(expect.includes)) {
|
|
248
|
-
for (const k of expect.includes) {
|
|
249
|
-
if (!keys.includes(String(k))) fail(`domKeys missing ${k}: ${JSON.stringify(keys)}`);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
if (Array.isArray(expect.excludes)) {
|
|
253
|
-
for (const k of expect.excludes) {
|
|
254
|
-
if (keys.includes(String(k))) fail(`domKeys should exclude ${k}: ${JSON.stringify(keys)}`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (kind === 'childDestroyed') {
|
|
261
|
-
const want = expect.value !== false;
|
|
262
|
-
const child = (host as any)._capturedChild;
|
|
263
|
-
if (!child) {
|
|
264
|
-
fail('childDestroyed: no captured child (use capture_child action)');
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
if (Boolean(child.__vmzDestroyed) !== want) {
|
|
268
|
-
fail(`child __vmzDestroyed want ${want}, got ${child.__vmzDestroyed}`);
|
|
269
|
-
}
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (kind === 'graph' || kind === 'plan' || kind === 'diagnostic' || kind === 'view') {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (kind === 'assert') {
|
|
278
|
-
// assertion kind nested: { kind: "assert", assertion: "text", expect: {...} }
|
|
279
|
-
const nested = { ...a, kind: String(a.assertion || 'text') };
|
|
280
|
-
runOneAssertion(nested, host, fail);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
fail(`unknown assertion kind ${JSON.stringify(kind)}`);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
export async function runLogicManifest(
|
|
288
|
-
manifest: Record<string, unknown>,
|
|
289
|
-
ctx: {
|
|
290
|
-
outDir: string;
|
|
291
|
-
},
|
|
292
|
-
): Promise<LogicResult> {
|
|
293
|
-
const diagnostics: Diag[] = [];
|
|
294
|
-
const program = manifest.program && typeof manifest.program === 'object' ? (manifest.program as Record<string, unknown>) : {};
|
|
295
|
-
const chunkId = String(program.chunkId || '');
|
|
296
|
-
const programId = chunkId || null;
|
|
297
|
-
|
|
298
|
-
const fail = (message: string, extra: Record<string, unknown> = {}) => {
|
|
299
|
-
diagnostics.push({ severity: 'error', message, ...extra });
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
if (!chunkId) {
|
|
303
|
-
fail('program.chunkId missing');
|
|
304
|
-
return { status: 'error', diagnostics, planId: null, programId: null };
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
let planId: string | null = null;
|
|
308
|
-
const arts = resolveChunkArtifacts(ctx.outDir, chunkId);
|
|
309
|
-
if (arts.programPath) {
|
|
310
|
-
try {
|
|
311
|
-
const prog = JSON.parse(fs.readFileSync(arts.programPath, 'utf8'));
|
|
312
|
-
const unit = prog.units?.[0];
|
|
313
|
-
if (unit?.plan?.schema) planId = String(unit.plan.schema);
|
|
314
|
-
} catch {
|
|
315
|
-
/* optional */
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const components =
|
|
320
|
-
program.components && typeof program.components === 'object' ? (program.components as Record<string, string>) : undefined;
|
|
321
|
-
|
|
322
|
-
let host: LogicHost;
|
|
323
|
-
try {
|
|
324
|
-
host = await createLogicHost({ outDir: ctx.outDir, chunkId, components });
|
|
325
|
-
} catch (e) {
|
|
326
|
-
fail(e instanceof Error ? e.message : String(e));
|
|
327
|
-
return { status: 'error', diagnostics, planId, programId };
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const actions = Array.isArray(manifest.actions) ? manifest.actions : [];
|
|
331
|
-
for (const raw of actions) {
|
|
332
|
-
const a = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
|
|
333
|
-
const kind = String(a.kind || '');
|
|
334
|
-
try {
|
|
335
|
-
if (kind === 'mount') {
|
|
336
|
-
const props = a.props && typeof a.props === 'object' ? (a.props as object) : {};
|
|
337
|
-
await host.mount(props);
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
if (kind === 'click') {
|
|
341
|
-
host.click(typeof a.selector === 'string' ? a.selector : 'button');
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
if (kind === 'write') {
|
|
345
|
-
const field = String(a.field || '');
|
|
346
|
-
if (!field) {
|
|
347
|
-
fail('write missing field');
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
host.write(field, a.value);
|
|
351
|
-
continue;
|
|
352
|
-
}
|
|
353
|
-
if (kind === 'flush') {
|
|
354
|
-
await host.flush();
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
if (kind === 'destroy') {
|
|
358
|
-
host.destroy();
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
if (kind === 'precision_reset') {
|
|
362
|
-
host.precisionReset();
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
if (kind === 'precision_snapshot') {
|
|
366
|
-
host.precisionSnapshot();
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
if (kind === 'register_components') {
|
|
370
|
-
const map = a.components && typeof a.components === 'object' ? (a.components as Record<string, string>) : {};
|
|
371
|
-
const loaded: Record<string, any> = {};
|
|
372
|
-
for (const [name, chunk] of Object.entries(map)) {
|
|
373
|
-
const cArts = resolveChunkArtifacts(ctx.outDir, chunk);
|
|
374
|
-
if (!cArts.clientPath) {
|
|
375
|
-
fail(`register_components: missing ${chunk}.client.js`);
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
loaded[name] = (await import(pathToFileURL(cArts.clientPath).href)).default;
|
|
379
|
-
}
|
|
380
|
-
host.dom.registerComponents(loaded);
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
if (kind === 'stub_onMount') {
|
|
384
|
-
if (!host.inst && !host.Component) {
|
|
385
|
-
fail('stub_onMount before component load');
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
const Comp = host.Component;
|
|
389
|
-
Comp.prototype.onMount = async function () {};
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
if (kind === 'capture_child') {
|
|
393
|
-
const sel = String(a.selector || '');
|
|
394
|
-
const el = host.app.querySelector(sel) as any;
|
|
395
|
-
if (!el?.__vmzInst) {
|
|
396
|
-
fail(`capture_child: no inst for ${sel}`);
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
(host as any)._capturedChild = el.__vmzInst;
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
if (kind === 'assert') {
|
|
403
|
-
runOneAssertion(a, host, fail);
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
fail(`unknown action kind ${JSON.stringify(kind)}`);
|
|
407
|
-
} catch (e) {
|
|
408
|
-
fail(`action ${kind}: ${e instanceof Error ? e.message : String(e)}`);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
host.precisionSnapshot();
|
|
413
|
-
|
|
414
|
-
const assertions = Array.isArray(manifest.assertions) ? manifest.assertions : [];
|
|
415
|
-
for (const raw of assertions) {
|
|
416
|
-
const a = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
|
|
417
|
-
runOneAssertion(a, host, fail);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const failed = diagnostics.some((d) => d.severity === 'error');
|
|
421
|
-
return {
|
|
422
|
-
status: failed ? 'failed' : 'passed',
|
|
423
|
-
diagnostics,
|
|
424
|
-
planId,
|
|
425
|
-
programId,
|
|
426
|
-
};
|
|
427
|
-
}
|
package/src/protocol.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VMZ native test protocol (T0) — validators + report builders.
|
|
3
|
-
* Schema ids live in `@vmz/protocol` (mirrors Rust `vmz-protocol`).
|
|
4
|
-
* Design: 规划设计/vmz/16 — not a Test IR; references Program Graph + Execution Plan.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { EXECUTION_PLAN_REF_SCHEMA, PLAN_SCHEMA, TEST_MANIFEST_SCHEMA, TEST_REPORT_SCHEMA } from '@vmz/protocol';
|
|
8
|
-
|
|
9
|
-
export {
|
|
10
|
-
EXECUTION_PLAN_REF_SCHEMA,
|
|
11
|
-
PLAN_SCHEMA,
|
|
12
|
-
TEST_ACTION_SCHEMA,
|
|
13
|
-
TEST_ASSERTION_SCHEMA,
|
|
14
|
-
TEST_MANIFEST_SCHEMA,
|
|
15
|
-
TEST_PROTOCOL,
|
|
16
|
-
TEST_REPORT_SCHEMA,
|
|
17
|
-
testCatalog,
|
|
18
|
-
} from '@vmz/protocol';
|
|
19
|
-
|
|
20
|
-
export type TestMode = 'compile' | 'logic' | 'browser' | 'ssr' | 'resume' | 'deployment' | 'all';
|
|
21
|
-
|
|
22
|
-
export const TEST_MODES: readonly TestMode[] = Object.freeze(['compile', 'logic', 'browser', 'ssr', 'resume', 'deployment', 'all']);
|
|
23
|
-
|
|
24
|
-
export function isTestMode(v: unknown): v is TestMode {
|
|
25
|
-
return typeof v === 'string' && (TEST_MODES as readonly string[]).includes(v);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Normalize `--mode` flag: `compile,logic` or `all`. */
|
|
29
|
-
export function parseModes(raw: string | boolean | undefined | null): TestMode[] {
|
|
30
|
-
if (raw == null || raw === true || raw === '') return ['all'];
|
|
31
|
-
const parts = String(raw)
|
|
32
|
-
.split(',')
|
|
33
|
-
.map((s) => s.trim())
|
|
34
|
-
.filter(Boolean);
|
|
35
|
-
if (parts.length === 0) return ['all'];
|
|
36
|
-
for (const p of parts) {
|
|
37
|
-
if (!isTestMode(p)) {
|
|
38
|
-
throw new Error(`unknown test mode \`${p}\` (want ${TEST_MODES.join('|')})`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return parts as TestMode[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export type ManifestValidation = { ok: true; manifest: Record<string, unknown> } | { ok: false; error: string };
|
|
45
|
-
|
|
46
|
-
/** Validate a discovered manifest object (narrow T0 checks). */
|
|
47
|
-
export function validateManifest(raw: unknown, file: string): ManifestValidation {
|
|
48
|
-
if (!raw || typeof raw !== 'object') {
|
|
49
|
-
return { ok: false, error: `${file}: manifest must be an object` };
|
|
50
|
-
}
|
|
51
|
-
const m = raw as Record<string, unknown>;
|
|
52
|
-
if (m.schema !== TEST_MANIFEST_SCHEMA) {
|
|
53
|
-
return {
|
|
54
|
-
ok: false,
|
|
55
|
-
error: `${file}: schema want ${TEST_MANIFEST_SCHEMA}, got ${JSON.stringify(m.schema)}`,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
if (typeof m.id !== 'string' || !m.id) {
|
|
59
|
-
return { ok: false, error: `${file}: missing string id` };
|
|
60
|
-
}
|
|
61
|
-
if (!Array.isArray(m.modes) || m.modes.length === 0) {
|
|
62
|
-
return { ok: false, error: `${file}: modes must be a non-empty array` };
|
|
63
|
-
}
|
|
64
|
-
for (const mode of m.modes) {
|
|
65
|
-
if (!isTestMode(mode) || mode === 'all') {
|
|
66
|
-
return { ok: false, error: `${file}: invalid mode ${JSON.stringify(mode)}` };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (!m.program || typeof m.program !== 'object') {
|
|
70
|
-
return { ok: false, error: `${file}: missing program ref object` };
|
|
71
|
-
}
|
|
72
|
-
const program = m.program as Record<string, unknown>;
|
|
73
|
-
if (typeof program.chunkId !== 'string' || !program.chunkId) {
|
|
74
|
-
return { ok: false, error: `${file}: program.chunkId required` };
|
|
75
|
-
}
|
|
76
|
-
if (m.plan != null) {
|
|
77
|
-
if (typeof m.plan !== 'object') {
|
|
78
|
-
return { ok: false, error: `${file}: plan must be an object when present` };
|
|
79
|
-
}
|
|
80
|
-
const plan = m.plan as Record<string, unknown>;
|
|
81
|
-
if (plan.schema != null && plan.schema !== PLAN_SCHEMA && plan.schema !== EXECUTION_PLAN_REF_SCHEMA) {
|
|
82
|
-
return { ok: false, error: `${file}: plan.schema unexpected ${JSON.stringify(plan.schema)}` };
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
if (m.actions != null && !Array.isArray(m.actions)) {
|
|
86
|
-
return { ok: false, error: `${file}: actions must be an array` };
|
|
87
|
-
}
|
|
88
|
-
if (m.assertions != null && !Array.isArray(m.assertions)) {
|
|
89
|
-
return { ok: false, error: `${file}: assertions must be an array` };
|
|
90
|
-
}
|
|
91
|
-
return { ok: true, manifest: m };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export type TestReportEntryInput = {
|
|
95
|
-
testId: string;
|
|
96
|
-
file: string;
|
|
97
|
-
modes: string[];
|
|
98
|
-
programId?: string | null;
|
|
99
|
-
planId?: string | null;
|
|
100
|
-
status: string;
|
|
101
|
-
diagnostics?: unknown[];
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
export function buildTestReport(input: { project: string; modes: TestMode[]; tests: TestReportEntryInput[]; status?: string }) {
|
|
105
|
-
const tests = input.tests.map((t) => ({
|
|
106
|
-
testId: t.testId,
|
|
107
|
-
file: t.file,
|
|
108
|
-
modes: t.modes,
|
|
109
|
-
programId: t.programId ?? null,
|
|
110
|
-
planId: t.planId ?? null,
|
|
111
|
-
status: t.status,
|
|
112
|
-
diagnostics: t.diagnostics ?? [],
|
|
113
|
-
trace: null,
|
|
114
|
-
snapshots: null,
|
|
115
|
-
coverage: null,
|
|
116
|
-
unknownReasons: [] as string[],
|
|
117
|
-
}));
|
|
118
|
-
const failed = tests.some((t) => t.status === 'failed' || t.status === 'error');
|
|
119
|
-
const status = input.status ?? (failed ? 'failed' : tests.length === 0 ? 'empty' : 'listed');
|
|
120
|
-
return {
|
|
121
|
-
schema: TEST_REPORT_SCHEMA,
|
|
122
|
-
status,
|
|
123
|
-
project: input.project,
|
|
124
|
-
modes: input.modes,
|
|
125
|
-
generatedAt: new Date().toISOString(),
|
|
126
|
-
tests,
|
|
127
|
-
};
|
|
128
|
-
}
|