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