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