mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
This commit is contained in:
@@ -249,10 +249,39 @@ pub enum ExpectedErrorKind {
|
||||
/// contention (handled by the store's busy-retry loop) and unrelated DB
|
||||
/// failures in other domains still reach Sentry.
|
||||
SubconsciousSchemaUnavailable,
|
||||
/// The user invoked "Import Codex CLI login" (Settings → AI → Codex auth)
|
||||
/// but the Codex CLI auth at `~/.codex/auth.json` is absent or unusable:
|
||||
/// the file doesn't exist (the user never ran `codex login`), can't be
|
||||
/// parsed, or carries no tokens / no access token. The import RPC already
|
||||
/// returns an actionable error string ("Run `codex login` first, then try
|
||||
/// Codex auth again.") that the frontend surfaces inline
|
||||
/// (`AIPanel.tsx` → `setCodexAuthError`), and Sentry has no remediation
|
||||
/// path — we can't run `codex login` for the user. The error strings also
|
||||
/// embed the absolute `~/.codex/auth.json` path (home dir / username), so
|
||||
/// demoting these out of the event stream is a privacy win on top of the
|
||||
/// noise reduction (mirror `FilesystemUserPathInvalid` / `DiskFull`).
|
||||
///
|
||||
/// Drops Sentry TAURI-RUST-83A (~430 events / 40 users on
|
||||
/// `openhuman@0.57.13`). Anchored to the `codex cli auth` /
|
||||
/// `.codex/auth.json` envelope produced by
|
||||
/// [`crate::openhuman::inference::openai_oauth::store::import_codex_cli_auth_from_path`]
|
||||
/// — a genuine keyring/persist failure in `upsert_profile` carries neither
|
||||
/// anchor, so a real defect in the import code still reaches Sentry.
|
||||
CodexCliAuthUnavailable,
|
||||
}
|
||||
|
||||
pub fn expected_error_kind(message: &str) -> Option<ExpectedErrorKind> {
|
||||
let lower = message.to_ascii_lowercase();
|
||||
// Check the Codex-CLI import envelope first: it is highly specific
|
||||
// (literal `codex cli auth` / `.codex/auth.json`) and carries no overlap
|
||||
// with the generic matchers below, so ordering is for clarity, not
|
||||
// precedence. See `ExpectedErrorKind::CodexCliAuthUnavailable`. The
|
||||
// keyring/persist failure path (`upsert_profile`) stringifies to generic
|
||||
// keychain / "auth profile" / SQLite text that contains neither anchor,
|
||||
// so a real defect in the import still falls through to capture.
|
||||
if lower.contains("codex cli auth") || lower.contains(".codex/auth.json") {
|
||||
return Some(ExpectedErrorKind::CodexCliAuthUnavailable);
|
||||
}
|
||||
if lower.contains("local ai is disabled") {
|
||||
return Some(ExpectedErrorKind::LocalAiDisabled);
|
||||
}
|
||||
@@ -1577,6 +1606,21 @@ fn report_expected_message(kind: ExpectedErrorKind, message: &str, domain: &str,
|
||||
"[observability] {domain}.{operation} skipped expected subconscious schema DB-unavailable error"
|
||||
);
|
||||
}
|
||||
ExpectedErrorKind::CodexCliAuthUnavailable => {
|
||||
// User-state condition: the Codex CLI login at `~/.codex/auth.json`
|
||||
// is missing / unparseable / has no tokens. The import RPC already
|
||||
// returned the actionable "Run `codex login` first" string and the
|
||||
// UI surfaces it inline — Sentry has nothing to act on. Demote at
|
||||
// `info!` (mirrors `LocalAiBinaryMissing`). Do NOT include the raw
|
||||
// `message`: it embeds the absolute `~/.codex/auth.json` path
|
||||
// (home dir / username). Log only domain/operation/kind — no PII.
|
||||
tracing::info!(
|
||||
domain = domain,
|
||||
operation = operation,
|
||||
kind = "codex_cli_auth_unavailable",
|
||||
"[observability] {domain}.{operation} skipped expected codex-cli auth-unavailable error"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2041,6 +2085,62 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Sentry TAURI-RUST-83A: the Codex-CLI import user-state errors must
|
||||
/// classify as `CodexCliAuthUnavailable` so the ~430-event flood stays out
|
||||
/// of Sentry. Verbatim envelope shapes from
|
||||
/// `openai_oauth::store::import_codex_cli_auth_from_path` (with a fake path
|
||||
/// in place of the real `~/.codex/auth.json`).
|
||||
#[test]
|
||||
fn classifies_codex_cli_import_user_state_as_expected() {
|
||||
for msg in [
|
||||
"Could not read Codex CLI auth at /home/u/.codex/auth.json: No such file \
|
||||
or directory. Run `codex login` first, then try Codex auth again.",
|
||||
"Could not parse Codex CLI auth at /home/u/.codex/auth.json: expected value. \
|
||||
Run `codex login` again, then try Codex auth again.",
|
||||
"Codex CLI auth at /home/u/.codex/auth.json has no tokens. Run `codex login` first.",
|
||||
"Codex CLI auth at /home/u/.codex/auth.json has no access token. Run `codex login` first.",
|
||||
"home directory is not set; cannot find ~/.codex/auth.json",
|
||||
] {
|
||||
assert_eq!(
|
||||
expected_error_kind(msg),
|
||||
Some(ExpectedErrorKind::CodexCliAuthUnavailable),
|
||||
"must classify as CodexCliAuthUnavailable: {msg}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Exercise the full demotion path (classifier -> report arm) for a codex
|
||||
/// auth-unavailable message: it must take the expected branch and not panic.
|
||||
#[test]
|
||||
fn report_error_or_expected_demotes_codex_auth_unavailable() {
|
||||
report_error_or_expected(
|
||||
"Could not read Codex CLI auth at /home/u/.codex/auth.json: No such file \
|
||||
or directory. Run `codex login` first, then try Codex auth again.",
|
||||
"inference",
|
||||
"openai_oauth_import_codex_cli",
|
||||
&[],
|
||||
);
|
||||
}
|
||||
|
||||
/// Guard against over-suppression on the import path: a genuine
|
||||
/// keyring/persist failure (`upsert_profile`) or an unrelated error carries
|
||||
/// neither the `codex cli auth` nor the `.codex/auth.json` anchor and MUST
|
||||
/// still reach Sentry (stay `None`) so a real defect isn't blinded.
|
||||
#[test]
|
||||
fn does_not_classify_codex_persist_failure_as_codex_auth_unavailable() {
|
||||
for msg in [
|
||||
"failed to write auth profile store: keyring error: access denied",
|
||||
"Auth profile not found: provider:openai/oauth",
|
||||
"unable to open database file",
|
||||
] {
|
||||
assert_ne!(
|
||||
expected_error_kind(msg),
|
||||
Some(ExpectedErrorKind::CodexCliAuthUnavailable),
|
||||
"real-defect/unrelated error must NOT be demoted as codex auth-unavailable: {msg}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sentry TAURI-RUST-R4: the composio direct-mode factory bail must
|
||||
/// classify as `ApiKeyMissing` so any residual emit (explicit
|
||||
/// execute/authorize call with no key) stays out of Sentry. Uses the
|
||||
|
||||
@@ -466,6 +466,67 @@ fn import_codex_cli_auth_file_reports_missing_file_with_login_hint() {
|
||||
assert!(err.contains("codex login"));
|
||||
}
|
||||
|
||||
/// Drift-proof coupling: every user-state error the real Codex-CLI import
|
||||
/// producer emits MUST classify as `CodexCliAuthUnavailable`, so the Sentry
|
||||
/// demotion at `ops.rs` (TAURI-RUST-83A) keeps working even if the wording
|
||||
/// changes. If a future edit to `store.rs` drops the `codex cli auth` /
|
||||
/// `.codex/auth.json` anchor from a message, this test fails in CI.
|
||||
#[test]
|
||||
fn codex_import_user_state_errors_classify_as_expected() {
|
||||
use crate::core::observability::{expected_error_kind, ExpectedErrorKind};
|
||||
|
||||
let tmp = tempdir().unwrap();
|
||||
let config = test_config(&tmp);
|
||||
|
||||
// Missing file (no `codex login`).
|
||||
let missing = import_codex_cli_auth_from_path(&config, &tmp.path().join("missing-auth.json"))
|
||||
.unwrap_err();
|
||||
|
||||
// Unparseable file.
|
||||
let garbage_path = tmp.path().join("garbage-auth.json");
|
||||
std::fs::write(&garbage_path, b"not json").unwrap();
|
||||
let garbage = import_codex_cli_auth_from_path(&config, &garbage_path).unwrap_err();
|
||||
|
||||
// Parses but carries no tokens.
|
||||
let no_tokens_path = tmp.path().join("no-tokens-auth.json");
|
||||
std::fs::write(&no_tokens_path, b"{}").unwrap();
|
||||
let no_tokens = import_codex_cli_auth_from_path(&config, &no_tokens_path).unwrap_err();
|
||||
|
||||
// Parses with a tokens object but no access token.
|
||||
let no_access_path = tmp.path().join("no-access-auth.json");
|
||||
std::fs::write(&no_access_path, br#"{"tokens":{"refresh_token":"r"}}"#).unwrap();
|
||||
let no_access = import_codex_cli_auth_from_path(&config, &no_access_path).unwrap_err();
|
||||
|
||||
for err in [&missing, &garbage, &no_tokens, &no_access] {
|
||||
assert_eq!(
|
||||
expected_error_kind(err),
|
||||
Some(ExpectedErrorKind::CodexCliAuthUnavailable),
|
||||
"codex import user-state error must classify as CodexCliAuthUnavailable: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Exercise the ops entry point (`inference_openai_oauth_import_codex_cli`) on
|
||||
/// the failure path so the `report_error_or_expected` call at the match arm is
|
||||
/// covered: point `CODEX_HOME` at an empty dir (no `auth.json`) and assert the
|
||||
/// RPC surfaces the actionable error.
|
||||
#[tokio::test]
|
||||
async fn inference_import_codex_cli_surfaces_error_when_auth_missing() {
|
||||
let _env_lock = crate::openhuman::config::TEST_ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let tmp = tempdir().unwrap();
|
||||
let config = test_config(&tmp);
|
||||
let _env_guard = EnvVarGuard::set("CODEX_HOME", tmp.path());
|
||||
|
||||
let err = crate::openhuman::inference::ops::inference_openai_oauth_import_codex_cli(&config)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err.contains("Could not read Codex CLI auth"));
|
||||
assert!(err.contains("codex login"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_oauth_status_reports_token_profile_as_disconnected() {
|
||||
let tmp = tempdir().unwrap();
|
||||
|
||||
@@ -446,7 +446,17 @@ pub async fn inference_openai_oauth_import_codex_cli(
|
||||
.map(|payload| RpcOutcome::single_log(payload, "openai oauth imported from codex cli"));
|
||||
match &result {
|
||||
Ok(_) => debug!("{LOG_PREFIX} openai_oauth_import_codex_cli:ok"),
|
||||
Err(err) => error!(error = %err, "{LOG_PREFIX} openai_oauth_import_codex_cli:error"),
|
||||
// Most failures here are expected user-state (no `~/.codex/auth.json`,
|
||||
// user never ran `codex login`, stale/empty file) — the UI already
|
||||
// surfaces the actionable error, so route through the observability
|
||||
// classifier to keep that flood out of Sentry (TAURI-RUST-83A) while a
|
||||
// genuine keyring/persist defect still falls through to a real event.
|
||||
Err(err) => crate::core::observability::report_error_or_expected(
|
||||
err,
|
||||
"inference",
|
||||
"openai_oauth_import_codex_cli",
|
||||
&[],
|
||||
),
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user