MCPcopy Create free account
hub / github.com/TestSprite/testsprite-cli / commitBundle

Function commitBundle

src/lib/bundle.ts:518–572  ·  view source on GitHub ↗

* Rename ` / ` → ` / ` for every file in `files`. * * Critical ordering for atomicity (the §3 "agent-safe" contract): * * 1. **Remove the OLD `meta.json` first.** The bundle's completion * signal is `meta.json`'s presence; an agent reading ` ` while * we're mu

(
  tmpDir: string,
  dir: string,
  files: ReadonlyArray<string>,
)

Source from the content-addressed store, hash-verified

516 * (per §7.3). That's what we want.
517 */
518async function commitBundle(
519 tmpDir: string,
520 dir: string,
521 files: ReadonlyArray<string>,
522): Promise<void> {
523 // (1) Remove the prior bundle's completion signal FIRST.
524 await unlink(join(dir, 'meta.json')).catch(() => undefined);
525
526 // (2) Sweep stale top-level files that the new bundle won't write.
527 // If the prior run wrote `video.mp4` and the new run has no video,
528 // an in-place rename leaves the old video lingering. Enumerate
529 // current top-level entries and remove anything that isn't being
530 // freshly renamed in.
531 const topLevel = files.filter(f => !f.startsWith('steps/'));
532 const newTopLevelSet = new Set(topLevel);
533 newTopLevelSet.add('meta.json'); // about to land last, do not delete
534 const existing = await readdir(dir).catch(() => [] as string[]);
535 for (const entry of existing) {
536 // Preserve the writer's own scratch dir + the .partial marker
537 // (we'll re-evaluate .partial at the end of commit). Anything else
538 // not-listed in the new bundle is stale.
539 if (entry === '.tmp' || entry === '.partial') continue;
540 if (newTopLevelSet.has(entry)) continue;
541 if (entry === 'steps') continue; // handled below
542 await rm(join(dir, entry), { recursive: true, force: true });
543 }
544
545 // (3) Replace `<dir>/steps/` with `<tmp>/steps/`.
546 const stepsTmp = join(tmpDir, 'steps');
547 const stepsDir = join(dir, 'steps');
548 await rm(stepsDir, { recursive: true, force: true });
549 if (await dirExists(stepsTmp)) {
550 await rename(stepsTmp, stepsDir);
551 }
552
553 // (4) Top-level files (result/failure/code/video). meta.json renames
554 // LAST; track it separately.
555 const metaIdx = topLevel.indexOf('meta.json');
556 const beforeMeta = metaIdx >= 0 ? topLevel.filter((_, i) => i !== metaIdx) : topLevel;
557 for (const file of beforeMeta) {
558 await rename(join(tmpDir, file), join(dir, file));
559 }
560
561 // (5) meta.json LAST → atomic completion signal.
562 if (metaIdx >= 0) {
563 await rename(join(tmpDir, 'meta.json'), join(dir, 'meta.json'));
564 }
565
566 // .partial from a prior aborted run is now stale. Remove it so an
567 // agent inspecting the dir sees only the fresh bundle.
568 await unlink(join(dir, '.partial')).catch(() => undefined);
569
570 // Clean up the now-empty tmp dir.
571 await rm(tmpDir, { recursive: true, force: true });
572}
573
574async function dirExists(path: string): Promise<boolean> {
575 try {

Callers 1

writeBundleFunction · 0.85

Calls 1

dirExistsFunction · 0.85

Tested by

no test coverage detected