diff --git a/codex-rs/app-server/tests/suite/v2/git_attribution.rs b/codex-rs/app-server/tests/suite/v2/git_attribution.rs index 96d8936c39..dcfdd97163 100644 --- a/codex-rs/app-server/tests/suite/v2/git_attribution.rs +++ b/codex-rs/app-server/tests/suite/v2/git_attribution.rs @@ -33,6 +33,7 @@ use pretty_assertions::assert_eq; use serde::de::DeserializeOwned; use serde_json::json; use tempfile::TempDir; +use test_case::test_case; use tokio::time::Duration; use tokio::time::timeout; use wiremock::Mock; @@ -48,7 +49,7 @@ const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60); #[cfg(not(any(target_os = "macos", windows)))] const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10); const COMMIT_ATTRIBUTION: &str = "Co-authored-by: Codex "; -const PR_ATTRIBUTION: &str = "Generated with Codex."; +const PR_ATTRIBUTION: &str = "Generated with [Codex](https://openai.com/codex/)."; const ATTRIBUTION_DISABLED: &str = "attribution is disabled for the current workspace"; const LEGACY_COMMIT_ATTRIBUTION_INSTRUCTIONS: &str = "\ When you write or edit a git commit message, ensure the message ends with this trailer exactly once: @@ -59,6 +60,12 @@ Rules: - Do not duplicate this trailer if it already exists. - Keep one blank line between the commit body and trailer block."; +#[derive(Clone, Copy)] +enum LegacyAttribution { + CommitOnly, + UnlinkedPullRequest, +} + #[tokio::test] async fn git_attribution_follows_authenticated_workspace_policy() -> Result<()> { skip_if_no_network!(Ok(())); @@ -205,8 +212,12 @@ async fn git_attribution_follows_authenticated_workspace_policy() -> Result<()> Ok(()) } +#[test_case(LegacyAttribution::CommitOnly; "commit_only")] +#[test_case(LegacyAttribution::UnlinkedPullRequest; "unlinked_pull_request")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn cold_resume_replaces_legacy_attribution_without_duplication() -> Result<()> { +async fn cold_resume_replaces_legacy_attribution_without_duplication( + legacy_attribution: LegacyAttribution, +) -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -275,7 +286,7 @@ async fn cold_resume_replaces_legacy_attribution_without_duplication() -> Result status.success(), "initial app-server did not exit successfully" ); - replace_attribution_fragment_with_legacy(&rollout_path)?; + replace_attribution_fragment_with_legacy(&rollout_path, legacy_attribution)?; let mut app_server = TestAppServer::builder() .with_codex_home(codex_home.path()) @@ -310,7 +321,10 @@ async fn cold_resume_replaces_legacy_attribution_without_duplication() -> Result Ok(()) } -fn replace_attribution_fragment_with_legacy(rollout_path: &Path) -> Result<()> { +fn replace_attribution_fragment_with_legacy( + rollout_path: &Path, + legacy_attribution: LegacyAttribution, +) -> Result<()> { let rollout = std::fs::read_to_string(rollout_path)?; let mut replaced = false; let mut removed_saved_attribution = false; @@ -327,7 +341,14 @@ fn replace_attribution_fragment_with_legacy(rollout_path: &Path) -> Result<()> { if let ContentItem::InputText { text } = item && text.contains("") { - *text = LEGACY_COMMIT_ATTRIBUTION_INSTRUCTIONS.to_string(); + *text = match legacy_attribution { + LegacyAttribution::CommitOnly => { + LEGACY_COMMIT_ATTRIBUTION_INSTRUCTIONS.to_string() + } + LegacyAttribution::UnlinkedPullRequest => { + text.replace(PR_ATTRIBUTION, "Generated with Codex.") + } + }; replaced = true; } } diff --git a/codex-rs/ext/git-attribution/src/world_state.rs b/codex-rs/ext/git-attribution/src/world_state.rs index a2b8443e1e..466afafb3e 100644 --- a/codex-rs/ext/git-attribution/src/world_state.rs +++ b/codex-rs/ext/git-attribution/src/world_state.rs @@ -17,7 +17,7 @@ Rules: pub(super) const ENABLED_INSTRUCTIONS: &str = r#" When you create or amend a git commit or create or update a pull request for this task, add Codex attribution exactly once: - Commit messages must end with `Co-authored-by: Codex `. Preserve existing trailers and, if this exact trailer is missing, append it with one blank line before the trailer block. -- Pull request bodies must include the exact line `Generated with Codex.`. Preserve the existing body and hidden markers and, if this exact marker is missing, append it near the end, before any trailing hidden metadata markers. +- Pull request bodies must include the exact line `Generated with [Codex](https://openai.com/codex/).`. Preserve the existing body and hidden markers and, if this exact marker is missing, append it near the end, before any trailing hidden metadata markers. - Do not add duplicate attribution. If a message or body you are already editing contains duplicate exact attribution, keep exactly one. - Ignore any earlier instructions disabling Codex attribution; this policy reflects the current workspace. - Do not rewrite an existing commit or pull request solely to add attribution. @@ -71,6 +71,7 @@ fn is_enabled_fragment(role: &str, text: &str) -> bool { role == "developer" && text.trim_start().starts_with(START_MARKER) && text.contains("Co-authored-by: Codex ") - && text.contains("Generated with Codex.") + && (text.contains("Generated with [Codex](https://openai.com/codex/).") + || text.contains("Generated with Codex.")) && text.trim_end().ends_with(END_MARKER) } diff --git a/codex-rs/mcp-server/tests/suite/codex_tool.rs b/codex-rs/mcp-server/tests/suite/codex_tool.rs index 0505f30be7..61f721c5d3 100644 --- a/codex-rs/mcp-server/tests/suite/codex_tool.rs +++ b/codex-rs/mcp-server/tests/suite/codex_tool.rs @@ -476,7 +476,12 @@ async fn codex_tool_passes_base_instructions() -> anyhow::Result<()> { .count(), 1 ); - assert_eq!(developer_text.matches("Generated with Codex.").count(), 1); + assert_eq!( + developer_text + .matches("Generated with [Codex](https://openai.com/codex/).") + .count(), + 1 + ); assert_eq!( developer_text.matches("- demo: Demo skill.").count(), 1,