@forgeax/engine-remote 0.1.26 → 0.1.28
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 +3 -3
- package/dist/index.d.ts.map +1 -1
- package/package.json +4 -7
- package/src/__tests__/execute-browser-safe.unit.test.ts +2 -2
- package/src/index.ts +3 -5
- package/dist/__tests__/cli-defaults.unit.test.d.ts +0 -2
- package/dist/__tests__/cli-defaults.unit.test.d.ts.map +0 -1
- package/dist/__tests__/console.unit.test.d.ts +0 -2
- package/dist/__tests__/console.unit.test.d.ts.map +0 -1
- package/dist/cli.d.ts +0 -15
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.mjs +0 -336
- package/dist/cli.mjs.map +0 -1
- package/dist/defineSubcommand.d.ts +0 -40
- package/dist/defineSubcommand.d.ts.map +0 -1
- package/src/__tests__/cli-defaults.unit.test.ts +0 -69
- package/src/__tests__/console.unit.test.ts +0 -679
- package/src/cli.ts +0 -333
- package/src/defineSubcommand.ts +0 -169
|
@@ -1,679 +0,0 @@
|
|
|
1
|
-
// Consolidated by feat-20260609-test-pool-startup-reduction-merge-tiny-test-files
|
|
2
|
-
// biome-ignore-all lint/complexity/noUselessLoneBlockStatements: scope isolation between merged source files
|
|
3
|
-
//
|
|
4
|
-
// Source files (N=8):
|
|
5
|
-
// - packages/console/src/__tests__/check-no-ecs-literal-residue.test.ts
|
|
6
|
-
// - packages/console/src/__tests__/cli-no-inspect.test.ts
|
|
7
|
-
// - packages/console/src/__tests__/cli.test.ts
|
|
8
|
-
// - packages/console/src/__tests__/error-codes.test.ts
|
|
9
|
-
// - packages/console/src/__tests__/errors.test.ts
|
|
10
|
-
// - packages/console/src/__tests__/passthrough-policy.test.ts
|
|
11
|
-
// - packages/console/src/__tests__/wire-default-inspectors.test.ts
|
|
12
|
-
// - packages/console/__tests__/e2e/pack-inspect.test.ts
|
|
13
|
-
//
|
|
14
|
-
// Paradigm: each block-scoped describe('<source-filename>.test.ts', ...) preserves
|
|
15
|
-
// source as ancestorTitles[0]. Top-level imports merged + deduped.
|
|
16
|
-
|
|
17
|
-
// w12 DELETED: import { spawnSync } from 'node:child_process';
|
|
18
|
-
import { readFileSync } from 'node:fs';
|
|
19
|
-
import { dirname, resolve } from 'node:path';
|
|
20
|
-
import { fileURLToPath } from 'node:url';
|
|
21
|
-
import type { ConnectFn } from '@forgeax/engine-types/inspector-client';
|
|
22
|
-
import { describe, expect, it } from 'vitest';
|
|
23
|
-
import { WebSocket } from 'ws';
|
|
24
|
-
import { dispatch, FORGEAX_CLI_SPEC } from '../cli';
|
|
25
|
-
import { REMOTE_ERROR_CODE_TO_JSONRPC, RemoteError, type RemoteErrorCode } from '../errors';
|
|
26
|
-
import { type ConsoleHandle, startServer } from '../server';
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
// --- from cli-no-inspect.test.ts ---
|
|
30
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
31
|
-
const cliSourcePath = resolve(here, '..', 'cli.ts');
|
|
32
|
-
const cliSource = readFileSync(cliSourcePath, 'utf8');
|
|
33
|
-
|
|
34
|
-
describe('cli.ts inspect-subcommand removal (feat-20260517 D-4)', () => {
|
|
35
|
-
it('(a) FORGEAX_CLI_SPEC.subcommands length === 2 (script / eval only)', () => {
|
|
36
|
-
const subs = FORGEAX_CLI_SPEC.subcommands ?? [];
|
|
37
|
-
expect(subs.length).toBe(2);
|
|
38
|
-
const names = subs.map((s) => s.name).sort();
|
|
39
|
-
expect(names).toEqual(['eval', 'script']);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("(b1) cli.ts source contains 0 occurrences of `case 'inspect'`", () => {
|
|
43
|
-
expect(cliSource).not.toMatch(/case\s+'inspect'/);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("(b2) cli.ts source contains 0 occurrences of `name: 'inspect'`", () => {
|
|
47
|
-
expect(cliSource).not.toMatch(/name:\s*'inspect'/);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('(c) `forgeax-engine-remote inspect entities` -> unknown-subcommand usage error', async () => {
|
|
51
|
-
const stderr: string[] = [];
|
|
52
|
-
const stdout: string[] = [];
|
|
53
|
-
// No connect needed: dispatch sees 'inspect' is not a built-in
|
|
54
|
-
// subcommand (plugin discovery removed in M2) and exits non-zero with
|
|
55
|
-
// a CLI usage error (not a RemoteErrorCode — that union is the wire/eval
|
|
56
|
-
// failure vocabulary, not a usage-error channel).
|
|
57
|
-
const exitCode = await dispatch({
|
|
58
|
-
argv: ['node', 'forgeax', 'inspect', 'entities'],
|
|
59
|
-
stdoutWrite: (line: string) => stdout.push(line),
|
|
60
|
-
stderrWrite: (line: string) => stderr.push(line),
|
|
61
|
-
connect: async () => ({
|
|
62
|
-
ok: true,
|
|
63
|
-
value: {
|
|
64
|
-
eval: async () => null,
|
|
65
|
-
dispose: async () => {},
|
|
66
|
-
},
|
|
67
|
-
}),
|
|
68
|
-
});
|
|
69
|
-
expect(exitCode).not.toBe(0);
|
|
70
|
-
const joined = stderr.join('\n');
|
|
71
|
-
expect(joined).toContain("unknown subcommand 'inspect'");
|
|
72
|
-
// M2 removed plugin discovery, so an unknown subcommand reports the
|
|
73
|
-
// built-in roster + the removal note rather than a 'did you mean' plugin hint.
|
|
74
|
-
expect(joined).toContain('script, eval');
|
|
75
|
-
expect(joined).toMatch(/plugin discovery removed/);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('(d) renderTopLevelHelp Built-in section lists only script + eval', async () => {
|
|
79
|
-
const stdout: string[] = [];
|
|
80
|
-
const stderr: string[] = [];
|
|
81
|
-
const exitCode = await dispatch({
|
|
82
|
-
argv: ['node', 'forgeax', '--help'],
|
|
83
|
-
stdoutWrite: (line: string) => stdout.push(line),
|
|
84
|
-
stderrWrite: (line: string) => stderr.push(line),
|
|
85
|
-
connect: async () => ({
|
|
86
|
-
ok: true,
|
|
87
|
-
value: {
|
|
88
|
-
eval: async () => null,
|
|
89
|
-
dispose: async () => {},
|
|
90
|
-
},
|
|
91
|
-
}),
|
|
92
|
-
});
|
|
93
|
-
expect(exitCode).toBe(0);
|
|
94
|
-
const joined = stdout.join('\n');
|
|
95
|
-
expect(joined).toContain('Built-in commands:');
|
|
96
|
-
expect(joined).toContain('script');
|
|
97
|
-
expect(joined).toContain('eval');
|
|
98
|
-
// The Built-in section must not advertise `inspect`. Match an indented
|
|
99
|
-
// bullet (' inspect' at start of line) rather than every prose mention.
|
|
100
|
-
expect(joined).not.toMatch(/^\s\s+inspect\b/m);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
{
|
|
106
|
-
// --- from cli.test.ts ---
|
|
107
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
108
|
-
const cliSourcePath = resolve(here, '..', 'cli.ts');
|
|
109
|
-
const cliSource = readFileSync(cliSourcePath, 'utf8');
|
|
110
|
-
|
|
111
|
-
type DispatchEnv = {
|
|
112
|
-
argv: string[];
|
|
113
|
-
stdout: string[];
|
|
114
|
-
stderr: string[];
|
|
115
|
-
connect?: ConnectFn;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// Build a minimal RemoteError-shaped object satisfying the structural
|
|
119
|
-
// interface (the runtime class lives in ../errors but tests stay package-
|
|
120
|
-
// internal here; the shape is what matters).
|
|
121
|
-
function makeRemoteError(code: RemoteErrorCode, expected: string, hint: string): RemoteError {
|
|
122
|
-
return Object.assign(new Error(`[RemoteError ${code}] expected: ${expected}; hint: ${hint}`), {
|
|
123
|
-
name: 'RemoteError',
|
|
124
|
-
code,
|
|
125
|
-
expected,
|
|
126
|
-
hint,
|
|
127
|
-
}) as unknown as RemoteError;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function run(env: DispatchEnv): Promise<{ exitCode: number }> {
|
|
131
|
-
const stdoutWrite = (line: string): void => {
|
|
132
|
-
env.stdout.push(line);
|
|
133
|
-
};
|
|
134
|
-
const stderrWrite = (line: string): void => {
|
|
135
|
-
env.stderr.push(line);
|
|
136
|
-
};
|
|
137
|
-
const connect: ConnectFn =
|
|
138
|
-
env.connect ??
|
|
139
|
-
(async () => ({
|
|
140
|
-
ok: false,
|
|
141
|
-
error: makeRemoteError(
|
|
142
|
-
'server-not-running',
|
|
143
|
-
'console server is reachable at ws://localhost:5732',
|
|
144
|
-
'start the demo first: pnpm --filter inspector-demo dev; verify the host called startServer({port, registry}); pass --port to override default 5732',
|
|
145
|
-
),
|
|
146
|
-
}));
|
|
147
|
-
const exitCode = await dispatch({
|
|
148
|
-
argv: env.argv,
|
|
149
|
-
stdoutWrite,
|
|
150
|
-
stderrWrite,
|
|
151
|
-
connect,
|
|
152
|
-
});
|
|
153
|
-
return { exitCode };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
describe('cli spec shape (feat-20260517 D-4 inspect removal)', () => {
|
|
157
|
-
it('FORGEAX_CLI_SPEC.subcommands exposes exactly script + eval (zero inspect)', () => {
|
|
158
|
-
const subs = FORGEAX_CLI_SPEC.subcommands ?? [];
|
|
159
|
-
const names = subs.map((s) => s.name).sort();
|
|
160
|
-
expect(names).toEqual(['eval', 'script']);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('cli.ts source uses node:util parseArgs (no commander / sade dep)', () => {
|
|
164
|
-
expect(cliSource).toContain('parseArgs');
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('cli.ts client connect path uses the @forgeax/engine-types/inspector-client SSOT', () => {
|
|
168
|
-
expect(cliSource).toContain('@forgeax/engine-types/inspector-client');
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
describe('dispatch happy paths (with stubbed connect)', () => {
|
|
173
|
-
it('`forgeax-engine-console script /tmp/x.js` invokes execute with the file body', async () => {
|
|
174
|
-
let executedScript: string | undefined;
|
|
175
|
-
const env: DispatchEnv = {
|
|
176
|
-
argv: ['node', 'forgeax', 'script', '/tmp/__cli_test_does_not_exist__.js'],
|
|
177
|
-
stdout: [],
|
|
178
|
-
stderr: [],
|
|
179
|
-
connect: async () => ({
|
|
180
|
-
ok: true,
|
|
181
|
-
value: {
|
|
182
|
-
eval: async (s: string): Promise<unknown> => {
|
|
183
|
-
executedScript = s;
|
|
184
|
-
return null;
|
|
185
|
-
},
|
|
186
|
-
dispose: async (): Promise<void> => {},
|
|
187
|
-
},
|
|
188
|
-
}),
|
|
189
|
-
};
|
|
190
|
-
const r = await run(env);
|
|
191
|
-
// Missing file -> non-zero exit before connect is even reached.
|
|
192
|
-
expect(r.exitCode).not.toBe(0);
|
|
193
|
-
expect(executedScript).toBeUndefined();
|
|
194
|
-
const joined = env.stderr.join('\n');
|
|
195
|
-
expect(joined.toLowerCase()).toContain('script');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('`forgeax-engine-console eval "world.inspect().entityCount"` forwards the inline script', async () => {
|
|
199
|
-
let executedScript: string | undefined;
|
|
200
|
-
const env: DispatchEnv = {
|
|
201
|
-
argv: ['node', 'forgeax', 'eval', 'world.inspect().entityCount'],
|
|
202
|
-
stdout: [],
|
|
203
|
-
stderr: [],
|
|
204
|
-
connect: async () => ({
|
|
205
|
-
ok: true,
|
|
206
|
-
value: {
|
|
207
|
-
eval: async (s: string): Promise<unknown> => {
|
|
208
|
-
executedScript = s;
|
|
209
|
-
return 7;
|
|
210
|
-
},
|
|
211
|
-
dispose: async (): Promise<void> => {},
|
|
212
|
-
},
|
|
213
|
-
}),
|
|
214
|
-
};
|
|
215
|
-
const r = await run(env);
|
|
216
|
-
expect(r.exitCode).toBe(0);
|
|
217
|
-
expect(executedScript).toBe('world.inspect().entityCount');
|
|
218
|
-
const joined = env.stdout.join('\n');
|
|
219
|
-
expect(joined).toContain('7');
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
describe('unknown subcommand usage error', () => {
|
|
224
|
-
it('`forgeax-engine-remote bogus` exits non-zero + stderr names the built-in roster', async () => {
|
|
225
|
-
const env: DispatchEnv = { argv: ['node', 'forgeax', 'bogus'], stdout: [], stderr: [] };
|
|
226
|
-
const r = await run(env);
|
|
227
|
-
expect(r.exitCode).not.toBe(0);
|
|
228
|
-
const joined = env.stderr.join('\n');
|
|
229
|
-
// CLI usage error, not a RemoteErrorCode (that union is the wire/eval
|
|
230
|
-
// failure vocabulary). M2 removed plugin discovery.
|
|
231
|
-
expect(joined).toContain("unknown subcommand 'bogus'");
|
|
232
|
-
expect(joined).toContain('script');
|
|
233
|
-
expect(joined).toContain('eval');
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
describe('server-not-running path (CLI cannot connect)', () => {
|
|
238
|
-
it('Result.err with code server-not-running prints the structured triple', async () => {
|
|
239
|
-
const env: DispatchEnv = {
|
|
240
|
-
argv: ['node', 'forgeax', 'eval', 'world.inspect().entityCount'],
|
|
241
|
-
stdout: [],
|
|
242
|
-
stderr: [],
|
|
243
|
-
connect: async () => ({
|
|
244
|
-
ok: false,
|
|
245
|
-
error: makeRemoteError(
|
|
246
|
-
'server-not-running',
|
|
247
|
-
'console server is reachable at ws://localhost:5732',
|
|
248
|
-
'start the demo first: pnpm --filter inspector-demo dev; verify the host called startServer({port, registry}); pass --port to override default 5732',
|
|
249
|
-
),
|
|
250
|
-
}),
|
|
251
|
-
};
|
|
252
|
-
const r = await run(env);
|
|
253
|
-
expect(r.exitCode).not.toBe(0);
|
|
254
|
-
const joined = env.stderr.join('\n');
|
|
255
|
-
expect(joined).toContain('server-not-running');
|
|
256
|
-
expect(joined).toContain('expected:');
|
|
257
|
-
expect(joined).toContain('hint:');
|
|
258
|
-
expect(joined).toContain('inspector-demo');
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
{
|
|
264
|
-
// --- from error-codes.test.ts ---
|
|
265
|
-
|
|
266
|
-
const CLOSED_4: ReadonlyArray<RemoteErrorCode> = [
|
|
267
|
-
'script-syntax-error',
|
|
268
|
-
'script-runtime-error',
|
|
269
|
-
'server-startup-failed',
|
|
270
|
-
'server-not-running',
|
|
271
|
-
'eval-result-not-serializable',
|
|
272
|
-
];
|
|
273
|
-
|
|
274
|
-
const WIRE_SEGMENT: Readonly<Record<RemoteErrorCode, number>> = {
|
|
275
|
-
'script-syntax-error': -32001,
|
|
276
|
-
'script-runtime-error': -32002,
|
|
277
|
-
'server-startup-failed': -32003,
|
|
278
|
-
'server-not-running': -32004,
|
|
279
|
-
'eval-result-not-serializable': -32005,
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
describe('RemoteErrorCode closed union (4 members) + 4-field surface', () => {
|
|
283
|
-
it('all 4 members instantiate with .code / .expected / .hint / .message', () => {
|
|
284
|
-
for (const code of CLOSED_4) {
|
|
285
|
-
const e = new RemoteError({
|
|
286
|
-
code,
|
|
287
|
-
expected: `expected for ${code}`,
|
|
288
|
-
hint: `hint for ${code}`,
|
|
289
|
-
});
|
|
290
|
-
expect(e.code).toBe(code);
|
|
291
|
-
expect(e.expected).toBe(`expected for ${code}`);
|
|
292
|
-
expect(e.hint).toBe(`hint for ${code}`);
|
|
293
|
-
expect(e.message).toContain(code);
|
|
294
|
-
expect(e.name).toBe('RemoteError');
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
describe('REMOTE_ERROR_CODE_TO_JSONRPC wire segment -32001..-32004', () => {
|
|
300
|
-
it('maps every closed-union member to the locked numeric', () => {
|
|
301
|
-
for (const code of CLOSED_4) {
|
|
302
|
-
expect(REMOTE_ERROR_CODE_TO_JSONRPC[code]).toBe(WIRE_SEGMENT[code]);
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it('table key set equals the 4-member closed union (no add-only drift)', () => {
|
|
307
|
-
const tableKeys = Object.keys(REMOTE_ERROR_CODE_TO_JSONRPC).sort();
|
|
308
|
-
const expectedKeys = [...CLOSED_4].sort();
|
|
309
|
-
expect(tableKeys).toEqual(expectedKeys);
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
{
|
|
315
|
-
// --- from errors.test.ts ---
|
|
316
|
-
const REMOTE_ERROR_CODES_4: ReadonlySet<RemoteErrorCode> = new Set([
|
|
317
|
-
'script-syntax-error',
|
|
318
|
-
'script-runtime-error',
|
|
319
|
-
'server-startup-failed',
|
|
320
|
-
'server-not-running',
|
|
321
|
-
'eval-result-not-serializable',
|
|
322
|
-
]);
|
|
323
|
-
|
|
324
|
-
describe('RemoteError runtime - construction + 4-field surface', () => {
|
|
325
|
-
it('all 4 members instantiate with three readonly string fields + auto-composed message', () => {
|
|
326
|
-
for (const code of REMOTE_ERROR_CODES_4) {
|
|
327
|
-
const e = new RemoteError({
|
|
328
|
-
code,
|
|
329
|
-
expected: `expected for ${code}`,
|
|
330
|
-
hint: `hint for ${code}`,
|
|
331
|
-
});
|
|
332
|
-
expect(e).toBeInstanceOf(RemoteError);
|
|
333
|
-
expect(e).toBeInstanceOf(Error);
|
|
334
|
-
expect(e.code).toBe(code);
|
|
335
|
-
expect(typeof e.expected).toBe('string');
|
|
336
|
-
expect(typeof e.hint).toBe('string');
|
|
337
|
-
expect(e.expected.length).toBeGreaterThan(0);
|
|
338
|
-
expect(e.hint.length).toBeGreaterThan(0);
|
|
339
|
-
expect(typeof e.message).toBe('string');
|
|
340
|
-
expect(e.message).toContain(code);
|
|
341
|
-
expect(e.message).toContain(e.expected);
|
|
342
|
-
expect(e.message).toContain(e.hint);
|
|
343
|
-
expect(e.name).toBe('RemoteError');
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it('AI-user consumption contract: read .code / .expected / .hint / .message via property access', () => {
|
|
348
|
-
const e = new RemoteError({
|
|
349
|
-
code: 'script-syntax-error',
|
|
350
|
-
expected: 'script body is valid JavaScript',
|
|
351
|
-
hint: 'check syntax position in errMessage; fix and resubmit',
|
|
352
|
-
});
|
|
353
|
-
expect(e.code).toBe('script-syntax-error');
|
|
354
|
-
expect(e.expected).toBe('script body is valid JavaScript');
|
|
355
|
-
expect(e.hint).toBe('check syntax position in errMessage; fix and resubmit');
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
// 10.2 templates locked in requirements (feat-20260629 D-5).
|
|
360
|
-
const TEMPLATES_4 = {
|
|
361
|
-
'script-syntax-error': {
|
|
362
|
-
expected: 'script body is valid JavaScript',
|
|
363
|
-
hint: 'check syntax position in errMessage; fix and resubmit',
|
|
364
|
-
},
|
|
365
|
-
'script-runtime-error': {
|
|
366
|
-
expected: 'script executes without throwing',
|
|
367
|
-
hint: 'inspect error; verify symbol availability; eval has full access to world/renderer/assets',
|
|
368
|
-
},
|
|
369
|
-
'server-startup-failed': {
|
|
370
|
-
expected: 'server starts successfully on requested port',
|
|
371
|
-
hint: 'check if port is already in use (default 5732); pass different port; or kill existing process holding the port',
|
|
372
|
-
},
|
|
373
|
-
'server-not-running': {
|
|
374
|
-
expected: 'server is reachable at ws://localhost:<port>',
|
|
375
|
-
hint: 'start the demo first; verify app.remote is wired; pass --port to override default 5732',
|
|
376
|
-
},
|
|
377
|
-
'eval-result-not-serializable': {
|
|
378
|
-
expected: 'eval result is JSON-serializable',
|
|
379
|
-
hint: 'return a JSON-safe value; BigInt and cyclic objects are unsupported over JSON-RPC',
|
|
380
|
-
},
|
|
381
|
-
} as const satisfies Record<
|
|
382
|
-
RemoteErrorCode,
|
|
383
|
-
{ readonly expected: string; readonly hint: string }
|
|
384
|
-
>;
|
|
385
|
-
|
|
386
|
-
describe('RemoteError runtime - templates compose into .message', () => {
|
|
387
|
-
for (const code of REMOTE_ERROR_CODES_4) {
|
|
388
|
-
it(`'${code}' template composes .expected / .hint into .message`, () => {
|
|
389
|
-
const t = TEMPLATES_4[code];
|
|
390
|
-
const e = new RemoteError({ code, expected: t.expected, hint: t.hint });
|
|
391
|
-
expect(e.code).toBe(code);
|
|
392
|
-
expect(e.expected).toBe(t.expected);
|
|
393
|
-
expect(e.hint).toBe(t.hint);
|
|
394
|
-
expect(e.message).toContain(code);
|
|
395
|
-
expect(e.message).toContain(t.expected);
|
|
396
|
-
expect(e.message).toContain(t.hint);
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
describe('RemoteError runtime - JSON.stringify field preservation', () => {
|
|
402
|
-
it('JSON.stringify preserves all four fields (Error class defaults drop these)', () => {
|
|
403
|
-
const e = new RemoteError({
|
|
404
|
-
code: 'server-startup-failed',
|
|
405
|
-
expected: 'world / engine / assets context is read-only in P0',
|
|
406
|
-
hint: 'write API is deferred to asset-system-v1 loop (todo-079); use inspect / script / eval for read-only introspection only',
|
|
407
|
-
});
|
|
408
|
-
const serialized = JSON.stringify(e);
|
|
409
|
-
const parsed = JSON.parse(serialized) as {
|
|
410
|
-
code: string;
|
|
411
|
-
expected: string;
|
|
412
|
-
hint: string;
|
|
413
|
-
message: string;
|
|
414
|
-
};
|
|
415
|
-
expect(parsed.code).toBe('server-startup-failed');
|
|
416
|
-
expect(parsed.expected).toBe('world / engine / assets context is read-only in P0');
|
|
417
|
-
expect(parsed.hint).toContain('write API is deferred to asset-system-v1 loop');
|
|
418
|
-
expect(typeof parsed.message).toBe('string');
|
|
419
|
-
expect(parsed.message.length).toBeGreaterThan(0);
|
|
420
|
-
expect(parsed.message).toContain('server-startup-failed');
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
it('JSON.stringify is roundtrip-stable across two passes', () => {
|
|
424
|
-
const original = new RemoteError({
|
|
425
|
-
code: 'server-not-running',
|
|
426
|
-
expected: TEMPLATES_4['server-not-running'].expected,
|
|
427
|
-
hint: TEMPLATES_4['server-not-running'].hint,
|
|
428
|
-
});
|
|
429
|
-
const first = JSON.stringify(original);
|
|
430
|
-
const reparsed = JSON.parse(first) as {
|
|
431
|
-
code: RemoteErrorCode;
|
|
432
|
-
expected: string;
|
|
433
|
-
hint: string;
|
|
434
|
-
message: string;
|
|
435
|
-
};
|
|
436
|
-
const second = JSON.stringify(reparsed);
|
|
437
|
-
expect(JSON.parse(second)).toEqual(JSON.parse(first));
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
it('JSON.stringify within an array preserves all 4 fields per entry (JSON-RPC batch payload contract)', () => {
|
|
441
|
-
const errs = [
|
|
442
|
-
new RemoteError({
|
|
443
|
-
code: 'script-syntax-error',
|
|
444
|
-
expected: TEMPLATES_4['script-syntax-error'].expected,
|
|
445
|
-
hint: TEMPLATES_4['script-syntax-error'].hint,
|
|
446
|
-
}),
|
|
447
|
-
new RemoteError({
|
|
448
|
-
code: 'server-not-running',
|
|
449
|
-
expected: TEMPLATES_4['server-not-running'].expected,
|
|
450
|
-
hint: TEMPLATES_4['server-not-running'].hint,
|
|
451
|
-
}),
|
|
452
|
-
];
|
|
453
|
-
const out = JSON.parse(JSON.stringify(errs)) as Array<{
|
|
454
|
-
code: string;
|
|
455
|
-
expected: string;
|
|
456
|
-
hint: string;
|
|
457
|
-
message: string;
|
|
458
|
-
}>;
|
|
459
|
-
expect(out).toHaveLength(2);
|
|
460
|
-
expect(out[0]?.code).toBe('script-syntax-error');
|
|
461
|
-
expect(out[1]?.code).toBe('server-not-running');
|
|
462
|
-
for (const entry of out) {
|
|
463
|
-
expect(entry).toHaveProperty('code');
|
|
464
|
-
expect(entry).toHaveProperty('expected');
|
|
465
|
-
expect(entry).toHaveProperty('hint');
|
|
466
|
-
expect(entry).toHaveProperty('message');
|
|
467
|
-
}
|
|
468
|
-
});
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
describe('RemoteError runtime - instanceof guards', () => {
|
|
472
|
-
it('instances are detected via instanceof RemoteError (try/catch boundary)', () => {
|
|
473
|
-
const e = new RemoteError({
|
|
474
|
-
code: 'server-startup-failed',
|
|
475
|
-
expected: TEMPLATES_4['server-startup-failed'].expected,
|
|
476
|
-
hint: TEMPLATES_4['server-startup-failed'].hint,
|
|
477
|
-
});
|
|
478
|
-
let caught: unknown = null;
|
|
479
|
-
try {
|
|
480
|
-
throw e;
|
|
481
|
-
} catch (err) {
|
|
482
|
-
caught = err;
|
|
483
|
-
}
|
|
484
|
-
expect(caught).toBeInstanceOf(RemoteError);
|
|
485
|
-
expect(caught).toBeInstanceOf(Error);
|
|
486
|
-
expect(caught).toBe(e);
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
it('non-RemoteError throws are NOT mis-detected', () => {
|
|
490
|
-
const plain = new Error('not an RemoteError');
|
|
491
|
-
expect(plain).toBeInstanceOf(Error);
|
|
492
|
-
expect(plain).not.toBeInstanceOf(RemoteError);
|
|
493
|
-
});
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
// Closed-ness runtime mirror of the static grep gate. tsc strict-mode reports
|
|
497
|
-
// 'Function lacks ending return statement' if a future commit adds a member
|
|
498
|
-
// without updating this switch (charter proposition 4 explicit failure +
|
|
499
|
-
// proposition 3 machine-readable union > prose).
|
|
500
|
-
function describeCode(code: RemoteErrorCode): string {
|
|
501
|
-
switch (code) {
|
|
502
|
-
case 'script-syntax-error':
|
|
503
|
-
return 'syntax';
|
|
504
|
-
case 'script-runtime-error':
|
|
505
|
-
return 'runtime';
|
|
506
|
-
case 'server-startup-failed':
|
|
507
|
-
return 'startup-failed';
|
|
508
|
-
case 'server-not-running':
|
|
509
|
-
return 'not-running';
|
|
510
|
-
case 'eval-result-not-serializable':
|
|
511
|
-
return 'eval-result-not-serializable';
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
describe('RemoteErrorCode exhaustive switch (closed-ness runtime mirror)', () => {
|
|
516
|
-
it('every member maps to a distinct describeCode() result', () => {
|
|
517
|
-
const results = new Set<string>();
|
|
518
|
-
for (const code of REMOTE_ERROR_CODES_4) {
|
|
519
|
-
const r = describeCode(code);
|
|
520
|
-
expect(typeof r).toBe('string');
|
|
521
|
-
expect(r.length).toBeGreaterThan(0);
|
|
522
|
-
results.add(r);
|
|
523
|
-
}
|
|
524
|
-
expect(results.size).toBe(REMOTE_ERROR_CODES_4.size);
|
|
525
|
-
});
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
{
|
|
530
|
-
// --- from pack-inspect.test.ts ---
|
|
531
|
-
|
|
532
|
-
type JsonRpcRequest = {
|
|
533
|
-
jsonrpc: '2.0';
|
|
534
|
-
method: string;
|
|
535
|
-
params?: unknown;
|
|
536
|
-
id?: number | string | null;
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
type JsonRpcResponse = {
|
|
540
|
-
jsonrpc: '2.0';
|
|
541
|
-
result?: unknown;
|
|
542
|
-
error?: {
|
|
543
|
-
code: number;
|
|
544
|
-
message: string;
|
|
545
|
-
data?: {
|
|
546
|
-
code: string;
|
|
547
|
-
expected?: string;
|
|
548
|
-
hint?: string;
|
|
549
|
-
message?: string;
|
|
550
|
-
};
|
|
551
|
-
};
|
|
552
|
-
id: number | string | null;
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
async function connect(port: number): Promise<WebSocket> {
|
|
556
|
-
const ws = new WebSocket(`ws://127.0.0.1:${port}/inspector`);
|
|
557
|
-
await new Promise<void>((resolve, reject) => {
|
|
558
|
-
ws.once('open', () => resolve());
|
|
559
|
-
ws.once('error', reject);
|
|
560
|
-
});
|
|
561
|
-
return ws;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
async function rpc(ws: WebSocket, req: JsonRpcRequest): Promise<JsonRpcResponse> {
|
|
565
|
-
return new Promise<JsonRpcResponse>((resolve) => {
|
|
566
|
-
ws.once('message', (raw) => {
|
|
567
|
-
resolve(JSON.parse(raw.toString()) as JsonRpcResponse);
|
|
568
|
-
});
|
|
569
|
-
ws.send(JSON.stringify(req));
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Mock assets object that exposes guidToHandle map for inspect packs
|
|
574
|
-
function makeMockAssetsWithGuids(guids: string[]): object {
|
|
575
|
-
const guidToHandle = new Map<string, unknown>();
|
|
576
|
-
for (let i = 0; i < guids.length; i++) {
|
|
577
|
-
guidToHandle.set(guids[i] ?? '', i + 1);
|
|
578
|
-
}
|
|
579
|
-
return {
|
|
580
|
-
guidToHandle,
|
|
581
|
-
// Expose iterableEntries for the packs handler script
|
|
582
|
-
_packEntries: guids.map((guid, i) => ({ guid, handle: i + 1 })),
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
describe('inspect packs e2e', () => {
|
|
587
|
-
it('happy path: 2 GUIDs registered → result contains 2 entries', async () => {
|
|
588
|
-
const guid1 = 'aaaaaaaa-bbbb-7ccc-8ddd-eeeeeeeeeeee';
|
|
589
|
-
const guid2 = 'ffffffff-aaaa-7bbb-9ccc-111111111111';
|
|
590
|
-
const assets = makeMockAssetsWithGuids([guid1, guid2]);
|
|
591
|
-
|
|
592
|
-
const result = await startServer({ port: 0, world: {}, assets });
|
|
593
|
-
expect(result.ok).toBe(true);
|
|
594
|
-
if (!result.ok) return;
|
|
595
|
-
const handle: ConsoleHandle = result.value;
|
|
596
|
-
|
|
597
|
-
const ws = await connect(handle.port);
|
|
598
|
-
|
|
599
|
-
// Execute the packs inspect script via execute() method
|
|
600
|
-
const script = `
|
|
601
|
-
(() => {
|
|
602
|
-
const entries = assets._packEntries;
|
|
603
|
-
return { count: entries.length, entries };
|
|
604
|
-
})()
|
|
605
|
-
`;
|
|
606
|
-
const resp = await rpc(ws, {
|
|
607
|
-
jsonrpc: '2.0',
|
|
608
|
-
method: 'eval',
|
|
609
|
-
params: { script },
|
|
610
|
-
id: 1,
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
ws.close();
|
|
614
|
-
await handle.close();
|
|
615
|
-
|
|
616
|
-
expect(resp.error).toBeUndefined();
|
|
617
|
-
const r = resp.result as { count: number; entries: { guid: string }[] };
|
|
618
|
-
expect(r.count).toBe(2);
|
|
619
|
-
expect(r.entries).toHaveLength(2);
|
|
620
|
-
const guids = r.entries.map((e) => e.guid);
|
|
621
|
-
expect(guids).toContain(guid1);
|
|
622
|
-
expect(guids).toContain(guid2);
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
it('collision path: scanner pack-guid-collision → JSON-RPC error with pack-guid-collision code', async () => {
|
|
626
|
-
// Simulate the packs inspect handler receiving a collision error
|
|
627
|
-
// The handler exposes collision errors as JSON-RPC errors with
|
|
628
|
-
// error.data.code = 'pack-guid-collision'
|
|
629
|
-
const assets = {
|
|
630
|
-
_packCollision: {
|
|
631
|
-
code: 'pack-guid-collision' as const,
|
|
632
|
-
paths: ['/a/foo.pack.json', '/b/foo.pack.json'],
|
|
633
|
-
guid: 'aaaaaaaa-bbbb-7ccc-8ddd-eeeeeeeeeeee',
|
|
634
|
-
},
|
|
635
|
-
_packEntries: null as unknown as unknown[],
|
|
636
|
-
};
|
|
637
|
-
|
|
638
|
-
const result = await startServer({ port: 0, world: {}, assets });
|
|
639
|
-
expect(result.ok).toBe(true);
|
|
640
|
-
if (!result.ok) return;
|
|
641
|
-
const handle: ConsoleHandle = result.value;
|
|
642
|
-
|
|
643
|
-
const ws = await connect(handle.port);
|
|
644
|
-
|
|
645
|
-
// Execute script that returns collision error signal
|
|
646
|
-
// The packs handler (w25) should detect collision and surface it as JSON-RPC error
|
|
647
|
-
// For TDD purposes: script that surfaces collision from assets context
|
|
648
|
-
const script = `
|
|
649
|
-
(() => {
|
|
650
|
-
if (assets._packCollision) {
|
|
651
|
-
// Throw a structured error that the server can translate to JSON-RPC error
|
|
652
|
-
const e = new Error('pack-guid-collision');
|
|
653
|
-
e.code = assets._packCollision.code;
|
|
654
|
-
e.detail = assets._packCollision;
|
|
655
|
-
throw e;
|
|
656
|
-
}
|
|
657
|
-
return { count: 0, entries: [] };
|
|
658
|
-
})()
|
|
659
|
-
`;
|
|
660
|
-
const resp = await rpc(ws, {
|
|
661
|
-
jsonrpc: '2.0',
|
|
662
|
-
method: 'eval',
|
|
663
|
-
params: { script },
|
|
664
|
-
id: 2,
|
|
665
|
-
});
|
|
666
|
-
|
|
667
|
-
ws.close();
|
|
668
|
-
await handle.close();
|
|
669
|
-
|
|
670
|
-
// Script-runtime-error is expected because we throw from the script
|
|
671
|
-
// The JSON-RPC error should have data containing info about the failure
|
|
672
|
-
expect(resp.error).toBeDefined();
|
|
673
|
-
expect(typeof resp.error?.code).toBe('number');
|
|
674
|
-
// The error code should be in the inspector range -32001..-32006
|
|
675
|
-
expect(resp.error?.code).toBeGreaterThanOrEqual(-32006);
|
|
676
|
-
expect(resp.error?.code).toBeLessThanOrEqual(-32001);
|
|
677
|
-
});
|
|
678
|
-
});
|
|
679
|
-
}
|