Files
codex/codex-rs/app-server-test-client/src/plugin_analytics_capture_tests.rs
T
jameswt-oai 8a40200880 [codex-app-server-test-client] Plugin Install/Uninstall Analytics Smoke Test (#27100)
## This PR

The original [combined remote plugin analytics PR
#26281](https://github.com/openai/codex/pull/26281) mixed reusable
analytics test infrastructure, two manual smoke workflows, a metadata
refactor, and the final identity behavior. This PR adds the
account-mutating validation workflow separately so its cleanup and
recovery guarantees can be reviewed without the final analytics behavior
change.

- Add a manually invoked remote plugin install/uninstall smoke workflow.
- Require explicit account-mutation confirmation and an initially
uninstalled plugin.
- Validate the current `codex_plugin_installed` contract, where
`plugin_id` is the backend ID.
- Restore and verify the original uninstalled state, with a dedicated
recovery command.

This baseline intentionally does not require `codex_plugin_uninstalled`,
because production does not emit that event yet. The final PR will
update this smoke to require local `plugin_id`, `remote_plugin_id`, and
uninstall emission. Review this PR as the net diff against #27099.

## Testing

- `just test -p codex-app-server-test-client` (3 focused
capture/validation tests passed)
- The live workflow was previously exercised on the green combined
reference branch, and the original uninstalled account state was
restored.
- CI is green across the required platform matrix.

## Split Overview

```text
main
├── #27093  Debug analytics capture
│   └── #27099  Non-mutating plugin smoke
│       └── #27100  Remote install/uninstall smoke  ← you are here
└── #27102  Plugin telemetry metadata refactor

After #27093, #27099, #27100, and #27102 merge:
└── Final PR: add remote_plugin_id to plugin analytics
```

Review order and dependencies:

1. [#27093 Add debug-only analytics event
capture](https://github.com/openai/codex/pull/27093) (based on `main`)
2. [#27099 Add a plugin analytics smoke
workflow](https://github.com/openai/codex/pull/27099) (stacked on
#27093)
3. [#27100 Add a remote plugin analytics mutation smoke
workflow](https://github.com/openai/codex/pull/27100) **(this PR,
stacked on #27099)**
4. [#27102 Centralize plugin telemetry metadata
construction](https://github.com/openai/codex/pull/27102) (independent,
based on `main`)
5. Final remote-ID behavior PR (created after PRs 1-4 merge)

The original [#26281](https://github.com/openai/codex/pull/26281)
remains open as the green aggregate reference until the final PR is
published.
2026-06-16 12:28:45 -07:00

94 lines
2.9 KiB
Rust

use super::PluginEventIdentity;
use super::read_events_for_remote_plugin;
use super::validate_mutation_events;
use serde_json::Value;
use serde_json::json;
use std::fs;
use std::path::PathBuf;
use std::process;
use std::time::SystemTime;
const REMOTE_PLUGIN_ID: &str = "plugins~Plugin_test";
#[test]
fn reads_and_validates_remote_plugin_mutation_events() {
let path = unique_capture_path("valid");
let installed = mutation_event("codex_plugin_installed");
let unrelated = json!({
"event_type": "codex_plugin_installed",
"event_params": {
"plugin_id": "plugins~Plugin_other"
}
});
let contents = [
json!({"events": [unrelated]}),
json!({"events": [installed]}),
]
.into_iter()
.map(|payload| serde_json::to_string(&payload).expect("serialize capture payload"))
.collect::<Vec<_>>()
.join("\n");
fs::write(&path, contents).expect("write capture file");
let events = read_events_for_remote_plugin(&path, REMOTE_PLUGIN_ID)
.expect("read matching plugin events");
let validated =
validate_mutation_events(events, expected_identity()).expect("validate mutation events");
assert_eq!(validated, vec![installed]);
fs::remove_file(path).expect("remove capture file");
}
#[test]
fn rejects_duplicate_mutation_events() {
let installed = mutation_event("codex_plugin_installed");
let error = validate_mutation_events(vec![installed.clone(), installed], expected_identity())
.expect_err("duplicate install events should fail validation");
assert!(error.to_string().contains("found 2"));
}
#[test]
fn rejects_missing_capability_metadata() {
let mut installed = mutation_event("codex_plugin_installed");
installed["event_params"]["has_skills"] = Value::Null;
let error = validate_mutation_events(vec![installed], expected_identity())
.expect_err("missing capability metadata should fail validation");
assert!(error.to_string().contains("has_skills"));
}
fn mutation_event(event_type: &str) -> Value {
json!({
"event_type": event_type,
"event_params": {
"plugin_id": REMOTE_PLUGIN_ID,
"plugin_name": "sample",
"marketplace_name": "openai-curated-remote",
"has_skills": true,
"mcp_server_count": 0,
"connector_ids": [],
"product_client_id": "test-client"
}
})
}
fn expected_identity() -> PluginEventIdentity<'static> {
PluginEventIdentity {
plugin_id: REMOTE_PLUGIN_ID,
plugin_name: "sample",
marketplace_name: "openai-curated-remote",
}
}
fn unique_capture_path(name: &str) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("system clock should be after Unix epoch")
.as_nanos();
std::env::temp_dir().join(format!(
"codex-plugin-analytics-capture-{name}-{}-{nonce}.jsonl",
process::id()
))
}