Skip to content

Commit fbe2248

Browse files
committed
test: cover one-way text bundle behavior
1 parent f40fd1b commit fbe2248

2 files changed

Lines changed: 83 additions & 17 deletions

File tree

docs/plans/active/one-way-output-bundle-previews.md renamed to docs/plans/completed/one-way-output-bundle-previews.md

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99

1010
## Status
1111

12-
- State: active
12+
- State: completed
1313
- Last updated: 2026-04-17
14-
- Current phase: implementation
14+
- Current phase: completed
1515

1616
## Current Direction
1717

@@ -38,12 +38,12 @@
3838
- Define the one-way contract and the bounded in-memory preview state.
3939
- Phase 1: completed
4040
- Add cached image-preview state to `ActiveOutputBundle` and remove old-image rereads from later reply rendering.
41-
- Phase 2: pending
42-
- Add cached text head/tail preview state and stop reconstructing later previews from spilled file state.
43-
- Phase 3: pending
44-
- Replace tests that depend on disk rereads with tests for the one-way contract.
45-
- Phase 4: pending
46-
- Run full validation and update plan status for any remaining follow-on work.
41+
- Phase 2: completed
42+
- Confirm that later text replies already render from in-memory retained reply items and do not reread `transcript.txt`.
43+
- Phase 3: completed
44+
- Replace reread-oriented regressions with one-way contract coverage for deleted bundle image and transcript files.
45+
- Phase 4: completed
46+
- Run full validation and close the initiative.
4747

4848
## Locked Decisions
4949

@@ -55,18 +55,11 @@
5555

5656
## Open Questions
5757

58-
- What exact text preview state should be cached in memory?
59-
- Likely one bounded head window plus one bounded tail window, but the final shape should preserve current preview wording and stream ordering.
60-
- Should the preview cache live only on `ActiveOutputBundle`, or should `StagedTimeoutOutput` also keep a lightweight preview summary before a bundle is materialized?
61-
- When an output-bundle directory or subdirectory disappears mid-session, what minimal recreation behavior is worth supporting for continued appends without expanding this slice into generic filesystem recovery logic?
62-
- Can image preview caching reuse the existing `ReplyImage` payloads directly, or should it store a more compact internal representation?
58+
- None for this slice.
6359

6460
## Next Safe Slice
6561

66-
- Decide whether the text-preview slice is needed at all.
67-
- If text preview caching is needed, start that slice only after confirming that the remaining text path still violates the one-way contract in a user-visible way.
68-
- Decide whether text preview caching is needed purely for architectural consistency or whether the current text spill path is already sufficiently one-way.
69-
- If text caching is needed, define the exact bounded head/tail representation before changing text compaction behavior.
62+
- None. The planned work is complete.
7063

7164
## Stop Conditions
7265

@@ -82,3 +75,4 @@
8275
- 2026-04-17: Kept the public bundle layout unchanged for this initiative so the refactor can land without redefining the client-facing files-mode contract.
8376
- 2026-04-17: Began the image-preview implementation by caching the first-history and latest image previews on `ActiveOutputBundle` and moving the public regression toward “later replies do not depend on old bundle image files remaining on disk”.
8477
- 2026-04-17: Completed the image-preview slice. Later image-bundle replies now render from cached preview images in memory instead of rereading old image files from the bundle directory.
78+
- 2026-04-17: Confirmed the text spill path already satisfied the one-way contract for visible replies. Later text replies render from in-memory retained reply items, and a new public regression now covers transcript deletion and recreation without replaying previously spilled text.

tests/write_stdin_behavior.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,78 @@ async fn timeout_spill_file_path_stays_stable_across_later_small_poll() -> TestR
10481048
Ok(())
10491049
}
10501050

1051+
#[tokio::test(flavor = "multi_thread")]
1052+
async fn timeout_spill_recreates_deleted_transcript_without_replaying_old_text() -> TestResult<()> {
1053+
let _guard = lock_test_mutex();
1054+
let mut session = spawn_behavior_session().await?;
1055+
1056+
let input = "big <- paste(rep('y', 120), collapse = ''); cat('start\\n'); flush.console(); Sys.sleep(0.2); for (i in 1:80) cat(sprintf('mid%03d %s\\n', i, big)); flush.console(); Sys.sleep(0.35); cat('tail\\n')";
1057+
let first = session.write_stdin_raw_with(input, Some(0.05)).await?;
1058+
let first_text = result_text(&first);
1059+
if backend_unavailable(&first_text) {
1060+
eprintln!("write_stdin_behavior backend unavailable in this environment; skipping");
1061+
session.cancel().await?;
1062+
return Ok(());
1063+
}
1064+
1065+
sleep(Duration::from_millis(260)).await;
1066+
let spilled = session.write_stdin_raw_with("", Some(0.1)).await?;
1067+
let spilled_text = result_text(&spilled);
1068+
let transcript_path = match bundle_transcript_path(&spilled_text) {
1069+
Some(path) => path,
1070+
None if spilled_text.contains("<<repl status: busy") => {
1071+
eprintln!("write_stdin_behavior spill poll remained busy; skipping");
1072+
session.cancel().await?;
1073+
return Ok(());
1074+
}
1075+
None => {
1076+
panic!("expected transcript path in first oversized poll reply, got: {spilled_text:?}")
1077+
}
1078+
};
1079+
1080+
fs::remove_file(&transcript_path)?;
1081+
1082+
sleep(Duration::from_millis(450)).await;
1083+
let final_poll = session.write_stdin_raw_with("", Some(2.0)).await?;
1084+
let final_text = result_text(&final_poll);
1085+
if final_text.contains("<<repl status: busy") {
1086+
eprintln!("write_stdin_behavior final poll remained busy; skipping");
1087+
session.cancel().await?;
1088+
return Ok(());
1089+
}
1090+
let recreated_transcript = fs::read_to_string(&transcript_path)?;
1091+
1092+
let follow_up = session.write_stdin_raw_with("1+1", Some(2.0)).await?;
1093+
let follow_up_text = result_text(&follow_up);
1094+
1095+
session.cancel().await?;
1096+
1097+
if let Some(path) = bundle_transcript_path(&final_text) {
1098+
assert_eq!(
1099+
path, transcript_path,
1100+
"did not expect later polls to switch transcript paths after transcript deletion, got: {final_text:?}"
1101+
);
1102+
}
1103+
assert!(
1104+
recreated_transcript.contains("tail"),
1105+
"expected later small poll output to recreate the deleted spill file, got: {recreated_transcript:?}"
1106+
);
1107+
assert!(
1108+
!recreated_transcript.contains("mid080"),
1109+
"did not expect earlier spilled text to be replayed after transcript deletion, got: {recreated_transcript:?}"
1110+
);
1111+
assert!(
1112+
final_text.contains("tail") || final_text.contains("<<repl status: idle>>"),
1113+
"expected later small poll to either return inline tail text or settle idle after recreating the spill file, got: {final_text:?}"
1114+
);
1115+
assert!(
1116+
follow_up_text.contains("[1] 2"),
1117+
"expected session to stay alive after transcript deletion, got: {follow_up_text:?}"
1118+
);
1119+
1120+
Ok(())
1121+
}
1122+
10511123
#[tokio::test(flavor = "multi_thread")]
10521124
async fn timeout_bundle_file_creation_failure_preserves_inline_content() -> TestResult<()> {
10531125
let _guard = lock_test_mutex();

0 commit comments

Comments
 (0)