@ryanfw/prompt-orchestration-pipeline 1.2.7 → 1.2.9
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/package.json +1 -1
- package/src/config/__tests__/models.test.ts +31 -1
- package/src/config/models.ts +81 -35
- package/src/config/paths.ts +13 -8
- package/src/core/__tests__/config.test.ts +121 -0
- package/src/core/__tests__/job-concurrency.test.ts +554 -0
- package/src/core/__tests__/orchestrator.test.ts +353 -0
- package/src/core/__tests__/pipeline-runner.test.ts +430 -2
- package/src/core/__tests__/task-runner.test.ts +1 -2
- package/src/core/config.ts +48 -1
- package/src/core/job-concurrency.ts +462 -0
- package/src/core/orchestrator.ts +370 -57
- package/src/core/pipeline-runner.ts +79 -15
- package/src/core/status-writer.ts +4 -0
- package/src/core/task-runner.ts +1 -1
- package/src/providers/__tests__/base.test.ts +1 -1
- package/src/ui/client/__tests__/api.test.ts +101 -1
- package/src/ui/client/__tests__/job-adapter.test.ts +12 -0
- package/src/ui/client/__tests__/useConcurrencyStatus.test.ts +126 -0
- package/src/ui/client/adapters/job-adapter.ts +1 -0
- package/src/ui/client/api.ts +77 -7
- package/src/ui/client/hooks/useConcurrencyStatus.ts +102 -0
- package/src/ui/client/types.ts +34 -1
- package/src/ui/components/DAGGrid.tsx +11 -1
- package/src/ui/components/JobDetail.tsx +2 -1
- package/src/ui/components/__tests__/DAGGrid.test.tsx +92 -0
- package/src/ui/components/__tests__/JobDetail.test.tsx +62 -0
- package/src/ui/components/types.ts +2 -0
- package/src/ui/dist/assets/{index-SKy2shWc.js → index-BnAqY4_n.js} +336 -52
- package/src/ui/dist/assets/index-BnAqY4_n.js.map +1 -0
- package/src/ui/dist/assets/style-BKG0bHu-.css +2 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/embedded-assets.js +6 -6
- package/src/ui/pages/PromptPipelineDashboard.tsx +186 -4
- package/src/ui/pages/__tests__/PromptPipelineDashboard.test.tsx +272 -1
- package/src/ui/server/__tests__/concurrency-endpoint.test.ts +190 -0
- package/src/ui/server/__tests__/index.test.ts +92 -3
- package/src/ui/server/__tests__/job-control-endpoints.test.ts +660 -3
- package/src/ui/server/endpoints/concurrency-endpoint.ts +72 -0
- package/src/ui/server/endpoints/job-control-endpoints.ts +248 -37
- package/src/ui/server/index.ts +21 -2
- package/src/ui/server/router.ts +2 -0
- package/src/ui/state/__tests__/watcher.test.ts +31 -0
- package/src/ui/state/transformers/__tests__/status-transformer.test.ts +15 -0
- package/src/ui/state/transformers/status-transformer.ts +1 -0
- package/src/ui/state/types.ts +3 -0
- package/src/ui/state/watcher.ts +9 -1
- package/src/utils/__tests__/dag.test.ts +35 -0
- package/src/utils/dag.ts +1 -0
- package/src/ui/dist/assets/index-SKy2shWc.js.map +0 -1
- package/src/ui/dist/assets/style-DA1Ma4YS.css +0 -2
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { mkdtemp, mkdir, writeFile, utimes, rm, readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { drainPendingQueue, handleChildExit } from "../orchestrator";
|
|
7
|
+
import {
|
|
8
|
+
getConcurrencyRuntimePaths,
|
|
9
|
+
getJobConcurrencyStatus,
|
|
10
|
+
} from "../job-concurrency";
|
|
11
|
+
|
|
12
|
+
async function setupDir(prefix: string): Promise<string> {
|
|
13
|
+
const dir = await mkdtemp(join(tmpdir(), prefix));
|
|
14
|
+
await mkdir(join(dir, "pending"), { recursive: true });
|
|
15
|
+
await mkdir(join(dir, "current"), { recursive: true });
|
|
16
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(dir);
|
|
17
|
+
await mkdir(runningJobsDir, { recursive: true });
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function writeSeed(
|
|
22
|
+
dir: string,
|
|
23
|
+
jobId: string,
|
|
24
|
+
body: unknown,
|
|
25
|
+
mtimeSec: number,
|
|
26
|
+
): Promise<string> {
|
|
27
|
+
const filePath = join(dir, "pending", `${jobId}-seed.json`);
|
|
28
|
+
await writeFile(filePath, JSON.stringify(body));
|
|
29
|
+
await utimes(filePath, mtimeSec, mtimeSec);
|
|
30
|
+
return filePath;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fakeSpawnRunner(jobIdToPid: Map<string, number>): (jobId: string) => Promise<{ pid: number }> {
|
|
34
|
+
return async (jobId: string) => {
|
|
35
|
+
const pid = process.pid;
|
|
36
|
+
jobIdToPid.set(jobId, pid);
|
|
37
|
+
return { pid };
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("drainPendingQueue", () => {
|
|
42
|
+
test("with limit 2 and three seeds, promotes exactly two and one remains in pending", async () => {
|
|
43
|
+
const dir = await setupDir("drain-limit2-");
|
|
44
|
+
try {
|
|
45
|
+
await writeSeed(dir, "job-a", { pipeline: "p" }, 1700000000);
|
|
46
|
+
await writeSeed(dir, "job-b", { pipeline: "p" }, 1700000100);
|
|
47
|
+
await writeSeed(dir, "job-c", { pipeline: "p" }, 1700000200);
|
|
48
|
+
const pids = new Map<string, number>();
|
|
49
|
+
const result = await drainPendingQueue({
|
|
50
|
+
dataDir: dir,
|
|
51
|
+
maxConcurrentJobs: 2,
|
|
52
|
+
lockTimeoutMs: 1000,
|
|
53
|
+
spawnRunner: fakeSpawnRunner(pids),
|
|
54
|
+
});
|
|
55
|
+
expect(result.promoted).toEqual(["job-a", "job-b"]);
|
|
56
|
+
expect(result.remaining).toBe(1);
|
|
57
|
+
expect(await readdir(join(dir, "pending"))).toEqual(["job-c-seed.json"]);
|
|
58
|
+
expect(existsSync(join(dir, "current", "job-a", "seed.json"))).toBe(true);
|
|
59
|
+
expect(existsSync(join(dir, "current", "job-b", "seed.json"))).toBe(true);
|
|
60
|
+
expect(existsSync(join(dir, "current", "job-c"))).toBe(false);
|
|
61
|
+
} finally {
|
|
62
|
+
await rm(dir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("with limit 0, no seeds are promoted", async () => {
|
|
67
|
+
const dir = await setupDir("drain-limit0-");
|
|
68
|
+
try {
|
|
69
|
+
await writeSeed(dir, "job-a", { pipeline: "p" }, 1700000000);
|
|
70
|
+
await writeSeed(dir, "job-b", { pipeline: "p" }, 1700000100);
|
|
71
|
+
const pids = new Map<string, number>();
|
|
72
|
+
const result = await drainPendingQueue({
|
|
73
|
+
dataDir: dir,
|
|
74
|
+
maxConcurrentJobs: 0,
|
|
75
|
+
lockTimeoutMs: 1000,
|
|
76
|
+
spawnRunner: fakeSpawnRunner(pids),
|
|
77
|
+
});
|
|
78
|
+
expect(result.promoted).toEqual([]);
|
|
79
|
+
expect(result.remaining).toBe(2);
|
|
80
|
+
expect((await readdir(join(dir, "pending"))).sort()).toEqual([
|
|
81
|
+
"job-a-seed.json",
|
|
82
|
+
"job-b-seed.json",
|
|
83
|
+
]);
|
|
84
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(dir);
|
|
85
|
+
expect(await readdir(runningJobsDir)).toEqual([]);
|
|
86
|
+
} finally {
|
|
87
|
+
await rm(dir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("seed whose current/<jobId>/ already exists is not promoted, slot is released, seed remains in pending", async () => {
|
|
92
|
+
const dir = await setupDir("drain-existing-");
|
|
93
|
+
try {
|
|
94
|
+
await writeSeed(dir, "job-existing", { pipeline: "p" }, 1700000000);
|
|
95
|
+
await writeSeed(dir, "job-fresh", { pipeline: "p" }, 1700000100);
|
|
96
|
+
await mkdir(join(dir, "current", "job-existing"), { recursive: true });
|
|
97
|
+
const pids = new Map<string, number>();
|
|
98
|
+
const result = await drainPendingQueue({
|
|
99
|
+
dataDir: dir,
|
|
100
|
+
maxConcurrentJobs: 5,
|
|
101
|
+
lockTimeoutMs: 1000,
|
|
102
|
+
spawnRunner: fakeSpawnRunner(pids),
|
|
103
|
+
});
|
|
104
|
+
expect(result.promoted).toEqual(["job-fresh"]);
|
|
105
|
+
const status = await getJobConcurrencyStatus(dir, 5, 1000);
|
|
106
|
+
expect(status.activeJobs.map((l) => l.jobId).sort()).toEqual(["job-fresh"]);
|
|
107
|
+
expect(existsSync(join(dir, "pending", "job-existing-seed.json"))).toBe(true);
|
|
108
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(dir);
|
|
109
|
+
expect(existsSync(join(runningJobsDir, "job-existing.json"))).toBe(false);
|
|
110
|
+
} finally {
|
|
111
|
+
await rm(dir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("promotes seeds in deterministic order (mtime asc)", async () => {
|
|
116
|
+
const dir = await setupDir("drain-order-");
|
|
117
|
+
try {
|
|
118
|
+
await writeSeed(dir, "job-c", { pipeline: "p" }, 1700000300);
|
|
119
|
+
await writeSeed(dir, "job-a", { pipeline: "p" }, 1700000100);
|
|
120
|
+
await writeSeed(dir, "job-b", { pipeline: "p" }, 1700000200);
|
|
121
|
+
const pids = new Map<string, number>();
|
|
122
|
+
const result = await drainPendingQueue({
|
|
123
|
+
dataDir: dir,
|
|
124
|
+
maxConcurrentJobs: 5,
|
|
125
|
+
lockTimeoutMs: 1000,
|
|
126
|
+
spawnRunner: fakeSpawnRunner(pids),
|
|
127
|
+
});
|
|
128
|
+
expect(result.promoted).toEqual(["job-a", "job-b", "job-c"]);
|
|
129
|
+
} finally {
|
|
130
|
+
await rm(dir, { recursive: true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("if spawnRunner throws, the slot is released and the seed remains queued", async () => {
|
|
135
|
+
const dir = await setupDir("drain-spawn-err-");
|
|
136
|
+
try {
|
|
137
|
+
await writeSeed(dir, "job-bad", { pipeline: "p" }, 1700000000);
|
|
138
|
+
const failingSpawn = async (): Promise<{ pid: number }> => {
|
|
139
|
+
throw new Error("boom");
|
|
140
|
+
};
|
|
141
|
+
const result = await drainPendingQueue({
|
|
142
|
+
dataDir: dir,
|
|
143
|
+
maxConcurrentJobs: 2,
|
|
144
|
+
lockTimeoutMs: 1000,
|
|
145
|
+
spawnRunner: failingSpawn,
|
|
146
|
+
});
|
|
147
|
+
expect(result.promoted).toEqual([]);
|
|
148
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(dir);
|
|
149
|
+
expect(existsSync(join(runningJobsDir, "job-bad.json"))).toBe(false);
|
|
150
|
+
expect(existsSync(join(dir, "pending", "job-bad-seed.json"))).toBe(true);
|
|
151
|
+
const status = await getJobConcurrencyStatus(dir, 2, 1000);
|
|
152
|
+
expect(status.activeJobs).toEqual([]);
|
|
153
|
+
} finally {
|
|
154
|
+
await rm(dir, { recursive: true });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("repeated spawn failures move the seed to rejected", async () => {
|
|
159
|
+
const dir = await setupDir("drain-spawn-reject-");
|
|
160
|
+
try {
|
|
161
|
+
await writeSeed(dir, "job-bad", { pipeline: "p" }, 1700000000);
|
|
162
|
+
const failingSpawn = async (): Promise<{ pid: number }> => {
|
|
163
|
+
throw new Error("boom");
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < 3; i++) {
|
|
167
|
+
await drainPendingQueue({
|
|
168
|
+
dataDir: dir,
|
|
169
|
+
maxConcurrentJobs: 2,
|
|
170
|
+
lockTimeoutMs: 1000,
|
|
171
|
+
spawnRunner: failingSpawn,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
expect(existsSync(join(dir, "pending", "job-bad-seed.json"))).toBe(false);
|
|
176
|
+
expect(existsSync(join(dir, "rejected", "job-bad", "seed.json"))).toBe(true);
|
|
177
|
+
const rejection = JSON.parse(await readFile(join(dir, "rejected", "job-bad", "rejection.json"), "utf-8")) as Record<string, unknown>;
|
|
178
|
+
expect(rejection["reason"]).toBe("spawn_failed");
|
|
179
|
+
} finally {
|
|
180
|
+
await rm(dir, { recursive: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("repeated invalid seed reads move the seed to rejected", async () => {
|
|
185
|
+
const dir = await setupDir("drain-invalid-reject-");
|
|
186
|
+
try {
|
|
187
|
+
const seedPath = join(dir, "pending", "job-invalid-seed.json");
|
|
188
|
+
await writeFile(seedPath, "not json {");
|
|
189
|
+
await utimes(seedPath, 1700000000, 1700000000);
|
|
190
|
+
|
|
191
|
+
for (let i = 0; i < 3; i++) {
|
|
192
|
+
await drainPendingQueue({
|
|
193
|
+
dataDir: dir,
|
|
194
|
+
maxConcurrentJobs: 2,
|
|
195
|
+
lockTimeoutMs: 1000,
|
|
196
|
+
spawnRunner: fakeSpawnRunner(new Map()),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
expect(existsSync(seedPath)).toBe(false);
|
|
201
|
+
expect(existsSync(join(dir, "rejected", "job-invalid", "seed.json"))).toBe(true);
|
|
202
|
+
const rejection = JSON.parse(await readFile(join(dir, "rejected", "job-invalid", "rejection.json"), "utf-8")) as Record<string, unknown>;
|
|
203
|
+
expect(rejection["reason"]).toBe("invalid");
|
|
204
|
+
} finally {
|
|
205
|
+
await rm(dir, { recursive: true });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("pid update failure kills the spawned runner and does not promote the job", async () => {
|
|
210
|
+
const dir = await setupDir("drain-pid-update-");
|
|
211
|
+
try {
|
|
212
|
+
await writeSeed(dir, "job-pid-fail", { pipeline: "p" }, 1700000000);
|
|
213
|
+
const { lockDir } = getConcurrencyRuntimePaths(dir);
|
|
214
|
+
let killed = 0;
|
|
215
|
+
const result = await drainPendingQueue({
|
|
216
|
+
dataDir: dir,
|
|
217
|
+
maxConcurrentJobs: 2,
|
|
218
|
+
lockTimeoutMs: 10,
|
|
219
|
+
spawnRunner: async () => {
|
|
220
|
+
await mkdir(lockDir, { recursive: true });
|
|
221
|
+
await writeFile(join(lockDir, "owner.json"), JSON.stringify({ pid: process.pid, acquiredAt: new Date().toISOString() }));
|
|
222
|
+
return { pid: process.pid, kill: () => { killed++; } };
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(result.promoted).toEqual([]);
|
|
227
|
+
expect(killed).toBe(1);
|
|
228
|
+
expect(existsSync(join(dir, "pending", "job-pid-fail-seed.json"))).toBe(true);
|
|
229
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
230
|
+
// The lease's missing_current_job grace window (lockTimeoutMs) must elapse
|
|
231
|
+
// before status-driven pruning will reclaim the slot.
|
|
232
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
233
|
+
const status = await getJobConcurrencyStatus(dir, 2, 10);
|
|
234
|
+
expect(status.activeJobs).toEqual([]);
|
|
235
|
+
} finally {
|
|
236
|
+
await rm(dir, { recursive: true });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("handleChildExit", () => {
|
|
242
|
+
test("releases the exited job's slot and triggers a drain that promotes the next pending seed", async () => {
|
|
243
|
+
const dir = await setupDir("exit-promotes-");
|
|
244
|
+
try {
|
|
245
|
+
await writeSeed(dir, "job-a", { pipeline: "p" }, 1700000000);
|
|
246
|
+
await writeSeed(dir, "job-b", { pipeline: "p" }, 1700000100);
|
|
247
|
+
const pids = new Map<string, number>();
|
|
248
|
+
const spawn = fakeSpawnRunner(pids);
|
|
249
|
+
|
|
250
|
+
const first = await drainPendingQueue({
|
|
251
|
+
dataDir: dir,
|
|
252
|
+
maxConcurrentJobs: 1,
|
|
253
|
+
lockTimeoutMs: 1000,
|
|
254
|
+
spawnRunner: spawn,
|
|
255
|
+
});
|
|
256
|
+
expect(first.promoted).toEqual(["job-a"]);
|
|
257
|
+
expect(first.remaining).toBe(1);
|
|
258
|
+
|
|
259
|
+
let drainResult: { promoted: string[]; remaining: number } | null = null;
|
|
260
|
+
const triggerDrain = (): void => {
|
|
261
|
+
void drainPendingQueue({
|
|
262
|
+
dataDir: dir,
|
|
263
|
+
maxConcurrentJobs: 1,
|
|
264
|
+
lockTimeoutMs: 1000,
|
|
265
|
+
spawnRunner: spawn,
|
|
266
|
+
}).then((r) => {
|
|
267
|
+
drainResult = r;
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
await handleChildExit({ dataDir: dir, jobId: "job-a", triggerDrain });
|
|
272
|
+
|
|
273
|
+
// Wait for the triggered drain to complete.
|
|
274
|
+
while (drainResult === null) {
|
|
275
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
276
|
+
}
|
|
277
|
+
const finalResult = drainResult as { promoted: string[]; remaining: number };
|
|
278
|
+
expect(finalResult.promoted).toEqual(["job-b"]);
|
|
279
|
+
expect(existsSync(join(dir, "current", "job-b", "seed.json"))).toBe(true);
|
|
280
|
+
const status = await getJobConcurrencyStatus(dir, 1, 1000);
|
|
281
|
+
expect(status.activeJobs.map((l) => l.jobId)).toEqual(["job-b"]);
|
|
282
|
+
} finally {
|
|
283
|
+
await rm(dir, { recursive: true });
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("handleChildExit on an already-released slot is a no-op", async () => {
|
|
288
|
+
const dir = await setupDir("exit-idempotent-");
|
|
289
|
+
try {
|
|
290
|
+
let called = 0;
|
|
291
|
+
await handleChildExit({
|
|
292
|
+
dataDir: dir,
|
|
293
|
+
jobId: "job-missing",
|
|
294
|
+
triggerDrain: () => {
|
|
295
|
+
called++;
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
expect(called).toBe(1);
|
|
299
|
+
const { runningJobsDir } = getConcurrencyRuntimePaths(dir);
|
|
300
|
+
expect(existsSync(join(runningJobsDir, "job-missing.json"))).toBe(false);
|
|
301
|
+
} finally {
|
|
302
|
+
await rm(dir, { recursive: true });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("drainPendingQueue concurrency", () => {
|
|
308
|
+
test("overlapping drain calls do not double-promote (limit 2, four seeds → exactly 2 promoted overall)", async () => {
|
|
309
|
+
const dir = await setupDir("drain-overlap-");
|
|
310
|
+
try {
|
|
311
|
+
await writeSeed(dir, "job-a", { pipeline: "p" }, 1700000000);
|
|
312
|
+
await writeSeed(dir, "job-b", { pipeline: "p" }, 1700000100);
|
|
313
|
+
await writeSeed(dir, "job-c", { pipeline: "p" }, 1700000200);
|
|
314
|
+
await writeSeed(dir, "job-d", { pipeline: "p" }, 1700000300);
|
|
315
|
+
const pids = new Map<string, number>();
|
|
316
|
+
const spawn = fakeSpawnRunner(pids);
|
|
317
|
+
|
|
318
|
+
const results = await Promise.allSettled([
|
|
319
|
+
drainPendingQueue({
|
|
320
|
+
dataDir: dir,
|
|
321
|
+
maxConcurrentJobs: 2,
|
|
322
|
+
lockTimeoutMs: 1000,
|
|
323
|
+
spawnRunner: spawn,
|
|
324
|
+
}),
|
|
325
|
+
drainPendingQueue({
|
|
326
|
+
dataDir: dir,
|
|
327
|
+
maxConcurrentJobs: 2,
|
|
328
|
+
lockTimeoutMs: 1000,
|
|
329
|
+
spawnRunner: spawn,
|
|
330
|
+
}),
|
|
331
|
+
]);
|
|
332
|
+
|
|
333
|
+
const promotedUnion = new Set<string>();
|
|
334
|
+
for (const r of results) {
|
|
335
|
+
if (r.status === "fulfilled") {
|
|
336
|
+
for (const id of r.value.promoted) promotedUnion.add(id);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Exactly 2 distinct promotions across all overlapping drains.
|
|
340
|
+
expect(promotedUnion.size).toBe(2);
|
|
341
|
+
|
|
342
|
+
const status = await getJobConcurrencyStatus(dir, 2, 1000);
|
|
343
|
+
expect(status.activeJobs.length).toBe(2);
|
|
344
|
+
expect(status.runningCount).toBe(2);
|
|
345
|
+
|
|
346
|
+
// Two seeds remain in pending (the two not promoted).
|
|
347
|
+
const remainingPending = (await readdir(join(dir, "pending"))).sort();
|
|
348
|
+
expect(remainingPending.length).toBe(2);
|
|
349
|
+
} finally {
|
|
350
|
+
await rm(dir, { recursive: true });
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
});
|