From c260aed6b1920303e81f27ce2fb6806963b297bf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 19 Jun 2026 22:44:45 +0000 Subject: [PATCH] chore: sync public mirror from internal --- .github/BUGBOT.md | 34 +++++ .github/actions/setup-rust/action.yml | 9 ++ packages/control-plane-rs/src/a2a/ledger.rs | 13 +- packages/control-plane-rs/src/tests.rs | 8 +- scripts/check-guardrail-regression-suite.mjs | 7 +- scripts/guardrail-regression-suite.json | 117 ++++++++++++++++++ ...k-guardrail-regression-suite-entry.test.ts | 22 ---- test/workflows/tag-release.test.ts | 2 +- 8 files changed, 186 insertions(+), 26 deletions(-) create mode 100644 .github/BUGBOT.md delete mode 100644 test/scripts/check-guardrail-regression-suite-entry.test.ts diff --git a/.github/BUGBOT.md b/.github/BUGBOT.md new file mode 100644 index 000000000..b45c0df96 --- /dev/null +++ b/.github/BUGBOT.md @@ -0,0 +1,34 @@ +# Cursor Bugbot Autofix + +## Race condition with concurrent pushes + +Cursor Bugbot Autofix pushes commits directly to PR branches. When a human +or agent is also pushing to the same branch, this causes rebase conflicts +and rejected pushes. + +### Recommended workflow + +1. **Always `git pull --rebase` before pushing** to integrate any autofix + commits that landed while you were working. +2. **If a push is rejected**, fetch and check for autofix commits: + ```bash + git fetch origin + git log --oneline HEAD..origin/ + ``` +3. **If an autofix commit addresses the same issue you just fixed**, skip + your commit (`git rebase --skip`) and keep the autofix, or resolve the + conflict in favor of whichever fix is more complete. +4. **Do not force-push** to overwrite autofix commits — rebase instead. + +### Configuration + +Bugbot autofix behavior is configured at +[cursor.com/dashboard/bugbot](https://www.cursor.com/dashboard/bugbot), +not in this repo. To disable auto-push entirely and keep comment-only +reviews, uncheck "Autofix" in the dashboard. + +### When autofix is helpful + +Autofix is most valuable for generated mirror PRs (`sync/public-release-mirror`) +where no human is actively pushing. For feature branches under active +development, consider disabling autofix to avoid the race. diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index 8c92e443f..8ecb8a4c9 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -131,6 +131,15 @@ runs: with: workspaces: ${{ inputs.workspaces }} + - name: Configure Cargo network resilience + shell: bash + run: | + # Retry transient crates.io fetch failures and bound HTTP timeouts so + # spurious network errors (e.g. "download of ba/se/base64 failed") do + # not fail CI runs that would otherwise pass. + echo "CARGO_NET_RETRY=5" >> "$GITHUB_ENV" + echo "CARGO_HTTP_TIMEOUT=30" >> "$GITHUB_ENV" + - name: Install cargo-llvm-cov if: ${{ inputs.install-cargo-llvm-cov == 'true' }} uses: taiki-e/install-action@d850aa816998e5cf15f67a78c7b933f2a5033f8a # v2.63.3 diff --git a/packages/control-plane-rs/src/a2a/ledger.rs b/packages/control-plane-rs/src/a2a/ledger.rs index fadefa78e..baf51c2b5 100644 --- a/packages/control-plane-rs/src/a2a/ledger.rs +++ b/packages/control-plane-rs/src/a2a/ledger.rs @@ -255,9 +255,13 @@ fn a2a_merge_history_parts(existing: Option<&Value>, candidate: &Value) -> Value return candidate.clone(); }; let candidate_parts = candidate.as_array(); + // Preserve existing parts that carry no text (attachments, data payloads). + // Drop existing parts that have a `text` field — whether pure plain-text or + // decorated with extra keys like `partId` — so stale summary text does not + // survive alongside the refreshed transcript text. let mut merged: Vec = existing_parts .iter() - .filter(|part| !a2a_history_part_is_plain_text(part)) + .filter(|part| !a2a_history_part_has_text(part)) .cloned() .collect(); if let Some(candidate_parts) = candidate_parts { @@ -270,6 +274,13 @@ fn a2a_merge_history_parts(existing: Option<&Value>, candidate: &Value) -> Value Value::Array(merged) } +/// True when the part has a `text` field (plain or decorated). Used to decide +/// which existing parts to drop during a transcript-driven refresh. +fn a2a_history_part_has_text(part: &Value) -> bool { + part.as_object() + .is_some_and(|object| object.get("text").is_some_and(Value::is_string)) +} + fn a2a_history_part_is_plain_text(part: &Value) -> bool { let Some(object) = part.as_object() else { return false; diff --git a/packages/control-plane-rs/src/tests.rs b/packages/control-plane-rs/src/tests.rs index d4eeabafa..bc709eb21 100644 --- a/packages/control-plane-rs/src/tests.rs +++ b/packages/control-plane-rs/src/tests.rs @@ -5409,7 +5409,7 @@ async fn a2a_task_load_preserves_rich_parts_when_history_message_is_refreshed() "contextId": "ctx-rich-parts-refresh", "role": "ROLE_AGENT", "parts": [ - { "text": "stale summary" }, + { "text": "stale summary", "partId": "part-stale" }, { "data": { "attachmentId": "artifact-rich-parts" } } ] } @@ -5452,6 +5452,12 @@ async fn a2a_task_load_preserves_rich_parts_when_history_message_is_refreshed() "stale summary", "stale plain-text part must not outlive the transcript refresh" ); + assert!( + !refreshed_parts_array + .iter() + .any(|part| part.get("partId").is_some()), + "decorated text part with partId must be replaced, not preserved with stale text" + ); } #[tokio::test(flavor = "current_thread")] diff --git a/scripts/check-guardrail-regression-suite.mjs b/scripts/check-guardrail-regression-suite.mjs index 617a86e74..47bbcadea 100644 --- a/scripts/check-guardrail-regression-suite.mjs +++ b/scripts/check-guardrail-regression-suite.mjs @@ -18,6 +18,11 @@ export const REQUIRED_GUARDRAIL_IDS = [ "opaque-git-parser-state", "bounded-output-and-json-repair", "a2a-ledger-evidence-parity", + "a2a-cancel-canonical-state-guard", + "a2a-compound-secret-redaction", + "a2a-history-rich-part-refresh", + "learner-transient-quarantine-parity", + "learner-promote-repair-consistency", ]; export function loadGuardrailManifest(path = DEFAULT_GUARDRAIL_MANIFEST) { @@ -151,6 +156,6 @@ function main() { console.log(`Guardrail regression suite covers ${result.guardrailCount} bug class(es).`); } -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { +if (import.meta.url === pathToFileURL(process.argv[1]).href) { main(); } diff --git a/scripts/guardrail-regression-suite.json b/scripts/guardrail-regression-suite.json index e1f6b3f60..07bf8dc1b 100644 --- a/scripts/guardrail-regression-suite.json +++ b/scripts/guardrail-regression-suite.json @@ -187,6 +187,123 @@ ] } ] + }, + { + "id": "a2a-cancel-canonical-state-guard", + "title": "A2A cancel canonical state guard", + "owner": "a2a", + "bugClass": "canceled tasks overwritten by later stores when status aliases are not canonicalized", + "why": "store_a2a_task_unless_canceled and a2a_canceled_task must canonicalize the status state before comparing so aliases like CANCELLED or STATE_CANCELED are respected and a canceled row is not overwritten.", + "evidence": [ + { + "path": "packages/control-plane-rs/src/a2a/tasks.rs", + "contains": [ + "fn a2a_task_is_canceled", + "canonical_a2a_task_state(state) == \"TASK_STATE_CANCELED\"", + "a2a_task_is_canceled(existing)" + ] + }, + { + "path": "packages/control-plane-rs/src/tests.rs", + "contains": [ + "a2a_task_store_preserves_canceled_task_across_canonical_state_aliases" + ] + } + ] + }, + { + "id": "a2a-compound-secret-redaction", + "title": "A2A compound secret redaction", + "owner": "a2a", + "bugClass": "compound credential metadata keys not redacted by exact-match-only secret detection", + "why": "a2a_ledger_metadata_key_is_secret must match suffix forms (webhookSecret, oauthToken, apiPassword) so compound credential fields are redacted, while preserving nonSecret/nonCredentials/notASecret exceptions and token-metric carve-outs.", + "evidence": [ + { + "path": "packages/control-plane-rs/src/a2a/ledger.rs", + "contains": [ + "fn a2a_ledger_metadata_key_has_secret_suffix", + "SECRET_SUFFIXES", + "NEGATION_PREFIXES" + ] + }, + { + "path": "packages/control-plane-rs/src/tests.rs", + "contains": [ + "a2a_task_ledger_redacts_compound_secret_metadata_keys" + ] + } + ] + }, + { + "id": "a2a-history-rich-part-refresh", + "title": "A2A history rich part refresh", + "owner": "a2a", + "bugClass": "non-text history parts dropped or stale text preserved during transcript-driven refresh", + "why": "a2a_merge_history_parts must preserve non-text parts (attachments, data payloads) while replacing all text-bearing parts (plain or decorated) with the refreshed transcript candidate so stale summary text does not survive.", + "evidence": [ + { + "path": "packages/control-plane-rs/src/a2a/ledger.rs", + "contains": [ + "fn a2a_merge_history_parts", + "fn a2a_history_part_has_text", + "!a2a_history_part_has_text(part)" + ] + }, + { + "path": "packages/control-plane-rs/src/tests.rs", + "contains": [ + "a2a_task_load_preserves_rich_parts_when_history_message_is_refreshed" + ] + } + ] + }, + { + "id": "learner-transient-quarantine-parity", + "title": "Learner transient quarantine parity", + "owner": "learner", + "bugClass": "transient setup failures depressing durable routing confidence across record, load, and trim paths", + "why": "record_outcome, load, and trim must all exclude transient failures from pattern updates so get_label_success_rate and get_confidence_adjustment stay consistent with get_recommendations. Ambiguous transient phrases must be gated behind environment context.", + "evidence": [ + { + "path": "packages/ambient-agent-rs/src/learner.rs", + "contains": [ + "fn is_transient_failure_reason", + "has_transient_environment_context", + "fn rebuild_patterns", + "outcome_is_transient_failure" + ] + }, + { + "path": "packages/ambient-agent-rs/src/learner.rs", + "contains": [ + "test_transient_classifier_requires_environment_context_for_product_terms", + "test_recommendations_use_consistent_non_transient_stats_after_trim" + ] + } + ] + }, + { + "id": "learner-promote-repair-consistency", + "title": "Learner promote repair consistency", + "owner": "learner", + "bugClass": "same pattern qualifying for both promote and repair or using inconsistent stats", + "why": "get_recommendations must use pattern_non_transient_stats for both promote and repair decisions and exclude promoted patterns from repair candidates so a single pattern cannot appear in both lists.", + "evidence": [ + { + "path": "packages/ambient-agent-rs/src/learner.rs", + "contains": [ + "fn pattern_non_transient_stats", + "promoted.contains", + "problematic_pattern_stats" + ] + }, + { + "path": "packages/ambient-agent-rs/src/learner.rs", + "contains": [ + "test_recommendations_do_not_promote_and_repair_the_same_pattern" + ] + } + ] } ] } diff --git a/test/scripts/check-guardrail-regression-suite-entry.test.ts b/test/scripts/check-guardrail-regression-suite-entry.test.ts deleted file mode 100644 index dfeac55a3..000000000 --- a/test/scripts/check-guardrail-regression-suite-entry.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { fileURLToPath } from "node:url"; -import { describe, expect, it } from "vitest"; - -const repoRoot = fileURLToPath(new URL("../../", import.meta.url)); -const scriptPath = fileURLToPath( - new URL( - "../../scripts/check-guardrail-regression-suite.mjs", - import.meta.url, - ), -); - -describe("check-guardrail-regression-suite entrypoint", () => { - it("runs main when invoked directly", () => { - const output = execFileSync(process.execPath, [scriptPath], { - cwd: repoRoot, - encoding: "utf8", - }); - - expect(output).toContain("Guardrail regression suite covers"); - }); -}); diff --git a/test/workflows/tag-release.test.ts b/test/workflows/tag-release.test.ts index 386cfe42c..a9e594bc6 100644 --- a/test/workflows/tag-release.test.ts +++ b/test/workflows/tag-release.test.ts @@ -268,7 +268,7 @@ describe("tag-release workflow", () => { 'if (( release_run_count > existing_count )) && [[ "${dispatch_status}" == "0" ]]; then', ); expect(dispatchStep?.run).toContain( - 'if [[ "${dispatch_status}" == "0" && -n "${latest_release_run_id}" && "${latest_release_run_id}" != "${existing_latest_run_id}" ]]; then', + 'if [[ "${dispatch_status}" == "0" && -n "${latest_release_run_id}" && "${latest_release_run_id}" -gt "${existing_latest_run_id:-0}" ]]; then', ); expect(dispatchStep?.run).toContain( "up from ${existing_count} before dispatch",