@ryanfw/prompt-orchestration-pipeline 1.2.11 → 1.2.12
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryanfw/prompt-orchestration-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.12",
|
|
4
4
|
"description": "A Prompt-orchestration pipeline (POP) is a framework for building, running, and experimenting with complex chains of LLM tasks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/ui/server/index.ts",
|
|
@@ -3,7 +3,13 @@ import { mkdtemp, mkdir, writeFile, utimes, rm, readdir, readFile } from "node:f
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
drainPendingQueue,
|
|
8
|
+
handleChildExit,
|
|
9
|
+
resolveDirs,
|
|
10
|
+
promoteSeedFile,
|
|
11
|
+
restoreSeedFile,
|
|
12
|
+
} from "../orchestrator";
|
|
7
13
|
import {
|
|
8
14
|
getConcurrencyRuntimePaths,
|
|
9
15
|
getJobConcurrencyStatus,
|
|
@@ -38,6 +44,81 @@ function fakeSpawnRunner(jobIdToPid: Map<string, number>): (jobId: string) => Pr
|
|
|
38
44
|
};
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
describe("resolveDirs", () => {
|
|
48
|
+
test("returns a staging dir under the pipeline-data root", () => {
|
|
49
|
+
expect(resolveDirs("/x/pipeline-data").staging).toBe("/x/pipeline-data/staging");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("promoteSeedFile", () => {
|
|
54
|
+
test("moves staged artifacts into current/files/artifacts (byte-identical) then the seed, removing staging", async () => {
|
|
55
|
+
const dir = await setupDir("promote-staged-");
|
|
56
|
+
try {
|
|
57
|
+
const jobId = "job-promote";
|
|
58
|
+
const stagingJobDir = join(dir, "staging", jobId);
|
|
59
|
+
const currentJobDir = join(dir, "current", jobId);
|
|
60
|
+
const bytes = new Uint8Array(256);
|
|
61
|
+
for (let i = 0; i < 256; i++) bytes[i] = i;
|
|
62
|
+
await mkdir(join(stagingJobDir, "a"), { recursive: true });
|
|
63
|
+
await writeFile(join(stagingJobDir, "top.txt"), "top");
|
|
64
|
+
await writeFile(join(stagingJobDir, "a", "b.bin"), bytes);
|
|
65
|
+
const seedPath = await writeSeed(dir, jobId, { pipeline: "p" }, 1700000000);
|
|
66
|
+
|
|
67
|
+
await promoteSeedFile(seedPath, currentJobDir, stagingJobDir);
|
|
68
|
+
|
|
69
|
+
expect(existsSync(join(currentJobDir, "seed.json"))).toBe(true);
|
|
70
|
+
expect(await readFile(join(currentJobDir, "files", "artifacts", "top.txt"), "utf-8")).toBe("top");
|
|
71
|
+
const readBytes = new Uint8Array(await readFile(join(currentJobDir, "files", "artifacts", "a", "b.bin")));
|
|
72
|
+
expect(Array.from(readBytes)).toEqual(Array.from(bytes));
|
|
73
|
+
expect(existsSync(stagingJobDir)).toBe(false);
|
|
74
|
+
expect(existsSync(seedPath)).toBe(false);
|
|
75
|
+
} finally {
|
|
76
|
+
await rm(dir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("with no staging dir, produces only current/seed.json (no files/artifacts)", async () => {
|
|
81
|
+
const dir = await setupDir("promote-nostage-");
|
|
82
|
+
try {
|
|
83
|
+
const jobId = "job-nostage";
|
|
84
|
+
const currentJobDir = join(dir, "current", jobId);
|
|
85
|
+
const stagingJobDir = join(dir, "staging", jobId);
|
|
86
|
+
const seedPath = await writeSeed(dir, jobId, { pipeline: "p" }, 1700000000);
|
|
87
|
+
|
|
88
|
+
await promoteSeedFile(seedPath, currentJobDir, stagingJobDir);
|
|
89
|
+
|
|
90
|
+
expect(existsSync(join(currentJobDir, "seed.json"))).toBe(true);
|
|
91
|
+
expect(existsSync(join(currentJobDir, "files"))).toBe(false);
|
|
92
|
+
} finally {
|
|
93
|
+
await rm(dir, { recursive: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("restoreSeedFile", () => {
|
|
99
|
+
test("returns artifacts to staging, seed to pending, and removes current/", async () => {
|
|
100
|
+
const dir = await setupDir("restore-");
|
|
101
|
+
try {
|
|
102
|
+
const jobId = "job-restore";
|
|
103
|
+
const currentJobDir = join(dir, "current", jobId);
|
|
104
|
+
const stagingJobDir = join(dir, "staging", jobId);
|
|
105
|
+
const pendingSeedPath = join(dir, "pending", `${jobId}-seed.json`);
|
|
106
|
+
await mkdir(join(dir, "staging"), { recursive: true });
|
|
107
|
+
await mkdir(join(currentJobDir, "files", "artifacts"), { recursive: true });
|
|
108
|
+
await writeFile(join(currentJobDir, "files", "artifacts", "x.bin"), "data");
|
|
109
|
+
await writeFile(join(currentJobDir, "seed.json"), JSON.stringify({ pipeline: "p" }));
|
|
110
|
+
|
|
111
|
+
await restoreSeedFile(currentJobDir, pendingSeedPath, stagingJobDir);
|
|
112
|
+
|
|
113
|
+
expect(existsSync(pendingSeedPath)).toBe(true);
|
|
114
|
+
expect(await readFile(join(stagingJobDir, "x.bin"), "utf-8")).toBe("data");
|
|
115
|
+
expect(existsSync(currentJobDir)).toBe(false);
|
|
116
|
+
} finally {
|
|
117
|
+
await rm(dir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
41
122
|
describe("drainPendingQueue", () => {
|
|
42
123
|
test("with limit 2 and three seeds, promotes exactly two and one remains in pending", async () => {
|
|
43
124
|
const dir = await setupDir("drain-limit2-");
|
|
@@ -88,12 +169,13 @@ describe("drainPendingQueue", () => {
|
|
|
88
169
|
}
|
|
89
170
|
});
|
|
90
171
|
|
|
91
|
-
test("seed whose current/<jobId>/ already exists is
|
|
92
|
-
const dir = await setupDir("drain-existing-");
|
|
172
|
+
test("AC5: seed whose current/<jobId>/seed.json already exists is skipped, slot released, seed remains in pending", async () => {
|
|
173
|
+
const dir = await setupDir("drain-existing-seed-");
|
|
93
174
|
try {
|
|
94
175
|
await writeSeed(dir, "job-existing", { pipeline: "p" }, 1700000000);
|
|
95
176
|
await writeSeed(dir, "job-fresh", { pipeline: "p" }, 1700000100);
|
|
96
177
|
await mkdir(join(dir, "current", "job-existing"), { recursive: true });
|
|
178
|
+
await writeFile(join(dir, "current", "job-existing", "seed.json"), JSON.stringify({ pipeline: "p" }));
|
|
97
179
|
const pids = new Map<string, number>();
|
|
98
180
|
const result = await drainPendingQueue({
|
|
99
181
|
dataDir: dir,
|
|
@@ -102,6 +184,7 @@ describe("drainPendingQueue", () => {
|
|
|
102
184
|
spawnRunner: fakeSpawnRunner(pids),
|
|
103
185
|
});
|
|
104
186
|
expect(result.promoted).toEqual(["job-fresh"]);
|
|
187
|
+
expect(pids.has("job-existing")).toBe(false);
|
|
105
188
|
const status = await getJobConcurrencyStatus(dir, 5, 1000);
|
|
106
189
|
expect(status.activeJobs.map((l) => l.jobId).sort()).toEqual(["job-fresh"]);
|
|
107
190
|
expect(existsSync(join(dir, "pending", "job-existing-seed.json"))).toBe(true);
|
|
@@ -112,6 +195,55 @@ describe("drainPendingQueue", () => {
|
|
|
112
195
|
}
|
|
113
196
|
});
|
|
114
197
|
|
|
198
|
+
test("AC3: seed with staged artifacts (current/ absent) promotes, moves artifacts, spawns once", async () => {
|
|
199
|
+
const dir = await setupDir("drain-staged-");
|
|
200
|
+
try {
|
|
201
|
+
await writeSeed(dir, "job-staged", { pipeline: "p" }, 1700000000);
|
|
202
|
+
await mkdir(join(dir, "staging", "job-staged"), { recursive: true });
|
|
203
|
+
await writeFile(join(dir, "staging", "job-staged", "notes.md"), "hello");
|
|
204
|
+
const pids = new Map<string, number>();
|
|
205
|
+
const result = await drainPendingQueue({
|
|
206
|
+
dataDir: dir,
|
|
207
|
+
maxConcurrentJobs: 5,
|
|
208
|
+
lockTimeoutMs: 1000,
|
|
209
|
+
spawnRunner: fakeSpawnRunner(pids),
|
|
210
|
+
});
|
|
211
|
+
expect(result.promoted).toEqual(["job-staged"]);
|
|
212
|
+
expect(pids.has("job-staged")).toBe(true);
|
|
213
|
+
expect(existsSync(join(dir, "pending", "job-staged-seed.json"))).toBe(false);
|
|
214
|
+
expect(existsSync(join(dir, "current", "job-staged", "seed.json"))).toBe(true);
|
|
215
|
+
expect(
|
|
216
|
+
await readFile(join(dir, "current", "job-staged", "files", "artifacts", "notes.md"), "utf-8"),
|
|
217
|
+
).toBe("hello");
|
|
218
|
+
expect(existsSync(join(dir, "staging", "job-staged"))).toBe(false);
|
|
219
|
+
} finally {
|
|
220
|
+
await rm(dir, { recursive: true });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("AC6: crash-mid-promote self-heal — artifacts present, seed absent, no staging → promotes", async () => {
|
|
225
|
+
const dir = await setupDir("drain-selfheal-");
|
|
226
|
+
try {
|
|
227
|
+
await writeSeed(dir, "job-heal", { pipeline: "p" }, 1700000000);
|
|
228
|
+
await mkdir(join(dir, "current", "job-heal", "files", "artifacts"), { recursive: true });
|
|
229
|
+
await writeFile(join(dir, "current", "job-heal", "files", "artifacts", "x.bin"), "x");
|
|
230
|
+
const pids = new Map<string, number>();
|
|
231
|
+
const result = await drainPendingQueue({
|
|
232
|
+
dataDir: dir,
|
|
233
|
+
maxConcurrentJobs: 5,
|
|
234
|
+
lockTimeoutMs: 1000,
|
|
235
|
+
spawnRunner: fakeSpawnRunner(pids),
|
|
236
|
+
});
|
|
237
|
+
expect(result.promoted).toEqual(["job-heal"]);
|
|
238
|
+
expect(pids.has("job-heal")).toBe(true);
|
|
239
|
+
expect(existsSync(join(dir, "pending", "job-heal-seed.json"))).toBe(false);
|
|
240
|
+
expect(existsSync(join(dir, "current", "job-heal", "seed.json"))).toBe(true);
|
|
241
|
+
expect(existsSync(join(dir, "current", "job-heal", "files", "artifacts", "x.bin"))).toBe(true);
|
|
242
|
+
} finally {
|
|
243
|
+
await rm(dir, { recursive: true });
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
115
247
|
test("promotes seeds in deterministic order (mtime asc)", async () => {
|
|
116
248
|
const dir = await setupDir("drain-order-");
|
|
117
249
|
try {
|
|
@@ -181,6 +313,35 @@ describe("drainPendingQueue", () => {
|
|
|
181
313
|
}
|
|
182
314
|
});
|
|
183
315
|
|
|
316
|
+
test("AC8: rejection relocates staged artifacts under rejected/<jobId>/files/artifacts and removes staging", async () => {
|
|
317
|
+
const dir = await setupDir("drain-reject-staging-");
|
|
318
|
+
try {
|
|
319
|
+
await writeSeed(dir, "job-bad", { pipeline: "p" }, 1700000000);
|
|
320
|
+
await mkdir(join(dir, "staging", "job-bad"), { recursive: true });
|
|
321
|
+
await writeFile(join(dir, "staging", "job-bad", "notes.md"), "hello");
|
|
322
|
+
const failingSpawn = async (): Promise<{ pid: number }> => {
|
|
323
|
+
throw new Error("boom");
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
for (let i = 0; i < 3; i++) {
|
|
327
|
+
await drainPendingQueue({
|
|
328
|
+
dataDir: dir,
|
|
329
|
+
maxConcurrentJobs: 2,
|
|
330
|
+
lockTimeoutMs: 1000,
|
|
331
|
+
spawnRunner: failingSpawn,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
expect(existsSync(join(dir, "rejected", "job-bad", "seed.json"))).toBe(true);
|
|
336
|
+
expect(
|
|
337
|
+
await readFile(join(dir, "rejected", "job-bad", "files", "artifacts", "notes.md"), "utf-8"),
|
|
338
|
+
).toBe("hello");
|
|
339
|
+
expect(existsSync(join(dir, "staging", "job-bad"))).toBe(false);
|
|
340
|
+
} finally {
|
|
341
|
+
await rm(dir, { recursive: true });
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
184
345
|
test("repeated invalid seed reads move the seed to rejected", async () => {
|
|
185
346
|
const dir = await setupDir("drain-invalid-reject-");
|
|
186
347
|
try {
|
package/src/core/orchestrator.ts
CHANGED
|
@@ -20,6 +20,7 @@ interface ResolvedDirs {
|
|
|
20
20
|
current: string;
|
|
21
21
|
complete: string;
|
|
22
22
|
rejected: string;
|
|
23
|
+
staging: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/** Parsed seed file content (fields consumed by orchestrator). */
|
|
@@ -138,6 +139,7 @@ export function resolveDirs(dataDir: string): ResolvedDirs {
|
|
|
138
139
|
current: join(root, "current"),
|
|
139
140
|
complete: join(root, "complete"),
|
|
140
141
|
rejected: join(root, "rejected"),
|
|
142
|
+
staging: join(root, "staging"),
|
|
141
143
|
};
|
|
142
144
|
}
|
|
143
145
|
|
|
@@ -438,18 +440,30 @@ async function dirExists(path: string): Promise<boolean> {
|
|
|
438
440
|
}
|
|
439
441
|
}
|
|
440
442
|
|
|
441
|
-
async function promoteSeedFile(
|
|
443
|
+
export async function promoteSeedFile(
|
|
442
444
|
pendingSeedPath: string,
|
|
443
445
|
currentJobDir: string,
|
|
446
|
+
stagingJobDir: string,
|
|
444
447
|
): Promise<void> {
|
|
445
448
|
await mkdir(currentJobDir, { recursive: true });
|
|
449
|
+
if (await dirExists(stagingJobDir)) {
|
|
450
|
+
await mkdir(join(currentJobDir, "files"), { recursive: true });
|
|
451
|
+
await rename(stagingJobDir, join(currentJobDir, "files", "artifacts"));
|
|
452
|
+
}
|
|
446
453
|
await rename(pendingSeedPath, join(currentJobDir, "seed.json"));
|
|
447
454
|
}
|
|
448
455
|
|
|
449
|
-
async function restoreSeedFile(
|
|
450
|
-
|
|
456
|
+
export async function restoreSeedFile(
|
|
457
|
+
currentJobDir: string,
|
|
458
|
+
pendingSeedPath: string,
|
|
459
|
+
stagingJobDir: string,
|
|
460
|
+
): Promise<void> {
|
|
461
|
+
const artifactsDir = join(currentJobDir, "files", "artifacts");
|
|
462
|
+
if (await dirExists(artifactsDir)) {
|
|
463
|
+
await rename(artifactsDir, stagingJobDir);
|
|
464
|
+
}
|
|
451
465
|
try {
|
|
452
|
-
await rename(
|
|
466
|
+
await rename(join(currentJobDir, "seed.json"), pendingSeedPath);
|
|
453
467
|
} catch (err) {
|
|
454
468
|
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
455
469
|
}
|
|
@@ -465,6 +479,11 @@ async function rejectSeedFile(
|
|
|
465
479
|
): Promise<void> {
|
|
466
480
|
const rejectedJobDir = join(dataDir, "rejected", jobId);
|
|
467
481
|
await mkdir(rejectedJobDir, { recursive: true });
|
|
482
|
+
const stagingJobDir = join(dataDir, "staging", jobId);
|
|
483
|
+
if (await dirExists(stagingJobDir)) {
|
|
484
|
+
await mkdir(join(rejectedJobDir, "files"), { recursive: true });
|
|
485
|
+
await rename(stagingJobDir, join(rejectedJobDir, "files", "artifacts"));
|
|
486
|
+
}
|
|
468
487
|
try {
|
|
469
488
|
await rename(seedPath, join(rejectedJobDir, "seed.json"));
|
|
470
489
|
} catch (err) {
|
|
@@ -558,14 +577,15 @@ export async function drainPendingQueue(
|
|
|
558
577
|
}
|
|
559
578
|
|
|
560
579
|
const currentJobDir = join(dataDir, "current", jobId);
|
|
561
|
-
|
|
580
|
+
const stagingJobDir = join(dataDir, "staging", jobId);
|
|
581
|
+
if (await Bun.file(join(currentJobDir, "seed.json")).exists()) {
|
|
562
582
|
await releaseJobSlot(dataDir, jobId, lockTimeoutMs);
|
|
563
|
-
logger.warn(`
|
|
583
|
+
logger.warn(`seed already promoted for ${jobId}; skipping`);
|
|
564
584
|
continue;
|
|
565
585
|
}
|
|
566
586
|
|
|
567
587
|
try {
|
|
568
|
-
await promoteSeedFile(seedPath, currentJobDir);
|
|
588
|
+
await promoteSeedFile(seedPath, currentJobDir, stagingJobDir);
|
|
569
589
|
} catch (err) {
|
|
570
590
|
await releaseJobSlot(dataDir, jobId, lockTimeoutMs);
|
|
571
591
|
logger.error(`failed to promote seed file for job ${jobId}`, err);
|
|
@@ -578,7 +598,7 @@ export async function drainPendingQueue(
|
|
|
578
598
|
} catch (err) {
|
|
579
599
|
await releaseJobSlot(dataDir, jobId, lockTimeoutMs);
|
|
580
600
|
try {
|
|
581
|
-
await restoreSeedFile(currentJobDir, seedPath);
|
|
601
|
+
await restoreSeedFile(currentJobDir, seedPath, stagingJobDir);
|
|
582
602
|
} catch (restoreErr) {
|
|
583
603
|
logger.error(`failed to restore queued seed after spawn failure for job ${jobId}`, restoreErr);
|
|
584
604
|
}
|
|
@@ -607,7 +627,7 @@ export async function drainPendingQueue(
|
|
|
607
627
|
logger.error(`failed to release slot after pid update failure for job ${jobId}`, releaseErr);
|
|
608
628
|
}
|
|
609
629
|
try {
|
|
610
|
-
await restoreSeedFile(currentJobDir, seedPath);
|
|
630
|
+
await restoreSeedFile(currentJobDir, seedPath, stagingJobDir);
|
|
611
631
|
} catch (restoreErr) {
|
|
612
632
|
logger.error(`failed to restore queued seed after pid update failure for job ${jobId}`, restoreErr);
|
|
613
633
|
}
|
|
@@ -643,6 +663,7 @@ export function startOrchestrator(opts: OrchestratorOptions): Promise<Orchestrat
|
|
|
643
663
|
.then(() => mkdir(dirs.current, { recursive: true }))
|
|
644
664
|
.then(() => mkdir(dirs.complete, { recursive: true }))
|
|
645
665
|
.then(() => mkdir(dirs.rejected, { recursive: true }))
|
|
666
|
+
.then(() => mkdir(dirs.staging, { recursive: true }))
|
|
646
667
|
.then(() => new Promise<OrchestratorHandle>((resolve, reject) => {
|
|
647
668
|
const running = new Map<string, ChildHandle>();
|
|
648
669
|
const spawnFn = opts.spawn ?? createDefaultSpawn();
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
1
5
|
import { zipSync } from "fflate";
|
|
2
6
|
import { describe, expect, it } from "vitest";
|
|
3
7
|
|
|
4
|
-
import { normalizeSeedUpload } from "../upload-endpoints";
|
|
8
|
+
import { handleSeedUploadDirect, normalizeSeedUpload } from "../upload-endpoints";
|
|
9
|
+
|
|
10
|
+
async function pathExists(target: string): Promise<boolean> {
|
|
11
|
+
try {
|
|
12
|
+
await stat(target);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
5
18
|
|
|
6
19
|
function concatBytes(parts: Uint8Array[]): Uint8Array<ArrayBuffer> {
|
|
7
20
|
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
@@ -55,3 +68,35 @@ describe("normalizeSeedUpload", () => {
|
|
|
55
68
|
expect(Array.from(blob.content)).toEqual(Array.from(artifact));
|
|
56
69
|
});
|
|
57
70
|
});
|
|
71
|
+
|
|
72
|
+
describe("handleSeedUploadDirect", () => {
|
|
73
|
+
it("stages artifacts under staging/{jobId}/ without creating current/{jobId}/", async () => {
|
|
74
|
+
const dataDir = await mkdtemp(path.join(tmpdir(), "upload-direct-"));
|
|
75
|
+
try {
|
|
76
|
+
const content = new TextEncoder().encode("# notes\nhello\n");
|
|
77
|
+
const seedObject = { name: "demo", pipeline: "x" };
|
|
78
|
+
|
|
79
|
+
const response = await handleSeedUploadDirect(seedObject, dataDir, [
|
|
80
|
+
{ filename: "notes.md", content },
|
|
81
|
+
]);
|
|
82
|
+
expect(response.status).toBe(201);
|
|
83
|
+
const body = (await response.json()) as { data: { jobId: string } };
|
|
84
|
+
const jobId = body.data.jobId;
|
|
85
|
+
|
|
86
|
+
const pipelineData = path.join(dataDir, "pipeline-data");
|
|
87
|
+
|
|
88
|
+
// Artifact staged byte-identical under staging/{jobId}/.
|
|
89
|
+
const staged = path.join(pipelineData, "staging", jobId, "notes.md");
|
|
90
|
+
const stagedBytes = await readFile(staged);
|
|
91
|
+
expect(Array.from(stagedBytes)).toEqual(Array.from(content));
|
|
92
|
+
|
|
93
|
+
// Pending seed trigger written.
|
|
94
|
+
expect(await pathExists(path.join(pipelineData, "pending", `${jobId}-seed.json`))).toBe(true);
|
|
95
|
+
|
|
96
|
+
// The endpoint must not create current/{jobId}/.
|
|
97
|
+
expect(await pathExists(path.join(pipelineData, "current", jobId))).toBe(false);
|
|
98
|
+
} finally {
|
|
99
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -35,12 +35,13 @@ export async function handleSeedUploadDirect(
|
|
|
35
35
|
const pipelineData = path.join(dataDir, "pipeline-data");
|
|
36
36
|
const pendingDir = path.join(pipelineData, "pending");
|
|
37
37
|
|
|
38
|
-
//
|
|
39
|
-
//
|
|
38
|
+
// Stage artifacts under staging/{jobId}/ (before the trigger file).
|
|
39
|
+
// The orchestrator owns current/{jobId}/ and moves staged artifacts into
|
|
40
|
+
// current/{jobId}/files/artifacts/ during promotion.
|
|
40
41
|
if (artifacts.length > 0) {
|
|
41
|
-
const
|
|
42
|
+
const stagingJobDir = path.join(pipelineData, "staging", jobId);
|
|
42
43
|
for (const artifact of artifacts) {
|
|
43
|
-
const artifactPath = path.join(
|
|
44
|
+
const artifactPath = path.join(stagingJobDir, artifact.filename);
|
|
44
45
|
await mkdir(path.dirname(artifactPath), { recursive: true });
|
|
45
46
|
await Bun.write(artifactPath, artifact.content);
|
|
46
47
|
}
|