Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 14 additions & 6 deletions apps/desktop-tauri/src/components/CodexAccountsMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ describe("CodexAccountsMenu", () => {
expect(tauriMocks.codexAccountSwitch).toHaveBeenCalledWith("2");
expect(tauriMocks.refreshProviders).toHaveBeenCalledTimes(1);
});
it("uses an opaque ordinal only for the ambient account while hideEmail is on", async () => {
it("uses an opaque ordinal for every account while hideEmail is on", async () => {
const { container: hidden } = renderMenu(true, {
accounts: [account("1", { source: "ambient" }), account("2")],
accountOrdinals: { "1": 1, "2": 2 },
Expand All @@ -213,6 +213,8 @@ describe("CodexAccountsMenu", () => {
hidden.querySelectorAll(".codex-menu-accounts__email").length,
).toBe(2);
});
// Upstream 0.60.5 #3702 (141ecf642): every account is redacted, not only
// ambient ones — managed accounts get the generic label too.
const ambientLabel = hidden.querySelectorAll(
".codex-menu-accounts__email",
)[0] as HTMLElement;
Expand All @@ -221,10 +223,12 @@ describe("CodexAccountsMenu", () => {
expect(ambientLabel.firstChild?.textContent).not.toContain("@");
expect(ambientLabel.firstChild?.textContent).not.toContain("example.com");

const managedEmail = hidden.querySelectorAll(
const managedLabel = hidden.querySelectorAll(
".codex-menu-accounts__email",
)[1] as HTMLElement;
expect(managedEmail.textContent).toBe("user-2@example.com");
expect(managedLabel.getAttribute("title")).toBe("Account 2");
expect(managedLabel.textContent).toBe("Account 2");
expect(managedLabel.textContent).not.toContain("user-2@example.com");
const switches = hidden.querySelectorAll(
".codex-menu-accounts__switch",
) as NodeListOf<HTMLButtonElement>;
Expand All @@ -247,7 +251,7 @@ describe("CodexAccountsMenu", () => {
expect(rawEmail.getAttribute("title")).toBe("user-2@example.com");
});

it("uses canonical opaque ordinals supplied by the account bridge", async () => {
it("uses canonical opaque ordinals for every account supplied by the bridge", async () => {
const first = account("uuid-b", {
emailHint: "alice@example.com",
nickname: "team@example.com",
Expand All @@ -263,12 +267,16 @@ describe("CodexAccountsMenu", () => {
accountOrdinals: { "uuid-b": 2, "uuid-a": 1 },
snapshots: {},
});
// Upstream 0.60.5 #3702 (141ecf642): every account is redacted, not only
// ambient ones — managed accounts get the generic label too.
await screen.findByText("Account 2");
await screen.findByText("Account 1");
const labels = container.querySelectorAll(".codex-menu-accounts__email");
expect(labels[0].firstChild?.textContent).toBe("Account 2");
expect(labels[1].textContent).toBe("bob@example.com — Private workspace");
expect(labels[1].textContent).toBe("Account 1");
expect(labels[0].firstChild?.textContent).not.toContain("@");
expect(labels[1].textContent).toContain("Private workspace");
expect(labels[1].textContent).not.toContain("bob@example.com");
expect(labels[1].textContent).not.toContain("Private workspace");
});
});

12 changes: 9 additions & 3 deletions apps/desktop-tauri/src/components/codexAccountDisplay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,12 @@ describe("Codex account display labels", () => {
expect(label).not.toContain("C:/private");
});

it("redacts only the ambient account while preserving managed labels", () => {
it("redacts every account under hide personal info (ambient + managed)", () => {
const system = account("11111111-1111-1111-1111-111111111111", "system");
const managed = account("22222222-2222-2222-2222-222222222222", "managed");
const accounts = [
{ ...system, source: "ambient" as const, nickname: "Private System Name" },
{ ...managed, nickname: "Work" },
{ ...managed, source: "managedByApp" as const, nickname: "Work" },
];
const hidden = buildCodexAccountSurfaceLabels(
accounts,
Expand All @@ -100,10 +100,15 @@ describe("Codex account display labels", () => {
true,
"Account",
);
// Upstream 0.60.5 #3702 (141ecf642): every account is redacted, not only
// ambient ones — managed accounts get the generic label too.
expect(hidden[system.id]).toBe("Account 1");
expect(hidden[system.id]).not.toContain("@");
expect(hidden[system.id]).not.toContain("Private System Name");
expect(hidden[managed.id]).toBe("same@example.com — Work");
expect(hidden[managed.id]).toBe("Account 2");
expect(hidden[managed.id]).not.toContain("@");
expect(hidden[managed.id]).not.toContain("Work");
expect(hidden[system.id]).not.toBe(hidden[managed.id]);

const reordered = buildCodexAccountSurfaceLabels(
[accounts[1], accounts[0]],
Expand All @@ -113,6 +118,7 @@ describe("Codex account display labels", () => {
"Account",
);
expect(reordered[system.id]).toBe("Account 1");
expect(reordered[managed.id]).toBe("Account 2");

const visible = buildCodexAccountSurfaceLabels(
accounts,
Expand Down
10 changes: 5 additions & 5 deletions apps/desktop-tauri/src/components/codexAccountDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ export function buildCodexAccountDisplayNames(
/**
* Project the labels rendered by account rows in the tray and Settings.
* The redaction rule mirrors `CodexAccount::privacy_safe_display_name` in the
* Rust model: privacy mode redacts only the ambient/System account; managed
* account labels keep the canonical display-name projection. Client-side
* projection keeps the relabel reactive when the setting toggles without a
* refetch.
* Rust model (upstream 0.60.5 #3702, 141ecf642): privacy mode redacts EVERY
* account, not only the ambient/System one — managed accounts get the same
* generic label too. Client-side projection keeps the relabel reactive when
* the setting toggles without a refetch.
*/
export function buildCodexAccountSurfaceLabels(
accounts: readonly CodexAccount[],
Expand All @@ -55,7 +55,7 @@ export function buildCodexAccountSurfaceLabels(

return Object.fromEntries(
accounts.map((account) => {
if (hidePersonalInfo && account.source === "ambient") {
if (hidePersonalInfo) {
return [account.id, `${accountWord.trim()} ${accountOrdinals[account.id]}`];
}
return [account.id, displayNames[account.id]];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe("CodexAccountsSection", () => {
expect(screen.getAllByText("CodexAccountsReauthenticateButton")).toHaveLength(1);
});

it("redacts the ambient row while preserving the managed row when privacy is on", async () => {
it("redacts every row (ambient + managed) when privacy is on", async () => {
const ambient = account("ambient", {
source: "ambient",
nickname: "Private System Name",
Expand All @@ -106,8 +106,12 @@ describe("CodexAccountsSection", () => {

render(<CodexAccountsSection t={t} hidePersonalInfo />);
await screen.findByText("Account 1");
expect(screen.queryByText(/system@example\.com|Private System Name/)).toBeNull();
expect(screen.getByText("work@example.com — Work")).toBeDefined();
// Upstream 0.60.5 #3702 (141ecf642): every account is redacted, not only
// ambient ones — managed accounts get the generic label too.
expect(screen.getByText("Account 2")).toBeDefined();
expect(
screen.queryByText(/system@example\.com|work@example\.com|Private System Name|Work/),
).toBeNull();
expect(screen.getByText("CodexAccountsSourceAmbient")).toBeDefined();
expect(screen.getByText("CodexAccountsSourceManaged")).toBeDefined();
});
Expand Down
23 changes: 19 additions & 4 deletions rust/src/codex_accounts/labels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::collections::HashMap;

use uuid::Uuid;

use super::models::{CodexAccount, CodexAccountSource};
use super::models::CodexAccount;

impl CodexAccount {
/// The user-facing account label.
Expand All @@ -18,13 +18,17 @@ impl CodexAccount {
}

/// The label used by a user-facing account surface.
///
/// Upstream 0.60.5 (#3702, `141ecf642`): Hide Personal Info redacts every
/// account in the System Account submenu, not only ambient ones — managed
/// accounts get the same generic label while the setting is on.
pub fn privacy_safe_display_name(
&self,
hide_personal_info: bool,
ordinal: usize,
generic_label: &str,
) -> String {
if hide_personal_info && self.source == CodexAccountSource::Ambient {
if hide_personal_info {
let label = generic_label.trim();
let label = if label.is_empty() { "Account" } else { label };
return format!("{label} {ordinal}");
Expand Down Expand Up @@ -134,6 +138,8 @@ pub fn ordinals_by_id(accounts: &[CodexAccount]) -> HashMap<Uuid, usize> {
#[cfg(test)]
mod tests {
use super::*;

use crate::codex_accounts::models::CodexAccountSource;
use crate::codex_accounts::models::utc_now;
use std::path::PathBuf;

Expand Down Expand Up @@ -211,22 +217,31 @@ mod tests {
}

#[test]
fn privacy_safe_display_name_hides_system_identity_but_preserves_managed_labels() {
fn privacy_safe_display_name_hides_every_account_under_hide_personal_info() {
let mut system =
display_account("11111111-1111-1111-1111-111111111111", "system-workspace");
system.source = CodexAccountSource::Ambient;
system.nickname = Some("Private System Name".to_string());
let mut managed =
display_account("22222222-2222-2222-2222-222222222222", "managed-workspace");
managed.source = CodexAccountSource::ManagedByApp;
managed.nickname = Some("Work".to_string());

// Upstream 0.60.5 #3702 (141ecf642): every account is redacted, not only
// ambient ones — managed accounts get the generic label too.
let hidden_system = system.privacy_safe_display_name(true, 1, "Account");
let hidden_managed = managed.privacy_safe_display_name(true, 2, "Account");
assert_eq!(hidden_system, "Account 1");
assert_eq!(hidden_managed, "Account 2");
assert!(!hidden_system.contains("@"));
assert!(!hidden_system.contains("Private System Name"));
assert_eq!(hidden_managed, "user@example.com — Work");
assert!(!hidden_managed.contains("Work"));
assert!(!hidden_managed.contains("@"));

// Ordinals stay account-specific so entries remain distinguishable.
assert_ne!(hidden_system, hidden_managed);

// With privacy off every account keeps its raw label.
assert_eq!(
system.privacy_safe_display_name(false, 1, "Account"),
"user@example.com — Private System Name"
Expand Down