Update public browser login session flow

This commit is contained in:
Michel Paulissen 2026-07-04 09:37:17 +02:00
parent afd7456423
commit cec080aa98
4 changed files with 388 additions and 29 deletions

View file

@ -1,7 +1,7 @@
{ {
"kind": "disasmer-filtered-public-tree", "kind": "disasmer-filtered-public-tree",
"source_commit": "09993cb75187f1e8798d6429946085e167059e9f", "source_commit": "f3285b63dd9e8a320e8ca1d1dd188aa18fa9a59c",
"release_name": "dryrun-09993cb75187", "release_name": "dryrun-f3285b63dd9e",
"filtered_out": [ "filtered_out": [
"private/**", "private/**",
"experiments/**", "experiments/**",

View file

@ -617,6 +617,8 @@ struct LoginCompletionBoundaryEvidence {
cli_contacted_coordinator: bool, cli_contacted_coordinator: bool,
coordinator_address: String, coordinator_address: String,
scoped_cli_session_received: bool, scoped_cli_session_received: bool,
local_cli_session_file_written: bool,
provider_tokens_persisted_locally: bool,
provider_tokens_exposed_to_cli: bool, provider_tokens_exposed_to_cli: bool,
provider_tokens_sent_to_nodes: bool, provider_tokens_sent_to_nodes: bool,
coordinator_session_requests: u64, coordinator_session_requests: u64,
@ -724,6 +726,21 @@ struct ProjectConfig {
coordinator: Option<String>, coordinator: Option<String>,
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct StoredCliSession {
kind: String,
coordinator: String,
tenant: String,
project: String,
user: String,
cli_session_credential_kind: String,
token_expiry_posture: String,
expires_at: Option<String>,
provider_tokens_exposed_to_cli: bool,
provider_tokens_sent_to_nodes: bool,
created_at_unix_seconds: u64,
}
fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> { fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
let config = read_project_config(&cwd)?; let config = read_project_config(&cwd)?;
let coordinator = args.scope.coordinator.or_else(|| { let coordinator = args.scope.coordinator.or_else(|| {
@ -737,7 +754,7 @@ fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
"cwd": cwd, "cwd": cwd,
"coordinator": coordinator, "coordinator": coordinator,
"coordinator_reachability": coordinator_reachability, "coordinator_reachability": coordinator_reachability,
"auth": auth_state_value(), "auth": auth_state_value(&cwd)?,
"project": config, "project": config,
"dependencies": { "dependencies": {
"cargo": command_available("cargo"), "cargo": command_available("cargo"),
@ -826,13 +843,61 @@ fn ping_coordinator(coordinator: &str, timeout: Duration) -> Result<Value> {
fn auth_status_report(args: AuthStatusArgs, cwd: PathBuf) -> Result<Value> { fn auth_status_report(args: AuthStatusArgs, cwd: PathBuf) -> Result<Value> {
let config = read_project_config(&cwd)?; let config = read_project_config(&cwd)?;
let stored_session = read_cli_session(&cwd)?;
let active_coordinator = args
.scope
.coordinator
.clone()
.or_else(|| {
config
.as_ref()
.and_then(|config| config.coordinator.clone())
})
.or_else(|| {
stored_session
.as_ref()
.map(|session| session.coordinator.clone())
})
.unwrap_or_else(default_operator_endpoint);
let tenant = effective_scope_value(
&args.scope.tenant,
config
.as_ref()
.map(|config| config.tenant.as_str())
.or_else(|| {
stored_session
.as_ref()
.map(|session| session.tenant.as_str())
}),
"tenant",
);
let project = effective_scope_value(
&args.scope.project,
config
.as_ref()
.map(|config| config.project.as_str())
.or_else(|| {
stored_session
.as_ref()
.map(|session| session.project.as_str())
}),
"project",
);
let principal = effective_scope_value(
&args.scope.user,
config
.as_ref()
.map(|config| config.user.as_str())
.or_else(|| stored_session.as_ref().map(|session| session.user.as_str())),
"user",
);
Ok(json!({ Ok(json!({
"command": "auth status", "command": "auth status",
"active_coordinator": args.scope.coordinator.or_else(|| config.as_ref().and_then(|config| config.coordinator.clone())).unwrap_or_else(default_operator_endpoint), "active_coordinator": active_coordinator,
"principal": args.scope.user, "principal": principal,
"tenant": args.scope.tenant, "tenant": tenant,
"project": args.scope.project, "project": project,
"session": auth_state_value(), "session": auth_state_value(&cwd)?,
"project_config": config, "project_config": config,
})) }))
} }
@ -2739,7 +2804,10 @@ fn run_cli() -> Result<()> {
} }
Commands::Run(args) => { Commands::Run(args) => {
let json_output = args.json; let json_output = args.json;
let report = run_report(args, std::env::current_dir()?, session_from_env())?; let cwd = std::env::current_dir()?;
let session_project = args.project.clone().unwrap_or_else(|| cwd.clone());
let session = session_from_sources(&session_project)?;
let report = run_report(args, cwd, session)?;
emit_report(&report, json_output)?; emit_report(&report, json_output)?;
} }
Commands::Node { Commands::Node {
@ -2912,6 +2980,29 @@ fn write_project_config(project: &Path, config: &ProjectConfig) -> Result<()> {
.with_context(|| format!("failed to write {}", file.display())) .with_context(|| format!("failed to write {}", file.display()))
} }
fn read_cli_session(project: &Path) -> Result<Option<StoredCliSession>> {
let file = session_config_file(project);
if !file.exists() {
return Ok(None);
}
let bytes =
std::fs::read(&file).with_context(|| format!("failed to read {}", file.display()))?;
let session = serde_json::from_slice(&bytes)
.with_context(|| format!("failed to parse {}", file.display()))?;
Ok(Some(session))
}
fn write_cli_session(project: &Path, session: &StoredCliSession) -> Result<PathBuf> {
let file = session_config_file(project);
if let Some(parent) = file.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
std::fs::write(&file, serde_json::to_vec_pretty(session)?)
.with_context(|| format!("failed to write {}", file.display()))?;
Ok(file)
}
fn effective_project_scope(scope: &CliScopeArgs, config: Option<&ProjectConfig>) -> CliScopeArgs { fn effective_project_scope(scope: &CliScopeArgs, config: Option<&ProjectConfig>) -> CliScopeArgs {
CliScopeArgs { CliScopeArgs {
coordinator: scope coordinator: scope
@ -2949,30 +3040,48 @@ fn effective_scope_value(
} }
} }
fn auth_state_value() -> Value { fn auth_state_value(cwd: &Path) -> Result<Value> {
match session_from_env() { match session_from_env() {
CliSession::Anonymous => json!({ CliSession::Anonymous => {
"kind": "anonymous", if let Some(session) = read_cli_session(cwd)? {
"authenticated": false, return Ok(json!({
"source": "environment", "kind": session.kind,
"token_expiry_posture": "no_session", "authenticated": true,
}), "source": "session_file",
"coordinator": session.coordinator,
"tenant": session.tenant,
"project": session.project,
"principal": session.user,
"cli_session_credential_kind": session.cli_session_credential_kind,
"provider_tokens_exposed_to_cli": session.provider_tokens_exposed_to_cli,
"provider_tokens_exposed_to_nodes": session.provider_tokens_sent_to_nodes,
"expires_at": session.expires_at,
"token_expiry_posture": session.token_expiry_posture,
}));
}
Ok(json!({
"kind": "anonymous",
"authenticated": false,
"source": "environment",
"token_expiry_posture": "no_session",
}))
}
CliSession::HumanSession => { CliSession::HumanSession => {
let expires_at = std::env::var("DISASMER_TOKEN_EXPIRES_AT").ok(); let expires_at = std::env::var("DISASMER_TOKEN_EXPIRES_AT").ok();
json!({ Ok(json!({
"kind": "human", "kind": "human",
"authenticated": true, "authenticated": true,
"source": "DISASMER_TOKEN", "source": "DISASMER_TOKEN",
"provider_tokens_exposed_to_nodes": false, "provider_tokens_exposed_to_nodes": false,
"expires_at": expires_at, "expires_at": expires_at,
"token_expiry_posture": if expires_at.is_some() { "expires_at" } else { "unknown_env_token" }, "token_expiry_posture": if expires_at.is_some() { "expires_at" } else { "unknown_env_token" },
}) }))
} }
CliSession::AgentPublicKey { CliSession::AgentPublicKey {
agent, agent,
public_key_fingerprint, public_key_fingerprint,
browser_interaction_required, browser_interaction_required,
} => json!({ } => Ok(json!({
"kind": "agent_public_key", "kind": "agent_public_key",
"authenticated": true, "authenticated": true,
"agent": agent, "agent": agent,
@ -2980,7 +3089,7 @@ fn auth_state_value() -> Value {
"public_key_fingerprint": public_key_fingerprint, "public_key_fingerprint": public_key_fingerprint,
"browser_interaction_required": browser_interaction_required, "browser_interaction_required": browser_interaction_required,
"token_expiry_posture": "not_applicable_public_key", "token_expiry_posture": "not_applicable_public_key",
}), })),
} }
} }
@ -3682,6 +3791,20 @@ fn execute_browser_login_completion_for_plan(
.and_then(Value::as_bool) .and_then(Value::as_bool)
.unwrap_or(true); .unwrap_or(true);
let provider_tokens_exposed_to_cli = contains_provider_token_field(&coordinator_response); let provider_tokens_exposed_to_cli = contains_provider_token_field(&coordinator_response);
let local_cli_session_file_written = if scoped_cli_session_received {
let cwd = std::env::current_dir()?;
let stored_session = stored_cli_session_from_login_response(
&args,
&coordinator,
&coordinator_response,
provider_tokens_exposed_to_cli,
provider_tokens_sent_to_nodes,
);
write_cli_session(&cwd, &stored_session)?;
true
} else {
false
};
Ok(LoginCompletionReport { Ok(LoginCompletionReport {
plan, plan,
@ -3689,6 +3812,8 @@ fn execute_browser_login_completion_for_plan(
cli_contacted_coordinator: true, cli_contacted_coordinator: true,
coordinator_address: coordinator, coordinator_address: coordinator,
scoped_cli_session_received, scoped_cli_session_received,
local_cli_session_file_written,
provider_tokens_persisted_locally: false,
provider_tokens_exposed_to_cli, provider_tokens_exposed_to_cli,
provider_tokens_sent_to_nodes, provider_tokens_sent_to_nodes,
coordinator_session_requests: session.requests(), coordinator_session_requests: session.requests(),
@ -3697,6 +3822,54 @@ fn execute_browser_login_completion_for_plan(
}) })
} }
fn stored_cli_session_from_login_response(
args: &LoginArgs,
coordinator: &str,
coordinator_response: &Value,
provider_tokens_exposed_to_cli: bool,
provider_tokens_sent_to_nodes: bool,
) -> StoredCliSession {
let session = coordinator_response.get("session").unwrap_or(&Value::Null);
let expires_at = session
.get("expires_at")
.or_else(|| session.get("token_expires_at"))
.and_then(Value::as_str)
.map(str::to_owned);
StoredCliSession {
kind: "human".to_owned(),
coordinator: coordinator.to_owned(),
tenant: session
.get("tenant")
.and_then(Value::as_str)
.unwrap_or(&args.tenant)
.to_owned(),
project: session
.get("project")
.and_then(Value::as_str)
.unwrap_or(&args.project)
.to_owned(),
user: session
.get("user")
.and_then(Value::as_str)
.unwrap_or(&args.user)
.to_owned(),
cli_session_credential_kind: session
.get("cli_session_credential_kind")
.and_then(Value::as_str)
.unwrap_or("CliDeviceSession")
.to_owned(),
token_expiry_posture: if expires_at.is_some() {
"expires_at".to_owned()
} else {
"unknown_coordinator_session".to_owned()
},
expires_at,
provider_tokens_exposed_to_cli,
provider_tokens_sent_to_nodes,
created_at_unix_seconds: unix_timestamp_seconds(),
}
}
fn execute_interactive_browser_login(mut args: LoginArgs) -> Result<LoginCompletionReport> { fn execute_interactive_browser_login(mut args: LoginArgs) -> Result<LoginCompletionReport> {
args.browser = true; args.browser = true;
let callback_path = browser_callback_url(); let callback_path = browser_callback_url();
@ -3868,6 +4041,13 @@ fn browser_callback_timeout() -> Duration {
Duration::from_secs(seconds) Duration::from_secs(seconds)
} }
fn unix_timestamp_seconds() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
fn read_browser_callback_request(stream: &TcpStream) -> Result<BrowserCallback> { fn read_browser_callback_request(stream: &TcpStream) -> Result<BrowserCallback> {
let mut reader = BufReader::new( let mut reader = BufReader::new(
stream stream
@ -4724,6 +4904,17 @@ fn session_from_env() -> CliSession {
CliSession::Anonymous CliSession::Anonymous
} }
fn session_from_sources(project: &Path) -> Result<CliSession> {
let session = session_from_env();
if session.is_authenticated() {
return Ok(session);
}
if read_cli_session(project)?.is_some() {
return Ok(CliSession::HumanSession);
}
Ok(CliSession::Anonymous)
}
fn add_workflow_actor_fields(request: &mut Value, session: &CliSession, fallback_user: &str) { fn add_workflow_actor_fields(request: &mut Value, session: &CliSession, fallback_user: &str) {
let Value::Object(map) = request else { let Value::Object(map) = request else {
return; return;
@ -5586,6 +5777,60 @@ mod tests {
}))); })));
} }
#[test]
fn stored_browser_login_session_omits_provider_token_values() {
let Cli {
command: Commands::Login(args),
} = parse(&[
"disasmer",
"login",
"--browser",
"--coordinator",
"https://coord.example.test",
"--tenant",
"tenant-cli",
"--project-id",
"project-cli",
"--user",
"user-cli",
])
else {
panic!("wrong command");
};
let stored = stored_cli_session_from_login_response(
&args,
"https://coord.example.test",
&json!({
"session": {
"tenant": "tenant-live",
"project": "project-live",
"user": "user-live",
"cli_session_credential_kind": "CliDeviceSession",
"expires_at": "2026-07-04T00:00:00Z",
"access_token": "provider-secret",
"id_token": "provider-id-token",
"provider_tokens_sent_to_nodes": false
}
}),
true,
false,
);
let serialized = serde_json::to_string(&stored).unwrap();
assert_eq!(stored.kind, "human");
assert_eq!(stored.coordinator, "https://coord.example.test");
assert_eq!(stored.tenant, "tenant-live");
assert_eq!(stored.project, "project-live");
assert_eq!(stored.user, "user-live");
assert_eq!(stored.token_expiry_posture, "expires_at");
assert!(stored.provider_tokens_exposed_to_cli);
assert!(!stored.provider_tokens_sent_to_nodes);
assert!(!serialized.contains("provider-secret"));
assert!(!serialized.contains("provider-id-token"));
assert!(!serialized.contains("access_token"));
assert!(!serialized.contains("id_token"));
}
#[test] #[test]
fn agent_enroll_uses_public_key_without_browser_each_run() { fn agent_enroll_uses_public_key_without_browser_each_run() {
let Cli { let Cli {
@ -5977,6 +6222,57 @@ mod tests {
assert_eq!(report["coordinator_reachability"]["response"]["epoch"], 42); assert_eq!(report["coordinator_reachability"]["response"]["epoch"], 42);
} }
#[test]
fn auth_status_reads_stored_cli_session_without_provider_tokens() {
let temp = tempfile::tempdir().unwrap();
write_cli_session(
temp.path(),
&StoredCliSession {
kind: "human".to_owned(),
coordinator: "https://coord.example.test".to_owned(),
tenant: "tenant-session".to_owned(),
project: "project-session".to_owned(),
user: "user-session".to_owned(),
cli_session_credential_kind: "CliDeviceSession".to_owned(),
token_expiry_posture: "expires_at".to_owned(),
expires_at: Some("2026-07-04T00:00:00Z".to_owned()),
provider_tokens_exposed_to_cli: false,
provider_tokens_sent_to_nodes: false,
created_at_unix_seconds: 1,
},
)
.unwrap();
let report = auth_status_report(
AuthStatusArgs {
scope: CliScopeArgs {
coordinator: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
},
temp.path().to_path_buf(),
)
.unwrap();
assert_eq!(report["active_coordinator"], "https://coord.example.test");
assert_eq!(report["principal"], "user-session");
assert_eq!(report["tenant"], "tenant-session");
assert_eq!(report["project"], "project-session");
assert_eq!(report["session"]["kind"], "human");
assert_eq!(report["session"]["source"], "session_file");
assert_eq!(report["session"]["authenticated"], true);
assert_eq!(
report["session"]["cli_session_credential_kind"],
"CliDeviceSession"
);
assert_eq!(report["session"]["token_expiry_posture"], "expires_at");
assert_eq!(report["session"]["provider_tokens_exposed_to_cli"], false);
assert_eq!(report["session"]["provider_tokens_exposed_to_nodes"], false);
}
#[test] #[test]
fn cli_first_mvp_command_surface_parses() { fn cli_first_mvp_command_surface_parses() {
for args in [ for args in [

View file

@ -9,7 +9,10 @@ const path = require("path");
const repo = path.resolve(__dirname, ".."); const repo = path.resolve(__dirname, "..");
const tmp = path.join(repo, "target", "acceptance", "tmp", "cli-browser-login-flow"); const tmp = path.join(repo, "target", "acceptance", "tmp", "cli-browser-login-flow");
const project = path.join(tmp, "project");
fs.rmSync(tmp, { recursive: true, force: true });
fs.mkdirSync(tmp, { recursive: true }); fs.mkdirSync(tmp, { recursive: true });
fs.mkdirSync(project, { recursive: true });
function writeOpener() { function writeOpener() {
const opener = path.join(tmp, "browser-opener.js"); const opener = path.join(tmp, "browser-opener.js");
@ -86,6 +89,7 @@ async function startCoordinator() {
user: request.user, user: request.user,
browser_credential_kind: "BrowserSession", browser_credential_kind: "BrowserSession",
cli_session_credential_kind: "CliDeviceSession", cli_session_credential_kind: "CliDeviceSession",
expires_at: "2026-07-04T00:00:00Z",
provider_tokens_sent_to_nodes: false, provider_tokens_sent_to_nodes: false,
flow: { flow: {
authorization_url: "http://127.0.0.1:1/application/o/authorize/", authorization_url: "http://127.0.0.1:1/application/o/authorize/",
@ -115,8 +119,19 @@ async function startCoordinator() {
function runDisasmer(args, env) { function runDisasmer(args, env) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const child = cp.spawn("cargo", args, { const child = cp.spawn("cargo", [
cwd: repo, "run",
"-q",
"--manifest-path",
path.join(repo, "Cargo.toml"),
"-p",
"disasmer-cli",
"--bin",
"disasmer",
"--",
...args,
], {
cwd: project,
env, env,
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
}); });
@ -157,13 +172,6 @@ function runDisasmer(args, env) {
try { try {
const stdout = await runDisasmer( const stdout = await runDisasmer(
[ [
"run",
"-q",
"-p",
"disasmer-cli",
"--bin",
"disasmer",
"--",
"login", "login",
"--browser", "--browser",
"--json", "--json",
@ -190,10 +198,45 @@ function runDisasmer(args, env) {
assert.strictEqual(report.plan.coordinator, coordinator.url); assert.strictEqual(report.plan.coordinator, coordinator.url);
assert.strictEqual(report.boundary.cli_contacted_coordinator, true); assert.strictEqual(report.boundary.cli_contacted_coordinator, true);
assert.strictEqual(report.boundary.scoped_cli_session_received, true); assert.strictEqual(report.boundary.scoped_cli_session_received, true);
assert.strictEqual(report.boundary.local_cli_session_file_written, true);
assert.strictEqual(report.boundary.provider_tokens_persisted_locally, false);
assert.strictEqual(report.boundary.provider_tokens_exposed_to_cli, false); assert.strictEqual(report.boundary.provider_tokens_exposed_to_cli, false);
assert.strictEqual(report.boundary.provider_tokens_sent_to_nodes, false); assert.strictEqual(report.boundary.provider_tokens_sent_to_nodes, false);
assert.strictEqual(report.boundary.coordinator_session_requests, 1); assert.strictEqual(report.boundary.coordinator_session_requests, 1);
assert.strictEqual(coordinator.requests.length, 1); assert.strictEqual(coordinator.requests.length, 1);
const sessionFile = path.join(project, ".disasmer", "session.json");
assert(fs.existsSync(sessionFile), "browser login should write a local CLI session file");
const sessionText = fs.readFileSync(sessionFile, "utf8");
const session = JSON.parse(sessionText);
assert.strictEqual(session.kind, "human");
assert.strictEqual(session.coordinator, coordinator.url);
assert.strictEqual(session.tenant, "tenant-smoke");
assert.strictEqual(session.project, "project-smoke");
assert.strictEqual(session.user, "user-smoke");
assert.strictEqual(session.cli_session_credential_kind, "CliDeviceSession");
assert.strictEqual(session.token_expiry_posture, "expires_at");
assert.strictEqual(session.expires_at, "2026-07-04T00:00:00Z");
assert.strictEqual(session.provider_tokens_exposed_to_cli, false);
assert.strictEqual(session.provider_tokens_sent_to_nodes, false);
assert.doesNotMatch(
sessionText,
/access_token|refresh_token|id_token|provider-secret|browser-smoke-code|Bearer/
);
const authStatus = JSON.parse(
await runDisasmer(["auth", "status", "--json"], {
...process.env,
})
);
assert.strictEqual(authStatus.active_coordinator, coordinator.url);
assert.strictEqual(authStatus.principal, "user-smoke");
assert.strictEqual(authStatus.tenant, "tenant-smoke");
assert.strictEqual(authStatus.project, "project-smoke");
assert.strictEqual(authStatus.session.kind, "human");
assert.strictEqual(authStatus.session.source, "session_file");
assert.strictEqual(authStatus.session.provider_tokens_exposed_to_cli, false);
assert.strictEqual(authStatus.session.provider_tokens_exposed_to_nodes, false);
} finally { } finally {
if (coordinator.requests.length === 0) { if (coordinator.requests.length === 0) {
await coordinator.close().catch(() => {}); await coordinator.close().catch(() => {});

View file

@ -36,6 +36,7 @@ const coordinator = read("crates/disasmer-coordinator/src/service.rs");
const cliFirstAcceptance = read("scripts/acceptance-cli-first.sh"); const cliFirstAcceptance = read("scripts/acceptance-cli-first.sh");
const outputModeSmoke = read("scripts/cli-output-mode-smoke.js"); const outputModeSmoke = read("scripts/cli-output-mode-smoke.js");
const errorExitSmoke = read("scripts/cli-error-exit-smoke.js"); const errorExitSmoke = read("scripts/cli-error-exit-smoke.js");
const browserLoginFlowSmoke = read("scripts/cli-browser-login-flow-smoke.js");
expect(criteria, "header", /^# Disasmer CLI-First MVP Acceptance Criteria/m); expect(criteria, "header", /^# Disasmer CLI-First MVP Acceptance Criteria/m);
expect( expect(
@ -143,6 +144,12 @@ for (const [name, pattern] of [
["top-level logout command", /enum Commands[\s\S]*Logout\(AuthLogoutArgs\)[\s\S]*Auth \{/], ["top-level logout command", /enum Commands[\s\S]*Logout\(AuthLogoutArgs\)[\s\S]*Auth \{/],
["login non-interactive flag", /struct LoginArgs[\s\S]*non_interactive: bool/], ["login non-interactive flag", /struct LoginArgs[\s\S]*non_interactive: bool/],
["run non-interactive flag", /struct RunArgs[\s\S]*non_interactive: bool/], ["run non-interactive flag", /struct RunArgs[\s\S]*non_interactive: bool/],
["stored CLI session model", /struct StoredCliSession[\s\S]*cli_session_credential_kind[\s\S]*provider_tokens_exposed_to_cli[\s\S]*provider_tokens_sent_to_nodes/],
["read CLI session helper", /fn read_cli_session\(project: &Path\) -> Result<Option<StoredCliSession>>/],
["write CLI session helper", /fn write_cli_session\(project: &Path, session: &StoredCliSession\) -> Result<PathBuf>/],
["session source fallback", /fn session_from_sources\(project: &Path\) -> Result<CliSession>[\s\S]*read_cli_session\(project\)\?\.is_some\(\)/],
["browser login writes local CLI session", /local_cli_session_file_written[\s\S]*write_cli_session\(&cwd, &stored_session\)\?/],
["browser login does not persist provider tokens", /provider_tokens_persisted_locally:\s*false/],
["non-interactive auth report", /fn non_interactive_auth_machine_error[\s\S]*browser_opened[\s\S]*false/], ["non-interactive auth report", /fn non_interactive_auth_machine_error[\s\S]*browser_opened[\s\S]*false/],
["human report renderer", /fn human_report\(value: &Value\) -> String/], ["human report renderer", /fn human_report\(value: &Value\) -> String/],
["shared report emitter", /fn emit_report<T: Serialize>\(report: &T, json_output: bool\) -> Result<\(\)>/], ["shared report emitter", /fn emit_report<T: Serialize>\(report: &T, json_output: bool\) -> Result<\(\)>/],
@ -171,6 +178,8 @@ for (const [name, pattern] of [
["CLI primary workflow help coverage", /fn top_level_help_exposes_primary_workflow_without_auth\(\)/], ["CLI primary workflow help coverage", /fn top_level_help_exposes_primary_workflow_without_auth\(\)/],
["CLI non-interactive run auth coverage", /fn non_interactive_run_without_session_requires_explicit_auth_or_local\(\)/], ["CLI non-interactive run auth coverage", /fn non_interactive_run_without_session_requires_explicit_auth_or_local\(\)/],
["CLI non-interactive browser login coverage", /fn browser_login_non_interactive_fails_before_opening_browser\(\)/], ["CLI non-interactive browser login coverage", /fn browser_login_non_interactive_fails_before_opening_browser\(\)/],
["CLI stored browser session coverage", /fn stored_browser_login_session_omits_provider_token_values\(\)/],
["CLI auth status session-file coverage", /fn auth_status_reads_stored_cli_session_without_provider_tokens\(\)/],
["CLI version coverage", /fn top_level_version_is_available\(\)/], ["CLI version coverage", /fn top_level_version_is_available\(\)/],
["CLI JSON parse coverage", /fn cli_first_json_mode_parses_for_primary_commands\(\)/], ["CLI JSON parse coverage", /fn cli_first_json_mode_parses_for_primary_commands\(\)/],
["CLI human output coverage", /fn human_report_is_text_not_json\(\)/], ["CLI human output coverage", /fn human_report_is_text_not_json\(\)/],
@ -204,6 +213,17 @@ for (const [name, pattern] of [
expect(cli, name, pattern); expect(cli, name, pattern);
} }
expect(
browserLoginFlowSmoke,
"browser login smoke checks session file",
/provider_tokens_persisted_locally[\s\S]*path\.join\(project, "\.disasmer", "session\.json"\)[\s\S]*\["auth", "status", "--json"\]/
);
expect(
browserLoginFlowSmoke,
"browser login smoke rejects provider token persistence",
/assert\.doesNotMatch\([\s\S]*access_token\|refresh_token\|id_token/
);
expect( expect(
coordinator, coordinator,
"coordinator whole-process cancellation coverage", "coordinator whole-process cancellation coverage",