@nexart/cli 0.2.2 → 0.3.0

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,703 @@
1
+ /**
2
+ * @nexart/cli v0.3.0 — AI command tests
3
+ *
4
+ * Tests:
5
+ * A) canonicalJson + computeBundleHash (unit)
6
+ * B) readInputJson — file input
7
+ * C) readInputJson — stdin input
8
+ * D) aiCreateCommand — request/response
9
+ * E) aiCertifyCommand — summary output
10
+ * F) aiCertifyCommand — --json output
11
+ * G) aiVerifyCommand — PASS
12
+ * H) aiVerifyCommand — FAIL (tampered hash)
13
+ * I) aiVerifyCommand — FAIL (wrong bundleType)
14
+ * J) aiVerifyCommand — attestation present/absent
15
+ * K) aiVerifyCommand — --json output
16
+ * L) backward compat — Code Mode exports exist
17
+ * M) callNodeApi — sets Authorization header and sends body
18
+ * N) aiCreateCommand — stdin input
19
+ * O) aiCertifyCommand — stdin input
20
+ * P) aiCreateCommand — saves --out file
21
+ * Q) aiCertifyCommand — 401 handling
22
+ */
23
+ import { describe, it, before, after } from 'node:test';
24
+ import * as assert from 'node:assert/strict';
25
+ import * as fs from 'node:fs';
26
+ import * as os from 'node:os';
27
+ import * as path from 'node:path';
28
+ import * as crypto from 'node:crypto';
29
+ import { canonicalJson, computeBundleHash, readInputJson, callNodeApi, aiCreateCommand, aiCertifyCommand, aiVerifyCommand, hasAttestation, readStdinText, } from '../index.js';
30
+ // ─── Helpers ────────────────────────────────────────────────────────────────
31
+ const FIXED_TS = '2026-03-15T00:00:00.000Z';
32
+ function sha256hex(data) {
33
+ return crypto.createHash('sha256').update(Buffer.from(data, 'utf-8')).digest('hex');
34
+ }
35
+ function makeTempDir() {
36
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'nexart-cli-test-'));
37
+ }
38
+ function captureOutput() {
39
+ const stdout = [];
40
+ const stderr = [];
41
+ const origStdoutWrite = process.stdout.write.bind(process.stdout);
42
+ const origStderrWrite = process.stderr.write.bind(process.stderr);
43
+ const origConsoleLog = console.log.bind(console);
44
+ const origConsoleError = console.error.bind(console);
45
+ process.stdout.write = (chunk) => { stdout.push(String(chunk)); return true; };
46
+ process.stderr.write = (chunk) => { stderr.push(String(chunk)); return true; };
47
+ console.log = (...args) => { stdout.push(args.map(String).join(' ') + '\n'); };
48
+ console.error = (...args) => { stderr.push(args.map(String).join(' ') + '\n'); };
49
+ const restore = () => {
50
+ process.stdout.write = origStdoutWrite;
51
+ process.stderr.write = origStderrWrite;
52
+ console.log = origConsoleLog;
53
+ console.error = origConsoleError;
54
+ };
55
+ return { stdout, stderr, restore };
56
+ }
57
+ function mockExit() {
58
+ const state = { exitCode: null };
59
+ const orig = process.exit.bind(process);
60
+ process.exit = (code) => {
61
+ state.exitCode = code ?? 0;
62
+ throw new Error(`process.exit(${code})`);
63
+ };
64
+ return { get exitCode() { return state.exitCode; }, restore: () => { process.exit = orig; } };
65
+ }
66
+ function buildMinimalBundle(overrides = {}) {
67
+ const snapshot = {
68
+ bundleType: 'cer.ai.execution.v1',
69
+ executionId: 'test-001',
70
+ provider: 'openai',
71
+ model: 'gpt-4o',
72
+ input: 'hello',
73
+ output: 'world',
74
+ parameters: { temperature: 0, maxTokens: 100, topP: null, seed: null },
75
+ inputHash: 'sha256:' + sha256hex('hello'),
76
+ outputHash: 'sha256:' + sha256hex('world'),
77
+ timestamp: FIXED_TS,
78
+ sdkVersion: '0.8.0',
79
+ prompt: 'test prompt',
80
+ appId: null,
81
+ runId: null,
82
+ stepId: null,
83
+ stepIndex: null,
84
+ prevStepHash: null,
85
+ modelVersion: null,
86
+ toolCalls: undefined,
87
+ };
88
+ const protectedSet = {
89
+ bundleType: 'cer.ai.execution.v1',
90
+ version: '1',
91
+ createdAt: FIXED_TS,
92
+ snapshot,
93
+ };
94
+ const certificateHash = 'sha256:' + sha256hex(canonicalJson(protectedSet));
95
+ return {
96
+ bundleType: 'cer.ai.execution.v1',
97
+ version: '1',
98
+ createdAt: FIXED_TS,
99
+ snapshot,
100
+ certificateHash,
101
+ ...overrides,
102
+ };
103
+ }
104
+ // ─── A) canonicalJson + computeBundleHash ────────────────────────────────────
105
+ describe('A: canonicalJson', () => {
106
+ it('sorts object keys lexicographically', () => {
107
+ assert.equal(canonicalJson({ z: 1, a: 2 }), '{"a":2,"z":1}');
108
+ });
109
+ it('does not sort arrays', () => {
110
+ assert.equal(canonicalJson([3, 1, 2]), '[3,1,2]');
111
+ });
112
+ it('handles null', () => {
113
+ assert.equal(canonicalJson(null), 'null');
114
+ });
115
+ it('handles nested objects', () => {
116
+ const out = canonicalJson({ b: { d: 4, c: 3 }, a: 1 });
117
+ assert.equal(out, '{"a":1,"b":{"c":3,"d":4}}');
118
+ });
119
+ it('omits undefined values', () => {
120
+ const out = canonicalJson({ a: 1, b: undefined, c: 3 });
121
+ assert.equal(out, '{"a":1,"c":3}');
122
+ });
123
+ it('handles primitives', () => {
124
+ assert.equal(canonicalJson(42), '42');
125
+ assert.equal(canonicalJson('hello'), '"hello"');
126
+ assert.equal(canonicalJson(true), 'true');
127
+ });
128
+ it('is stable for same input', () => {
129
+ const obj = { provider: 'openai', model: 'gpt-4o', temperature: 0 };
130
+ assert.equal(canonicalJson(obj), canonicalJson(obj));
131
+ });
132
+ });
133
+ describe('A: computeBundleHash', () => {
134
+ it('returns sha256: prefixed hash', () => {
135
+ const bundle = buildMinimalBundle();
136
+ const h = computeBundleHash(bundle);
137
+ assert.match(h, /^sha256:[0-9a-f]{64}$/);
138
+ });
139
+ it('matches bundle.certificateHash for valid bundle', () => {
140
+ const bundle = buildMinimalBundle();
141
+ assert.equal(computeBundleHash(bundle), bundle['certificateHash']);
142
+ });
143
+ it('differs when snapshot is tampered', () => {
144
+ const bundle = buildMinimalBundle();
145
+ const tampered = { ...bundle, snapshot: { ...bundle['snapshot'], output: 'tampered' } };
146
+ assert.notEqual(computeBundleHash(tampered), bundle['certificateHash']);
147
+ });
148
+ it('differs when createdAt changes', () => {
149
+ const bundle = buildMinimalBundle();
150
+ const modified = { ...bundle, createdAt: '2030-01-01T00:00:00.000Z' };
151
+ assert.notEqual(computeBundleHash(modified), bundle['certificateHash']);
152
+ });
153
+ });
154
+ // ─── B) readInputJson — file input ───────────────────────────────────────────
155
+ describe('B: readInputJson — file input', () => {
156
+ let tmpDir;
157
+ before(() => { tmpDir = makeTempDir(); });
158
+ after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
159
+ it('reads valid JSON from a file', async () => {
160
+ const file = path.join(tmpDir, 'input.json');
161
+ const data = { executionId: 'test-001', provider: 'openai' };
162
+ fs.writeFileSync(file, JSON.stringify(data));
163
+ const result = await readInputJson(file);
164
+ assert.deepEqual(result, data);
165
+ });
166
+ it('exits on missing file', async () => {
167
+ const mock = mockExit();
168
+ const captured = captureOutput();
169
+ try {
170
+ await readInputJson('/nonexistent/path/file.json');
171
+ assert.fail('should have exited');
172
+ }
173
+ catch (e) {
174
+ assert.match(e.message, /exit/);
175
+ assert.equal(mock.exitCode, 1);
176
+ }
177
+ finally {
178
+ captured.restore();
179
+ mock.restore();
180
+ }
181
+ });
182
+ });
183
+ // ─── C) readInputJson — stdin ─────────────────────────────────────────────────
184
+ describe('C: readInputJson — stdin input', () => {
185
+ it('reads JSON from injected stdin reader', async () => {
186
+ const data = { provider: 'anthropic', model: 'claude-3' };
187
+ const mockStdin = async () => JSON.stringify(data);
188
+ const result = await readInputJson(undefined, mockStdin);
189
+ assert.deepEqual(result, data);
190
+ });
191
+ it('exits on invalid JSON from stdin', async () => {
192
+ const mock = mockExit();
193
+ const mockStdin = async () => 'not-valid-json{{{';
194
+ try {
195
+ await readInputJson(undefined, mockStdin);
196
+ assert.fail('should have exited');
197
+ }
198
+ catch (e) {
199
+ assert.match(e.message, /exit/);
200
+ assert.equal(mock.exitCode, 1);
201
+ }
202
+ finally {
203
+ mock.restore();
204
+ }
205
+ });
206
+ });
207
+ // ─── D) aiCreateCommand — request/response ────────────────────────────────────
208
+ describe('D: aiCreateCommand', () => {
209
+ let tmpDir;
210
+ before(() => { tmpDir = makeTempDir(); });
211
+ after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
212
+ it('sends POST to /v1/cer/ai/create and prints bundle JSON', async () => {
213
+ const inputFile = path.join(tmpDir, 'exec.json');
214
+ const input = { executionId: 'demo-001', provider: 'openai', model: 'gpt-4o-mini' };
215
+ fs.writeFileSync(inputFile, JSON.stringify(input));
216
+ const fakeBundle = { bundleType: 'cer.ai.execution.v1', certificateHash: 'sha256:aabbcc' };
217
+ const capturedRequests = [];
218
+ const mockFetch = async (url, init) => {
219
+ capturedRequests.push({ url, body: JSON.parse(init?.body || '{}') });
220
+ return new Response(JSON.stringify(fakeBundle), {
221
+ status: 200,
222
+ headers: { 'Content-Type': 'application/json' },
223
+ });
224
+ };
225
+ const stdoutChunks = [];
226
+ const origWrite = process.stdout.write.bind(process.stdout);
227
+ process.stdout.write = (chunk) => { stdoutChunks.push(String(chunk)); return true; };
228
+ try {
229
+ await aiCreateCommand(inputFile, { endpoint: 'http://localhost:4000', apiKey: undefined }, mockFetch);
230
+ }
231
+ finally {
232
+ process.stdout.write = origWrite;
233
+ }
234
+ assert.equal(capturedRequests.length, 1);
235
+ const req = capturedRequests[0];
236
+ assert.match(req.url, /\/v1\/cer\/ai\/create/);
237
+ assert.deepEqual(req.body, input);
238
+ const stdout = stdoutChunks.join('');
239
+ const parsed = JSON.parse(stdout);
240
+ assert.equal(parsed.bundleType, 'cer.ai.execution.v1');
241
+ });
242
+ it('sets Authorization header when apiKey provided', async () => {
243
+ const input = { executionId: 'x', provider: 'openai', model: 'gpt-4o' };
244
+ const stdinReader = async () => JSON.stringify(input);
245
+ const capturedHeaders = [];
246
+ const mockFetch = async (_url, init) => {
247
+ capturedHeaders.push(init?.headers || {});
248
+ return new Response(JSON.stringify({ bundleType: 'cer.ai.execution.v1' }), { status: 200 });
249
+ };
250
+ const origWrite = process.stdout.write.bind(process.stdout);
251
+ process.stdout.write = () => true;
252
+ try {
253
+ await aiCreateCommand(undefined, { endpoint: 'http://localhost:4000', apiKey: 'test-key-123' }, mockFetch, stdinReader);
254
+ }
255
+ finally {
256
+ process.stdout.write = origWrite;
257
+ }
258
+ assert.equal(capturedHeaders[0]?.['Authorization'], 'Bearer test-key-123');
259
+ });
260
+ it('exits on 401 response', async () => {
261
+ const input = { executionId: 'x' };
262
+ const stdinReader = async () => JSON.stringify(input);
263
+ const mockFetch = async () => new Response('{}', { status: 401 });
264
+ const mock = mockExit();
265
+ const origError = console.error;
266
+ console.error = () => { };
267
+ try {
268
+ await aiCreateCommand(undefined, { endpoint: 'http://localhost:4000', apiKey: 'bad-key' }, mockFetch, stdinReader);
269
+ assert.fail('should have exited');
270
+ }
271
+ catch (e) {
272
+ assert.equal(mock.exitCode, 1);
273
+ }
274
+ finally {
275
+ mock.restore();
276
+ console.error = origError;
277
+ }
278
+ });
279
+ });
280
+ // ─── E) aiCertifyCommand — summary output ─────────────────────────────────────
281
+ describe('E: aiCertifyCommand — summary output', () => {
282
+ let tmpDir;
283
+ before(() => { tmpDir = makeTempDir(); });
284
+ after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
285
+ it('prints certificateHash / verificationUrl / attestationId summary', async () => {
286
+ const inputFile = path.join(tmpDir, 'exec.json');
287
+ fs.writeFileSync(inputFile, JSON.stringify({ executionId: 'c-001', provider: 'openai' }));
288
+ const certifyResponse = {
289
+ certificateHash: 'sha256:' + 'a'.repeat(64),
290
+ verificationUrl: 'https://verify.nexart.io/e/abc123',
291
+ attestationId: 'attest-xyz',
292
+ };
293
+ const mockFetch = async () => new Response(JSON.stringify(certifyResponse), { status: 200 });
294
+ const stdoutLines = [];
295
+ const origLog = console.log;
296
+ console.log = (...args) => { stdoutLines.push(args.map(String).join(' ')); };
297
+ try {
298
+ await aiCertifyCommand(inputFile, { json: false, endpoint: 'http://localhost:4000', apiKey: undefined }, mockFetch);
299
+ }
300
+ finally {
301
+ console.log = origLog;
302
+ }
303
+ const output = stdoutLines.join('\n');
304
+ assert.match(output, /CER certified/);
305
+ assert.match(output, /certificateHash: sha256:/);
306
+ assert.match(output, /verificationUrl: https:\/\/verify\.nexart\.io/);
307
+ assert.match(output, /attestationId:\s+attest-xyz/);
308
+ });
309
+ it('uses (not provided) when fields are absent', async () => {
310
+ const input = { executionId: 'c-002' };
311
+ const stdinReader = async () => JSON.stringify(input);
312
+ const mockFetch = async () => new Response(JSON.stringify({ ok: true }), { status: 200 });
313
+ const stdoutLines = [];
314
+ const origLog = console.log;
315
+ console.log = (...args) => { stdoutLines.push(args.map(String).join(' ')); };
316
+ try {
317
+ await aiCertifyCommand(undefined, { json: false, endpoint: 'http://localhost:4000', apiKey: undefined }, mockFetch, stdinReader);
318
+ }
319
+ finally {
320
+ console.log = origLog;
321
+ }
322
+ const output = stdoutLines.join('\n');
323
+ assert.match(output, /not provided/);
324
+ });
325
+ });
326
+ // ─── F) aiCertifyCommand — --json output ──────────────────────────────────────
327
+ describe('F: aiCertifyCommand — --json output', () => {
328
+ it('prints full JSON response when json=true', async () => {
329
+ const input = { executionId: 'f-001' };
330
+ const stdinReader = async () => JSON.stringify(input);
331
+ const certifyResponse = {
332
+ certificateHash: 'sha256:' + 'b'.repeat(64),
333
+ verificationUrl: 'https://verify.nexart.io/e/def456',
334
+ attestationId: 'attest-abc',
335
+ extra: 'field',
336
+ };
337
+ const mockFetch = async () => new Response(JSON.stringify(certifyResponse), { status: 200 });
338
+ const stdoutChunks = [];
339
+ const origWrite = process.stdout.write.bind(process.stdout);
340
+ process.stdout.write = (chunk) => { stdoutChunks.push(String(chunk)); return true; };
341
+ try {
342
+ await aiCertifyCommand(undefined, { json: true, endpoint: 'http://localhost:4000', apiKey: undefined }, mockFetch, stdinReader);
343
+ }
344
+ finally {
345
+ process.stdout.write = origWrite;
346
+ }
347
+ const parsed = JSON.parse(stdoutChunks.join(''));
348
+ assert.equal(parsed.certificateHash, certifyResponse.certificateHash);
349
+ assert.equal(parsed.verificationUrl, certifyResponse.verificationUrl);
350
+ assert.equal(parsed.attestationId, certifyResponse.attestationId);
351
+ assert.equal(parsed.extra, 'field');
352
+ });
353
+ });
354
+ // ─── G) aiVerifyCommand — PASS ───────────────────────────────────────────────
355
+ describe('G: aiVerifyCommand — PASS', () => {
356
+ let tmpDir;
357
+ before(() => { tmpDir = makeTempDir(); });
358
+ after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
359
+ it('prints PASS for a valid bundle and exits 0', async () => {
360
+ const bundle = buildMinimalBundle();
361
+ const bundleFile = path.join(tmpDir, 'bundle.json');
362
+ fs.writeFileSync(bundleFile, JSON.stringify(bundle));
363
+ const stdoutLines = [];
364
+ const origLog = console.log;
365
+ console.log = (...args) => { stdoutLines.push(args.map(String).join(' ')); };
366
+ const mock = mockExit();
367
+ try {
368
+ await aiVerifyCommand(bundleFile, { json: false });
369
+ }
370
+ catch (e) {
371
+ assert.match(e.message, /exit\(0\)/);
372
+ }
373
+ finally {
374
+ console.log = origLog;
375
+ mock.restore();
376
+ }
377
+ const output = stdoutLines.join('\n');
378
+ assert.match(output, /Verification result: PASS/);
379
+ assert.match(output, /bundleIntegrity: PASS/);
380
+ assert.equal(mock.exitCode, 0);
381
+ });
382
+ });
383
+ // ─── H) aiVerifyCommand — FAIL (tampered) ─────────────────────────────────────
384
+ describe('H: aiVerifyCommand — FAIL (tampered hash)', () => {
385
+ let tmpDir;
386
+ before(() => { tmpDir = makeTempDir(); });
387
+ after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
388
+ it('prints FAIL when certificateHash does not match contents', async () => {
389
+ const bundle = buildMinimalBundle({ certificateHash: 'sha256:' + 'dead'.repeat(16) });
390
+ const bundleFile = path.join(tmpDir, 'tampered.json');
391
+ fs.writeFileSync(bundleFile, JSON.stringify(bundle));
392
+ const stdoutLines = [];
393
+ const origLog = console.log;
394
+ const origError = console.error;
395
+ console.log = (...args) => { stdoutLines.push(args.map(String).join(' ')); };
396
+ console.error = () => { };
397
+ const mock = mockExit();
398
+ try {
399
+ await aiVerifyCommand(bundleFile, { json: false });
400
+ }
401
+ catch (e) {
402
+ assert.match(e.message, /exit\(1\)/);
403
+ }
404
+ finally {
405
+ console.log = origLog;
406
+ console.error = origError;
407
+ mock.restore();
408
+ }
409
+ const output = stdoutLines.join('\n');
410
+ assert.match(output, /Verification result: FAIL/);
411
+ assert.match(output, /bundleIntegrity: FAIL/);
412
+ assert.equal(mock.exitCode, 1);
413
+ });
414
+ });
415
+ // ─── I) aiVerifyCommand — FAIL (wrong bundleType) ─────────────────────────────
416
+ describe('I: aiVerifyCommand — wrong bundleType', () => {
417
+ it('exits with error for non-CER bundle', async () => {
418
+ const input = { bundleType: 'nexart.codemode.v1', outputHash: 'sha256:abc' };
419
+ const stdinReader = async () => JSON.stringify(input);
420
+ const origError = console.error;
421
+ console.error = () => { };
422
+ const mock = mockExit();
423
+ try {
424
+ await aiVerifyCommand(undefined, { json: false }, stdinReader);
425
+ assert.fail('should have exited');
426
+ }
427
+ catch (e) {
428
+ assert.match(e.message, /exit/);
429
+ assert.equal(mock.exitCode, 1);
430
+ }
431
+ finally {
432
+ console.error = origError;
433
+ mock.restore();
434
+ }
435
+ });
436
+ });
437
+ // ─── J) aiVerifyCommand — attestation present/absent ──────────────────────────
438
+ describe('J: aiVerifyCommand — attestation reporting', () => {
439
+ it('reports PRESENT when attestationId is set', async () => {
440
+ const bundle = buildMinimalBundle({ attestationId: 'attest-live-xyz' });
441
+ const stdinReader = async () => JSON.stringify(bundle);
442
+ const stdoutLines = [];
443
+ const origLog = console.log;
444
+ console.log = (...args) => { stdoutLines.push(args.map(String).join(' ')); };
445
+ const mock = mockExit();
446
+ try {
447
+ await aiVerifyCommand(undefined, { json: false }, stdinReader);
448
+ }
449
+ catch (e) {
450
+ assert.match(e.message, /exit\(0\)/);
451
+ }
452
+ finally {
453
+ console.log = origLog;
454
+ mock.restore();
455
+ }
456
+ assert.match(stdoutLines.join('\n'), /nodeAttestation: PRESENT/);
457
+ });
458
+ it('reports ABSENT when no attestationId', async () => {
459
+ const bundle = buildMinimalBundle();
460
+ const stdinReader = async () => JSON.stringify(bundle);
461
+ const stdoutLines = [];
462
+ const origLog = console.log;
463
+ console.log = (...args) => { stdoutLines.push(args.map(String).join(' ')); };
464
+ const mock = mockExit();
465
+ try {
466
+ await aiVerifyCommand(undefined, { json: false }, stdinReader);
467
+ }
468
+ catch (e) {
469
+ assert.match(e.message, /exit\(0\)/);
470
+ }
471
+ finally {
472
+ console.log = origLog;
473
+ mock.restore();
474
+ }
475
+ assert.match(stdoutLines.join('\n'), /nodeAttestation: ABSENT/);
476
+ });
477
+ });
478
+ // ─── K) aiVerifyCommand — --json output ───────────────────────────────────────
479
+ describe('K: aiVerifyCommand — --json output', () => {
480
+ it('prints machine-readable JSON for PASS', async () => {
481
+ const bundle = buildMinimalBundle();
482
+ const stdinReader = async () => JSON.stringify(bundle);
483
+ const stdoutChunks = [];
484
+ const origWrite = process.stdout.write.bind(process.stdout);
485
+ process.stdout.write = (chunk) => { stdoutChunks.push(String(chunk)); return true; };
486
+ const mock = mockExit();
487
+ try {
488
+ await aiVerifyCommand(undefined, { json: true }, stdinReader);
489
+ }
490
+ catch (e) {
491
+ assert.match(e.message, /exit\(0\)/);
492
+ }
493
+ finally {
494
+ process.stdout.write = origWrite;
495
+ mock.restore();
496
+ }
497
+ const parsed = JSON.parse(stdoutChunks.join(''));
498
+ assert.equal(parsed.result, 'PASS');
499
+ assert.equal(parsed.bundleIntegrity, 'PASS');
500
+ assert.equal(parsed.nodeAttestation, 'ABSENT');
501
+ });
502
+ it('includes hash mismatch details in --json FAIL output', async () => {
503
+ const bundle = buildMinimalBundle({ certificateHash: 'sha256:' + 'dead'.repeat(16) });
504
+ const stdinReader = async () => JSON.stringify(bundle);
505
+ const stdoutChunks = [];
506
+ const origWrite = process.stdout.write.bind(process.stdout);
507
+ const origError = console.error;
508
+ process.stdout.write = (chunk) => { stdoutChunks.push(String(chunk)); return true; };
509
+ console.error = () => { };
510
+ const mock = mockExit();
511
+ try {
512
+ await aiVerifyCommand(undefined, { json: true }, stdinReader);
513
+ }
514
+ catch (e) {
515
+ assert.match(e.message, /exit\(1\)/);
516
+ }
517
+ finally {
518
+ process.stdout.write = origWrite;
519
+ console.error = origError;
520
+ mock.restore();
521
+ }
522
+ const parsed = JSON.parse(stdoutChunks.join(''));
523
+ assert.equal(parsed.result, 'FAIL');
524
+ assert.equal(parsed.bundleIntegrity, 'FAIL');
525
+ assert.ok(typeof parsed.computed === 'string');
526
+ assert.ok(typeof parsed.expected === 'string');
527
+ });
528
+ });
529
+ // ─── L) Backward compatibility — Code Mode exports ───────────────────────────
530
+ describe('L: backward compat — module exports', () => {
531
+ it('exports canonicalJson, computeBundleHash, readInputJson, callNodeApi', () => {
532
+ assert.equal(typeof canonicalJson, 'function');
533
+ assert.equal(typeof computeBundleHash, 'function');
534
+ assert.equal(typeof readInputJson, 'function');
535
+ assert.equal(typeof callNodeApi, 'function');
536
+ });
537
+ it('exports aiCreateCommand, aiCertifyCommand, aiVerifyCommand', () => {
538
+ assert.equal(typeof aiCreateCommand, 'function');
539
+ assert.equal(typeof aiCertifyCommand, 'function');
540
+ assert.equal(typeof aiVerifyCommand, 'function');
541
+ });
542
+ it('exports hasAttestation', () => {
543
+ assert.equal(typeof hasAttestation, 'function');
544
+ assert.equal(hasAttestation({ attestationId: 'abc' }), true);
545
+ assert.equal(hasAttestation({ attestationId: '' }), false);
546
+ assert.equal(hasAttestation({}), false);
547
+ });
548
+ it('exports readStdinText', () => {
549
+ assert.equal(typeof readStdinText, 'function');
550
+ });
551
+ });
552
+ // ─── M) callNodeApi ───────────────────────────────────────────────────────────
553
+ describe('M: callNodeApi', () => {
554
+ it('constructs correct URL and sends JSON body', async () => {
555
+ const capturedCalls = [];
556
+ const mockFetch = async (url, init) => {
557
+ capturedCalls.push({ url, method: init?.method, body: JSON.parse(init?.body) });
558
+ return new Response(JSON.stringify({ ok: true }), { status: 200 });
559
+ };
560
+ const result = await callNodeApi('https://node.nexart.art', '/v1/cer/ai/create', { executionId: 'test' }, 'my-api-key', mockFetch);
561
+ assert.equal(result.status, 200);
562
+ assert.deepEqual(result.data, { ok: true });
563
+ const call = capturedCalls[0];
564
+ assert.equal(call.url, 'https://node.nexart.art/v1/cer/ai/create');
565
+ assert.equal(call.method, 'POST');
566
+ assert.deepEqual(call.body, { executionId: 'test' });
567
+ });
568
+ it('includes Authorization header when apiKey provided', async () => {
569
+ const capturedHeaders = [];
570
+ const mockFetch = async (_url, init) => {
571
+ capturedHeaders.push((init?.headers || {}));
572
+ return new Response('{}', { status: 200 });
573
+ };
574
+ await callNodeApi('https://node.nexart.art', '/v1/test', {}, 'Bearer-key', mockFetch);
575
+ assert.equal(capturedHeaders[0]?.['Authorization'], 'Bearer Bearer-key');
576
+ });
577
+ it('omits Authorization header when no apiKey', async () => {
578
+ const capturedHeaders = [];
579
+ const mockFetch = async (_url, init) => {
580
+ capturedHeaders.push((init?.headers || {}));
581
+ return new Response('{}', { status: 200 });
582
+ };
583
+ await callNodeApi('http://localhost:4000', '/v1/test', {}, undefined, mockFetch);
584
+ assert.equal(capturedHeaders[0]?.['Authorization'], undefined);
585
+ });
586
+ });
587
+ // ─── N) aiCreateCommand — stdin input ────────────────────────────────────────
588
+ describe('N: aiCreateCommand — stdin input', () => {
589
+ it('reads input from injected stdinReader', async () => {
590
+ const input = { executionId: 'stdin-001', provider: 'openai' };
591
+ const stdinReader = async () => JSON.stringify(input);
592
+ const capturedBodies = [];
593
+ const mockFetch = async (_url, init) => {
594
+ capturedBodies.push(JSON.parse(init?.body));
595
+ return new Response(JSON.stringify({ bundleType: 'cer.ai.execution.v1' }), { status: 200 });
596
+ };
597
+ const origWrite = process.stdout.write.bind(process.stdout);
598
+ process.stdout.write = () => true;
599
+ try {
600
+ await aiCreateCommand(undefined, { endpoint: 'http://localhost:4000', apiKey: undefined }, mockFetch, stdinReader);
601
+ }
602
+ finally {
603
+ process.stdout.write = origWrite;
604
+ }
605
+ assert.deepEqual(capturedBodies[0], input);
606
+ });
607
+ });
608
+ // ─── O) aiCertifyCommand — stdin input ────────────────────────────────────────
609
+ describe('O: aiCertifyCommand — stdin input', () => {
610
+ it('reads input from injected stdinReader', async () => {
611
+ const input = { executionId: 'stdin-c-001', provider: 'anthropic' };
612
+ const stdinReader = async () => JSON.stringify(input);
613
+ const capturedBodies = [];
614
+ const mockFetch = async (_url, init) => {
615
+ capturedBodies.push(JSON.parse(init?.body));
616
+ return new Response(JSON.stringify({
617
+ certificateHash: 'sha256:aabb',
618
+ verificationUrl: 'https://verify.nexart.io/e/x',
619
+ attestationId: 'a1',
620
+ }), { status: 200 });
621
+ };
622
+ const origLog = console.log;
623
+ console.log = () => { };
624
+ try {
625
+ await aiCertifyCommand(undefined, { json: false, endpoint: 'http://localhost:4000', apiKey: undefined }, mockFetch, stdinReader);
626
+ }
627
+ finally {
628
+ console.log = origLog;
629
+ }
630
+ assert.deepEqual(capturedBodies[0], input);
631
+ });
632
+ });
633
+ // ─── P) aiCreateCommand — saves --out file ─────────────────────────────────────
634
+ describe('P: aiCreateCommand — --out file', () => {
635
+ let tmpDir;
636
+ before(() => { tmpDir = makeTempDir(); });
637
+ after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
638
+ it('saves CER bundle JSON to --out file', async () => {
639
+ const input = { executionId: 'out-001' };
640
+ const stdinReader = async () => JSON.stringify(input);
641
+ const fakeBundle = { bundleType: 'cer.ai.execution.v1', certificateHash: 'sha256:cc' };
642
+ const outPath = path.join(tmpDir, 'output.json');
643
+ const mockFetch = async () => new Response(JSON.stringify(fakeBundle), { status: 200 });
644
+ const origWrite = process.stdout.write.bind(process.stdout);
645
+ const origError = console.error;
646
+ process.stdout.write = () => true;
647
+ console.error = () => { };
648
+ try {
649
+ await aiCreateCommand(undefined, { out: outPath, endpoint: 'http://localhost:4000', apiKey: undefined }, mockFetch, stdinReader);
650
+ }
651
+ finally {
652
+ process.stdout.write = origWrite;
653
+ console.error = origError;
654
+ }
655
+ assert.ok(fs.existsSync(outPath));
656
+ const saved = JSON.parse(fs.readFileSync(outPath, 'utf-8'));
657
+ assert.equal(saved.bundleType, 'cer.ai.execution.v1');
658
+ });
659
+ });
660
+ // ─── Q) aiCertifyCommand — 401 handling ───────────────────────────────────────
661
+ describe('Q: aiCertifyCommand — error handling', () => {
662
+ it('exits 1 on 401 Unauthorized', async () => {
663
+ const input = { executionId: 'q-001' };
664
+ const stdinReader = async () => JSON.stringify(input);
665
+ const mockFetch = async () => new Response('{}', { status: 401 });
666
+ const origError = console.error;
667
+ console.error = () => { };
668
+ const mock = mockExit();
669
+ try {
670
+ await aiCertifyCommand(undefined, { json: false, endpoint: 'http://localhost:4000', apiKey: 'bad' }, mockFetch, stdinReader);
671
+ assert.fail('should have exited');
672
+ }
673
+ catch (e) {
674
+ assert.equal(mock.exitCode, 1);
675
+ }
676
+ finally {
677
+ console.error = origError;
678
+ mock.restore();
679
+ }
680
+ });
681
+ it('exits 1 on server error with error message', async () => {
682
+ const input = { executionId: 'q-002' };
683
+ const stdinReader = async () => JSON.stringify(input);
684
+ const mockFetch = async () => new Response(JSON.stringify({ error: 'Invalid execution schema' }), { status: 422 });
685
+ const origError = console.error;
686
+ const errLines = [];
687
+ console.error = (...args) => { errLines.push(args.map(String).join(' ')); };
688
+ const mock = mockExit();
689
+ try {
690
+ await aiCertifyCommand(undefined, { json: false, endpoint: 'http://localhost:4000', apiKey: 'key' }, mockFetch, stdinReader);
691
+ assert.fail('should have exited');
692
+ }
693
+ catch (e) {
694
+ assert.equal(mock.exitCode, 1);
695
+ assert.ok(errLines.some(l => l.includes('Invalid execution schema')));
696
+ }
697
+ finally {
698
+ console.error = origError;
699
+ mock.restore();
700
+ }
701
+ });
702
+ });
703
+ //# sourceMappingURL=ai.test.js.map