@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,204 @@
1
+ /**
2
+ * Deployment host for `vmz test --mode deployment` (T3).
3
+ * Proves deployment IR + server capability isolation (client stubs vs #server body).
4
+ */
5
+
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+
9
+ type Diag = { severity: string; message: string; [k: string]: unknown };
10
+
11
+ export type DeploymentResult = {
12
+ status: 'passed' | 'failed' | 'error';
13
+ diagnostics: Diag[];
14
+ planId: string | null;
15
+ programId: string | null;
16
+ };
17
+
18
+ function readJson(p: string): Record<string, unknown> | null {
19
+ try {
20
+ return JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ function resolveServerArtifact(outDir: string, moduleId: string): string | null {
27
+ // moduleId like "#server/components/UserCard"
28
+ const rel = moduleId.replace(/^#server\//, '').replace(/^\/+/, '');
29
+ const candidates = [
30
+ path.join(outDir, '#server', `${rel}.js`),
31
+ path.join(outDir, '_vmz_server', `${rel}.js`),
32
+ path.join(outDir, 'src', path.dirname(rel), `${path.basename(rel)}Server.server.js`),
33
+ path.join(outDir, 'src', `${rel}.server.js`),
34
+ ];
35
+ for (const c of candidates) {
36
+ if (fs.existsSync(c)) return c;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ export function runDeploymentManifest(manifest: Record<string, unknown>, ctx: { outDir: string }): DeploymentResult {
42
+ const diagnostics: Diag[] = [];
43
+ const fail = (message: string, extra: Record<string, unknown> = {}) => {
44
+ diagnostics.push({ severity: 'error', message, ...extra });
45
+ };
46
+
47
+ const program = manifest.program && typeof manifest.program === 'object' ? (manifest.program as Record<string, unknown>) : {};
48
+ const chunkId = String(program.chunkId || '');
49
+ const programId = chunkId || null;
50
+ const plan = manifest.plan && typeof manifest.plan === 'object' ? (manifest.plan as Record<string, unknown>) : {};
51
+ const planId = plan.ref ? String(plan.ref) : plan.schema ? String(plan.schema) : null;
52
+
53
+ const depPath = path.join(ctx.outDir, 'vmz-deployment.json');
54
+ if (!fs.existsSync(depPath)) {
55
+ fail('missing vmz-deployment.json');
56
+ return { status: 'failed', diagnostics, planId, programId };
57
+ }
58
+ const deploy = readJson(depPath);
59
+ if (!deploy) {
60
+ fail('unreadable vmz-deployment.json');
61
+ return { status: 'error', diagnostics, planId, programId };
62
+ }
63
+
64
+ const units = (deploy.units as Array<Record<string, unknown>>) || [];
65
+ const unit =
66
+ (chunkId && units.find((u) => String(u.chunkId || '') === chunkId)) ||
67
+ (chunkId && units.find((u) => String(u.chunkId || '').includes(chunkId))) ||
68
+ null;
69
+
70
+ const assertions = Array.isArray(manifest.assertions) ? manifest.assertions : [];
71
+ for (const raw of assertions) {
72
+ const a = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
73
+ const kind = String(a.kind || '');
74
+ const expect = (a.expect && typeof a.expect === 'object' ? (a.expect as Record<string, unknown>) : {}) || {};
75
+
76
+ if (kind === 'deploymentFile' || kind === 'deployment') {
77
+ if (expect.schema != null && deploy.schema !== expect.schema) {
78
+ fail(`deployment schema want ${expect.schema}, got ${deploy.schema}`);
79
+ }
80
+ if (expect.deploymentFileSchema != null && deploy.schema !== expect.deploymentFileSchema) {
81
+ fail(`deployment schema want ${expect.deploymentFileSchema}, got ${deploy.schema}`);
82
+ }
83
+ if (expect.resumeComponent != null) {
84
+ const name = String(expect.resumeComponent);
85
+ const resumes =
86
+ (unit?.resumeEntries as Array<Record<string, unknown>>) || (unit?.resume_entries as Array<Record<string, unknown>>) || [];
87
+ const hit = resumes.find((e) => (e.component || e.Component) === name);
88
+ if (!hit) {
89
+ // also search all units
90
+ const any = units.some((u) => {
91
+ const rs = (u.resumeEntries as Array<Record<string, unknown>>) || [];
92
+ return rs.some((e) => e.component === name);
93
+ });
94
+ if (!any) fail(`resumeEntries missing ${name}`);
95
+ } else if (expect.strategy != null && String(hit.strategy || '') !== String(expect.strategy)) {
96
+ fail(`resume strategy want ${expect.strategy}, got ${hit.strategy}`);
97
+ }
98
+ }
99
+ continue;
100
+ }
101
+
102
+ if (kind === 'serverCapability') {
103
+ const targetChunk = String(expect.chunkId || chunkId || '');
104
+ const u = units.find((x) => String(x.chunkId || '') === targetChunk);
105
+ if (!u) {
106
+ fail(`deployment unit missing ${targetChunk}`);
107
+ continue;
108
+ }
109
+ const moduleId = u.serverModuleId != null ? String(u.serverModuleId) : '';
110
+ if (expect.serverModuleId != null && moduleId !== String(expect.serverModuleId)) {
111
+ fail(`serverModuleId want ${expect.serverModuleId}, got ${moduleId || 'null'}`);
112
+ }
113
+ if (!moduleId) {
114
+ fail(`unit ${targetChunk} has no serverModuleId`);
115
+ continue;
116
+ }
117
+ const caps = (u.capabilities as string[]) || [];
118
+ const wantCaps = Array.isArray(expect.capabilities) ? expect.capabilities.map(String) : [];
119
+ for (const c of wantCaps) {
120
+ if (!caps.includes(c)) fail(`capability missing ${c}: ${JSON.stringify(caps)}`);
121
+ }
122
+ continue;
123
+ }
124
+
125
+ if (kind === 'serverIsolation') {
126
+ const targetChunk = String(expect.chunkId || chunkId || '');
127
+ const u = units.find((x) => String(x.chunkId || '') === targetChunk);
128
+ if (!u) {
129
+ fail(`deployment unit missing ${targetChunk}`);
130
+ continue;
131
+ }
132
+ const moduleId = u.serverModuleId != null ? String(u.serverModuleId) : '';
133
+ if (!moduleId) {
134
+ fail(`unit ${targetChunk} has no serverModuleId for isolation check`);
135
+ continue;
136
+ }
137
+ const serverPath = resolveServerArtifact(ctx.outDir, moduleId);
138
+ if (!serverPath) {
139
+ fail(`server artifact missing for ${moduleId}`);
140
+ continue;
141
+ }
142
+ const serverSrc = fs.readFileSync(serverPath, 'utf8');
143
+ const clientEntry = u.clientEntry != null ? String(u.clientEntry) : `${targetChunk}.client.js`;
144
+ const clientPath = path.join(ctx.outDir, clientEntry);
145
+ if (!fs.existsSync(clientPath)) {
146
+ fail(`client entry missing ${clientEntry}`);
147
+ continue;
148
+ }
149
+ const clientSrc = fs.readFileSync(clientPath, 'utf8');
150
+
151
+ // Server body must exist as a real module (not only a callServer stub file).
152
+ if (!/export\s+(default\s+)?class\s+\w+/.test(serverSrc) && !/export\s+\{/.test(serverSrc)) {
153
+ fail(`server artifact looks empty: ${path.relative(ctx.outDir, serverPath)}`);
154
+ }
155
+
156
+ // Client must route through callServer for declared capabilities (stub isolation).
157
+ const caps = Array.isArray(expect.capabilities)
158
+ ? expect.capabilities.map(String)
159
+ : ((u.capabilities as string[]) || []).map(String);
160
+ for (const c of caps) {
161
+ if (!clientSrc.includes('callServer')) {
162
+ fail(`client entry missing callServer stub (${clientEntry})`);
163
+ break;
164
+ }
165
+ if (!clientSrc.includes(JSON.stringify(c)) && !clientSrc.includes(`"${c}"`) && !clientSrc.includes(`'${c}'`)) {
166
+ fail(`client stub missing capability name ${c}`);
167
+ }
168
+ }
169
+
170
+ // Client must not embed the full server file content (naive leak check).
171
+ const serverBodyMarker = serverSrc
172
+ .split('\n')
173
+ .map((l) => l.trim())
174
+ .find((l) => l.startsWith('return ') || l.includes('Ada') || l.includes('profile'));
175
+ if (serverBodyMarker && serverBodyMarker.length > 12 && clientSrc.includes(serverBodyMarker)) {
176
+ fail(`client appears to embed server body marker: ${serverBodyMarker.slice(0, 80)}`);
177
+ }
178
+ continue;
179
+ }
180
+
181
+ if (kind === 'graph' || kind === 'plan' || kind === 'diagnostic') {
182
+ continue;
183
+ }
184
+
185
+ fail(`unknown deployment assertion ${JSON.stringify(kind)}`);
186
+ }
187
+
188
+ // Optional actions are reserved (no runtime schedule for deployment IR checks).
189
+ const actions = Array.isArray(manifest.actions) ? manifest.actions : [];
190
+ for (const raw of actions) {
191
+ const a = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
192
+ const kind = String(a.kind || '');
193
+ if (kind === 'noop' || kind === '') continue;
194
+ fail(`unknown deployment action ${JSON.stringify(kind)}`);
195
+ }
196
+
197
+ const failed = diagnostics.some((d) => d.severity === 'error');
198
+ return {
199
+ status: failed ? 'failed' : 'passed',
200
+ diagnostics,
201
+ planId,
202
+ programId,
203
+ };
204
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Discover VMZ native test manifests (T0).
3
+ */
4
+
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { validateManifest } from './protocol.js';
8
+
9
+ const MANIFEST_RE = /\.vmz\.(test|spec)\.json$/i;
10
+
11
+ export function listManifestFiles(root: string): string[] {
12
+ const abs = path.resolve(root);
13
+ const out: string[] = [];
14
+ walk(abs, out);
15
+ out.sort();
16
+ return out;
17
+ }
18
+
19
+ function walk(dir: string, out: string[]) {
20
+ let entries;
21
+ try {
22
+ entries = fs.readdirSync(dir, { withFileTypes: true });
23
+ } catch {
24
+ return;
25
+ }
26
+ for (const ent of entries) {
27
+ const name = ent.name;
28
+ if (name === 'node_modules' || name === 'dist' || name === '.git' || name === 'target') {
29
+ continue;
30
+ }
31
+ const full = path.join(dir, name);
32
+ if (ent.isDirectory()) {
33
+ walk(full, out);
34
+ continue;
35
+ }
36
+ if (ent.isFile() && MANIFEST_RE.test(name)) {
37
+ out.push(full);
38
+ }
39
+ }
40
+ }
41
+
42
+ export type DiscoveredManifest = Record<string, unknown> & {
43
+ file: string;
44
+ absoluteFile: string;
45
+ };
46
+
47
+ export function discoverTestManifests(projectRoot: string): {
48
+ manifests: DiscoveredManifest[];
49
+ errors: string[];
50
+ } {
51
+ const root = path.resolve(projectRoot);
52
+ const manifests: DiscoveredManifest[] = [];
53
+ const errors: string[] = [];
54
+ for (const file of listManifestFiles(root)) {
55
+ let raw: unknown;
56
+ try {
57
+ raw = JSON.parse(fs.readFileSync(file, 'utf8'));
58
+ } catch (e) {
59
+ errors.push(`${file}: ${e instanceof Error ? e.message : String(e)}`);
60
+ continue;
61
+ }
62
+ const v = validateManifest(raw, file);
63
+ if (!v.ok) {
64
+ errors.push(v.error);
65
+ continue;
66
+ }
67
+ manifests.push({
68
+ ...v.manifest,
69
+ file: path.relative(root, file).split(path.sep).join('/'),
70
+ absoluteFile: file,
71
+ });
72
+ }
73
+ return { manifests, errors };
74
+ }
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @vmz/test — programmable VMZ native test surface.
3
+ * CLI automation lives in `vmz` (`vmz test`); this package runs without it.
4
+ */
5
+
6
+ export * from './protocol.js';
7
+ export * from './discover.js';
8
+ export {
9
+ buildForCompile,
10
+ resolveChunkArtifacts,
11
+ runCompileManifest,
12
+ type BuildOptions,
13
+ type BuildResult,
14
+ type CompileResult,
15
+ type CreateWorkspaceFn,
16
+ } from './compile.js';
17
+ export {
18
+ createLogicHost,
19
+ installHeadlessDocument,
20
+ runLogicManifest,
21
+ type LogicHost,
22
+ type LogicResult,
23
+ } from './logic.js';
24
+ export { runSsrManifest, type SsrResult } from './ssr.js';
25
+ export { runResumeManifest, type ResumeResult } from './resume.js';
26
+ export { runDeploymentManifest, type DeploymentResult } from './deployment.js';
27
+ export {
28
+ runBrowserManifest,
29
+ resolveBrowserExecutable,
30
+ type BrowserResult,
31
+ } from './browser.js';
32
+ export {
33
+ runManifest,
34
+ resultsToReport,
35
+ type ManifestRunResult,
36
+ type RunManifestOptions,
37
+ } from './run.js';