From 14a4a48a023fa8977b1ded512a148c76cce42cb5 Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 17 Jun 2026 07:53:09 +0000 Subject: [PATCH 1/4] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/link-foundation/start/issues/136 --- .gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 0000000..8e20ac8 --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-06-17T07:53:08.901Z for PR creation at branch issue-136-cffa39dbc470 for issue https://github.com/link-foundation/start/issues/136 \ No newline at end of file From 855656d670dc932beb867c84b6e602e8d08243c3 Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 17 Jun 2026 08:05:47 +0000 Subject: [PATCH 2/4] fix: detached docker --status stays executing while container runs A detached `--isolated docker` session was recorded with a terminal status (`executed`) and `exitCode -1` while its container was still running (or not yet visible on a slow Docker-in-Docker host). Root cause: `isDetachedSessionAlive`/`is_detached_session_alive` treated a failed `docker inspect` as "not alive" (false), so `enrichDetachedStatus`/`enrich_detached_status` marked the session `executed` with the `-1` sentinel. Fix (JS + Rust parity): - Distinguish "unknown" (inspect failed -> null/None) from "stopped" (inspect succeeded, Running=false). Unknown liveness keeps the session `executing` instead of fabricating a terminal result. - When a container has genuinely stopped, resolve the real exit code from the log footer, then `docker inspect .State.ExitCode`, only falling back to `-1` as a last resort. Adds JS + Rust tests guarded on docker availability, plus changelog fragments. Closes #136 --- .../detached-docker-running-status.md | 5 + js/src/lib/status-formatter.js | 102 ++++++++++++-- js/test/session-name-status.js | 113 +++++++++++++++ rust/changelog.d/136.md | 5 + rust/src/lib/status_formatter.rs | 105 ++++++++++++-- rust/tests/session_name_status.rs | 133 ++++++++++++++++++ 6 files changed, 441 insertions(+), 22 deletions(-) create mode 100644 js/.changeset/detached-docker-running-status.md create mode 100644 rust/changelog.d/136.md diff --git a/js/.changeset/detached-docker-running-status.md b/js/.changeset/detached-docker-running-status.md new file mode 100644 index 0000000..8d5eefb --- /dev/null +++ b/js/.changeset/detached-docker-running-status.md @@ -0,0 +1,5 @@ +--- +'start-command': patch +--- + +Fix detached docker `--status`/`--list` reporting a terminal status (`executed`) with the `-1` sentinel while the container is still running (or not visible yet on a slow Docker-in-Docker host). `isDetachedSessionAlive()` now treats a failed `docker inspect` as "unknown" (returns `null`) instead of "stopped", so a session whose container has not appeared yet stays `executing` rather than being marked finished. When a container has genuinely stopped, `enrichDetachedStatus()` resolves the real exit code from the `Exit Code:` log footer and then `docker inspect .State.ExitCode`, only falling back to `-1` when no real code can be obtained. diff --git a/js/src/lib/status-formatter.js b/js/src/lib/status-formatter.js index 04625c1..7d6984a 100644 --- a/js/src/lib/status-formatter.js +++ b/js/src/lib/status-formatter.js @@ -7,7 +7,7 @@ * - Text: Human-readable text format */ -const { execSync } = require('child_process'); +const { execSync, spawnSync } = require('child_process'); const fs = require('fs'); const { escapeForLinksNotation, @@ -15,6 +15,58 @@ const { } = require('./output-blocks'); const { collectProcessIds } = require('./execution-control'); +/** + * Inspect the live state of a detached docker container by name. + * + * Distinguishes three outcomes that matter for status reporting: + * - the container exists and is running, + * - the container exists but has stopped (with a real exit code), and + * - the container cannot be inspected at all. + * + * The last case is crucial on slow Docker-in-Docker hosts (issue #136): right + * after `docker run -d` returns, `docker inspect ` can transiently fail + * because the container is not visible yet. A failed inspect must NOT be read + * as "stopped"; it means "unknown", so callers can keep the session running + * instead of fabricating a terminal `-1` result. + * + * @param {string} sessionName - Container name + * @returns {{running: boolean, exitCode: number|null}|null} State, or null when + * the container cannot be inspected (not found yet, removed, or docker error) + */ +function inspectDockerState(sessionName) { + const result = spawnSync( + 'docker', + ['inspect', '-f', '{{.State.Running}} {{.State.ExitCode}}', sessionName], + { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] } + ); + if (result.error || result.status !== 0 || !result.stdout) { + return null; + } + const [runningRaw, exitRaw] = result.stdout.trim().split(/\s+/); + const exitCode = Number.parseInt(exitRaw, 10); + return { + running: runningRaw === 'true', + exitCode: Number.isFinite(exitCode) ? exitCode : null, + }; +} + +/** + * Best-effort terminal exit code reported by the isolation backend itself + * (currently docker via `docker inspect .State.ExitCode`). Returns null when + * the backend cannot provide a real code, so callers never surface the `-1` + * sentinel for a session whose real exit code is simply not available yet. + * @param {Object} record - Execution record + * @returns {number|null} + */ +function readBackendExitCode(record) { + const opts = record.options || {}; + if (opts.isolated !== 'docker' || !opts.sessionName) { + return null; + } + const state = inspectDockerState(opts.sessionName); + return state && !state.running ? state.exitCode : null; +} + /** * Check if a detached isolation session is still running * @param {Object} record - Execution record @@ -46,11 +98,11 @@ function isDetachedSessionAlive(record) { return true; } case 'docker': { - const output = execSync( - `docker inspect -f "{{.State.Running}}" ${sessionName}`, - { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] } - ); - return output.trim() === 'true'; + // A failed inspect means the container is not visible yet (still being + // created on a slow DinD host) or already removed — not "stopped". + // Return null (unknown) so the session is not falsely marked finished. + const state = inspectDockerState(sessionName); + return state === null ? null : state.running; } case 'ssh': { // For SSH, check if the PID is still running on remote would require @@ -100,13 +152,37 @@ function readExitCodeFromLog(logPath) { */ function enrichDetachedStatus(record) { const alive = isDetachedSessionAlive(record); + const footerExit = readExitCodeFromLog(record.logPath); + + // Create a shallow copy to avoid mutating the original + const cloneRecord = () => { + const enriched = Object.create(Object.getPrototypeOf(record)); + Object.assign(enriched, record); + return enriched; + }; + if (alive === null) { + // Liveness is unknown: the backend could not be probed (e.g. a detached + // docker container that is not visible yet on a slow Docker-in-Docker host, + // or one that has already been removed). Honor a terminal `Exit Code:` + // footer if the command wrote one; otherwise leave the record untouched + // (still executing) rather than fabricating a `-1` terminal result that + // orchestrators misread as a finished/failed run (issue #136). + const isDetached = + record.options && record.options.isolationMode === 'detached'; + if (isDetached && record.status === 'executing' && footerExit !== null) { + const enriched = cloneRecord(); + enriched.status = 'executed'; + enriched.exitCode = footerExit; + if (!enriched.endTime) { + enriched.endTime = new Date().toISOString(); + } + return enriched; + } return record; } - // Create a shallow copy to avoid mutating the original - const enriched = Object.create(Object.getPrototypeOf(record)); - Object.assign(enriched, record); + const enriched = cloneRecord(); if (alive && enriched.status === 'executed') { // A live `screen -ls` (or `tmux`/`docker`) session does NOT mean the command @@ -115,7 +191,6 @@ function enrichDetachedStatus(record) { // window after `start` already wrote the terminal footer). The footer/recorded // exit code is authoritative. Only flip back to 'executing' when there is NO // recorded terminal exit code AND no `Exit Code:` footer in the log. - const footerExit = readExitCodeFromLog(enriched.logPath); const hasRecordedExit = enriched.exitCode !== null && enriched.exitCode !== undefined; if (!hasRecordedExit && footerExit === null) { @@ -126,10 +201,13 @@ function enrichDetachedStatus(record) { } // Otherwise keep the recorded/footer exit code - the command has finished. } else if (!alive && enriched.status === 'executing') { - // Session ended but record says executing - correct it + // Session ended but record says executing - correct it. Resolve a real exit + // code: prefer the log footer, then the backend's own record (e.g. + // `docker inspect .State.ExitCode`), and only fall back to the `-1` sentinel + // as a last resort when no real code can be obtained (issue #136). enriched.status = 'executed'; if (enriched.exitCode === null || enriched.exitCode === undefined) { - enriched.exitCode = readExitCodeFromLog(enriched.logPath) ?? -1; + enriched.exitCode = footerExit ?? readBackendExitCode(enriched) ?? -1; } if (!enriched.endTime) { enriched.endTime = new Date().toISOString(); diff --git a/js/test/session-name-status.js b/js/test/session-name-status.js index 72b824e..97aabf3 100644 --- a/js/test/session-name-status.js +++ b/js/test/session-name-status.js @@ -429,6 +429,119 @@ describe('Issue #101: Detached status enrichment', () => { }); }); +// Issue #136: a detached docker session must not be reported with a terminal +// status (`executed`) and the `-1` sentinel while its container is still +// running (or not visible yet on a slow Docker-in-Docker host). +describe('Issue #136: detached docker session liveness', () => { + const dockerAvailable = (() => { + const probe = spawnSync('docker', ['info'], { stdio: 'ignore' }); + return probe.status === 0; + })(); + + function makeDockerRecord(sessionName, extra = {}) { + return new ExecutionRecord({ + command: 'sleep 120; echo done', + options: { + sessionName, + isolated: 'docker', + isolationMode: 'detached', + }, + ...extra, + }); + } + + function dockerRm(name) { + spawnSync('docker', ['rm', '-f', name], { stdio: 'ignore' }); + } + + it('reports unknown (null) — not false — when the container is not visible yet', () => { + if (!dockerAvailable) { + return; + } + const record = makeDockerRecord('issue136-container-does-not-exist-yet'); + expect(isDetachedSessionAlive(record)).toBeNull(); + }); + + it('keeps the record executing while the container is not visible yet', () => { + if (!dockerAvailable) { + return; + } + const record = makeDockerRecord('issue136-container-not-visible'); + // Defaults to executing with a null exit code. + const enriched = enrichDetachedStatus(record); + expect(enriched.status).toBe('executing'); + expect(enriched.exitCode).toBeNull(); + expect(enriched.endTime).toBeNull(); + }); + + it('keeps a running container executing', () => { + if (!dockerAvailable) { + return; + } + const name = `issue136-running-${process.pid}`; + dockerRm(name); + const started = spawnSync('docker', [ + 'run', + '-d', + '--name', + name, + 'alpine', + 'sh', + '-c', + 'sleep 30', + ]); + if (started.status !== 0) { + dockerRm(name); + return; + } + try { + const record = makeDockerRecord(name); + expect(isDetachedSessionAlive(record)).toBe(true); + const enriched = enrichDetachedStatus(record); + expect(enriched.status).toBe('executing'); + expect(enriched.exitCode).toBeNull(); + } finally { + dockerRm(name); + } + }); + + it('resolves a stopped container to its real exit code, never the -1 sentinel', () => { + if (!dockerAvailable) { + return; + } + const name = `issue136-stopped-${process.pid}`; + dockerRm(name); + const ran = spawnSync('docker', [ + 'run', + '--name', + name, + 'alpine', + 'sh', + '-c', + 'exit 1', + ]); + // `docker run` exits with the container's code (1 here); treat spawn errors + // (no daemon) as a skip. + if (ran.error) { + dockerRm(name); + return; + } + try { + // No log footer: force exit-code resolution through `docker inspect`. + const record = makeDockerRecord(name, { + logPath: '/nonexistent-issue136.log', + }); + expect(isDetachedSessionAlive(record)).toBe(false); + const enriched = enrichDetachedStatus(record); + expect(enriched.status).toBe('executed'); + expect(enriched.exitCode).toBe(1); + expect(enriched.endTime).not.toBeNull(); + } finally { + dockerRm(name); + } + }); +}); + describe('Issue #105: attachCurrentTime for executing status', () => { it('should add currentTime to serialization when status is executing', () => { const record = new ExecutionRecord({ diff --git a/rust/changelog.d/136.md b/rust/changelog.d/136.md new file mode 100644 index 0000000..8c420bb --- /dev/null +++ b/rust/changelog.d/136.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +Fixed detached docker `--status`/`--list` reporting a terminal status (`executed`) with the `-1` sentinel while the container is still running (or not visible yet on a slow Docker-in-Docker host). `is_detached_session_alive` now treats a failed `docker inspect` as "unknown" (`None`) instead of "stopped", so a session whose container has not appeared yet stays `executing` rather than being marked finished. When a container has genuinely stopped, `enrich_detached_status` resolves the real exit code from the `Exit Code:` log footer and then `docker inspect .State.ExitCode`, only falling back to `-1` when no real code can be obtained. diff --git a/rust/src/lib/status_formatter.rs b/rust/src/lib/status_formatter.rs index 6830fe2..ff22645 100644 --- a/rust/src/lib/status_formatter.rs +++ b/rust/src/lib/status_formatter.rs @@ -12,6 +12,64 @@ use serde_json::Value; use std::fs; use std::process::Command; +/// Live state of a detached docker container by name. +struct DockerState { + running: bool, + exit_code: Option, +} + +/// Inspect the live state of a detached docker container by name. +/// +/// Distinguishes "running", "stopped (with a real exit code)", and "cannot be +/// inspected at all". The last case matters on slow Docker-in-Docker hosts +/// (issue #136): right after `docker run -d` returns, `docker inspect ` +/// can transiently fail because the container is not visible yet. A failed +/// inspect must NOT be read as "stopped"; it means "unknown", so callers can +/// keep the session running instead of fabricating a terminal `-1` result. +/// +/// Returns None when the container cannot be inspected (not found yet, removed, +/// or docker error). +fn inspect_docker_state(session_name: &str) -> Option { + let output = Command::new("docker") + .args([ + "inspect", + "-f", + "{{.State.Running}} {{.State.ExitCode}}", + session_name, + ]) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let stdout = String::from_utf8_lossy(&output.stdout); + let trimmed = stdout.trim(); + if trimmed.is_empty() { + return None; + } + let mut parts = trimmed.split_whitespace(); + let running = parts.next() == Some("true"); + let exit_code = parts.next().and_then(|value| value.parse::().ok()); + Some(DockerState { running, exit_code }) +} + +/// Best-effort terminal exit code reported by the isolation backend itself +/// (currently docker via `docker inspect .State.ExitCode`). Returns None when +/// the backend cannot provide a real code, so callers never surface the `-1` +/// sentinel for a session whose real exit code is simply not available yet. +fn read_backend_exit_code(record: &ExecutionRecord) -> Option { + if record.options.get("isolated")?.as_str()? != "docker" { + return None; + } + let session_name = record.options.get("sessionName")?.as_str()?; + let state = inspect_docker_state(session_name)?; + if state.running { + None + } else { + state.exit_code + } +} + /// Check if a detached isolation session is still running /// Returns Some(true) if running, Some(false) if not, None if unable to determine pub fn is_detached_session_alive(record: &ExecutionRecord) -> Option { @@ -37,12 +95,11 @@ pub fn is_detached_session_alive(record: &ExecutionRecord) -> Option { Some(status.status.success()) } "docker" => { - let output = Command::new("docker") - .args(["inspect", "-f", "{{.State.Running}}", session_name]) - .output() - .ok()?; - let stdout = String::from_utf8_lossy(&output.stdout); - Some(stdout.trim() == "true") + // A failed inspect means the container is not visible yet (still + // being created on a slow DinD host) or already removed — not + // "stopped". Return None (unknown) so the session is not falsely + // marked finished (issue #136). + inspect_docker_state(session_name).map(|state| state.running) } "ssh" => { // For SSH, check if the local wrapper PID is still running @@ -79,9 +136,31 @@ fn read_exit_code_from_log(log_path: &str) -> Option { /// returns an updated copy with status "executed". If it shows "executed" but /// the session is still running, returns a copy with status "executing". pub fn enrich_detached_status(record: &ExecutionRecord) -> ExecutionRecord { + let footer_exit = read_exit_code_from_log(&record.log_path); + let alive = match is_detached_session_alive(record) { Some(v) => v, - None => return record.clone(), + None => { + // Liveness is unknown: the backend could not be probed (e.g. a + // detached docker container that is not visible yet on a slow + // Docker-in-Docker host, or one that has already been removed). + // Honor a terminal `Exit Code:` footer if the command wrote one; + // otherwise leave the record untouched (still executing) rather than + // fabricating a `-1` terminal result that orchestrators misread as a + // finished/failed run (issue #136). + let is_detached = + record.options.get("isolationMode").and_then(|v| v.as_str()) == Some("detached"); + if is_detached && record.status == ExecutionStatus::Executing && footer_exit.is_some() { + let mut enriched = record.clone(); + enriched.status = ExecutionStatus::Executed; + enriched.exit_code = footer_exit; + if enriched.end_time.is_none() { + enriched.end_time = Some(chrono::Utc::now().to_rfc3339()); + } + return enriched; + } + return record.clone(); + } }; let mut enriched = record.clone(); @@ -93,7 +172,6 @@ pub fn enrich_detached_status(record: &ExecutionRecord) -> ExecutionRecord { // window after `start` already wrote the terminal footer). The footer/recorded // exit code is authoritative. Only flip back to "executing" when there is NO // recorded terminal exit code AND no `Exit Code:` footer in the log. - let footer_exit = read_exit_code_from_log(&enriched.log_path); if enriched.exit_code.is_none() && footer_exit.is_none() { // Session still running and no terminal record - correct it enriched.status = ExecutionStatus::Executing; @@ -102,10 +180,17 @@ pub fn enrich_detached_status(record: &ExecutionRecord) -> ExecutionRecord { } // Otherwise keep the recorded/footer exit code - the command has finished. } else if !alive && enriched.status == ExecutionStatus::Executing { - // Session ended but record says executing - correct it + // Session ended but record says executing - correct it. Resolve a real + // exit code: prefer the log footer, then the backend's own record (e.g. + // `docker inspect .State.ExitCode`), and only fall back to the `-1` + // sentinel as a last resort when no real code can be obtained (issue #136). enriched.status = ExecutionStatus::Executed; if enriched.exit_code.is_none() { - enriched.exit_code = Some(read_exit_code_from_log(&enriched.log_path).unwrap_or(-1)); + enriched.exit_code = Some( + footer_exit + .or_else(|| read_backend_exit_code(&enriched)) + .unwrap_or(-1), + ); } if enriched.end_time.is_none() { enriched.end_time = Some(chrono::Utc::now().to_rfc3339()); diff --git a/rust/tests/session_name_status.rs b/rust/tests/session_name_status.rs index d5ccfa2..0a1ad99 100644 --- a/rust/tests/session_name_status.rs +++ b/rust/tests/session_name_status.rs @@ -412,6 +412,139 @@ fn test_enrich_flips_to_executing_when_no_terminal_record() { assert!(enriched.end_time.is_none()); } +// ===== Issue #136: detached docker session must not report a terminal -1 while the container is still running ===== + +/// Probe whether `docker` is usable (CLI present and daemon reachable). +fn docker_available() -> bool { + std::process::Command::new("docker") + .arg("info") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + +fn docker_rm(name: &str) { + let _ = std::process::Command::new("docker") + .args(["rm", "-f", name]) + .output(); +} + +/// A detached docker container that is not visible yet (still being created on a +/// slow Docker-in-Docker host) cannot be inspected. Its liveness must be +/// reported as `None` (unknown), NOT `Some(false)` — otherwise the enrich step +/// would mark the still-pending session terminal with the `-1` sentinel. +#[test] +fn test_is_detached_session_alive_unknown_docker_container_is_none() { + if !docker_available() { + return; + } + let record = ExecutionRecord::with_options(ExecutionRecordOptions { + command: "sleep 120".to_string(), + options: Some(make_isolation_options( + "issue136-container-does-not-exist-yet", + "docker", + "detached", + )), + ..Default::default() + }); + assert_eq!(is_detached_session_alive(&record), None); +} + +/// Regression for issue #136: while a detached docker container is not yet +/// inspectable, the record must stay `executing` with `None` exit code rather +/// than flipping to `executed` / `-1`. +#[test] +fn test_enrich_keeps_executing_when_docker_container_not_visible() { + if !docker_available() { + return; + } + let record = ExecutionRecord::with_options(ExecutionRecordOptions { + command: "sleep 120".to_string(), + options: Some(make_isolation_options( + "issue136-container-not-visible", + "docker", + "detached", + )), + ..Default::default() + }); + // Record defaults to executing with no exit code. + let enriched = enrich_detached_status(&record); + assert_eq!(enriched.status, ExecutionStatus::Executing); + assert_eq!(enriched.exit_code, None); + assert!(enriched.end_time.is_none()); +} + +/// A genuinely running detached container reports `Some(true)` and stays +/// `executing`. +#[test] +fn test_enrich_running_docker_container_stays_executing() { + if !docker_available() { + return; + } + let name = format!("issue136-running-{}", std::process::id()); + docker_rm(&name); + let started = std::process::Command::new("docker") + .args([ + "run", "-d", "--name", &name, "alpine", "sh", "-c", "sleep 30", + ]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false); + if !started { + docker_rm(&name); + return; + } + + let record = ExecutionRecord::with_options(ExecutionRecordOptions { + command: "sleep 30".to_string(), + options: Some(make_isolation_options(&name, "docker", "detached")), + ..Default::default() + }); + let alive = is_detached_session_alive(&record); + let enriched = enrich_detached_status(&record); + docker_rm(&name); + + assert_eq!(alive, Some(true)); + assert_eq!(enriched.status, ExecutionStatus::Executing); + assert_eq!(enriched.exit_code, None); +} + +/// A stopped detached container with no log footer must resolve to its real +/// exit code from `docker inspect`, never the `-1` sentinel. +#[test] +fn test_enrich_stopped_docker_container_uses_real_exit_code() { + if !docker_available() { + return; + } + let name = format!("issue136-stopped-{}", std::process::id()); + docker_rm(&name); + let started = std::process::Command::new("docker") + .args(["run", "--name", &name, "alpine", "sh", "-c", "exit 1"]) + .output() + .map(|o| o.status.success() || o.status.code() == Some(1)) + .unwrap_or(false); + if !started { + docker_rm(&name); + return; + } + + // No log footer: force exit-code resolution through `docker inspect`. + let record = ExecutionRecord::with_options(ExecutionRecordOptions { + command: "exit 1".to_string(), + log_path: Some("/nonexistent-issue136.log".to_string()), + options: Some(make_isolation_options(&name, "docker", "detached")), + ..Default::default() + }); + let alive = is_detached_session_alive(&record); + let enriched = enrich_detached_status(&record); + docker_rm(&name); + + assert_eq!(alive, Some(false)); + assert_eq!(enriched.status, ExecutionStatus::Executed); + assert_eq!(enriched.exit_code, Some(1)); + assert!(enriched.end_time.is_some()); +} + #[test] fn test_get_most_recent_session_name_match() { let (_temp_dir, store) = create_test_store(); From 2f1552416ee5f8cb9c825a66b454aa548b4dcf4f Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 17 Jun 2026 08:16:34 +0000 Subject: [PATCH 3/4] test: guard issue #136 docker tests against non-Linux docker (Windows CI) The stopped-container test fabricated a container with `docker run alpine`, but on Windows runners in Windows-containers mode the Linux image never materializes (`no matching manifest for windows`). `docker inspect` then fails and liveness is null (unknown), not false, so the assertion broke. - Probe with the repo's `canRunLinuxDockerImages()` instead of `docker info`, so the suite skips when Linux images can't run. - Skip the stopped-container test unless the container actually exists per `docker inspect`. --- js/test/session-name-status.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/js/test/session-name-status.js b/js/test/session-name-status.js index 97aabf3..fd19aea 100644 --- a/js/test/session-name-status.js +++ b/js/test/session-name-status.js @@ -433,10 +433,19 @@ describe('Issue #101: Detached status enrichment', () => { // status (`executed`) and the `-1` sentinel while its container is still // running (or not visible yet on a slow Docker-in-Docker host). describe('Issue #136: detached docker session liveness', () => { - const dockerAvailable = (() => { - const probe = spawnSync('docker', ['info'], { stdio: 'ignore' }); + // Use the repo's own probe: `docker` may be installed yet unable to run Linux + // images (e.g. Windows runners in Windows-containers mode, where `alpine` + // never starts). In that case `docker inspect` fails and liveness is `null` + // (unknown) rather than `false`, which would break the stopped-container + // assertions below. + const { canRunLinuxDockerImages } = require('../src/lib/isolation'); + const dockerAvailable = canRunLinuxDockerImages(); + + // Whether the container actually exists (was created) per `docker inspect`. + function dockerContainerExists(name) { + const probe = spawnSync('docker', ['inspect', name], { stdio: 'ignore' }); return probe.status === 0; - })(); + } function makeDockerRecord(sessionName, extra = {}) { return new ExecutionRecord({ @@ -521,8 +530,9 @@ describe('Issue #136: detached docker session liveness', () => { 'exit 1', ]); // `docker run` exits with the container's code (1 here); treat spawn errors - // (no daemon) as a skip. - if (ran.error) { + // (no daemon) or a container that never materialized (e.g. the Linux image + // could not be pulled) as a skip — there is nothing stopped to inspect. + if (ran.error || !dockerContainerExists(name)) { dockerRm(name); return; } From e633fe030a80b63b6d1c609fe193f7db251f9f90 Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 17 Jun 2026 08:22:09 +0000 Subject: [PATCH 4/4] Revert "Initial commit with task details" This reverts commit 14a4a48a023fa8977b1ded512a148c76cce42cb5. --- .gitkeep | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index 8e20ac8..0000000 --- a/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep file auto-generated at 2026-06-17T07:53:08.901Z for PR creation at branch issue-136-cffa39dbc470 for issue https://github.com/link-foundation/start/issues/136 \ No newline at end of file