|
26 | 26 | * dir, runs the pipeline, then tears everything down in `afterAll`. No reliance |
27 | 27 | * on the operator's `infra/docker/data` tree. |
28 | 28 | */ |
29 | | -import { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; |
| 29 | +import { |
| 30 | + cpSync, |
| 31 | + existsSync, |
| 32 | + mkdirSync, |
| 33 | + mkdtempSync, |
| 34 | + readdirSync, |
| 35 | + readFileSync, |
| 36 | + rmSync, |
| 37 | + statSync, |
| 38 | + writeFileSync, |
| 39 | +} from "node:fs"; |
30 | 40 | import { tmpdir } from "node:os"; |
31 | 41 | import { dirname, join, resolve } from "node:path"; |
32 | 42 | import { fileURLToPath } from "node:url"; |
@@ -180,12 +190,60 @@ async function failureDiagnostics( |
180 | 190 | ): Promise<string> { |
181 | 191 | const lines = [`${label}${message ? `: ${message}` : ""}`]; |
182 | 192 | try { |
183 | | - const logs = await execa("docker", ["logs", "--tail", "80", service], { reject: false }); |
| 193 | + const logs = await execa("docker", ["logs", "--tail", "200", service], { reject: false }); |
184 | 194 | if (logs.stdout) lines.push(`--- docker logs ${service} (stdout) ---`, logs.stdout); |
185 | 195 | if (logs.stderr) lines.push(`--- docker logs ${service} (stderr) ---`, logs.stderr); |
186 | 196 | } catch (err) { |
187 | 197 | lines.push(`(could not fetch ${service} logs: ${(err as Error).message})`); |
188 | 198 | } |
| 199 | + // Container-side view of the mount MOTIS imports from. On an import failure the |
| 200 | + // container entrypoint drops into a keep-alive sleep (see motisService), so |
| 201 | + // `exec` still works — this shows the archive sizes MOTIS itself sees, which |
| 202 | + // reveals a hardlink/bind-mount that resolved to an empty or unreadable file. |
| 203 | + const view = await execa("docker", ["exec", service, "sh", "-c", "ls -la /motis-data"], { |
| 204 | + reject: false, |
| 205 | + }); |
| 206 | + if (view.stdout) lines.push(`--- ${service}:/motis-data (container view) ---`, view.stdout); |
| 207 | + if (view.exitCode !== 0 && view.stderr) lines.push(`--- ${service} exec stderr ---`, view.stderr); |
| 208 | + return lines.join("\n"); |
| 209 | +} |
| 210 | + |
| 211 | +/** |
| 212 | + * Host-side view of the assembled staging dir: entry names, byte sizes, and |
| 213 | + * whether each `*.zip` still opens with a valid local-file-header magic |
| 214 | + * (`PK\x03\x04`). This distinguishes the two very different roots of a MOTIS |
| 215 | + * import `iostream error`: assemble-staging produced a bad/zero-byte archive |
| 216 | + * (the host sees it broken) vs. the container can't read a structurally-valid |
| 217 | + * archive the host sees intact (a bind-mount/hardlink visibility problem on the |
| 218 | + * runner's storage driver). |
| 219 | + */ |
| 220 | +function stagingDirReport(dir: string): string { |
| 221 | + const lines = [`--- host view of staging dir ${dir} ---`]; |
| 222 | + try { |
| 223 | + for (const name of readdirSync(dir).sort()) { |
| 224 | + const p = join(dir, name); |
| 225 | + try { |
| 226 | + const st = statSync(p); |
| 227 | + if (st.isDirectory()) { |
| 228 | + lines.push(` ${name}/ (dir)`); |
| 229 | + continue; |
| 230 | + } |
| 231 | + let note = ""; |
| 232 | + if (name.endsWith(".zip")) { |
| 233 | + const head = readFileSync(p).subarray(0, 4); |
| 234 | + const ok = head[0] === 0x50 && head[1] === 0x4b; // "PK" |
| 235 | + note = ok |
| 236 | + ? " zip-magic OK" |
| 237 | + : ` BAD zip-magic <${[...head].map((b) => b.toString(16).padStart(2, "0")).join(" ")}>`; |
| 238 | + } |
| 239 | + lines.push(` ${name} ${st.size}B${note}`); |
| 240 | + } catch (err) { |
| 241 | + lines.push(` ${name} (stat failed: ${(err as Error).message})`); |
| 242 | + } |
| 243 | + } |
| 244 | + } catch (err) { |
| 245 | + lines.push(` (could not read staging dir: ${(err as Error).message})`); |
| 246 | + } |
189 | 247 | return lines.join("\n"); |
190 | 248 | } |
191 | 249 |
|
@@ -365,7 +423,7 @@ describeLive("transitous pipeline end-to-end against real motis containers", () |
365 | 423 | for (const stage of ["motis-import", "motis-health"] as const) { |
366 | 424 | if (byStage[stage]?.status !== "ok") { |
367 | 425 | const diag = await failureDiagnostics(stage, STAGING_SERVICE, byStage[stage]?.message); |
368 | | - expect(byStage[stage]?.status, diag).toBe("ok"); |
| 426 | + expect(byStage[stage]?.status, `${diag}\n${stagingDirReport(stagingDataDir)}`).toBe("ok"); |
369 | 427 | } |
370 | 428 | } |
371 | 429 | if (byStage.promote?.status !== "ok") { |
|
0 commit comments