Skip to content

Commit 83b7bff

Browse files
authored
test(tbtc/signer): multi-seat capacity new-vs-replacement + abort-by-attempt edge cases (#4100)
## What Two follow-up edge tests Codex itemized during the multi-seat interactive engine fix (#4098), test-only: - **`interactive_capacity_counts_new_members_not_replacements`** — at a live-member cap of 1: a member advancing to a newer attempt *replaces* its own entry (no new slot, succeeds), while a *different* member is a new entry that trips the cap and fails closed. Pins that the cap counts member entries, not sessions. - **`interactive_abort_by_attempt_removes_all_members_on_that_attempt`** — abort with an `attempt_id` filter is session-level over the member map: it removes every local seat on that attempt while a sibling seat on a *different* attempt survives. All 299 lib tests pass; `cargo fmt` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents ca6ee3b + 13a8103 commit 83b7bff

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

pkg/tbtc/signer/src/engine/tests.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12184,6 +12184,92 @@ fn interactive_aggregate_cleanup_is_message_bound() {
1218412184
}
1218512185
}
1218612186

12187+
#[test]
12188+
fn interactive_capacity_counts_new_members_not_replacements() {
12189+
// The live-member cap counts member ENTRIES: a new member takes a slot, but a
12190+
// same-member replacement (re-open on a newer attempt) reuses its own slot.
12191+
let _guard = lock_test_state();
12192+
reset_for_tests();
12193+
12194+
let key_group = "interactive-test-key-group";
12195+
let message = [0xc4u8; 32];
12196+
let included = [1u16, 2];
12197+
let session_id = "interactive-cap-multiseat";
12198+
12199+
std::env::set_var(TBTC_SIGNER_MAX_LIVE_INTERACTIVE_SESSIONS_ENV, "1");
12200+
let outcome = (|| -> Result<(), EngineError> {
12201+
// Member 1 takes the one live-member slot.
12202+
open_interactive_for_test(session_id, key_group, &message, &included, 1, 1, 2)?;
12203+
12204+
// Member 1 advancing to a NEWER attempt replaces its own entry - no new slot,
12205+
// so it succeeds even at capacity 1 (a replacement, not an idempotent reopen).
12206+
let advanced =
12207+
open_interactive_for_test(session_id, key_group, &message, &included, 2, 1, 2)?;
12208+
assert!(
12209+
!advanced.idempotent,
12210+
"a newer attempt is a replacement, not idempotent"
12211+
);
12212+
12213+
// A DIFFERENT member is a new entry, so it trips the cap and fails closed.
12214+
let at_capacity =
12215+
open_interactive_for_test(session_id, key_group, &message, &included, 2, 2, 2)
12216+
.expect_err("a new member must trip the live-member cap");
12217+
assert!(
12218+
matches!(at_capacity, EngineError::Internal(ref m)
12219+
if m.contains("live interactive member count")),
12220+
"unexpected error: {at_capacity:?}"
12221+
);
12222+
Ok(())
12223+
})();
12224+
std::env::remove_var(TBTC_SIGNER_MAX_LIVE_INTERACTIVE_SESSIONS_ENV);
12225+
outcome.expect("capacity new-vs-replacement lifecycle");
12226+
}
12227+
12228+
#[test]
12229+
fn interactive_abort_by_attempt_removes_all_members_on_that_attempt() {
12230+
// Abort with an attempt_id filter is session-level over the member map: it removes
12231+
// EVERY local seat on that attempt, while a sibling seat on a different attempt
12232+
// survives.
12233+
let _guard = lock_test_state();
12234+
reset_for_tests();
12235+
12236+
let key_group = "interactive-test-key-group";
12237+
let message = [0xa4u8; 32];
12238+
let included = [1u16, 2, 3];
12239+
let session_id = "interactive-abort-multiseat";
12240+
12241+
// Members 1 and 2 on attempt 1; member 3 on attempt 2 (a different attempt id).
12242+
let opened1 = open_interactive_for_test(session_id, key_group, &message, &included, 1, 1, 2)
12243+
.expect("member 1 opens attempt 1");
12244+
open_interactive_for_test(session_id, key_group, &message, &included, 1, 2, 2)
12245+
.expect("member 2 opens attempt 1");
12246+
open_interactive_for_test(session_id, key_group, &message, &included, 2, 3, 2)
12247+
.expect("member 3 opens attempt 2");
12248+
12249+
// Abort attempt 1: removes BOTH members on it; member 3 (attempt 2) is untouched.
12250+
let result = interactive_session_abort(InteractiveSessionAbortRequest {
12251+
session_id: session_id.to_string(),
12252+
attempt_id: Some(opened1.attempt_id.clone()),
12253+
})
12254+
.expect("abort attempt 1");
12255+
assert!(result.aborted, "abort removed live state");
12256+
12257+
let guard = state().expect("state").lock().expect("lock");
12258+
let session = guard.sessions.get(session_id).expect("session exists");
12259+
assert!(
12260+
!session.interactive_signing.contains_key(&1),
12261+
"member 1 (attempt 1) is aborted"
12262+
);
12263+
assert!(
12264+
!session.interactive_signing.contains_key(&2),
12265+
"member 2 (attempt 1) is aborted"
12266+
);
12267+
assert!(
12268+
session.interactive_signing.contains_key(&3),
12269+
"member 3 (attempt 2) survives the attempt-1 abort"
12270+
);
12271+
}
12272+
1218712273
#[test]
1218812274
fn interactive_round1_is_idempotent_until_consumed() {
1218912275
let _guard = lock_test_state();

0 commit comments

Comments
 (0)