agent-gear 2.1.1 → 2.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.
- package/AGENTS.md +16 -2
- package/dist/socketio-client.d.ts.map +1 -1
- package/dist/socketio-client.js +35 -5
- package/dist/socketio-client.js.map +1 -1
- package/dist/workload-executor.d.ts +85 -0
- package/dist/workload-executor.d.ts.map +1 -0
- package/dist/workload-executor.js +327 -0
- package/dist/workload-executor.js.map +1 -0
- package/dist/workload-executor.test.d.ts +2 -0
- package/dist/workload-executor.test.d.ts.map +1 -0
- package/dist/workload-executor.test.js +328 -0
- package/dist/workload-executor.test.js.map +1 -0
- package/package.json +1 -1
- package/src/socketio-client.ts +36 -6
- package/src/workload-executor.test.ts +441 -0
- package/src/workload-executor.ts +393 -0
- package/tests/socketio-client.test.ts +2 -2
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtemp, mkdir, rm, access } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
import {
|
|
6
|
+
WorkloadExecutor,
|
|
7
|
+
MAX_OUTPUT_BYTES,
|
|
8
|
+
MAX_PENDING_RESULTS,
|
|
9
|
+
EMPTY_SCRIPT_MESSAGE,
|
|
10
|
+
type WorkloadResult,
|
|
11
|
+
} from './workload-executor.js';
|
|
12
|
+
|
|
13
|
+
const TMP_BASE = '/tmp/opencode';
|
|
14
|
+
|
|
15
|
+
function collect() {
|
|
16
|
+
const results: WorkloadResult[] = [];
|
|
17
|
+
return { results, sink: (result: WorkloadResult) => results.push(result) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function makeTmpDir(): Promise<string> {
|
|
21
|
+
await mkdir(TMP_BASE, { recursive: true });
|
|
22
|
+
return mkdtemp(join(TMP_BASE, 'we-'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
26
|
+
try {
|
|
27
|
+
await access(path);
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('WorkloadExecutor', () => {
|
|
35
|
+
const dirs: string[] = [];
|
|
36
|
+
|
|
37
|
+
afterEach(async () => {
|
|
38
|
+
await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('T-WE1 runs items serially, merges stdout+stderr and reports success in item order', async () => {
|
|
42
|
+
const executor = new WorkloadExecutor();
|
|
43
|
+
const { results, sink } = collect();
|
|
44
|
+
|
|
45
|
+
await executor.runBatch(
|
|
46
|
+
{
|
|
47
|
+
phase: 'init',
|
|
48
|
+
items: [
|
|
49
|
+
{ logId: 'a', workloadName: 'A', script: 'echo out-a; echo err-a >&2', timeoutMs: 5000 },
|
|
50
|
+
{ logId: 'b', workloadName: 'B', script: 'echo out-b', timeoutMs: 5000 },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
sink,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(results.map((r) => r.logId)).toEqual(['a', 'b']);
|
|
57
|
+
expect(results[0]).toMatchObject({ status: 'success', exitCode: 0 });
|
|
58
|
+
expect(results[0].log).toContain('out-a');
|
|
59
|
+
expect(results[0].log).toContain('err-a');
|
|
60
|
+
expect(results[1]).toMatchObject({ status: 'success', exitCode: 0 });
|
|
61
|
+
expect(results[1].log).toContain('out-b');
|
|
62
|
+
expect(results[0].startedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
63
|
+
expect(results[0].finishedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('T-WE2 reports failed with exitCode on non-zero exit and continues the batch', async () => {
|
|
67
|
+
const executor = new WorkloadExecutor();
|
|
68
|
+
const { results, sink } = collect();
|
|
69
|
+
|
|
70
|
+
await executor.runBatch(
|
|
71
|
+
{
|
|
72
|
+
phase: 'init',
|
|
73
|
+
items: [
|
|
74
|
+
{ logId: 'fail', workloadName: 'F', script: 'echo boom >&2; exit 3', timeoutMs: 5000 },
|
|
75
|
+
{ logId: 'ok', workloadName: 'O', script: 'echo ok', timeoutMs: 5000 },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
sink,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect(results[0]).toMatchObject({ logId: 'fail', status: 'failed', exitCode: 3 });
|
|
82
|
+
expect(results[0].log).toContain('boom');
|
|
83
|
+
expect(results[1]).toMatchObject({ logId: 'ok', status: 'success' });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('T-WE3 kills the whole process group on per-item timeout and continues', async () => {
|
|
87
|
+
const dir = await makeTmpDir();
|
|
88
|
+
dirs.push(dir);
|
|
89
|
+
const marker = join(dir, 'leaked.txt');
|
|
90
|
+
const executor = new WorkloadExecutor();
|
|
91
|
+
const { results, sink } = collect();
|
|
92
|
+
|
|
93
|
+
const started = Date.now();
|
|
94
|
+
await executor.runBatch(
|
|
95
|
+
{
|
|
96
|
+
phase: 'init',
|
|
97
|
+
items: [
|
|
98
|
+
{ logId: 'slow', workloadName: 'S', script: `( sleep 1.5; echo leaked > ${marker} ) & sleep 30`, timeoutMs: 300 },
|
|
99
|
+
{ logId: 'next', workloadName: 'N', script: 'echo next', timeoutMs: 5000 },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
sink,
|
|
103
|
+
);
|
|
104
|
+
const elapsed = Date.now() - started;
|
|
105
|
+
|
|
106
|
+
expect(elapsed).toBeLessThan(5000);
|
|
107
|
+
expect(results[0]).toMatchObject({ logId: 'slow', status: 'timeout', exitCode: null });
|
|
108
|
+
expect(results[1]).toMatchObject({ logId: 'next', status: 'success' });
|
|
109
|
+
|
|
110
|
+
// the backgrounded child would write the marker at 1.5s if the group was not killed
|
|
111
|
+
await delay(2000);
|
|
112
|
+
expect(await fileExists(marker)).toBe(false);
|
|
113
|
+
}, 15000);
|
|
114
|
+
|
|
115
|
+
it('T-WE4 truncates oversized output keeping the tail <= 256 KiB', async () => {
|
|
116
|
+
const executor = new WorkloadExecutor();
|
|
117
|
+
const { results, sink } = collect();
|
|
118
|
+
|
|
119
|
+
await executor.runBatch(
|
|
120
|
+
{
|
|
121
|
+
phase: 'init',
|
|
122
|
+
items: [{ logId: 'big', workloadName: 'Big', script: "yes A | head -c 300000; printf 'ENDMARK'", timeoutMs: 10000 }],
|
|
123
|
+
},
|
|
124
|
+
sink,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(results).toHaveLength(1);
|
|
128
|
+
expect(results[0].status).toBe('success');
|
|
129
|
+
expect(Buffer.byteLength(results[0].log, 'utf8')).toBeLessThanOrEqual(MAX_OUTPUT_BYTES);
|
|
130
|
+
expect(results[0].log.endsWith('ENDMARK')).toBe(true);
|
|
131
|
+
}, 15000);
|
|
132
|
+
|
|
133
|
+
it('T-WE4b: skips a leading split UTF-8 continuation byte when truncating the tail', async () => {
|
|
134
|
+
const executor = new WorkloadExecutor();
|
|
135
|
+
const { results, sink } = collect();
|
|
136
|
+
|
|
137
|
+
// 总长 262145 字节(3 字节「中」+ 262142 个 A),尾部保留 262144 字节 => 起点落在
|
|
138
|
+
// 「中」的第 2 个字节(续字节)上;截断后必须跳过续字节到字符边界,首字符不得是乱码。
|
|
139
|
+
await executor.runBatch(
|
|
140
|
+
{
|
|
141
|
+
phase: 'init',
|
|
142
|
+
items: [
|
|
143
|
+
{
|
|
144
|
+
logId: 'utf8',
|
|
145
|
+
workloadName: 'Utf8',
|
|
146
|
+
script: "printf '中'; head -c 262142 /dev/zero | tr '\\0' 'A'",
|
|
147
|
+
timeoutMs: 10000,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
sink,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
expect(results).toHaveLength(1);
|
|
155
|
+
const log = results[0].log;
|
|
156
|
+
expect(log.includes('\uFFFD')).toBe(false);
|
|
157
|
+
expect(log).toBe('A'.repeat(262142));
|
|
158
|
+
expect(Buffer.byteLength(log, 'utf8')).toBeLessThanOrEqual(MAX_OUTPUT_BYTES);
|
|
159
|
+
}, 15000);
|
|
160
|
+
|
|
161
|
+
it('T-WE5 shares the post budget and reports timeout without starting later items', async () => {
|
|
162
|
+
const executor = new WorkloadExecutor();
|
|
163
|
+
const { results, sink } = collect();
|
|
164
|
+
|
|
165
|
+
const started = Date.now();
|
|
166
|
+
await executor.runBatch(
|
|
167
|
+
{
|
|
168
|
+
phase: 'post',
|
|
169
|
+
budgetMs: 300,
|
|
170
|
+
items: [
|
|
171
|
+
{ logId: 'p1', workloadName: 'P1', script: 'sleep 2', timeoutMs: 10000 },
|
|
172
|
+
{ logId: 'p2', workloadName: 'P2', script: 'echo should-not-run', timeoutMs: 10000 },
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
sink,
|
|
176
|
+
);
|
|
177
|
+
const elapsed = Date.now() - started;
|
|
178
|
+
|
|
179
|
+
expect(elapsed).toBeLessThan(5000);
|
|
180
|
+
expect(results.map((r) => r.logId)).toEqual(['p1', 'p2']);
|
|
181
|
+
expect(results[0]).toMatchObject({ status: 'timeout', exitCode: null });
|
|
182
|
+
expect(results[1]).toMatchObject({ status: 'timeout', exitCode: null });
|
|
183
|
+
expect(results[1].log).not.toContain('should-not-run');
|
|
184
|
+
}, 15000);
|
|
185
|
+
|
|
186
|
+
it('T-WE6 buffers unconfirmed results and replays then clears on reconnect', async () => {
|
|
187
|
+
const executor = new WorkloadExecutor();
|
|
188
|
+
|
|
189
|
+
await executor.runBatch(
|
|
190
|
+
{
|
|
191
|
+
phase: 'init',
|
|
192
|
+
items: [
|
|
193
|
+
{ logId: 'r1', workloadName: 'R1', script: 'echo one', timeoutMs: 5000 },
|
|
194
|
+
{ logId: 'r2', workloadName: 'R2', script: 'echo two', timeoutMs: 5000 },
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
() => {},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(executor.getPendingResults().map((r) => r.logId)).toEqual(['r1', 'r2']);
|
|
201
|
+
|
|
202
|
+
const replayed: WorkloadResult[] = [];
|
|
203
|
+
executor.replayPendingResults((result) => replayed.push(result));
|
|
204
|
+
|
|
205
|
+
expect(replayed.map((r) => r.logId)).toEqual(['r1', 'r2']);
|
|
206
|
+
expect(executor.getPendingResults()).toHaveLength(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('T-WE6b caps the replay buffer at 50 results', async () => {
|
|
210
|
+
const executor = new WorkloadExecutor();
|
|
211
|
+
const items = Array.from({ length: MAX_PENDING_RESULTS + 5 }, (_, i) => ({
|
|
212
|
+
logId: `x${i}`,
|
|
213
|
+
workloadName: `X${i}`,
|
|
214
|
+
script: ':',
|
|
215
|
+
timeoutMs: 5000,
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
await executor.runBatch({ phase: 'init', items }, () => {});
|
|
219
|
+
|
|
220
|
+
const pending = executor.getPendingResults();
|
|
221
|
+
expect(pending).toHaveLength(MAX_PENDING_RESULTS);
|
|
222
|
+
expect(pending[pending.length - 1].logId).toBe(`x${MAX_PENDING_RESULTS + 4}`);
|
|
223
|
+
}, 30000);
|
|
224
|
+
|
|
225
|
+
it('T-WE7 terminates the running init batch when a new post batch arrives', async () => {
|
|
226
|
+
const dir = await makeTmpDir();
|
|
227
|
+
dirs.push(dir);
|
|
228
|
+
const startedFile = join(dir, 'init-started');
|
|
229
|
+
const executor = new WorkloadExecutor();
|
|
230
|
+
|
|
231
|
+
const initResults: WorkloadResult[] = [];
|
|
232
|
+
const initBatch = executor.runBatch(
|
|
233
|
+
{
|
|
234
|
+
phase: 'init',
|
|
235
|
+
items: [{ logId: 'init-1', workloadName: 'Init', script: `touch ${startedFile}; sleep 30`, timeoutMs: 60000 }],
|
|
236
|
+
},
|
|
237
|
+
(result) => initResults.push(result),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// wait until the init child process has actually started
|
|
241
|
+
const deadline = Date.now() + 5000;
|
|
242
|
+
let startedFlag = false;
|
|
243
|
+
while (Date.now() < deadline) {
|
|
244
|
+
if (await fileExists(startedFile)) {
|
|
245
|
+
startedFlag = true;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
await delay(25);
|
|
249
|
+
}
|
|
250
|
+
expect(startedFlag).toBe(true);
|
|
251
|
+
|
|
252
|
+
const postResults: WorkloadResult[] = [];
|
|
253
|
+
await executor.runBatch(
|
|
254
|
+
{ phase: 'post', items: [{ logId: 'post-1', workloadName: 'Post', script: 'echo post-ok', timeoutMs: 5000 }] },
|
|
255
|
+
(result) => postResults.push(result),
|
|
256
|
+
);
|
|
257
|
+
await initBatch;
|
|
258
|
+
|
|
259
|
+
expect(postResults.map((r) => r.logId)).toEqual(['post-1']);
|
|
260
|
+
expect(postResults[0].status).toBe('success');
|
|
261
|
+
// the interrupted init item is not reported; the server marks it TIMEOUT
|
|
262
|
+
expect(initResults).toHaveLength(0);
|
|
263
|
+
}, 20000);
|
|
264
|
+
|
|
265
|
+
it('T-WE8 reports failed for an empty script and does nothing for empty items', async () => {
|
|
266
|
+
const executor = new WorkloadExecutor();
|
|
267
|
+
const { results, sink } = collect();
|
|
268
|
+
|
|
269
|
+
await executor.runBatch(
|
|
270
|
+
{ phase: 'init', items: [{ logId: 'empty', workloadName: 'Empty', script: ' ', timeoutMs: 5000 }] },
|
|
271
|
+
sink,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
expect(results).toHaveLength(1);
|
|
275
|
+
expect(results[0]).toMatchObject({ logId: 'empty', status: 'failed', exitCode: null, log: EMPTY_SCRIPT_MESSAGE });
|
|
276
|
+
|
|
277
|
+
const empty = collect();
|
|
278
|
+
await executor.runBatch({ phase: 'init', items: [] }, empty.sink);
|
|
279
|
+
expect(empty.results).toHaveLength(0);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('T-WE10 injects TEXT/BOOLEAN params as WORKLOAD_PARAM_* and the full JSON as WORKLOAD_PARAMS', async () => {
|
|
283
|
+
const executor = new WorkloadExecutor();
|
|
284
|
+
const { results, sink } = collect();
|
|
285
|
+
|
|
286
|
+
await executor.runBatch(
|
|
287
|
+
{
|
|
288
|
+
phase: 'init',
|
|
289
|
+
items: [
|
|
290
|
+
{
|
|
291
|
+
logId: 'params',
|
|
292
|
+
workloadName: 'Params',
|
|
293
|
+
script: `printf 'P=%s\\n' "$WORKLOAD_PARAMS"; printf 'V=%s\\n' "$WORKLOAD_PARAM_VERSION"; printf 'D=%s\\n' "$WORKLOAD_PARAM_DEBUG"; printf 'E=%s\\n' "$WORKLOAD_PARAM_ENABLED"`,
|
|
294
|
+
timeoutMs: 5000,
|
|
295
|
+
workloadId: 'w-text',
|
|
296
|
+
params: { version: '22', debug: true, enabled: false },
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
sink,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
expect(results).toHaveLength(1);
|
|
304
|
+
expect(results[0].status).toBe('success');
|
|
305
|
+
expect(results[0].log).toContain('P={"version":"22","debug":true,"enabled":false}');
|
|
306
|
+
expect(results[0].log).toContain('V=22');
|
|
307
|
+
expect(results[0].log).toContain('D=true');
|
|
308
|
+
expect(results[0].log).toContain('E=false');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('T-WE11 injects MULTI_SELECT params as a JSON array string that scripts can parse', async () => {
|
|
312
|
+
const executor = new WorkloadExecutor();
|
|
313
|
+
const { results, sink } = collect();
|
|
314
|
+
|
|
315
|
+
await executor.runBatch(
|
|
316
|
+
{
|
|
317
|
+
phase: 'init',
|
|
318
|
+
items: [
|
|
319
|
+
{
|
|
320
|
+
logId: 'multi',
|
|
321
|
+
workloadName: 'Multi',
|
|
322
|
+
script: `printf 'PM=%s\\n' "$WORKLOAD_PARAM_PACKAGE_MANAGER"; node -e 'const v = JSON.parse(process.argv[1] || "[]"); process.stdout.write("parsed=" + v.join(","))' "$WORKLOAD_PARAM_PACKAGE_MANAGER"`,
|
|
323
|
+
timeoutMs: 10000,
|
|
324
|
+
workloadId: 'w-multi',
|
|
325
|
+
params: { package_manager: ['pnpm', 'bun'] },
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
sink,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
expect(results).toHaveLength(1);
|
|
333
|
+
expect(results[0].status).toBe('success');
|
|
334
|
+
expect(results[0].log).toContain('PM=["pnpm","bun"]');
|
|
335
|
+
expect(results[0].log).toContain('parsed=pnpm,bun');
|
|
336
|
+
}, 15000);
|
|
337
|
+
|
|
338
|
+
it('T-WE12 keeps legacy behavior for empty params (only WORKLOAD_PARAMS={}, host env inherited)', async () => {
|
|
339
|
+
const executor = new WorkloadExecutor();
|
|
340
|
+
const { results, sink } = collect();
|
|
341
|
+
|
|
342
|
+
process.env.ZAG_WORKLOAD_TEST_INHERITED = 'inherited';
|
|
343
|
+
try {
|
|
344
|
+
await executor.runBatch(
|
|
345
|
+
{
|
|
346
|
+
phase: 'init',
|
|
347
|
+
items: [
|
|
348
|
+
{
|
|
349
|
+
logId: 'empty-params',
|
|
350
|
+
workloadName: 'EmptyParams',
|
|
351
|
+
script: `printf 'P=%s\\n' "$WORKLOAD_PARAMS"; printf 'FOO=%s\\n' "\${WORKLOAD_PARAM_FOO-unset}"; printf 'INHERIT=%s\\n' "\${ZAG_WORKLOAD_TEST_INHERITED-unset}"`,
|
|
352
|
+
timeoutMs: 5000,
|
|
353
|
+
workloadId: 'w-empty',
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
sink,
|
|
358
|
+
);
|
|
359
|
+
} finally {
|
|
360
|
+
delete process.env.ZAG_WORKLOAD_TEST_INHERITED;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
expect(results).toHaveLength(1);
|
|
364
|
+
expect(results[0].status).toBe('success');
|
|
365
|
+
expect(results[0].log).toContain('P={}');
|
|
366
|
+
expect(results[0].log).toContain('FOO=unset');
|
|
367
|
+
expect(results[0].log).toContain('INHERIT=inherited');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('T-WE13 scopes params to the current item and does not leak them to later items', async () => {
|
|
371
|
+
const executor = new WorkloadExecutor();
|
|
372
|
+
const { results, sink } = collect();
|
|
373
|
+
|
|
374
|
+
await executor.runBatch(
|
|
375
|
+
{
|
|
376
|
+
phase: 'init',
|
|
377
|
+
items: [
|
|
378
|
+
{
|
|
379
|
+
logId: 'first',
|
|
380
|
+
workloadName: 'First',
|
|
381
|
+
script: `printf 'P=%s\\n' "$WORKLOAD_PARAMS"`,
|
|
382
|
+
timeoutMs: 5000,
|
|
383
|
+
workloadId: 'w-first',
|
|
384
|
+
params: { version: '22', package_manager: ['pnpm'] },
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
logId: 'second',
|
|
388
|
+
workloadName: 'Second',
|
|
389
|
+
script: `printf 'P=%s\\n' "$WORKLOAD_PARAMS"; printf 'V=%s\\n' "\${WORKLOAD_PARAM_VERSION-unset}"; printf 'PM=%s\\n' "\${WORKLOAD_PARAM_PACKAGE_MANAGER-unset}"`,
|
|
390
|
+
timeoutMs: 5000,
|
|
391
|
+
workloadId: 'w-second',
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
},
|
|
395
|
+
sink,
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
expect(results.map((r) => r.logId)).toEqual(['first', 'second']);
|
|
399
|
+
expect(results[0].log).toContain('P={"version":"22","package_manager":["pnpm"]}');
|
|
400
|
+
expect(results[1].log).toContain('P={}');
|
|
401
|
+
expect(results[1].log).toContain('V=unset');
|
|
402
|
+
expect(results[1].log).toContain('PM=unset');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('T-WE14 treats workloadId as log-only: order and result shape are unchanged', async () => {
|
|
406
|
+
const executor = new WorkloadExecutor();
|
|
407
|
+
const { results, sink } = collect();
|
|
408
|
+
|
|
409
|
+
await executor.runBatch(
|
|
410
|
+
{
|
|
411
|
+
phase: 'init',
|
|
412
|
+
items: [
|
|
413
|
+
{ logId: 'one', workloadName: 'One', workloadId: 'w-1', script: 'echo one', timeoutMs: 5000, params: { a: 'x' } },
|
|
414
|
+
{ logId: 'two', workloadName: 'Two', workloadId: 'w-2', script: 'echo two', timeoutMs: 5000 },
|
|
415
|
+
],
|
|
416
|
+
},
|
|
417
|
+
sink,
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
expect(results.map((r) => r.logId)).toEqual(['one', 'two']);
|
|
421
|
+
expect(results.map((r) => r.status)).toEqual(['success', 'success']);
|
|
422
|
+
expect(Object.keys(results[0]).sort()).toEqual(
|
|
423
|
+
['exitCode', 'finishedAt', 'log', 'logId', 'startedAt', 'status'].sort(),
|
|
424
|
+
);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('T-WE9 exposes running state and waitForIdle resolves after the batch completes', async () => {
|
|
428
|
+
const executor = new WorkloadExecutor();
|
|
429
|
+
expect(executor.isRunning()).toBe(false);
|
|
430
|
+
|
|
431
|
+
const batch = executor.runBatch(
|
|
432
|
+
{ phase: 'post', items: [{ logId: 'w', workloadName: 'W', script: 'sleep 0.4', timeoutMs: 5000 }] },
|
|
433
|
+
() => {},
|
|
434
|
+
);
|
|
435
|
+
expect(executor.isRunning()).toBe(true);
|
|
436
|
+
|
|
437
|
+
await executor.waitForIdle(2000);
|
|
438
|
+
expect(executor.isRunning()).toBe(false);
|
|
439
|
+
await batch;
|
|
440
|
+
}, 10000);
|
|
441
|
+
});
|