Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
3ccbfdb
test(browser-session): expose raw lifecycle port side door
seonghobae Sep 10, 2026
2fde15e
test(browser-session): format lifecycle port hostile RED
seonghobae Sep 10, 2026
d4537bc
fix(browser-session): bind lifecycle I/O to aggregate-issued requests
seonghobae Sep 10, 2026
97e0a4d
test(browser-session): verify aggregate-issued lifecycle requests
seonghobae Sep 10, 2026
f4b6faa
test(browser-session): expose same-id lifecycle port spoof
seonghobae Sep 10, 2026
d838856
test(browser-session): update recovery port contract
seonghobae Sep 10, 2026
b6432ab
test(browser-session): update incarnation reuse port contract
seonghobae Sep 10, 2026
9caf9bb
style(browser-session): apply canonical rustfmt
seonghobae Sep 10, 2026
d43a4d8
test(browser-session): expose lifecycle preflight side effect
seonghobae Sep 10, 2026
9cde981
fix(browser-session): bind lifecycle port structurally
seonghobae Sep 10, 2026
db09d3b
test(browser-session): add create transaction RED
seonghobae Sep 10, 2026
a2e5b27
test(browser-session): enforce transaction and port encapsulation RED
seonghobae Sep 10, 2026
32f87d6
test(browser-session): keep adapter observations outside bound capabi…
seonghobae Sep 10, 2026
b3ff343
fix(browser-session): settle exact create transactions
seonghobae Sep 10, 2026
6be4109
docs(browser-session): record create transaction authority
seonghobae Sep 10, 2026
aecb402
docs(browser-session): trace exact create settlement
seonghobae Sep 10, 2026
729603a
docs(browser-session): model create settlement transaction
seonghobae Sep 10, 2026
b161a8a
docs(browser-session): trace destroy failure contract
seonghobae Sep 11, 2026
b721f6a
test(browser-session): retain transport-loss recovery handle
seonghobae Sep 11, 2026
89706a1
test(browser-session): reject adapter Debug capability escape
seonghobae Sep 11, 2026
bd66235
fix(browser-session): preserve transport recovery evidence
seonghobae Sep 11, 2026
f660ec8
fix(browser-session): bind authorized operations and abandonment
seonghobae Sep 11, 2026
6bf2c0d
docs(browser-session): trace authorized operation and recovery bounda…
seonghobae Sep 11, 2026
37b37cd
docs(browser-session): correct lifecycle trace and BiDi reference
seonghobae Sep 11, 2026
cc7ecf9
test(browser-session): require verified BiDi publication
seonghobae Sep 11, 2026
7106662
docs(browser-session): align BiDi standards trace
seonghobae Sep 11, 2026
bdd4b9c
test(browser-session): prove failed finish retains ownership
seonghobae Sep 11, 2026
f096c94
test(browser-session): preserve sibling recovery handles
seonghobae Sep 11, 2026
1f02e0b
test(browser-session): cover authorized operation identity contract
seonghobae Sep 11, 2026
d5046e7
test(browser-session): serialize abandonment counter assertions
seonghobae Sep 11, 2026
561993c
fix(browser-session): retain recovery ownership on failed completion
seonghobae Sep 11, 2026
6e23406
test(browser-session): prove sibling recovery projection
seonghobae Sep 11, 2026
9a1caaa
test(browser-session): retain owner after failed finish
seonghobae Sep 11, 2026
730d8af
style(browser-session): apply canonical formatting
seonghobae Sep 11, 2026
bc07436
test(repo): lock recovery and finish ownership contracts
seonghobae Sep 11, 2026
e82d721
docs(browser-session): align recovery and finish invariants
seonghobae Sep 11, 2026
5992e34
docs(trace): record sibling recovery and finish retry
seonghobae Sep 11, 2026
7763971
docs(uml): show recovery sibling and finish retry flows
seonghobae Sep 11, 2026
16f92dd
style(browser-session): format transaction fixture
seonghobae Sep 11, 2026
371ca37
style(browser-session): format preflight fixture
seonghobae Sep 11, 2026
916646d
style(browser-session): format transport recovery fixture
seonghobae Sep 11, 2026
bd1bd85
style(browser-session): format abandonment fixture
seonghobae Sep 11, 2026
509866a
style(browser-session): apply hosted rustfmt diagnostics
seonghobae Sep 11, 2026
da4d049
style(browser-session): apply hosted rustfmt diagnostics
seonghobae Sep 11, 2026
ba3656a
style(browser-session): apply hosted rustfmt diagnostics
seonghobae Sep 11, 2026
56a96ad
docs(product): currentize browser-session buyer gap baseline
seonghobae Sep 11, 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
1,115 changes: 848 additions & 267 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,153 @@
use std::cell::{Cell, RefCell};
use std::rc::Rc;

use originweave_browser_session::{
AuthorizedContextOperationError, AuthorizedContextOperationPort,
AuthorizedContextOperationRequest, BrowserSession, BrowserSessionError,
BrowserSessionIncarnation, DisposableContextCreateCompletion,
DisposableContextCreateCompletionError, DisposableContextCreateError,
DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest,
DisposableContextHandle, DisposableContextPort, DisposableIsolationId,
};
use originweave_core::{BrowserSessionId, BrowsingContextId};

struct OperationPort {
handle: Option<DisposableContextHandle>,
operation_calls: Rc<Cell<usize>>,
observed_operations: Rc<RefCell<Vec<&'static str>>>,
observed_sessions: Rc<RefCell<Vec<BrowserSessionId>>>,
observed_incarnations: Rc<RefCell<Vec<BrowserSessionIncarnation>>>,
fail_operation: Rc<Cell<bool>>,
}

impl DisposableContextPort for OperationPort {
fn create_disposable_context(
&mut self,
_request: &DisposableContextCreateRequest,
) -> Result<DisposableContextHandle, DisposableContextCreateError> {
self.handle
.take()
.ok_or(DisposableContextCreateError::CreateFailedClean)
}

fn complete_disposable_context_creation(
&mut self,
_completion: &DisposableContextCreateCompletion,
) -> Result<(), DisposableContextCreateCompletionError> {
Ok(())
}

fn destroy_disposable_context(
&mut self,
_request: &DisposableContextDestroyRequest,
) -> Result<(), DisposableContextDestroyError> {
Ok(())
}
}

impl AuthorizedContextOperationPort for OperationPort {
type Operation = &'static str;
type Output = BrowsingContextId;
type Error = ();

fn execute_authorized_context_operation(
&mut self,
request: &AuthorizedContextOperationRequest<Self::Operation>,
) -> Result<Self::Output, Self::Error> {
self.operation_calls.set(self.operation_calls.get() + 1);
self.observed_operations
.borrow_mut()
.push(*request.operation());
self.observed_sessions
.borrow_mut()
.push(request.browser_session());
self.observed_incarnations
.borrow_mut()
.push(request.incarnation());
if self.fail_operation.get() {
Err(())
} else {
Ok(request.context().browsing_context())
}
}
}

#[test]
fn authorized_operation_uses_exact_bound_port_and_rejects_stale_authority_before_io() {
let operation_calls = Rc::new(Cell::new(0));
let observed_operations = Rc::new(RefCell::new(Vec::new()));
let observed_sessions = Rc::new(RefCell::new(Vec::new()));
let observed_incarnations = Rc::new(RefCell::new(Vec::new()));
let fail_operation = Rc::new(Cell::new(false));
let context = BrowsingContextId::new(503).expect("valid browsing context");
let port = OperationPort {
handle: Some(DisposableContextHandle::new(
DisposableIsolationId::parse("operation-user-context-503").expect("valid isolation id"),
context,
)),
operation_calls: Rc::clone(&operation_calls),
observed_operations: Rc::clone(&observed_operations),
observed_sessions: Rc::clone(&observed_sessions),
observed_incarnations: Rc::clone(&observed_incarnations),
fail_operation: Rc::clone(&fail_operation),
};
let session_id = BrowserSessionId::new(503).expect("valid session id");
let session = BrowserSession::start(session_id).expect("incarnation capacity");
let incarnation = session.incarnation();
let mut bound = session.bind_lifecycle_port(port);

let authority = bound
.create_disposable_context()
.expect("accepted disposable context");
assert_eq!(
bound.execute_authorized_context_operation(&authority, "set-viewport"),
Ok(context)
);
assert_eq!(operation_calls.get(), 1);
assert_eq!(observed_operations.borrow().as_slice(), &["set-viewport"]);
assert_eq!(observed_sessions.borrow().as_slice(), &[session_id]);
assert_eq!(observed_incarnations.borrow().as_slice(), &[incarnation]);

fail_operation.set(true);
assert_eq!(
bound.execute_authorized_context_operation(&authority, "remote-failure"),
Err(AuthorizedContextOperationError::Adapter(()))
);
assert_eq!(operation_calls.get(), 2);
fail_operation.set(false);

let current = bound
.advance_context_epoch(context)
.expect("advance authority epoch");
assert_eq!(
bound.execute_authorized_context_operation(&authority, "stale-operation"),
Err(AuthorizedContextOperationError::BrowserSession(
BrowserSessionError::AuthorityMismatch
))
);
assert_eq!(
operation_calls.get(),
2,
"stale authority must fail before the bound adapter observes an operation"
);

assert_eq!(
bound.execute_authorized_context_operation(&current, "reconcile-liveness"),
Ok(context)
);
assert_eq!(operation_calls.get(), 3);
assert_eq!(
observed_operations.borrow().as_slice(),
&["set-viewport", "remote-failure", "reconcile-liveness"]
);
assert_eq!(
observed_sessions.borrow().as_slice(),
&[session_id, session_id, session_id],
"the purpose-bounded adapter must observe only the bound Browser Session identity"
);
assert_eq!(
observed_incarnations.borrow().as_slice(),
&[incarnation, incarnation, incarnation],
"the purpose-bounded adapter must observe only the bound Browser Session incarnation"
);
}
148 changes: 148 additions & 0 deletions crates/originweave-browser-session/tests/bound_session_abandonment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Mutex;

use originweave_browser_session::{
BrowserSession, BrowserSessionError, DisposableContextCreateCompletion,
DisposableContextCreateCompletionError, DisposableContextCreateError,
DisposableContextCreateRequest, DisposableContextDestroyError, DisposableContextDestroyRequest,
DisposableContextHandle, DisposableContextPort, DisposableIsolationId,
abandoned_bound_session_count,
};
use originweave_core::{BrowserSessionId, BrowsingContextId};

static ABANDONMENT_COUNTER_LOCK: Mutex<()> = Mutex::new(());

struct AbandonmentPort {
handle: Option<DisposableContextHandle>,
destroy_calls: Rc<Cell<usize>>,
}

impl DisposableContextPort for AbandonmentPort {
fn create_disposable_context(
&mut self,
_request: &DisposableContextCreateRequest,
) -> Result<DisposableContextHandle, DisposableContextCreateError> {
self.handle
.take()
.ok_or(DisposableContextCreateError::CreateFailedClean)
}

fn complete_disposable_context_creation(
&mut self,
_completion: &DisposableContextCreateCompletion,
) -> Result<(), DisposableContextCreateCompletionError> {
Ok(())
}

fn destroy_disposable_context(
&mut self,
_request: &DisposableContextDestroyRequest,
) -> Result<(), DisposableContextDestroyError> {
self.destroy_calls.set(self.destroy_calls.get() + 1);
Ok(())
}
}

fn port_for(context: u64, destroy_calls: &Rc<Cell<usize>>) -> AbandonmentPort {
AbandonmentPort {
handle: Some(DisposableContextHandle::new(
DisposableIsolationId::parse(&format!("abandoned-user-context-{context}"))
.expect("valid isolation id"),
BrowsingContextId::new(context).expect("valid browsing context"),
)),
destroy_calls: Rc::clone(destroy_calls),
}
}

#[test]
fn dropping_unresolved_bound_session_is_observable_without_implicit_browser_io() {
let _guard = ABANDONMENT_COUNTER_LOCK
.lock()
.expect("abandonment counter test lock");
let destroy_calls = Rc::new(Cell::new(0));
let before = abandoned_bound_session_count();
let session = BrowserSession::start(BrowserSessionId::new(504).expect("valid session id"))
.expect("incarnation capacity");
let mut bound = session.bind_lifecycle_port(port_for(504, &destroy_calls));
let _authority = bound
.create_disposable_context()
.expect("accepted disposable context");

drop(bound);

assert_eq!(
destroy_calls.get(),
0,
"Drop must never pretend synchronous browser cleanup succeeded"
);
assert!(
abandoned_bound_session_count() > before,
"unresolved bound-session abandonment must be observable to recovery/operability code"
);
}

#[test]
fn failed_finish_retains_same_bound_owner_for_cleanup_and_retry() {
let _guard = ABANDONMENT_COUNTER_LOCK
.lock()
.expect("abandonment counter test lock");
let destroy_calls = Rc::new(Cell::new(0));
let before = abandoned_bound_session_count();
let session = BrowserSession::start(BrowserSessionId::new(506).expect("valid session id"))
.expect("incarnation capacity");
let mut bound = session.bind_lifecycle_port(port_for(506, &destroy_calls));
let authority = bound
.create_disposable_context()
.expect("accepted disposable context");

assert_eq!(
bound.finish(),
Err(BrowserSessionError::ActiveContextRemains)
);
assert_eq!(
abandoned_bound_session_count(),
before,
"a failed deliberate finish must retain the bound lifecycle owner instead of dropping it as abandonment"
);
assert_eq!(
destroy_calls.get(),
0,
"failed finish validation must not perform implicit browser cleanup"
);

bound
.destroy_disposable_context(&authority)
.expect("the same bound lifecycle owner must remain available for cleanup");
assert_eq!(destroy_calls.get(), 1);
bound
.finish()
.expect("retry succeeds after proven destruction");
drop(bound);
assert_eq!(
abandoned_bound_session_count(),
before,
"successful retry must leave no abandonment signal"
);
}

#[test]
fn proven_destruction_can_finish_without_abandonment_path() {
let _guard = ABANDONMENT_COUNTER_LOCK
.lock()
.expect("abandonment counter test lock");
let destroy_calls = Rc::new(Cell::new(0));
let session = BrowserSession::start(BrowserSessionId::new(505).expect("valid session id"))
.expect("incarnation capacity");
let mut bound = session.bind_lifecycle_port(port_for(505, &destroy_calls));
let authority = bound
.create_disposable_context()
.expect("accepted disposable context");
bound
.destroy_disposable_context(&authority)
.expect("proven destruction");
bound
.finish()
.expect("end normally after proven destruction");
assert_eq!(destroy_calls.get(), 1);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::cell::Cell;
use std::fmt;
use std::rc::Rc;

use originweave_browser_session::{
BrowserSession, DisposableContextCreateCompletion, DisposableContextCreateCompletionError,
DisposableContextCreateError, DisposableContextCreateRequest, DisposableContextDestroyError,
DisposableContextDestroyRequest, DisposableContextHandle, DisposableContextPort,
};
use originweave_core::BrowserSessionId;

struct SideEffectingDebugPort {
debug_callbacks: Rc<Cell<usize>>,
}

impl fmt::Debug for SideEffectingDebugPort {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.debug_callbacks
.set(self.debug_callbacks.get().saturating_add(1));
formatter.write_str("adapter-secret-sentinel")
}
}

impl DisposableContextPort for SideEffectingDebugPort {
fn create_disposable_context(
&mut self,
_request: &DisposableContextCreateRequest,
) -> Result<DisposableContextHandle, DisposableContextCreateError> {
Err(DisposableContextCreateError::CreateFailedClean)
}

fn complete_disposable_context_creation(
&mut self,
_completion: &DisposableContextCreateCompletion,
) -> Result<(), DisposableContextCreateCompletionError> {
Ok(())
}

fn destroy_disposable_context(
&mut self,
_request: &DisposableContextDestroyRequest,
) -> Result<(), DisposableContextDestroyError> {
Ok(())
}
}

#[test]
fn bound_session_debug_never_executes_or_exposes_adapter_debug() {
let debug_callbacks = Rc::new(Cell::new(0));
let port = SideEffectingDebugPort {
debug_callbacks: Rc::clone(&debug_callbacks),
};
let session = BrowserSession::start(BrowserSessionId::new(502).expect("valid session id"))
.expect("incarnation capacity");
let bound = session.bind_lifecycle_port(port);

let rendered = format!("{bound:?}");

assert_eq!(
debug_callbacks.get(),
0,
"formatting a bound session must not execute adapter-owned Debug code"
);
assert!(
!rendered.contains("adapter-secret-sentinel"),
"bound-session diagnostics must not expose adapter-internal state"
);
}
Loading
Loading