|
9 | 9 |
|
10 | 10 | import { test } from 'node:test'; |
11 | 11 | import assert from 'node:assert/strict'; |
12 | | -import { readFileSync, existsSync, statSync, readdirSync, writeFileSync, mkdtempSync, mkdirSync, rmSync, unlinkSync } from 'node:fs'; |
| 12 | +import { readFileSync, existsSync, statSync, readdirSync, writeFileSync, mkdtempSync, mkdirSync, rmSync, unlinkSync, openSync, readSync, closeSync } from 'node:fs'; |
13 | 13 | import { join, resolve } from 'node:path'; |
14 | 14 | import { tmpdir } from 'node:os'; |
15 | 15 | import { spawnSync } from 'node:child_process'; |
@@ -307,6 +307,26 @@ test('examples/ has 3 base.png samples', () => { |
307 | 307 | } |
308 | 308 | }); |
309 | 309 |
|
| 310 | +// The host may sniff the MIME by extension, so the file content must match the |
| 311 | +// .png suffix. (Reviewer feedback: these used to be JPEGs under a .png name and |
| 312 | +// the docs asked the agent to pass them to image_synthesize.) |
| 313 | +test('examples/*.png are actually PNG (signature 89 50 4E 47 0D 0A 1A 0A)', () => { |
| 314 | + const PNG_SIG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); |
| 315 | + for (const f of ['02-stay-late-base.png', '10-toilet-slacking-base.png', '11-touch-fish-base.png']) { |
| 316 | + const fd = openSync(join(EX_DIR, f), 'r'); |
| 317 | + try { |
| 318 | + const head = Buffer.alloc(8); |
| 319 | + readSync(fd, head, 0, 8, 0); |
| 320 | + assert.ok( |
| 321 | + head.equals(PNG_SIG), |
| 322 | + `${f} is not a real PNG (got bytes ${[...head].map((b) => b.toString(16).padStart(2, '0')).join(' ')})`, |
| 323 | + ); |
| 324 | + } finally { |
| 325 | + closeSync(fd); |
| 326 | + } |
| 327 | + } |
| 328 | +}); |
| 329 | + |
310 | 330 | // --------------------------------------------------------------------------- |
311 | 331 | // 6b. Host-tool contract vocabulary. |
312 | 332 | // |
@@ -545,6 +565,37 @@ test('make_gif.py refuses output names that cannot work on Windows', () => { |
545 | 565 | rmSync(base, { recursive: true, force: true }); |
546 | 566 | }); |
547 | 567 |
|
| 568 | +test('make_preview_strip.py rejects --workdir pointing at a regular file (no traceback)', () => { |
| 569 | + // Reviewer feedback (PR #40): when --workdir resolves to an existing regular |
| 570 | + // file, `os.makedirs(..., exist_ok=True)` used to leak an uncaught |
| 571 | + // FileExistsError traceback. The script must fail with a clear argument |
| 572 | + // error instead. |
| 573 | + if (pythonHasModule('os') !== true) return; |
| 574 | + |
| 575 | + const tmp = mkdtempSync(join(tmpdir(), 'octopus-workdir-')); |
| 576 | + const fakeFile = join(tmp, 'not-a-dir.txt'); |
| 577 | + writeFileSync(fakeFile, 'placeholder'); |
| 578 | + const video = join(REF_DIR, 'videos', 'breakdown-h3.mp4'); |
| 579 | + const out = join(tmp, 'preview.png'); |
| 580 | + |
| 581 | + const r = spawnSync( |
| 582 | + 'python3', |
| 583 | + [join(SCRIPTS_DIR, 'make_preview_strip.py'), video, out, |
| 584 | + '--workdir', fakeFile, '--labels', 'a,b,c,d,e'], |
| 585 | + { encoding: 'utf8' }, |
| 586 | + ); |
| 587 | + |
| 588 | + assert.notEqual(r.status, 0, 'script must exit non-zero on a non-directory --workdir'); |
| 589 | + assert.match(r.stderr, /--workdir/, 'stderr must name --workdir in the error'); |
| 590 | + assert.match(r.stderr, /not a directory/, 'stderr must explain the path is not a directory'); |
| 591 | + assert.doesNotMatch( |
| 592 | + r.stderr, |
| 593 | + /Traceback|FileExistsError/, |
| 594 | + 'stderr must not leak a Python traceback', |
| 595 | + ); |
| 596 | + rmSync(tmp, { recursive: true, force: true }); |
| 597 | +}); |
| 598 | + |
548 | 599 | test('path containment is compared with the platform normalisation', () => { |
549 | 600 | // Windows is case-insensitive and accepts both separators, so a raw string |
550 | 601 | // comparison of dirname(resolved) against the scene dir is wrong there. |
|
0 commit comments