-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathruntime.test.ts
More file actions
127 lines (113 loc) · 5.01 KB
/
Copy pathruntime.test.ts
File metadata and controls
127 lines (113 loc) · 5.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import { describe, it, expect } from 'vitest';
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
findOnPath,
resolveBinaries,
doctor,
buildHyperframesEnv,
hyperframesNpxArgs,
DEFAULT_HYPERFRAMES_SPEC,
resolveInside,
run,
providerErrorMessage,
} from '../src/runtime/index';
describe('binary resolution', () => {
it('finds node on PATH (it is running this test)', () => {
expect(findOnPath('node')).toBeTruthy();
});
it('returns null for a binary that does not exist', () => {
expect(findOnPath('definitely-not-a-real-binary-xyz')).toBeNull();
});
it('doctor reports a binaries map and notes', () => {
const d = doctor();
expect(d.binaries).toHaveProperty('ffmpeg');
expect(Array.isArray(d.notes)).toBe(true);
expect(typeof d.ok).toBe('boolean');
});
it('honors OVS_FFMPEG_PATH override', () => {
const prev = process.env.OVS_FFMPEG_PATH;
process.env.OVS_FFMPEG_PATH = '/custom/ffmpeg';
try {
expect(resolveBinaries().ffmpeg).toBe('/custom/ffmpeg');
} finally {
if (prev === undefined) delete process.env.OVS_FFMPEG_PATH;
else process.env.OVS_FFMPEG_PATH = prev;
}
});
});
describe('hyperframes env', () => {
it('points HyperFrames at the resolved ffmpeg/ffprobe and keeps fallback fetches quiet', () => {
const env = buildHyperframesEnv({ ffmpeg: '/x/ffmpeg', ffprobe: '/x/ffprobe' }, {});
expect(env.HYPERFRAMES_FFMPEG_PATH).toBe('/x/ffmpeg');
expect(env.HYPERFRAMES_FFPROBE_PATH).toBe('/x/ffprobe');
expect(env.NPM_CONFIG_PREFER_OFFLINE).toBe('true');
});
it('builds the compatibility npx arg vector with -y and the pinned spec', () => {
const a = hyperframesNpxArgs('render', ['proj', '-o', 'out.mp4']);
expect(a[0]).toBe('-y');
expect(a[1]).toBe(DEFAULT_HYPERFRAMES_SPEC);
expect(a).toEqual(expect.arrayContaining(['render', 'proj', '-o', 'out.mp4']));
});
});
describe('resolveInside', () => {
it('resolves a child path and rejects an escape', () => {
expect(resolveInside('/base', 'a/b.txt')).toBe('/base/a/b.txt');
expect(() => resolveInside('/base', '../escape.txt')).toThrow(/escape/);
});
});
describe('subprocess boundaries', () => {
it('terminates chatty processes instead of only truncating their output', async () => {
await expect(run(process.execPath, [
'-e',
"process.stdout.write('x'.repeat(256)); setInterval(() => {}, 1000)",
], { timeoutMs: 10_000, maxBuffer: 32 })).rejects.toThrow('process output exceeded 32 bytes');
});
it('settles a timeout promptly without waiting for process close', async () => {
const startedAt = Date.now();
await expect(run(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { timeoutMs: 50 }))
.rejects.toThrow('timed out after 50ms');
expect(Date.now() - startedAt).toBeLessThan(5_000);
});
it.runIf(process.platform === 'win32')('terminates a complete Windows subprocess tree', async () => {
const root = mkdtempSync(join(tmpdir(), 'ovs-process-tree-'));
const sentinel = join(root, 'orphan-wrote.txt');
const grandchild = [
"const fs = require('node:fs');",
`setTimeout(() => fs.writeFileSync(${JSON.stringify(sentinel)}, 'orphaned'), 700);`,
'setInterval(() => {}, 1000);',
].join('');
const parent = [
"const { spawn } = require('node:child_process');",
`spawn(process.execPath, ['-e', ${JSON.stringify(grandchild)}], { stdio: 'ignore' });`,
'setInterval(() => {}, 1000);',
].join('');
try {
await expect(run(process.execPath, ['-e', parent], { timeoutMs: 75 })).rejects.toThrow('timed out');
await new Promise((resolve) => setTimeout(resolve, 900));
expect(existsSync(sentinel)).toBe(false);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});
describe('providerErrorMessage', () => {
it('finds the human-readable message in the common provider error shapes', () => {
expect(providerErrorMessage('plain text')).toBe('plain text');
expect(providerErrorMessage({ error: { code: 'FORBIDDEN', message: 'Not authorized' } })).toBe('Not authorized');
expect(providerErrorMessage({ detail: 'Not authorized' })).toBe('Not authorized');
expect(providerErrorMessage({ error: 'quota exceeded' })).toBe('quota exceeded');
});
it('reads FastAPI-style validation arrays and names the offending field', () => {
const body = { detail: [{ type: 'enum', loc: ['body', 'duration'], msg: 'Input should be 5 or 10', input: 8 }] };
expect(providerErrorMessage(body)).toBe('duration: Input should be 5 or 10');
expect(providerErrorMessage([{ msg: 'field required', loc: ['body'] }])).toBe('field required');
});
it('returns undefined instead of echoing an unrecognized payload', () => {
expect(providerErrorMessage({ api_key: 'must not be shown' })).toBeUndefined();
expect(providerErrorMessage([])).toBeUndefined();
expect(providerErrorMessage(42)).toBeUndefined();
expect(providerErrorMessage(' ')).toBeUndefined();
});
});