fix(studio): review round 2 - a conversion that fails after its commit recovers without a restart

- The convert job reloads the course on success and on failure, so learners
  get the intact import (rolled back) or the published studio course, never a
  course whose folder moved away.
- POST /convert on a course with an incomplete conversion (.converting-<id>,
  staging or retired folders) queues a job that recovers and finishes it
  instead of answering 409; the studio page shows "Conversion incomplete" with
  Retry for the owner.
- The 409 for pending import jobs names their kinds and ids; the dead withLock
  in git.ts is gone; the dot-folder discovery rule is documented in
  docs/operations.md and spec section 5.
- Route tests (SQLite and Postgres) inject a failed cross-filesystem copy, a
  checksum mismatch and a pre-commit failure.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-30 21:00:45 +01:00
co-authored by Claude Opus 5.5
parent 771d755da4
commit f02e30450c
11 changed files with 168 additions and 34 deletions
-12
View File
@@ -55,15 +55,3 @@ export async function gitOk(cwd: string, args: string[], who?: Identity): Promis
export function splitZ(out: string): string[] {
return out.split("\0").filter(Boolean);
}
const locks = new Map<string, Promise<unknown>>();
/** Serialises git work per key (the course's bare repo path) across every store instance in the process. */
export function withLock<T>(key: string, fn: () => Promise<T>): Promise<T> {
const prev = locks.get(key) ?? Promise.resolve();
const next = prev.then(fn, fn);
const tail = next.catch(() => {});
locks.set(key, tail);
tail.then(() => { if (locks.get(key) === tail) locks.delete(key); });
return next;
}