start-command 0.32.1 → 0.34.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.
- package/CHANGELOG.md +27 -0
- package/README.md +3 -0
- package/eslint.config.mjs +8 -0
- package/package.json +7 -3
- package/src/bin/cli.js +32 -137
- package/src/lib/args-parser-queries.js +228 -0
- package/src/lib/args-parser.js +38 -179
- package/src/lib/detached-finalize.js +281 -0
- package/src/lib/docker-cleanup.js +240 -39
- package/src/lib/docker-post-mortem.js +289 -0
- package/src/lib/execution-attach.js +245 -0
- package/src/lib/execution-resume-all.js +211 -0
- package/src/lib/execution-resume.js +455 -0
- package/src/lib/execution-store.js +93 -6
- package/src/lib/exit-reason.js +264 -0
- package/src/lib/failure-handler.js +43 -41
- package/src/lib/isolation-log-utils.js +66 -1
- package/src/lib/isolation.js +28 -15
- package/src/lib/query-commands.js +202 -0
- package/src/lib/screen-isolation.js +2 -1
- package/src/lib/session-probe.js +121 -0
- package/src/lib/shell-utils.js +164 -10
- package/src/lib/status-formatter.js +233 -69
- package/src/lib/usage.js +13 -1
- package/src/lib/user-manager.js +18 -1
- package/test/args-parser-attach-resume.js +159 -0
- package/test/args-parser-control.js +3 -3
- package/test/args-parser.js +1 -1
- package/test/check-file-size.js +91 -0
- package/test/ci-workflow-invariants.js +191 -0
- package/test/echo-integration.js +20 -13
- package/test/execution-attach.js +221 -0
- package/test/execution-resume.js +433 -0
- package/test/execution-store.js +57 -2
- package/test/exit-reason.js +209 -0
- package/test/failure-handler.js +107 -0
- package/test/load-command-stream.mjs +83 -0
- package/test/regression-162.js +320 -0
- package/test/regression-164.js +308 -0
- package/test/regression-165.js +310 -0
- package/test/regression-170.js +321 -0
- package/test/regression-171.js +562 -0
- package/test/regression-84.js +8 -3
- package/test/release-name.mjs +57 -0
- package/test/session-probe.js +150 -0
- package/test/user-manager.js +23 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for exit-reason detection (issue #162).
|
|
4
|
+
*
|
|
5
|
+
* `exitCode 139` with `oomKilled false` is not actionable on its own: the
|
|
6
|
+
* kernel/cgroup OOM flag is unset when the process aborts itself after hitting
|
|
7
|
+
* a runtime memory limit. The log almost always carries the real cause, so the
|
|
8
|
+
* footer scan surfaces it as `exitReason`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { describe, it } = require('node:test');
|
|
12
|
+
const assert = require('assert');
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
detectExitReason,
|
|
16
|
+
detectMemoryMarker,
|
|
17
|
+
resolveExitReason,
|
|
18
|
+
resolveMemoryExhaustion,
|
|
19
|
+
signalNameForExitCode,
|
|
20
|
+
} = require('../src/lib/exit-reason');
|
|
21
|
+
|
|
22
|
+
describe('detectExitReason', () => {
|
|
23
|
+
it('should detect the V8 heap limit marker from the incident log', () => {
|
|
24
|
+
const tail = [
|
|
25
|
+
'some command output',
|
|
26
|
+
'',
|
|
27
|
+
'<--- Last few GCs --->',
|
|
28
|
+
'',
|
|
29
|
+
'FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory',
|
|
30
|
+
'',
|
|
31
|
+
'==================================================',
|
|
32
|
+
'Finished: 2026-09-01 12:00:00.000',
|
|
33
|
+
'Exit Code: 139',
|
|
34
|
+
].join('\n');
|
|
35
|
+
|
|
36
|
+
assert.strictEqual(
|
|
37
|
+
detectExitReason(tail),
|
|
38
|
+
'memory-exhaustion (v8-heap-limit)'
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should detect a bare JavaScript heap out of memory line', () => {
|
|
43
|
+
assert.strictEqual(
|
|
44
|
+
detectExitReason('JavaScript heap out of memory\n'),
|
|
45
|
+
'memory-exhaustion (v8-heap-limit)'
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should detect other runtimes reporting their own exhaustion', () => {
|
|
50
|
+
assert.strictEqual(
|
|
51
|
+
detectExitReason('fatal error: runtime: out of memory'),
|
|
52
|
+
'memory-exhaustion (go-runtime)'
|
|
53
|
+
);
|
|
54
|
+
assert.strictEqual(
|
|
55
|
+
detectExitReason('Array buffer allocation failed'),
|
|
56
|
+
'memory-exhaustion (allocation-failure)'
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should detect the kernel OOM killer marker', () => {
|
|
61
|
+
assert.strictEqual(
|
|
62
|
+
detectExitReason('Out of memory: Killed process 4242 (bun)\n'),
|
|
63
|
+
'memory-exhaustion (kernel-oom-killer)'
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should detect allocation failures', () => {
|
|
68
|
+
assert.strictEqual(
|
|
69
|
+
detectExitReason('terminate called after throwing std::bad_alloc'),
|
|
70
|
+
'memory-exhaustion (allocation-failure)'
|
|
71
|
+
);
|
|
72
|
+
assert.strictEqual(
|
|
73
|
+
detectExitReason('memory allocation of 1048576 bytes failed'),
|
|
74
|
+
'memory-exhaustion (allocation-failure)'
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should not invent a reason for ordinary output', () => {
|
|
79
|
+
assert.strictEqual(detectExitReason('all tests passed\n'), null);
|
|
80
|
+
assert.strictEqual(detectExitReason(''), null);
|
|
81
|
+
assert.strictEqual(detectExitReason(null), null);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('signalNameForExitCode', () => {
|
|
86
|
+
it('should map 139 to SIGSEGV', () => {
|
|
87
|
+
assert.strictEqual(signalNameForExitCode(139), 'SIGSEGV');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should map 137 to SIGKILL', () => {
|
|
91
|
+
assert.strictEqual(signalNameForExitCode(137), 'SIGKILL');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should ignore ordinary exit codes', () => {
|
|
95
|
+
assert.strictEqual(signalNameForExitCode(0), null);
|
|
96
|
+
assert.strictEqual(signalNameForExitCode(1), null);
|
|
97
|
+
assert.strictEqual(signalNameForExitCode(-1), null);
|
|
98
|
+
assert.strictEqual(signalNameForExitCode(null), null);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('resolveExitReason', () => {
|
|
103
|
+
it('should prefer the log marker over the signal fallback', () => {
|
|
104
|
+
const reason = resolveExitReason({
|
|
105
|
+
exitCode: 139,
|
|
106
|
+
logTail:
|
|
107
|
+
'FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory',
|
|
108
|
+
oomKilled: false,
|
|
109
|
+
});
|
|
110
|
+
assert.strictEqual(reason, 'memory-exhaustion (v8-heap-limit)');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should report the cgroup OOM observation when there is no marker', () => {
|
|
114
|
+
const reason = resolveExitReason({ exitCode: 137, oomKilled: true });
|
|
115
|
+
assert.strictEqual(reason, 'memory-exhaustion (cgroup-oom-killer)');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should fall back to the signal name for signal exit codes', () => {
|
|
119
|
+
assert.strictEqual(
|
|
120
|
+
resolveExitReason({ exitCode: 139, oomKilled: false }),
|
|
121
|
+
'signal (SIGSEGV)'
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should return null when nothing is known', () => {
|
|
126
|
+
assert.strictEqual(resolveExitReason({ exitCode: 0 }), null);
|
|
127
|
+
assert.strictEqual(resolveExitReason({}), null);
|
|
128
|
+
assert.strictEqual(resolveExitReason(null), null);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('detectMemoryMarker', () => {
|
|
133
|
+
it('should return the whole line carrying the marker', () => {
|
|
134
|
+
const tail = [
|
|
135
|
+
'<--- Last few GCs --->',
|
|
136
|
+
'FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory',
|
|
137
|
+
'----- Native stack trace -----',
|
|
138
|
+
].join('\n');
|
|
139
|
+
|
|
140
|
+
assert.deepStrictEqual(detectMemoryMarker(tail), {
|
|
141
|
+
reason: 'memory-exhaustion (v8-heap-limit)',
|
|
142
|
+
line: 'FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory',
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should bound an absurdly long line', () => {
|
|
147
|
+
const marker = detectMemoryMarker(
|
|
148
|
+
`${'x'.repeat(5000)} JavaScript heap out of memory`
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
assert.ok(marker.line.length <= 304);
|
|
152
|
+
assert.ok(marker.line.endsWith('...'));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('resolveMemoryExhaustion', () => {
|
|
157
|
+
const marker =
|
|
158
|
+
'FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory';
|
|
159
|
+
|
|
160
|
+
it('should report the evidence for an abnormal exit', () => {
|
|
161
|
+
assert.deepStrictEqual(
|
|
162
|
+
resolveMemoryExhaustion({
|
|
163
|
+
exitCode: 139,
|
|
164
|
+
logTail: `working\n${marker}\n`,
|
|
165
|
+
oomKilled: false,
|
|
166
|
+
}),
|
|
167
|
+
{ memoryExhausted: true, memoryExhaustedReason: marker }
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should fall back to the container flag when the log says nothing', () => {
|
|
172
|
+
assert.deepStrictEqual(
|
|
173
|
+
resolveMemoryExhaustion({
|
|
174
|
+
exitCode: 137,
|
|
175
|
+
logTail: 'killed\n',
|
|
176
|
+
oomKilled: true,
|
|
177
|
+
}),
|
|
178
|
+
{
|
|
179
|
+
memoryExhausted: true,
|
|
180
|
+
memoryExhaustedReason: 'Docker reported State.OOMKilled=true',
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should stay silent for a clean or unfinished run', () => {
|
|
186
|
+
// An observation, never a verdict (#151): a log that merely quotes the
|
|
187
|
+
// marker cannot turn a successful run into a memory failure.
|
|
188
|
+
assert.strictEqual(
|
|
189
|
+
resolveMemoryExhaustion({ exitCode: 0, logTail: marker }),
|
|
190
|
+
null
|
|
191
|
+
);
|
|
192
|
+
assert.strictEqual(
|
|
193
|
+
resolveMemoryExhaustion({ exitCode: null, logTail: marker }),
|
|
194
|
+
null
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should stay silent for an ordinary failure', () => {
|
|
199
|
+
assert.strictEqual(
|
|
200
|
+
resolveMemoryExhaustion({
|
|
201
|
+
exitCode: 1,
|
|
202
|
+
logTail: 'error: file not found\n',
|
|
203
|
+
oomKilled: false,
|
|
204
|
+
}),
|
|
205
|
+
null
|
|
206
|
+
);
|
|
207
|
+
assert.strictEqual(resolveMemoryExhaustion(null), null);
|
|
208
|
+
});
|
|
209
|
+
});
|
package/test/failure-handler.js
CHANGED
|
@@ -6,8 +6,56 @@
|
|
|
6
6
|
|
|
7
7
|
const { describe, it } = require('node:test');
|
|
8
8
|
const assert = require('assert');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { execFileSync } = require('child_process');
|
|
9
13
|
const { parseGitUrl, handleFailure } = require('../src/lib/failure-handler');
|
|
10
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Run `createIssue` in a child process whose PATH starts with a fake `gh`
|
|
17
|
+
* recording its argv (NUL-separated). A child process is required because the
|
|
18
|
+
* runtime resolves PATH at startup, so mutating `process.env.PATH` in-process
|
|
19
|
+
* does not affect command lookup under Bun.
|
|
20
|
+
*/
|
|
21
|
+
function createIssueWithFakeGh(fullCommand) {
|
|
22
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fake-gh-'));
|
|
23
|
+
try {
|
|
24
|
+
const argvFile = path.join(dir, 'argv');
|
|
25
|
+
const ghPath = path.join(dir, 'gh');
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
ghPath,
|
|
28
|
+
[
|
|
29
|
+
'#!/bin/sh',
|
|
30
|
+
`printf '%s\\0' "$@" > "${argvFile}"`,
|
|
31
|
+
'echo https://github.com/owner/repo/issues/1',
|
|
32
|
+
'',
|
|
33
|
+
].join('\n')
|
|
34
|
+
);
|
|
35
|
+
fs.chmodSync(ghPath, 0o755);
|
|
36
|
+
|
|
37
|
+
const modulePath = require.resolve('../src/lib/failure-handler');
|
|
38
|
+
const script = `
|
|
39
|
+
const { createIssue } = require(${JSON.stringify(modulePath)});
|
|
40
|
+
const repo = { owner: 'owner', repo: 'repo', url: 'https://github.com/owner/repo' };
|
|
41
|
+
process.stdout.write(
|
|
42
|
+
String(createIssue(repo, ${JSON.stringify(fullCommand)}, 1, null))
|
|
43
|
+
);
|
|
44
|
+
`;
|
|
45
|
+
const result = execFileSync(process.execPath, ['-e', script], {
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
env: {
|
|
48
|
+
...process.env,
|
|
49
|
+
PATH: `${dir}${path.delimiter}${process.env.PATH}`,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const argv = fs.readFileSync(argvFile, 'utf8').split('\0').slice(0, -1);
|
|
53
|
+
return { result, argv };
|
|
54
|
+
} finally {
|
|
55
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
11
59
|
describe('failure-handler', () => {
|
|
12
60
|
describe('parseGitUrl', () => {
|
|
13
61
|
it('should parse HTTPS GitHub URL', () => {
|
|
@@ -82,6 +130,65 @@ describe('failure-handler', () => {
|
|
|
82
130
|
});
|
|
83
131
|
});
|
|
84
132
|
|
|
133
|
+
describe('createIssue', () => {
|
|
134
|
+
// Regression for issue #168: the command was interpolated into a shell
|
|
135
|
+
// string and only `"` was escaped, so a backtick or $(...) in the failing
|
|
136
|
+
// command ran, and every newline of the body arrived as a literal "\\n".
|
|
137
|
+
const runCreateIssue = () =>
|
|
138
|
+
createIssueWithFakeGh('echo "quoted" $(id) `hostname`');
|
|
139
|
+
|
|
140
|
+
it('passes the title and body to gh as separate arguments', () => {
|
|
141
|
+
if (process.platform === 'win32') {
|
|
142
|
+
return; // /bin/sh stub is POSIX-only
|
|
143
|
+
}
|
|
144
|
+
const { result, argv } = runCreateIssue();
|
|
145
|
+
assert.strictEqual(result, 'https://github.com/owner/repo/issues/1');
|
|
146
|
+
assert.deepStrictEqual(argv.slice(0, 4), [
|
|
147
|
+
'issue',
|
|
148
|
+
'create',
|
|
149
|
+
'--repo',
|
|
150
|
+
'owner/repo',
|
|
151
|
+
]);
|
|
152
|
+
assert.strictEqual(argv[4], '--title');
|
|
153
|
+
assert.strictEqual(argv[6], '--body');
|
|
154
|
+
assert.strictEqual(argv.length, 8);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('keeps the failing command verbatim and does not escape it', () => {
|
|
158
|
+
if (process.platform === 'win32') {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const { argv } = runCreateIssue();
|
|
162
|
+
const [title, body] = [argv[5], argv[7]];
|
|
163
|
+
assert.ok(
|
|
164
|
+
title.includes('echo "quoted" $(id) `hostname`'),
|
|
165
|
+
`title must quote the command verbatim, got: ${title}`
|
|
166
|
+
);
|
|
167
|
+
assert.ok(
|
|
168
|
+
body.includes('echo "quoted" $(id) `hostname`'),
|
|
169
|
+
'body must quote the command verbatim'
|
|
170
|
+
);
|
|
171
|
+
assert.ok(
|
|
172
|
+
!title.includes('\\"'),
|
|
173
|
+
'title must not contain escaped quotes'
|
|
174
|
+
);
|
|
175
|
+
assert.ok(!body.includes('\\"'), 'body must not contain escaped quotes');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('writes real newlines into the issue body', () => {
|
|
179
|
+
if (process.platform === 'win32') {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const { argv } = runCreateIssue();
|
|
183
|
+
const body = argv[7];
|
|
184
|
+
assert.ok(body.includes('\n'), 'body must contain real newlines');
|
|
185
|
+
assert.ok(
|
|
186
|
+
!body.includes('\\n'),
|
|
187
|
+
'body must not contain literal backslash-n sequences'
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
85
192
|
describe('handleFailure', () => {
|
|
86
193
|
it('should return early when disableAutoIssue is true', () => {
|
|
87
194
|
// This should not throw and should return without calling external processes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
loadCommandStream,
|
|
5
|
+
resolveNamedExport,
|
|
6
|
+
} from '../../scripts/load-command-stream.mjs';
|
|
7
|
+
|
|
8
|
+
const $ = () => {};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Namespace shape produced by `import()` of a CommonJS file on Node < 22.12,
|
|
12
|
+
* before `use-m` unwraps it. use-m returns `default` here, so `const { $ }`
|
|
13
|
+
* works.
|
|
14
|
+
*/
|
|
15
|
+
const NODE_20_NAMESPACE = { default: Object.assign($, { $, exec: () => {} }) };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Same file on Node >= 22.12. Node adds a `module.exports` named export when
|
|
19
|
+
* cjs-module-lexer cannot infer the real names, so use-m sees two keys, skips
|
|
20
|
+
* the unwrap and hands the raw namespace to the caller (issue #168).
|
|
21
|
+
*/
|
|
22
|
+
const NODE_24_NAMESPACE = {
|
|
23
|
+
default: Object.assign($, { $, exec: () => {} }),
|
|
24
|
+
'module.exports': Object.assign($, { $, exec: () => {} }),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Shape use-m returns when it did unwrap: the `$` function itself. */
|
|
28
|
+
const UNWRAPPED = Object.assign($, { $, exec: () => {} });
|
|
29
|
+
|
|
30
|
+
describe('load-command-stream', () => {
|
|
31
|
+
it('reproduces the Node 24 namespace that broke the release jobs', () => {
|
|
32
|
+
// Regression guard for the actual failure: destructuring the raw
|
|
33
|
+
// namespace yields undefined, hence "$ is not a function".
|
|
34
|
+
const { $: destructured } = NODE_24_NAMESPACE;
|
|
35
|
+
expect(destructured).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('resolves $ from the Node >= 22.12 CommonJS namespace', () => {
|
|
39
|
+
expect(typeof resolveNamedExport(NODE_24_NAMESPACE, '$', 'x').$).toBe(
|
|
40
|
+
'function'
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('resolves $ from the Node < 22.12 CommonJS namespace', () => {
|
|
45
|
+
expect(typeof resolveNamedExport(NODE_20_NAMESPACE, '$', 'x').$).toBe(
|
|
46
|
+
'function'
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('resolves $ when use-m already unwrapped the module', () => {
|
|
51
|
+
expect(typeof resolveNamedExport(UNWRAPPED, '$', 'x').$).toBe('function');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('resolves $ from an ESM namespace with named exports', () => {
|
|
55
|
+
const namespace = { $, exec: () => {}, default: $ };
|
|
56
|
+
expect(typeof resolveNamedExport(namespace, '$', 'x').$).toBe('function');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('throws an actionable error instead of "$ is not a function"', () => {
|
|
60
|
+
expect(() =>
|
|
61
|
+
resolveNamedExport({ nope: 1 }, '$', 'command-stream')
|
|
62
|
+
).toThrow(
|
|
63
|
+
/use\('command-stream'\) did not expose a callable "\$".*keys \[nope\]/s
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('reports the received value when use-m resolved nothing', () => {
|
|
68
|
+
expect(() => resolveNamedExport(undefined, '$', 'command-stream')).toThrow(
|
|
69
|
+
/Received undefined/
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('loads command-stream through an injected use() implementation', async () => {
|
|
74
|
+
const calls = [];
|
|
75
|
+
const use = async (name) => {
|
|
76
|
+
calls.push(name);
|
|
77
|
+
return NODE_24_NAMESPACE;
|
|
78
|
+
};
|
|
79
|
+
const module = await loadCommandStream(use);
|
|
80
|
+
expect(calls).toEqual(['command-stream']);
|
|
81
|
+
expect(typeof module.$).toBe('function');
|
|
82
|
+
});
|
|
83
|
+
});
|