Skip to content

Commit b3a07e7

Browse files
committed
fix(conformance): address second-round code-review findings
- settings_management.rs's wait_for_ready dropped sandbox_lifecycle.rs's defensive name-mismatch check when its JSON polling loop was written. Added it back (and the missing `name` field on SandboxState) so a wrong-sandbox response fails fast with a clear diagnostic instead of silently comparing the wrong resource's phase. - provider_auto_create.rs's cleanup poll used a 30s overall retry budget with a 2-minute per-attempt command timeout -- a single slow attempt could exhaust the whole budget before a retry got a chance. Widened the retry budget to comfortably exceed the per-attempt ceiling. - provider_auto_create.rs's sandbox was no longer tracked via runner.track_sandbox() after switching to explicit ordered cleanup in the prior commit, losing the Drop-impl warning and finish() safety net if a panic skips the explicit cleanup. Re-added tracking (forgotten once explicit cleanup completes) alongside the explicit poll-retried delete, which is still what actually avoids the sandbox/provider detach race. - Added CommandResult::require_outcome(expect_success) to the shared crate and switched workspace_lifecycle.rs's run_cli and settings_management.rs's settings_delete to use it instead of two independent, byte-for-byte identical inline implementations. - Consolidated settings_management.rs's require_setting_line and require_setting_line_with_scope into one function taking an Option<&str> scope. Re-verified all four affected scenarios (8 tests) against a fresh Podman gateway. Signed-off-by: politerealism <burdcat17@gmail.com>
1 parent f29ff75 commit b3a07e7

4 files changed

Lines changed: 49 additions & 41 deletions

File tree

‎crates/openshell-conformance/src/lib.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,20 @@ impl CommandResult {
161161
Err(self.failure_diagnostic(&self.expectation))
162162
}
163163

164+
/// Assert the command's success matches `expect_success` (`false` asserts
165+
/// the command fails, e.g. a rejected duplicate-name or blocked-while-managed case).
166+
pub fn require_outcome(&self, expect_success: bool) -> Result<(), String> {
167+
if self.success() == expect_success {
168+
Ok(())
169+
} else {
170+
Err(self.failure_diagnostic(if expect_success {
171+
"command succeeds"
172+
} else {
173+
"command fails"
174+
}))
175+
}
176+
}
177+
164178
pub fn failure_diagnostic(&self, expectation: &str) -> String {
165179
format!(
166180
"{}\nexpected: {expectation}\nactual: exit {} after {:.1?}\ncommand: {}\nstdout:\n{}\nstderr:\n{}",

‎crates/openshell-conformance/src/scenarios/provider_auto_create.rs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ network_policies: {}
2323

2424
const CREATE_TIMEOUT: Duration = Duration::from_mins(10);
2525
const COMMAND_TIMEOUT: Duration = Duration::from_mins(2);
26-
const PROVIDER_DETACH_TIMEOUT: Duration = Duration::from_secs(30);
26+
// Must comfortably exceed COMMAND_TIMEOUT: poll_until only checks elapsed
27+
// time between attempts, so a single slow attempt must not be able to
28+
// consume the whole retry budget before a second attempt gets a chance.
29+
const PROVIDER_DETACH_TIMEOUT: Duration = Duration::from_mins(3);
2730
const PROVIDER_DETACH_INTERVAL: Duration = Duration::from_secs(1);
2831

2932
// "claude-code" is a recognized auto-provider type, not an arbitrary name:
@@ -55,9 +58,15 @@ fn run_provider_auto_create(runner: &mut OpenShellRunner) -> ScenarioFuture<'_>
5558
delete_provider_best_effort(runner).await;
5659

5760
let sandbox_name = format!("ct-{}-pa", runner.id());
61+
// Tracked as a safety net for runner.finish()'s generic sweep (and the
62+
// Drop-impl warning) if a panic skips the explicit cleanup() below;
63+
// the ordered, poll-retried delete in cleanup() is what actually
64+
// avoids the sandbox/provider detach race in the common case.
65+
runner.track_sandbox(&sandbox_name);
5866
let result =
5967
auto_created_provider_credential_available_in_sandbox(runner, &sandbox_name).await;
6068
cleanup(runner, &sandbox_name).await;
69+
runner.forget_sandbox(&sandbox_name);
6170
result
6271
})
6372
}

‎crates/openshell-conformance/src/scenarios/settings_management.rs‎

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const TEST_KEY: &str = "ocsf_json_enabled";
2222

2323
#[derive(Debug, Deserialize)]
2424
struct SandboxState {
25+
name: String,
2526
phase: String,
2627
}
2728

@@ -77,21 +78,21 @@ async fn settings_global_override_round_trip(runner: &mut OpenShellRunner) -> Re
7778
wait_for_ready(runner, &sandbox_name).await?;
7879

7980
let initial = settings_get(runner, &sandbox_name, "initial").await?;
80-
require_setting_line_with_scope(
81+
require_setting_line(
8182
&initial,
8283
"<unset>",
83-
"unset",
84+
Some("unset"),
8485
"initial sandbox setting is unset",
8586
)?;
8687

8788
settings_set(runner, &sandbox_name, "true", "sandbox-set").await?;
8889
wait_for_setting_value(runner, &sandbox_name, "true", "sandbox").await?;
8990

9091
let after_sandbox_set = settings_get(runner, &sandbox_name, "after-sandbox-set").await?;
91-
require_setting_line_with_scope(
92+
require_setting_line(
9293
&after_sandbox_set,
9394
"true",
94-
"sandbox",
95+
Some("sandbox"),
9596
"setting reflects the sandbox-scoped value",
9697
)?;
9798

@@ -101,10 +102,10 @@ async fn settings_global_override_round_trip(runner: &mut OpenShellRunner) -> Re
101102
}
102103

103104
let after_sandbox_delete = settings_get(runner, &sandbox_name, "after-sandbox-delete").await?;
104-
require_setting_line_with_scope(
105+
require_setting_line(
105106
&after_sandbox_delete,
106107
"<unset>",
107-
"unset",
108+
Some("unset"),
108109
"setting is unset again after sandbox delete",
109110
)?;
110111

@@ -164,6 +165,7 @@ async fn settings_global_override_round_trip(runner: &mut OpenShellRunner) -> Re
164165
require_setting_line(
165166
&global_get,
166167
"false",
168+
None,
167169
"global setting reflects the override value",
168170
)?;
169171

@@ -190,6 +192,7 @@ async fn settings_global_override_round_trip(runner: &mut OpenShellRunner) -> Re
190192
require_setting_line(
191193
&global_after_delete,
192194
"<unset>",
195+
None,
193196
"global setting is unset after delete",
194197
)?;
195198

@@ -203,10 +206,10 @@ async fn settings_global_override_round_trip(runner: &mut OpenShellRunner) -> Re
203206
wait_for_setting_value(runner, &sandbox_name, "false", "sandbox").await?;
204207

205208
let sandbox_after_delete = settings_get(runner, &sandbox_name, "after-global-delete").await?;
206-
require_setting_line_with_scope(
209+
require_setting_line(
207210
&sandbox_after_delete,
208211
"false",
209-
"sandbox",
212+
Some("sandbox"),
210213
"sandbox-level control resumes after global delete",
211214
)?;
212215

@@ -241,6 +244,10 @@ async fn wait_for_ready(runner: &mut OpenShellRunner, sandbox_name: &str) -> Res
241244
Poll::Pending(result.failure_diagnostic("sandbox can be retrieved"))
242245
}
243246
Ok(result) => match result.json::<SandboxState>() {
247+
Ok(state) if state.name != sandbox_name => Poll::Failed(format!(
248+
"sandbox get returned {:?}; expected '{sandbox_name}'",
249+
state.name
250+
)),
244251
Ok(state) if state.phase == "Ready" => Poll::Ready(()),
245252
Ok(state) => Poll::Pending(format!(
246253
"sandbox phase is {:?}; expected \"Ready\"",
@@ -359,15 +366,8 @@ async fn settings_delete(
359366
.run(&["settings", "delete", sandbox_name, "--key", TEST_KEY])
360367
.await
361368
.map_err(|error| error.to_string())?;
362-
if result.success() == expect_success {
363-
Ok(result)
364-
} else {
365-
Err(result.failure_diagnostic(if expect_success {
366-
"command succeeds"
367-
} else {
368-
"command fails"
369-
}))
370-
}
369+
result.require_outcome(expect_success)?;
370+
Ok(result)
371371
}
372372

373373
/// Best-effort teardown. Mirrors a `Drop`-guard cleanup: ignore failures since
@@ -386,22 +386,14 @@ async fn cleanup_global_setting(runner: &OpenShellRunner) {
386386
fn require_setting_line(
387387
result: &CommandResult,
388388
expected: &str,
389+
scope: Option<&str>,
389390
expectation: &str,
390391
) -> Result<(), String> {
391-
if result.output_contains(&format!("{TEST_KEY} = {expected}")) {
392-
Ok(())
393-
} else {
394-
Err(result.failure_diagnostic(expectation))
395-
}
396-
}
397-
398-
fn require_setting_line_with_scope(
399-
result: &CommandResult,
400-
expected: &str,
401-
scope: &str,
402-
expectation: &str,
403-
) -> Result<(), String> {
404-
if result.output_contains(&format!("{TEST_KEY} = {expected} ({scope})")) {
392+
let needle = match scope {
393+
Some(scope) => format!("{TEST_KEY} = {expected} ({scope})"),
394+
None => format!("{TEST_KEY} = {expected}"),
395+
};
396+
if result.output_contains(&needle) {
405397
Ok(())
406398
} else {
407399
Err(result.failure_diagnostic(expectation))

‎crates/openshell-conformance/src/scenarios/workspace_lifecycle.rs‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -322,15 +322,8 @@ async fn run_cli(
322322
.run(args)
323323
.await
324324
.map_err(|error| error.to_string())?;
325-
if result.success() == expect_success {
326-
Ok(result)
327-
} else {
328-
Err(result.failure_diagnostic(if expect_success {
329-
"command succeeds"
330-
} else {
331-
"command fails"
332-
}))
333-
}
325+
result.require_outcome(expect_success)?;
326+
Ok(result)
334327
}
335328

336329
fn require_contains(result: &CommandResult, needle: &str, expectation: &str) -> Result<(), String> {

0 commit comments

Comments
 (0)