diff --git a/docs/case-studies/issue-67/ANALYSIS.md b/docs/case-studies/issue-67/ANALYSIS.md new file mode 100644 index 0000000..5a13281 --- /dev/null +++ b/docs/case-studies/issue-67/ANALYSIS.md @@ -0,0 +1,159 @@ +# Case Study: Issue #67 - Display Session Name and Container Name When Different from Session UUID + +## Overview + +**Issue:** When using isolation backends (screen, docker, tmux), the output shows a session UUID but not the actual container/screen/tmux session name that users need to reconnect to the session, especially in detached mode. + +**Priority:** Bug / Enhancement / Documentation + +## Problem Description + +When running commands with isolation (screen, docker, tmux), the tool generates two different identifiers: +1. **Session UUID** - A unique identifier for tracking executions (e.g., `f1efebcd-5426-437b-92db-b94acaaf421c`) +2. **Session/Container Name** - The actual name used by the isolation backend (e.g., `docker-1767841051864-c0qs07`) + +Currently, only the session UUID is displayed in output blocks, but users need the actual session/container name to: +- Reconnect to detached sessions (`screen -r `, `tmux attach -t `) +- View container logs (`docker logs `) +- Attach to containers (`docker attach `) +- Kill sessions (`screen -S -X quit`, `docker rm -f `) + +## Root Cause Analysis + +### Timeline of Code Flow + +1. **Session UUID Generation** (`cli.js:234`): + ```javascript + const sessionId = wrapperOptions.sessionId || generateUUID(); + ``` + This generates the tracking UUID, e.g., `f1efebcd-5426-437b-92db-b94acaaf421c` + +2. **Session Name Generation** (`cli.js:410-412`): + ```javascript + const sessionName = options.session || + `${environment || 'start'}-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`; + ``` + This generates the backend session name, e.g., `docker-1767841051864-c0qs07` + +3. **Extra Lines for Isolation** (`cli.js:470-485`): + Currently only adds: + - `[Isolation] Environment: docker, Mode: attached` + - `[Isolation] Session: ` (only if explicitly provided via --session) + - `[Isolation] Image: ubuntu:latest` + - `[Isolation] Endpoint: user@host` (for SSH) + - `[Isolation] User: ` (for user isolation) + +4. **Output Block Generation** (`output-blocks.js:181-205`): + - Parses extraLines for isolation metadata + - Generates spine lines with container/screen/tmux names + - But the actual session name is **not passed** when auto-generated + +### The Gap + +The issue is in `cli.js` - when no explicit `--session` is provided, the auto-generated session name is not added to `extraLines`, so `output-blocks.js` cannot display it. + +Currently: +```javascript +if (options.session) { + extraLines.push(`[Isolation] Session: ${options.session}`); +} +``` + +This only adds the session line when user explicitly provides `--session`, but NOT when it's auto-generated. + +## Proposed Solution + +### Option 1: Always Add Session Name to extraLines (Recommended) + +In `cli.js`, after generating `sessionName`, always add it to extraLines regardless of whether it was explicitly provided: + +```javascript +// Always add the actual session/container name used +extraLines.push(`[Isolation] Session: ${sessionName}`); +``` + +This ensures that `output-blocks.js` can always display the correct session/container/screen name. + +### Option 2: Use Session UUID for Session Names + +Alternative approach: Try to use the session UUID as the session name when possible. However, this has limitations: +- UUIDs are long (36 characters) which may exceed name limits +- Some backends have character restrictions (docker container names must match `[a-zA-Z0-9][a-zA-Z0-9_.-]*`) +- UUIDs contain dashes which may cause issues in some contexts + +### Recommendation + +**Implement Option 1** - Always pass the actual session name to extraLines. This is: +- Minimal code change +- Backward compatible +- Provides users with the information they need +- Allows them to reconnect to detached sessions + +## Implementation Steps + +### JavaScript Implementation (completed) + +1. Modified `js/src/bin/cli.js` to always add `[Isolation] Session: ${sessionName}` to extraLines +2. Added tests in `js/test/cli.test.js` to verify the session name is displayed for screen, tmux, and docker +3. Created changeset `js/.changeset/issue-67-display-session-name.md` + +### Rust Implementation (completed) + +1. Modified `rust/src/bin/main.rs` to always add the session name to extraLines when using isolation +2. Added tests in `rust/src/lib/output_blocks.rs` to verify the session name is displayed for screen, tmux, and docker +3. Created changelog fragment `rust/changelog.d/67.md` + +## Impact + +- **Users** will see the actual session/container name in output, enabling them to: + - Reconnect to detached screen sessions + - Attach to tmux sessions + - View Docker container logs + - Remove containers +- **Backward Compatibility**: No breaking changes - just additional information displayed + +## References + +- Issue: https://github.com/link-foundation/start/issues/67 +- Related Files: + - JavaScript: + - `js/src/bin/cli.js` - Main CLI logic + - `js/src/lib/output-blocks.js` - Output formatting + - `js/src/lib/isolation.js` - Isolation backend runners + - `js/src/lib/args-parser.js` - Argument parsing + - `js/test/cli.test.js` - Tests for issue #67 fix + - Rust: + - `rust/src/bin/main.rs` - Main CLI logic + - `rust/src/lib/output_blocks.rs` - Output formatting (with tests) + - `rust/src/lib/isolation.rs` - Isolation backend runners + - `rust/src/lib/args_parser.rs` - Argument parsing + +## User-Provided Log Examples + +From the issue description: +``` +$ --isolated screen -- echo 'hi' +│ session a39a17a3-1064-480d-b508-80b8bdd3a93f +│ start 2026-01-08 02:57:28.115 +│ +│ isolation screen +│ mode attached +│ +$ echo hi +``` + +The screen session name (e.g., `screen-1767841048115-nch2wk`) is NOT shown, only the UUID. + +With the fix, users would see: +``` +│ session a39a17a3-1064-480d-b508-80b8bdd3a93f +│ start 2026-01-08 02:57:28.115 +│ +│ isolation screen +│ mode attached +│ screen screen-1767841048115-nch2wk +│ +$ echo hi +``` + +This allows users to reconnect with `screen -r screen-1767841048115-nch2wk`. diff --git a/docs/case-studies/issue-67/issue-comments.json b/docs/case-studies/issue-67/issue-comments.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/docs/case-studies/issue-67/issue-comments.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/docs/case-studies/issue-67/issue-data.json b/docs/case-studies/issue-67/issue-data.json new file mode 100644 index 0000000..06e0453 --- /dev/null +++ b/docs/case-studies/issue-67/issue-data.json @@ -0,0 +1 @@ +{"url":"https://api.github.com/repos/link-foundation/start/issues/67","repository_url":"https://api.github.com/repos/link-foundation/start","labels_url":"https://api.github.com/repos/link-foundation/start/issues/67/labels{/name}","comments_url":"https://api.github.com/repos/link-foundation/start/issues/67/comments","events_url":"https://api.github.com/repos/link-foundation/start/issues/67/events","html_url":"https://github.com/link-foundation/start/issues/67","id":3791066046,"node_id":"I_kwDOP85RQM7h9xO-","number":67,"title":"Make sure to also output session name and container name if they are not the same as main session uuid","user":{"login":"konard","id":1431904,"node_id":"MDQ6VXNlcjE0MzE5MDQ=","avatar_url":"https://avatars.githubusercontent.com/u/1431904?v=4","gravatar_id":"","url":"https://api.github.com/users/konard","html_url":"https://github.com/konard","followers_url":"https://api.github.com/users/konard/followers","following_url":"https://api.github.com/users/konard/following{/other_user}","gists_url":"https://api.github.com/users/konard/gists{/gist_id}","starred_url":"https://api.github.com/users/konard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/konard/subscriptions","organizations_url":"https://api.github.com/users/konard/orgs","repos_url":"https://api.github.com/users/konard/repos","events_url":"https://api.github.com/users/konard/events{/privacy}","received_events_url":"https://api.github.com/users/konard/received_events","type":"User","user_view_type":"public","site_admin":false},"labels":[{"id":9408517827,"node_id":"LA_kwDOP85RQM8AAAACMMqWww","url":"https://api.github.com/repos/link-foundation/start/labels/bug","name":"bug","color":"d73a4a","default":true,"description":"Something isn't working"},{"id":9408517830,"node_id":"LA_kwDOP85RQM8AAAACMMqWxg","url":"https://api.github.com/repos/link-foundation/start/labels/documentation","name":"documentation","color":"0075ca","default":true,"description":"Improvements or additions to documentation"},{"id":9408517834,"node_id":"LA_kwDOP85RQM8AAAACMMqWyg","url":"https://api.github.com/repos/link-foundation/start/labels/enhancement","name":"enhancement","color":"a2eeef","default":true,"description":"New feature or request"}],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":0,"created_at":"2026-01-08T03:17:41Z","updated_at":"2026-01-08T03:17:41Z","closed_at":null,"author_association":"MEMBER","type":{"id":22969355,"node_id":"IT_kwDOCoAzvc4BXnwL","name":"Task","description":"A specific piece of work","color":"yellow","created_at":"2024-07-20T19:06:39Z","updated_at":"2024-07-20T19:06:39Z","is_enabled":true},"active_lock_reason":null,"sub_issues_summary":{"total":0,"completed":0,"percent_completed":0},"issue_dependencies_summary":{"blocked_by":0,"total_blocked_by":0,"blocking":0,"total_blocking":0},"body":"```\nkonard@MacBook-Pro-Konstantin ~ % bun install -g start-command \nbun add v1.2.20 (6ad208bc)\n\ninstalled start-command@0.19.0 with binaries:\n - $\n\n[302.00ms] done\nkonard@MacBook-Pro-Konstantin ~ % $ echo 'hi' \n│ session 3098d3ae-07b2-418c-aede-c001b82ec31c\n│ start 2026-01-08 02:57:23.530\n│\n$ echo hi\n\nhi\n\n✓\n│ finish 2026-01-08 02:57:23.798\n│ duration 0.268s\n│ exit 0\n│\n│ log /var/folders/cl/831lqjgd58v5mb_m74cfdfcw0000gn/T/start-command-1767841043530-kyf19v.log\n│ session 3098d3ae-07b2-418c-aede-c001b82ec31c\nkonard@MacBook-Pro-Konstantin ~ % $ --isolated screen -- echo 'hi'\n│ session a39a17a3-1064-480d-b508-80b8bdd3a93f\n│ start 2026-01-08 02:57:28.115\n│\n│ isolation screen\n│ mode attached\n│\n$ echo hi\n\nhi\n\n✓\n│ finish 2026-01-08 02:57:28.423\n│ duration 0.415s\n│ exit 0\n│\n│ isolation screen\n│ mode attached\n│\n│ log /var/folders/cl/831lqjgd58v5mb_m74cfdfcw0000gn/T/start-command-screen-1767841048115-nch2wk.log\n│ session a39a17a3-1064-480d-b508-80b8bdd3a93f\nkonard@MacBook-Pro-Konstantin ~ % $ --isolated docker -- echo 'hi'\n│ session f1efebcd-5426-437b-92db-b94acaaf421c\n│ start 2026-01-08 02:57:31.864\n│\n│ isolation docker\n│ mode attached\n│ image alpine:latest\n│\n$ echo hi\n\nhi\n\n✓\n│ finish 2026-01-08 02:57:32.241\n│ duration 0.489s\n│ exit 0\n│\n│ isolation docker\n│ mode attached\n│ image alpine:latest\n│\n│ log /var/folders/cl/831lqjgd58v5mb_m74cfdfcw0000gn/T/start-command-docker-1767841051864-c0qs07.log\n│ session f1efebcd-5426-437b-92db-b94acaaf421c\nkonard@MacBook-Pro-Konstantin ~ %\n```\n\nIf for screen, docker, tmux and so on the screen name is not the same as session UUID, we should output it separately, but if we can we should try to use the same UUID for screen name and docker name and so on. But if it is not possible we should make sure at start and end blocks we do display that info, so if for example we use detached mode, or attached mode without closing of screen/docker container we should be able to see all the data to reconnect to them.\n\nPlease download all logs and data related about the issue to this repository, make sure we compile that data to `./docs/case-studies/issue-{id}` folder, and use it to do deep case study analysis (also make sure to search online for additional facts and data), in which we will reconstruct timeline/sequence of events, find root causes of the problem, and propose possible solutions.","closed_by":null,"reactions":{"url":"https://api.github.com/repos/link-foundation/start/issues/67/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"timeline_url":"https://api.github.com/repos/link-foundation/start/issues/67/timeline","performed_via_github_app":null,"state_reason":null} \ No newline at end of file diff --git a/js/.changeset/issue-67-display-session-name.md b/js/.changeset/issue-67-display-session-name.md new file mode 100644 index 0000000..8f266bf --- /dev/null +++ b/js/.changeset/issue-67-display-session-name.md @@ -0,0 +1,14 @@ +--- +'start-command': patch +--- + +fix: Always display session/container name in isolation output + +When using isolation backends (screen, docker, tmux), the output now shows the actual session/container name that users need to reconnect to sessions, especially in detached mode. Previously, only the session UUID was shown, but users need the actual backend name to: + +- Reconnect to detached screen sessions: `screen -r ` +- Attach to tmux sessions: `tmux attach -t ` +- View Docker container logs: `docker logs ` +- Remove containers: `docker rm -f ` + +Fixes #67 diff --git a/js/src/bin/cli.js b/js/src/bin/cli.js index 2e473b4..20f0b7f 100644 --- a/js/src/bin/cli.js +++ b/js/src/bin/cli.js @@ -470,9 +470,10 @@ async function runWithIsolation( // Add isolation info to extra lines if (environment) { extraLines.push(`[Isolation] Environment: ${environment}, Mode: ${mode}`); - } - if (options.session) { - extraLines.push(`[Isolation] Session: ${options.session}`); + // Always add the session name so users can reconnect to detached sessions + // This is important for screen, tmux, docker where the session/container name + // is different from the session UUID used for tracking (see issue #67) + extraLines.push(`[Isolation] Session: ${sessionName}`); } if (effectiveImage) { extraLines.push(`[Isolation] Image: ${effectiveImage}`); diff --git a/js/test/cli.test.js b/js/test/cli.test.js index cbdd2bd..e3ad6c6 100644 --- a/js/test/cli.test.js +++ b/js/test/cli.test.js @@ -122,3 +122,97 @@ describe('CLI basic behavior', () => { assert.ok(result.stdout.includes('Usage:'), 'Should display usage'); }); }); + +describe('CLI isolation output (issue #67)', () => { + const { isCommandAvailable } = require('../src/lib/isolation'); + + it('should display screen session name when using screen isolation', async () => { + if (!isCommandAvailable('screen')) { + console.log(' Skipping: screen not installed'); + return; + } + + const result = runCLI(['-i', 'screen', '--', 'echo', 'hello']); + + // The output should contain the screen session name (in format screen-timestamp-random) + // Check that the session UUID is displayed + assert.ok( + result.stdout.includes('│ session'), + 'Should display session UUID' + ); + // Check that screen isolation info is displayed + assert.ok( + result.stdout.includes('│ isolation screen'), + 'Should display screen isolation' + ); + // Check that the actual screen session name is displayed (issue #67 fix) + assert.ok( + result.stdout.includes('│ screen screen-'), + 'Should display actual screen session name for reconnection (issue #67)' + ); + }); + + it('should display tmux session name when using tmux isolation', async () => { + if (!isCommandAvailable('tmux')) { + console.log(' Skipping: tmux not installed'); + return; + } + + const result = runCLI(['-i', 'tmux', '--', 'echo', 'hello']); + + // The output should contain the tmux session name + assert.ok( + result.stdout.includes('│ session'), + 'Should display session UUID' + ); + assert.ok( + result.stdout.includes('│ isolation tmux'), + 'Should display tmux isolation' + ); + // Check that the actual tmux session name is displayed (issue #67 fix) + assert.ok( + result.stdout.includes('│ tmux tmux-'), + 'Should display actual tmux session name for reconnection (issue #67)' + ); + }); + + it('should display docker container name when using docker isolation', async () => { + const { canRunLinuxDockerImages } = require('../src/lib/isolation'); + + if (!canRunLinuxDockerImages()) { + console.log( + ' Skipping: docker not available or cannot run Linux images' + ); + return; + } + + const result = runCLI([ + '-i', + 'docker', + '--image', + 'alpine:latest', + '--', + 'echo', + 'hello', + ]); + + // The output should contain the docker container name + assert.ok( + result.stdout.includes('│ session'), + 'Should display session UUID' + ); + assert.ok( + result.stdout.includes('│ isolation docker'), + 'Should display docker isolation' + ); + assert.ok( + result.stdout.includes('│ image alpine:latest'), + 'Should display docker image' + ); + // Check that the actual container name is displayed (issue #67 fix) + assert.ok( + result.stdout.includes('│ container docker-'), + 'Should display actual container name for reconnection (issue #67)' + ); + }); +}); diff --git a/rust/changelog.d/67.md b/rust/changelog.d/67.md new file mode 100644 index 0000000..7b23b12 --- /dev/null +++ b/rust/changelog.d/67.md @@ -0,0 +1,13 @@ +fix: Always display session/container name in isolation output + +When using isolation backends (screen, docker, tmux), the output now always displays +the actual session/container name that users need to reconnect to sessions. Previously, +the session name was only shown if explicitly provided via `--session` flag. + +This allows users to: +- Reconnect to detached screen sessions: `screen -r ` +- Attach to tmux sessions: `tmux attach -t ` +- View Docker container logs: `docker logs ` +- Remove containers: `docker rm -f ` + +Fixes #67 diff --git a/rust/src/bin/main.rs b/rust/src/bin/main.rs index 9eceb36..561c836 100644 --- a/rust/src/bin/main.rs +++ b/rust/src/bin/main.rs @@ -507,9 +507,10 @@ fn run_with_isolation( // Add isolation info to extra lines if let Some(env) = environment { extra_lines.push(format!("[Isolation] Environment: {}, Mode: {}", env, mode)); - } - if let Some(ref session) = wrapper_options.session { - extra_lines.push(format!("[Isolation] Session: {}", session)); + // Always add the session name so users can reconnect to detached sessions + // This is important for screen, tmux, docker where the session/container name + // is different from the session UUID used for tracking (see issue #67) + extra_lines.push(format!("[Isolation] Session: {}", session_name)); } if let Some(ref image) = effective_image { extra_lines.push(format!("[Isolation] Image: {}", image)); diff --git a/rust/src/lib/output_blocks.rs b/rust/src/lib/output_blocks.rs index 03361df..dae5f0f 100644 --- a/rust/src/lib/output_blocks.rs +++ b/rust/src/lib/output_blocks.rs @@ -314,3 +314,210 @@ pub fn format_value_for_links_notation(value: &serde_json::Value) -> String { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_spine_line() { + let line = create_spine_line("session", "abc123"); + assert!(line.starts_with("│")); + assert!(line.contains("session")); + assert!(line.contains("abc123")); + } + + #[test] + fn test_create_command_line() { + let line = create_command_line("echo hello"); + assert_eq!(line, "$ echo hello"); + } + + #[test] + fn test_parse_isolation_metadata_screen() { + let extra_lines = vec![ + "[Isolation] Environment: screen, Mode: attached", + "[Isolation] Session: screen-1234567890-abc123", + ]; + let metadata = parse_isolation_metadata(&extra_lines); + assert_eq!(metadata.isolation, Some("screen".to_string())); + assert_eq!(metadata.mode, Some("attached".to_string())); + assert_eq!( + metadata.session, + Some("screen-1234567890-abc123".to_string()) + ); + } + + #[test] + fn test_parse_isolation_metadata_tmux() { + let extra_lines = vec![ + "[Isolation] Environment: tmux, Mode: detached", + "[Isolation] Session: tmux-1234567890-xyz789", + ]; + let metadata = parse_isolation_metadata(&extra_lines); + assert_eq!(metadata.isolation, Some("tmux".to_string())); + assert_eq!(metadata.mode, Some("detached".to_string())); + assert_eq!(metadata.session, Some("tmux-1234567890-xyz789".to_string())); + } + + #[test] + fn test_parse_isolation_metadata_docker() { + let extra_lines = vec![ + "[Isolation] Environment: docker, Mode: attached", + "[Isolation] Session: docker-1234567890-def456", + "[Isolation] Image: alpine:latest", + ]; + let metadata = parse_isolation_metadata(&extra_lines); + assert_eq!(metadata.isolation, Some("docker".to_string())); + assert_eq!(metadata.mode, Some("attached".to_string())); + assert_eq!( + metadata.session, + Some("docker-1234567890-def456".to_string()) + ); + assert_eq!(metadata.image, Some("alpine:latest".to_string())); + } + + #[test] + fn test_generate_isolation_lines_screen() { + let metadata = IsolationMetadata { + isolation: Some("screen".to_string()), + mode: Some("attached".to_string()), + session: Some("screen-1234567890-abc123".to_string()), + ..Default::default() + }; + let lines = generate_isolation_lines(&metadata, None); + assert!(lines + .iter() + .any(|l| l.contains("isolation") && l.contains("screen"))); + assert!(lines + .iter() + .any(|l| l.contains("mode") && l.contains("attached"))); + // Issue #67: Session name should be displayed for screen + assert!( + lines + .iter() + .any(|l| l.contains("screen") && l.contains("screen-1234567890-abc123")), + "Should display screen session name for reconnection (issue #67)" + ); + } + + #[test] + fn test_generate_isolation_lines_tmux() { + let metadata = IsolationMetadata { + isolation: Some("tmux".to_string()), + mode: Some("detached".to_string()), + session: Some("tmux-1234567890-xyz789".to_string()), + ..Default::default() + }; + let lines = generate_isolation_lines(&metadata, None); + assert!(lines + .iter() + .any(|l| l.contains("isolation") && l.contains("tmux"))); + assert!(lines + .iter() + .any(|l| l.contains("mode") && l.contains("detached"))); + // Issue #67: Session name should be displayed for tmux + assert!( + lines + .iter() + .any(|l| l.contains("tmux") && l.contains("tmux-1234567890-xyz789")), + "Should display tmux session name for reconnection (issue #67)" + ); + } + + #[test] + fn test_generate_isolation_lines_docker() { + let metadata = IsolationMetadata { + isolation: Some("docker".to_string()), + mode: Some("attached".to_string()), + session: Some("docker-1234567890-def456".to_string()), + image: Some("alpine:latest".to_string()), + ..Default::default() + }; + let lines = generate_isolation_lines(&metadata, None); + assert!(lines + .iter() + .any(|l| l.contains("isolation") && l.contains("docker"))); + assert!(lines + .iter() + .any(|l| l.contains("mode") && l.contains("attached"))); + assert!(lines + .iter() + .any(|l| l.contains("image") && l.contains("alpine:latest"))); + // Issue #67: Container name should be displayed for docker + assert!( + lines + .iter() + .any(|l| l.contains("container") && l.contains("docker-1234567890-def456")), + "Should display docker container name for reconnection (issue #67)" + ); + } + + #[test] + fn test_create_start_block_with_isolation() { + let extra_lines: Vec<&str> = vec![ + "[Isolation] Environment: screen, Mode: attached", + "[Isolation] Session: screen-1234567890-test", + ]; + let block = create_start_block(&StartBlockOptions { + session_id: "uuid-123", + timestamp: "2026-01-08 12:00:00", + command: "echo hello", + extra_lines: Some(extra_lines), + style: None, + width: None, + }); + // Issue #67: The start block should include the session name for reconnection + assert!(block.contains("│ session uuid-123")); + assert!(block.contains("│ isolation screen")); + assert!( + block.contains("│ screen screen-1234567890-test"), + "Start block should display screen session name for reconnection (issue #67)" + ); + } + + #[test] + fn test_create_finish_block_with_isolation() { + let extra_lines: Vec<&str> = vec![ + "[Isolation] Environment: docker, Mode: attached", + "[Isolation] Session: docker-1234567890-test", + "[Isolation] Image: alpine:latest", + ]; + let block = create_finish_block(&FinishBlockOptions { + session_id: "uuid-456", + timestamp: "2026-01-08 12:00:01", + exit_code: 0, + log_path: "/tmp/test.log", + duration_ms: Some(100.0), + result_message: None, + extra_lines: Some(extra_lines), + style: None, + width: None, + }); + // Issue #67: The finish block should include the container name for reconnection + assert!(block.contains("✓")); + assert!(block.contains("│ session uuid-456")); + assert!( + block.contains("│ container docker-1234567890-test"), + "Finish block should display docker container name for reconnection (issue #67)" + ); + } + + #[test] + fn test_format_duration() { + assert_eq!(format_duration(500.0), "0.500s"); + assert_eq!(format_duration(1500.0), "1.500s"); + assert_eq!(format_duration(15000.0), "15.00s"); + assert_eq!(format_duration(150000.0), "150.0s"); + } + + #[test] + fn test_escape_for_links_notation() { + // Simple value - no quoting needed + assert_eq!(escape_for_links_notation("simple"), "simple"); + // Value with space - needs quoting + assert_eq!(escape_for_links_notation("hello world"), "\"hello world\""); + // Value with colon - needs quoting + assert_eq!(escape_for_links_notation("key:value"), "\"key:value\""); + } +}