@vmz/test 0.0.1 → 0.0.2

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.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Compile-mode host for `vmz test` / `@vmz/test` (T1+).
3
+ * Builds the project and checks graph/plan/view assertions against dist artifacts.
4
+ */
5
+ export type CreateWorkspaceFn = (opts: {
6
+ root: string;
7
+ outDir: string;
8
+ }) => {
9
+ build: (clean: boolean) => {
10
+ diagnostics?: Array<{
11
+ severity?: string;
12
+ level?: string;
13
+ }>;
14
+ };
15
+ dispose: () => void;
16
+ };
17
+ export type BuildOptions = {
18
+ createWorkspace?: CreateWorkspaceFn;
19
+ repoRoot?: string;
20
+ };
21
+ export type BuildResult = {
22
+ ok: true;
23
+ outDir: string;
24
+ diagnostics: unknown[];
25
+ } | {
26
+ ok: false;
27
+ outDir: string;
28
+ diagnostics: unknown[];
29
+ error: string;
30
+ };
31
+ /** Build project for compile/logic evidence. Prefers vmz-tools; optional N-API inject. */
32
+ export declare function buildForCompile(project: string, outDir?: string, options?: BuildOptions): BuildResult;
33
+ export declare function resolveChunkArtifacts(dist: string, chunkId: string): {
34
+ programPath: string | null;
35
+ clientPath: string | null;
36
+ };
37
+ type Diag = {
38
+ severity: string;
39
+ kind?: string;
40
+ message: string;
41
+ expect?: unknown;
42
+ };
43
+ export type CompileResult = {
44
+ status: 'passed' | 'failed' | 'error';
45
+ diagnostics: Diag[];
46
+ planId: string | null;
47
+ programId: string | null;
48
+ };
49
+ export declare function runCompileManifest(manifest: Record<string, unknown>, ctx: {
50
+ outDir: string;
51
+ }): CompileResult;
52
+ export {};
@@ -0,0 +1,453 @@
1
+ /**
2
+ * Compile-mode host for `vmz test` / `@vmz/test` (T1+).
3
+ * Builds the project and checks graph/plan/view assertions against dist artifacts.
4
+ */
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { spawnSync } from 'node:child_process';
9
+ import { fileURLToPath } from 'node:url';
10
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11
+ const repoRootGuess = path.resolve(packageRoot, '../../..');
12
+ /** Build project for compile/logic evidence. Prefers vmz-tools; optional N-API inject. */
13
+ export function buildForCompile(project, outDir, options = {}) {
14
+ const dist = outDir || fs.mkdtempSync(path.join(os.tmpdir(), 'vmz-test-compile-'));
15
+ if (!fs.existsSync(dist))
16
+ fs.mkdirSync(dist, { recursive: true });
17
+ const preferNapi = process.env.VMZ_TEST_BUILD === 'napi';
18
+ const repoRoot = options.repoRoot || repoRootGuess;
19
+ if (!preferNapi) {
20
+ const cargoToml = path.join(repoRoot, 'Cargo.toml');
21
+ if (fs.existsSync(cargoToml)) {
22
+ const cargo = spawnSync('cargo', ['run', '--quiet', '--manifest-path', cargoToml, '-p', 'vmz-tools', '--', 'build', project, '--out-dir', dist], { cwd: repoRoot, encoding: 'utf8' });
23
+ if (cargo.status === 0) {
24
+ return { ok: true, outDir: dist, diagnostics: [] };
25
+ }
26
+ }
27
+ }
28
+ if (options.createWorkspace) {
29
+ try {
30
+ const ws = options.createWorkspace({ root: project, outDir: dist });
31
+ try {
32
+ const report = ws.build(false);
33
+ const diags = report.diagnostics ?? [];
34
+ const errors = diags.filter((d) => d && (d.severity === 'error' || d.level === 'error'));
35
+ if (errors.length) {
36
+ return {
37
+ ok: false,
38
+ outDir: dist,
39
+ diagnostics: diags,
40
+ error: 'workspace build reported errors',
41
+ };
42
+ }
43
+ return { ok: true, outDir: dist, diagnostics: diags };
44
+ }
45
+ finally {
46
+ ws.dispose();
47
+ }
48
+ }
49
+ catch (e) {
50
+ return {
51
+ ok: false,
52
+ outDir: dist,
53
+ diagnostics: [],
54
+ error: `build failed: ${e instanceof Error ? e.message : String(e)}`,
55
+ };
56
+ }
57
+ }
58
+ return {
59
+ ok: false,
60
+ outDir: dist,
61
+ diagnostics: [],
62
+ error: 'build failed: vmz-tools unavailable and no createWorkspace provided',
63
+ };
64
+ }
65
+ export function resolveChunkArtifacts(dist, chunkId) {
66
+ const rel = chunkId.replace(/\\/g, '/');
67
+ const programPath = path.join(dist, `${rel}.program.json`);
68
+ const clientPath = path.join(dist, `${rel}.client.js`);
69
+ return {
70
+ programPath: fs.existsSync(programPath) ? programPath : null,
71
+ clientPath: fs.existsSync(clientPath) ? clientPath : null,
72
+ };
73
+ }
74
+ function collectViewKinds(nodes, kinds, flags) {
75
+ for (const raw of nodes ?? []) {
76
+ if (!raw || typeof raw !== 'object')
77
+ continue;
78
+ const n = raw;
79
+ if (typeof n.kind === 'string')
80
+ kinds.add(n.kind);
81
+ if (n.each)
82
+ flags.each = true;
83
+ if (Array.isArray(n.children))
84
+ collectViewKinds(n.children, kinds, flags);
85
+ if (Array.isArray(n.branches)) {
86
+ for (const b of n.branches) {
87
+ if (b?.body)
88
+ collectViewKinds([b.body], kinds, flags);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ export function runCompileManifest(manifest, ctx) {
94
+ const diagnostics = [];
95
+ const program = manifest.program && typeof manifest.program === 'object' ? manifest.program : {};
96
+ const chunkId = String(program.chunkId || '');
97
+ const programId = chunkId || null;
98
+ if (!chunkId) {
99
+ return {
100
+ status: 'error',
101
+ diagnostics: [{ severity: 'error', message: 'program.chunkId missing' }],
102
+ planId: null,
103
+ programId: null,
104
+ };
105
+ }
106
+ const arts = resolveChunkArtifacts(ctx.outDir, chunkId);
107
+ if (!arts.programPath) {
108
+ return {
109
+ status: 'failed',
110
+ diagnostics: [
111
+ {
112
+ severity: 'error',
113
+ message: `missing ${chunkId}.program.json under ${ctx.outDir}`,
114
+ },
115
+ ],
116
+ planId: null,
117
+ programId,
118
+ };
119
+ }
120
+ let prog;
121
+ try {
122
+ prog = JSON.parse(fs.readFileSync(arts.programPath, 'utf8'));
123
+ }
124
+ catch (e) {
125
+ return {
126
+ status: 'error',
127
+ diagnostics: [
128
+ {
129
+ severity: 'error',
130
+ message: `parse program.json: ${e instanceof Error ? e.message : String(e)}`,
131
+ },
132
+ ],
133
+ planId: null,
134
+ programId,
135
+ };
136
+ }
137
+ const units = prog.units || [];
138
+ const unit = units.find((u) => u && (u.name === program.unitName || u.chunkId === chunkId || String(u.chunk_id || '') === chunkId)) || units[0];
139
+ if (!unit) {
140
+ diagnostics.push({ severity: 'error', message: 'program.json has no units' });
141
+ return { status: 'failed', diagnostics, planId: null, programId };
142
+ }
143
+ const plan = unit.plan || null;
144
+ const planId = plan?.schema ? String(plan.schema) : null;
145
+ const clientJs = arts.clientPath ? fs.readFileSync(arts.clientPath, 'utf8') : '';
146
+ const view = unit.view || null;
147
+ const assertions = Array.isArray(manifest.assertions) ? manifest.assertions : [];
148
+ for (const raw of assertions) {
149
+ const a = raw && typeof raw === 'object' ? raw : {};
150
+ const kind = String(a.kind || '');
151
+ const expect = a.expect && typeof a.expect === 'object' ? a.expect : {};
152
+ const fail = (message) => {
153
+ diagnostics.push({ severity: 'error', kind, message, expect });
154
+ };
155
+ if (kind === 'plan') {
156
+ if (expect.schema != null) {
157
+ if (!plan || plan.schema !== expect.schema) {
158
+ fail(`plan.schema want ${expect.schema}, got ${plan?.schema}`);
159
+ }
160
+ }
161
+ if (expect.status != null) {
162
+ if (!plan || plan.status !== expect.status) {
163
+ fail(`plan.status want ${expect.status}, got ${plan?.status}`);
164
+ }
165
+ }
166
+ if (expect.nonEmpty === true) {
167
+ if (!plan || !Array.isArray(plan.nodes) || plan.nodes.length === 0) {
168
+ fail('plan.nodes empty');
169
+ }
170
+ if (!plan || !Array.isArray(plan.root_ids) || plan.root_ids.length === 0) {
171
+ fail('plan.root_ids empty');
172
+ }
173
+ }
174
+ if (Array.isArray(expect.kinds)) {
175
+ const have = new Set((plan?.nodes || []).map((n) => String(n.kind || '')));
176
+ for (const k of expect.kinds) {
177
+ if (!have.has(String(k)))
178
+ fail(`plan.nodes missing kind ${k}: ${[...have]}`);
179
+ }
180
+ }
181
+ if (expect.anyStructuralKind === true) {
182
+ const have = new Set((plan?.nodes || []).map((n) => String(n.kind || '')));
183
+ if (!have.has('element') && !have.has('interp') && !have.has('text')) {
184
+ fail(`plan missing structural kinds: ${[...have]}`);
185
+ }
186
+ }
187
+ if (expect.nodeIdsInClient === true || typeof expect.nodeIdsInClient === 'number') {
188
+ if (!arts.clientPath) {
189
+ fail(`missing ${chunkId}.client.js`);
190
+ }
191
+ else {
192
+ const nodes = plan?.nodes || [];
193
+ const n = typeof expect.nodeIdsInClient === 'number' ? expect.nodeIdsInClient : Math.min(3, nodes.length);
194
+ for (const node of nodes.slice(0, n)) {
195
+ const id = node.id;
196
+ if (id == null || !clientJs.includes(`id:${id}`)) {
197
+ fail(`__vmzPlan missing node id ${id}`);
198
+ }
199
+ }
200
+ }
201
+ }
202
+ continue;
203
+ }
204
+ if (kind === 'view') {
205
+ if (expect.status != null) {
206
+ if (!view || view.status !== expect.status) {
207
+ fail(`view.status want ${expect.status}, got ${view?.status}`);
208
+ }
209
+ }
210
+ if (expect.nonEmptyRoots === true) {
211
+ if (!view || !Array.isArray(view.roots) || view.roots.length === 0) {
212
+ fail('view.roots empty');
213
+ }
214
+ }
215
+ const kinds = new Set();
216
+ const flags = { each: false };
217
+ collectViewKinds(view?.roots, kinds, flags);
218
+ if (Array.isArray(expect.kinds)) {
219
+ for (const k of expect.kinds) {
220
+ if (!kinds.has(String(k)))
221
+ fail(`view missing kind=${k}: ${[...kinds]}`);
222
+ }
223
+ }
224
+ if (expect.hasEach === true && !flags.each) {
225
+ fail('view missing each on element');
226
+ }
227
+ continue;
228
+ }
229
+ if (kind === 'graph') {
230
+ const unitGraph = unit.graph || null;
231
+ const needsClient = expect.direct ||
232
+ expect.create ||
233
+ expect.plan ||
234
+ expect.serialize ||
235
+ expect.noRender ||
236
+ expect.noRenderFallback ||
237
+ expect.includes ||
238
+ expect.includesAll ||
239
+ expect.includesAny;
240
+ if (needsClient && !arts.clientPath) {
241
+ fail(`missing ${chunkId}.client.js`);
242
+ continue;
243
+ }
244
+ if (arts.clientPath) {
245
+ if (expect.direct === true && !clientJs.includes('__vmzDirect = true')) {
246
+ fail('missing __vmzDirect = true');
247
+ }
248
+ if (expect.create === true && !clientJs.includes('__vmzCreate')) {
249
+ fail('missing __vmzCreate');
250
+ }
251
+ if (expect.plan === true && !clientJs.includes('__vmzPlan')) {
252
+ fail('missing __vmzPlan');
253
+ }
254
+ if (expect.serialize === true && !clientJs.includes('__vmzSerialize')) {
255
+ fail('missing __vmzSerialize');
256
+ }
257
+ if (expect.noRender === true || expect.noRenderFallback === true) {
258
+ if (clientJs.includes('prototype.render')) {
259
+ fail('production client must not emit prototype.render (Gate 3)');
260
+ }
261
+ }
262
+ if (typeof expect.includes === 'string' && !clientJs.includes(expect.includes)) {
263
+ fail(`client.js missing substring ${JSON.stringify(expect.includes)}`);
264
+ }
265
+ if (Array.isArray(expect.includesAll)) {
266
+ for (const s of expect.includesAll) {
267
+ if (!clientJs.includes(String(s))) {
268
+ fail(`client.js missing substring ${JSON.stringify(s)}`);
269
+ }
270
+ }
271
+ }
272
+ if (Array.isArray(expect.includesAny)) {
273
+ const ok = expect.includesAny.some((s) => clientJs.includes(String(s)));
274
+ if (!ok)
275
+ fail(`client.js missing any of ${JSON.stringify(expect.includesAny)}`);
276
+ }
277
+ }
278
+ if (expect.ownsUnitToRegion === true) {
279
+ const edges = unitGraph?.edges || [];
280
+ const owns = edges.filter((e) => e.kind === 'owns');
281
+ const hit = owns.some((e) => String(e.from).startsWith('unit:') && String(e.to).startsWith('region:'));
282
+ if (!hit)
283
+ fail(`missing owns unit→region edges: ${JSON.stringify(owns)}`);
284
+ }
285
+ if (expect.disposesMin != null) {
286
+ const edges = unitGraph?.edges || [];
287
+ const n = edges.filter((e) => e.kind === 'disposes').length;
288
+ if (n < Number(expect.disposesMin)) {
289
+ fail(`disposes edges want >= ${expect.disposesMin}, got ${n}`);
290
+ }
291
+ }
292
+ if (typeof expect.unknownsVia === 'string') {
293
+ const unknowns = unitGraph?.unknowns || [];
294
+ const via = String(expect.unknownsVia);
295
+ const hits = unknowns.filter((u) => u.via === via);
296
+ if (!hits.length)
297
+ fail(`graph.unknowns missing via ${via}`);
298
+ if (expect.unknownReason != null) {
299
+ if (!hits.some((u) => u.reason === expect.unknownReason)) {
300
+ fail(`unknown reason want ${expect.unknownReason}: ${JSON.stringify(hits)}`);
301
+ }
302
+ }
303
+ if (expect.unknownReasonNot === true || expect.unknownReasonNot === 'ir_unknown') {
304
+ if (!hits.every((u) => u.reason && u.reason !== 'ir_unknown')) {
305
+ fail(`opaque reasons must be specific: ${JSON.stringify(hits)}`);
306
+ }
307
+ }
308
+ }
309
+ continue;
310
+ }
311
+ if (kind === 'analysis') {
312
+ const unitGraph = unit.graph || null;
313
+ const analysis = unitGraph?.analysis || null;
314
+ if (!analysis || typeof analysis.exact !== 'number') {
315
+ fail(`missing graph.analysis: ${JSON.stringify(unitGraph)}`);
316
+ continue;
317
+ }
318
+ if (expect.exactMin != null && Number(analysis.exact) < Number(expect.exactMin)) {
319
+ fail(`analysis.exact want >= ${expect.exactMin}, got ${analysis.exact}`);
320
+ }
321
+ if (expect.widenedMin != null && Number(analysis.widened || 0) < Number(expect.widenedMin)) {
322
+ fail(`analysis.widened want >= ${expect.widenedMin}, got ${analysis.widened}`);
323
+ }
324
+ if (expect.unknownMin != null && Number(analysis.unknown || 0) < Number(expect.unknownMin)) {
325
+ fail(`analysis.unknown want >= ${expect.unknownMin}, got ${analysis.unknown}`);
326
+ }
327
+ if (expect.callEdgesMin != null && Number(analysis.call_edges || 0) < Number(expect.callEdgesMin)) {
328
+ fail(`analysis.call_edges want >= ${expect.callEdgesMin}, got ${analysis.call_edges}`);
329
+ }
330
+ if (expect.widenedOrUnknownMin != null) {
331
+ const sum = Number(analysis.widened || 0) + Number(analysis.unknown || 0);
332
+ if (sum < Number(expect.widenedOrUnknownMin)) {
333
+ fail(`widened+unknown want >= ${expect.widenedOrUnknownMin}, got ${sum}`);
334
+ }
335
+ }
336
+ continue;
337
+ }
338
+ if (kind === 'reactive') {
339
+ const reactive = unit.reactive || null;
340
+ const effects = reactive?.effects || [];
341
+ const name = expect.effect != null ? String(expect.effect) : '';
342
+ const effect = name ? effects.find((e) => e.name === name) : null;
343
+ if (name && !effect) {
344
+ fail(`missing reactive effect ${name}`);
345
+ continue;
346
+ }
347
+ if (effect && expect.writesInclude != null) {
348
+ const needle = String(expect.writesInclude);
349
+ const raw = JSON.stringify(effect);
350
+ if (!raw.includes(needle))
351
+ fail(`effect ${name} must write ${needle}: ${raw}`);
352
+ }
353
+ if (effect && expect.opaqueCallee === true && !effect.opaque_callee) {
354
+ fail(`effect ${name} must be opaque: ${JSON.stringify(effect)}`);
355
+ }
356
+ if (effect && Array.isArray(expect.starReasons)) {
357
+ const reasons = effect.star_reasons || [];
358
+ for (const want of expect.starReasons) {
359
+ const hit = reasons.some((r) => (want.field == null || r.field === want.field) && (want.reason == null || r.reason === want.reason));
360
+ if (!hit) {
361
+ fail(`star_reasons missing ${JSON.stringify(want)}: ${JSON.stringify(reasons)}`);
362
+ }
363
+ }
364
+ }
365
+ continue;
366
+ }
367
+ if (kind === 'lifetime') {
368
+ const life = unit.lifetime || null;
369
+ if (expect.status != null) {
370
+ if (!life || life.status !== expect.status) {
371
+ fail(`lifetime.status want ${expect.status}, got ${life?.status}`);
372
+ }
373
+ }
374
+ if (expect.nonEmptyRegions === true) {
375
+ if (!life || !Array.isArray(life.regions) || life.regions.length === 0) {
376
+ fail('lifetime.regions empty');
377
+ }
378
+ }
379
+ if (Array.isArray(expect.regionKinds)) {
380
+ const have = new Set((life?.regions || []).map((r) => String(r.kind || '')));
381
+ for (const k of expect.regionKinds) {
382
+ if (!have.has(String(k)))
383
+ fail(`lifetime missing region kind=${k}: ${[...have]}`);
384
+ }
385
+ }
386
+ if (expect.hasDisposeRegion === true) {
387
+ const nodes = (plan?.nodes || []).filter((n) => n.kind === 'dispose_region');
388
+ if (!nodes.length)
389
+ fail('plan missing dispose_region nodes');
390
+ for (const d of nodes) {
391
+ if (d.region == null)
392
+ fail(`dispose_region missing region: ${JSON.stringify(d)}`);
393
+ }
394
+ }
395
+ if (expect.disposeTag != null) {
396
+ const nodes = (plan?.nodes || []).filter((n) => n.kind === 'dispose_region');
397
+ if (!nodes.some((n) => n.tag === expect.disposeTag)) {
398
+ fail(`dispose_region missing tag=${expect.disposeTag}: ${JSON.stringify(nodes)}`);
399
+ }
400
+ }
401
+ continue;
402
+ }
403
+ if (kind === 'deployment') {
404
+ const deployment = unit.deployment || null;
405
+ const entries = deployment?.resume_entries ||
406
+ deployment?.resumeEntries ||
407
+ [];
408
+ if (expect.resumeComponent != null) {
409
+ const name = String(expect.resumeComponent);
410
+ const hit = entries.find((e) => (e.component || e.Component) === name);
411
+ if (!hit) {
412
+ fail(`resume entry missing ${name}: ${JSON.stringify(entries)}`);
413
+ }
414
+ else if (expect.strategy != null && (hit.strategy || '') !== expect.strategy) {
415
+ fail(`resume strategy want ${expect.strategy}, got ${hit.strategy}`);
416
+ }
417
+ }
418
+ if (expect.deploymentFileSchema != null) {
419
+ const depPath = path.join(ctx.outDir, 'vmz-deployment.json');
420
+ if (!fs.existsSync(depPath)) {
421
+ fail('missing vmz-deployment.json');
422
+ }
423
+ else {
424
+ const deploy = JSON.parse(fs.readFileSync(depPath, 'utf8'));
425
+ if (deploy.schema !== expect.deploymentFileSchema) {
426
+ fail(`deployment schema want ${expect.deploymentFileSchema}, got ${deploy.schema}`);
427
+ }
428
+ if (expect.deploymentResumeComponent != null) {
429
+ const name = String(expect.deploymentResumeComponent);
430
+ const unitsDep = deploy.units || [];
431
+ const chunk = unitsDep.find((u) => String(u.chunkId || '').includes(String(expect.deploymentChunkIncludes || chunkId)));
432
+ const resumes = chunk?.resumeEntries || [];
433
+ if (!resumes.some((e) => e.component === name)) {
434
+ fail(`deployment resumeEntries missing ${name}: ${JSON.stringify(resumes)}`);
435
+ }
436
+ }
437
+ }
438
+ }
439
+ continue;
440
+ }
441
+ if (kind === 'diagnostic') {
442
+ continue;
443
+ }
444
+ fail(`unknown assertion kind ${JSON.stringify(kind)}`);
445
+ }
446
+ const failed = diagnostics.some((d) => d.severity === 'error');
447
+ return {
448
+ status: failed ? 'failed' : 'passed',
449
+ diagnostics,
450
+ planId,
451
+ programId,
452
+ };
453
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Deployment host for `vmz test --mode deployment` (T3).
3
+ * Proves deployment IR + server capability isolation (client stubs vs #server body).
4
+ */
5
+ type Diag = {
6
+ severity: string;
7
+ message: string;
8
+ [k: string]: unknown;
9
+ };
10
+ export type DeploymentResult = {
11
+ status: 'passed' | 'failed' | 'error';
12
+ diagnostics: Diag[];
13
+ planId: string | null;
14
+ programId: string | null;
15
+ };
16
+ export declare function runDeploymentManifest(manifest: Record<string, unknown>, ctx: {
17
+ outDir: string;
18
+ }): DeploymentResult;
19
+ export {};