Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
197d79d
fix(browser-session): quarantine uncertain lifecycle outcomes
seonghobae Sep 10, 2026
e71df6a
test(browser-session): lock recovery-required lifecycle contract
seonghobae Sep 10, 2026
0d4592d
docs(adr): distinguish clean and uncertain browser creation
seonghobae Sep 10, 2026
98117fb
docs(traceability): record recovery-required causal evidence
seonghobae Sep 10, 2026
889d196
docs(uml): model browser lifecycle recovery-required state
seonghobae Sep 10, 2026
09a733a
style: apply canonical rustfmt to Browser Session recovery repair
seonghobae Sep 10, 2026
6da6015
test: require session recovery after uncertain destroy
seonghobae Sep 10, 2026
ac8b8bb
fix: quarantine Browser Session after uncertain destroy
seonghobae Sep 10, 2026
29e6c0f
docs: quarantine unproven Browser Session destruction
seonghobae Sep 10, 2026
3a7a4c6
docs: trace destroy-failure recovery invariant
seonghobae Sep 10, 2026
1579be8
docs: require recovery after unproven Browser Session cleanup
seonghobae Sep 10, 2026
376e85c
test: bind uncertain cleanup recovery to repository contracts
seonghobae Sep 10, 2026
f5780fb
test: satisfy strict recovery clippy contract
seonghobae Sep 10, 2026
0893414
docs: document Browser Session private invariants
seonghobae Sep 10, 2026
ab84a54
test: require phase-specific browser lifecycle errors
seonghobae Sep 10, 2026
cd5e2b5
fix: type browser lifecycle phase failures
seonghobae Sep 10, 2026
843cb40
test: use phase-specific lifecycle failures
seonghobae Sep 10, 2026
d9ec4aa
docs: make lifecycle failure phases explicit
seonghobae Sep 10, 2026
8431008
docs: trace phase-specific lifecycle failures
seonghobae Sep 10, 2026
98a28db
docs: show phase-specific lifecycle failures
seonghobae Sep 10, 2026
ab04f95
test: align Browser Session lifecycle contract
seonghobae Sep 10, 2026
ec14596
test: expose sequential Browser Session authority reuse
seonghobae Sep 10, 2026
e37a35a
fix: bind Browser Session recovery to incarnation evidence
seonghobae Sep 10, 2026
2bd4734
test: preserve recovery evidence across transport loss
seonghobae Sep 10, 2026
fa048fb
test: require port-scoped session incarnation
seonghobae Sep 10, 2026
0e2e754
test: bind lifecycle contracts to recovery and incarnation evidence
seonghobae Sep 10, 2026
518d6c8
docs: define Browser Session incarnation and recovery evidence
seonghobae Sep 10, 2026
f2295bc
docs: trace Browser Session recovery and ABA repair
seonghobae Sep 10, 2026
09a1de9
docs: model incarnation and orthogonal recovery state
seonghobae Sep 10, 2026
ad9530a
style: apply canonical Browser Session rustfmt
seonghobae Sep 10, 2026
110eb33
fix: reject unknown epoch advance without mutation
seonghobae Sep 10, 2026
6eacfe4
test: close Browser Session coverage edges
seonghobae Sep 10, 2026
3aa113b
style: apply canonical Rust formatting
seonghobae Sep 10, 2026
0c6ed89
test: cover incarnation exhaustion without macro branch
seonghobae Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
668 changes: 438 additions & 230 deletions crates/originweave-browser-session/src/lib.rs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use originweave_browser_session::{
BrowserSession, BrowserSessionError, BrowserSessionIncarnation, BrowserSessionRecoveryEvidence,
BrowserSessionState, DisposableContextCreateError, DisposableContextDestroyError,
DisposableContextHandle, DisposableContextPort, DisposableIsolationId,
};
use originweave_core::{BrowserSessionId, BrowsingContextId};

#[derive(Debug)]
struct FailingDestroyPort {
next_handle: DisposableContextHandle,
create_calls: usize,
destroy_calls: usize,
}

impl FailingDestroyPort {
fn new(context: u64, isolation: &str) -> Result<Self, &'static str> {
let isolation = DisposableIsolationId::parse(isolation)
.map_err(|_| "static fixture isolation id must be valid")?;
let browsing_context = BrowsingContextId::new(context)
.map_err(|_| "static fixture browsing context id must be valid")?;
Ok(Self {
next_handle: DisposableContextHandle::new(isolation, browsing_context),
create_calls: 0,
destroy_calls: 0,
})
}
}

impl DisposableContextPort for FailingDestroyPort {
fn create_disposable_context(
&mut self,
_browser_session: BrowserSessionId,
_incarnation: BrowserSessionIncarnation,
) -> Result<DisposableContextHandle, DisposableContextCreateError> {
self.create_calls += 1;
Ok(self.next_handle.clone())
}

fn destroy_disposable_context(
&mut self,
_browser_session: BrowserSessionId,
_incarnation: BrowserSessionIncarnation,
_context: &DisposableContextHandle,
) -> Result<(), DisposableContextDestroyError> {
self.destroy_calls += 1;
Err(DisposableContextDestroyError::DestroyFailed)
}
}

/// An unproven destroy must retain exact recovery evidence and reject later normal authority.
#[test]
fn destroy_failure_requires_recovery_before_any_new_authority() -> Result<(), &'static str> {
let session_id = BrowserSessionId::new(501)
.map_err(|_| "static fixture browser session id must be valid")?;
let context_id = BrowsingContextId::new(5010)
.map_err(|_| "static fixture browsing context id must be valid")?;
let expected_isolation = DisposableIsolationId::parse("user-context-501")
.map_err(|_| "static fixture recovery isolation id must be valid")?;
let expected_handle = DisposableContextHandle::new(expected_isolation, context_id);
let mut session = BrowserSession::start(session_id)
.map_err(|_| "browser session incarnation must be available")?;
let mut failing_port = FailingDestroyPort::new(5010, "user-context-501")?;

let authority = session
.create_disposable_context(&mut failing_port)
.map_err(|_| "fixture disposable context creation must succeed")?;
assert_eq!(
session.destroy_disposable_context(&authority, &mut failing_port),
Err(BrowserSessionError::ContextDestructionFailed)
);
assert_eq!(failing_port.destroy_calls, 1);
assert_eq!(session.state(), BrowserSessionState::RecoveryRequired);
assert_eq!(
session.recovery_evidence(),
&[BrowserSessionRecoveryEvidence::UnprovenDestruction(
expected_handle
)]
);
assert!(!session.transport_is_lost());

assert!(session.record_transport_loss());
assert!(session.transport_is_lost());
assert_eq!(session.state(), BrowserSessionState::RecoveryRequired);
assert!(!session.record_transport_loss());

let mut later_port = FailingDestroyPort::new(5011, "user-context-501-later")?;
assert_eq!(
session.create_disposable_context(&mut later_port),
Err(BrowserSessionError::SessionNotActive)
);
assert_eq!(later_port.create_calls, 0);
assert_eq!(
session.presentation_authority(context_id),
Err(BrowserSessionError::SessionNotActive)
);
assert_eq!(
session.advance_context_epoch(context_id),
Err(BrowserSessionError::SessionNotActive)
);
assert_eq!(session.end(), Err(BrowserSessionError::SessionNotActive));
Ok(())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use originweave_browser_session::{
BrowserSession, BrowserSessionError, BrowserSessionIncarnation, DisposableContextCreateError,
DisposableContextDestroyError, DisposableContextHandle, DisposableContextPort,
DisposableIsolationId,
};
use originweave_core::{BrowserSessionId, BrowsingContextId};

#[derive(Debug)]
struct ReusingPort {
handle: DisposableContextHandle,
create_incarnations: Vec<BrowserSessionIncarnation>,
destroy_incarnations: Vec<BrowserSessionIncarnation>,
}

impl ReusingPort {
fn new(context: u64, isolation: &str) -> Result<Self, &'static str> {
let isolation = DisposableIsolationId::parse(isolation)
.map_err(|_| "static fixture isolation id must be valid")?;
let browsing_context = BrowsingContextId::new(context)
.map_err(|_| "static fixture browsing context id must be valid")?;
Ok(Self {
handle: DisposableContextHandle::new(isolation, browsing_context),
create_incarnations: Vec::new(),
destroy_incarnations: Vec::new(),
})
}
}

impl DisposableContextPort for ReusingPort {
fn create_disposable_context(
&mut self,
_browser_session: BrowserSessionId,
incarnation: BrowserSessionIncarnation,
) -> Result<DisposableContextHandle, DisposableContextCreateError> {
self.create_incarnations.push(incarnation);
Ok(self.handle.clone())
}

fn destroy_disposable_context(
&mut self,
_browser_session: BrowserSessionId,
incarnation: BrowserSessionIncarnation,
_context: &DisposableContextHandle,
) -> Result<(), DisposableContextDestroyError> {
self.destroy_incarnations.push(incarnation);
Ok(())
}
}

/// A retained authority from a completed aggregate must not become valid again after identifier reuse.
#[test]
fn stale_authority_cannot_cross_sequential_session_incarnations() -> Result<(), &'static str> {
let session_id = BrowserSessionId::new(701)
.map_err(|_| "static fixture browser session id must be valid")?;

let mut port_a = ReusingPort::new(7010, "user-context-reused")?;
let mut session_a = BrowserSession::start(session_id)
.map_err(|_| "first browser session incarnation must be available")?;
let authority_a = session_a
.create_disposable_context(&mut port_a)
.map_err(|_| "first disposable context creation must succeed")?;
session_a
.destroy_disposable_context(&authority_a, &mut port_a)
.map_err(|_| "first disposable context destruction must succeed")?;
session_a
.end()
.map_err(|_| "first browser session must end normally")?;

let mut port_b = ReusingPort::new(7010, "user-context-reused")?;
let mut session_b = BrowserSession::start(session_id)
.map_err(|_| "second browser session incarnation must be available")?;
let authority_b = session_b
.create_disposable_context(&mut port_b)
.map_err(|_| "second disposable context creation must succeed")?;

assert_ne!(session_a.incarnation(), session_b.incarnation());
assert_eq!(port_a.create_incarnations, vec![session_a.incarnation()]);
assert_eq!(port_b.create_incarnations, vec![session_b.incarnation()]);
assert_eq!(
session_b.destroy_disposable_context(&authority_a, &mut port_b),
Err(BrowserSessionError::AuthorityMismatch)
);
assert!(port_b.destroy_incarnations.is_empty());

session_b
.destroy_disposable_context(&authority_b, &mut port_b)
.map_err(|_| "current incarnation authority must remain valid")?;
assert_eq!(port_b.destroy_incarnations, vec![session_b.incarnation()]);
Ok(())
}
Loading
Loading