fix(pdf): the renderer resolved every artifact path against the wrong root

`render_one` joined `missions_root()/<mission_id>/` before the artifact path,
producing `<root>/<mission>/_outputs/<mission>/<phase>/...` — the mission id
twice, and no such file.

Artifact paths are relative to the MISSIONS ROOT. All three registration sites
write `_outputs/<mission>/<phase>/...`, and `_outputs` is deliberately a sibling
of the per-mission directories so it survives their reaping; joining the mission
id first put the lookup inside the very directory `_outputs` exists to escape.

It went unnoticed because until now the only artifacts on the system were
`code_diff` rows registered with `render_pdf: false`, which this worker never
reads. `produces = ["md","pdf"]` was inert, so nothing ever asked for a render.
The first artifacts to ask were the first to find it — both failed with ENOENT
on the doubled path.

Negative control: restore the extra join and
`an_artifact_path_resolves_against_the_missions_root` fails.

The worker's error handling is sound and needed no change: it recorded
`render_pdf_status = 'failed'` with the full path in `render_pdf_error`, which
is how this was diagnosed in one read.

237 lib tests pass.
This commit is contained in:
Omar Sobh
2026-08-07 11:41:48 -07:00
parent 89bc53b53d
commit c28c7a148f
+34 -3
View File
@@ -37,6 +37,22 @@ fn missions_root() -> PathBuf {
.unwrap_or_else(|_| PathBuf::from("/var/lib/clawmates-missions")) .unwrap_or_else(|_| PathBuf::from("/var/lib/clawmates-missions"))
} }
/// Resolve `mission_artifacts.path` to a file on disk.
///
/// Artifact paths are relative to the MISSIONS ROOT, not to the per-mission
/// directory: every registration site writes `_outputs/<mission>/<phase>/...`,
/// and `_outputs` is deliberately a sibling of the per-mission dirs so it
/// survives their reaping.
///
/// This used to join `missions_root()/<mission_id>/` first, producing
/// `<root>/<mission>/_outputs/<mission>/<phase>/...` — the mission id twice and
/// no such file. It went unnoticed because the only artifacts that existed were
/// `code_diff` rows registered with `render_pdf: false`, which this worker never
/// reads. The first artifacts to ask for rendering were the first to find it.
fn artifact_abs(rel: &str) -> PathBuf {
missions_root().join(rel)
}
fn chromium_bin() -> String { fn chromium_bin() -> String {
std::env::var("CHROMIUM_BIN").unwrap_or_else(|_| "chromium".to_string()) std::env::var("CHROMIUM_BIN").unwrap_or_else(|_| "chromium".to_string())
} }
@@ -91,8 +107,7 @@ async fn render_one(
artifact: &cm_db::repo::missions::MissionArtifact, artifact: &cm_db::repo::missions::MissionArtifact,
) -> Result<String, String> { ) -> Result<String, String> {
// 1. Locate the source MD on disk. // 1. Locate the source MD on disk.
let mission_root = missions_root().join(artifact.mission_id.to_string()); let src_path = artifact_abs(&artifact.path);
let src_path = mission_root.join(&artifact.path);
let md = tokio::fs::read_to_string(&src_path) let md = tokio::fs::read_to_string(&src_path)
.await .await
.map_err(|e| format!("read {}: {e}", src_path.display()))?; .map_err(|e| format!("read {}: {e}", src_path.display()))?;
@@ -133,7 +148,7 @@ async fn render_one(
// 4. Move next to the source MD so the artifact tree stays self- // 4. Move next to the source MD so the artifact tree stays self-
// contained. Filename derived from the MD path (foo.md → foo.pdf). // contained. Filename derived from the MD path (foo.md → foo.pdf).
let out_rel = pdf_sibling(&artifact.path); let out_rel = pdf_sibling(&artifact.path);
let out_abs = mission_root.join(&out_rel); let out_abs = artifact_abs(&out_rel);
if let Some(parent) = out_abs.parent() { if let Some(parent) = out_abs.parent() {
tokio::fs::create_dir_all(parent) tokio::fs::create_dir_all(parent)
.await .await
@@ -250,6 +265,22 @@ fn pdf_sibling(md_path: &str) -> String {
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn an_artifact_path_resolves_against_the_missions_root() {
// Exactly what `mission_delivery` and `mission_outputs` register.
let rel = "_outputs/11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222/repo/research/01_summary.md";
let abs = artifact_abs(rel);
let root = missions_root();
assert_eq!(abs, root.join(rel), "{abs:?}");
// The regression this exists for: the mission id must appear ONCE.
let s = abs.to_string_lossy();
assert_eq!(
s.matches("11111111-1111-1111-1111-111111111111").count(),
1,
"the mission id must not be doubled: {s}"
);
}
#[test] #[test]
fn pdf_sibling_paths() { fn pdf_sibling_paths() {
assert_eq!(pdf_sibling("research/v3/spec.md"), "research/v3/spec.pdf"); assert_eq!(pdf_sibling("research/v3/spec.md"), "research/v3/spec.pdf");