oh-my-codex 0.17.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +13 -5
- package/Cargo.toml +2 -1
- package/README.md +1 -0
- package/crates/omx-api/Cargo.toml +19 -0
- package/crates/omx-api/src/lib.rs +2940 -0
- package/crates/omx-api/src/main.rs +10 -0
- package/crates/omx-api/tests/cli.rs +558 -0
- package/crates/omx-explore/src/main.rs +4 -0
- package/crates/omx-sparkshell/src/codex_bridge.rs +437 -123
- package/crates/omx-sparkshell/src/exec.rs +4 -0
- package/crates/omx-sparkshell/src/main.rs +738 -29
- package/crates/omx-sparkshell/src/prompt.rs +25 -3
- package/crates/omx-sparkshell/src/redaction.rs +241 -0
- package/crates/omx-sparkshell/tests/execution.rs +479 -238
- package/dist/cli/__tests__/api.test.d.ts +2 -0
- package/dist/cli/__tests__/api.test.d.ts.map +1 -0
- package/dist/cli/__tests__/api.test.js +175 -0
- package/dist/cli/__tests__/api.test.js.map +1 -0
- package/dist/cli/__tests__/ask.test.js +72 -5
- package/dist/cli/__tests__/ask.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-goal.test.js +14 -1
- package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +51 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/explore.test.js +23 -0
- package/dist/cli/__tests__/explore.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +123 -5
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +76 -0
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +4 -3
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js +45 -22
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +2 -0
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +138 -0
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +8 -2
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +5 -0
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/version-sync-contract.test.js +4 -0
- package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/api.d.ts +26 -0
- package/dist/cli/api.d.ts.map +1 -0
- package/dist/cli/api.js +153 -0
- package/dist/cli/api.js.map +1 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +39 -4
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/explore.d.ts +2 -0
- package/dist/cli/explore.d.ts.map +1 -1
- package/dist/cli/explore.js +43 -1
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +10 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +128 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/native-assets.d.ts +2 -1
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +1 -0
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +6 -1
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +20 -3
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +90 -0
- package/dist/config/generator.js.map +1 -1
- package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts +2 -0
- package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/best-practice-research-skill.test.js +27 -0
- package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +11 -0
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +6 -0
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +4 -0
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +1 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +2 -2
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +23 -18
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +7 -6
- package/dist/hud/tmux.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js +75 -1
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/bootstrap.d.ts +3 -1
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +71 -2
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +737 -26
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-dispatcher.test.js +183 -1
- package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +4 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/build-api.d.ts +2 -0
- package/dist/scripts/build-api.d.ts.map +1 -0
- package/dist/scripts/build-api.js +44 -0
- package/dist/scripts/build-api.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +208 -8
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +89 -24
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/notify-dispatcher.js +88 -0
- package/dist/scripts/notify-dispatcher.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +27 -9
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +26 -11
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -0
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +38 -0
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +27 -14
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/run-provider-advisor.js +9 -3
- package/dist/scripts/run-provider-advisor.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +1 -1
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +2 -0
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +2 -2
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +153 -25
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/tmux-session.d.ts +1 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +55 -10
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/utils/__tests__/agents-md.test.js +45 -1
- package/dist/utils/__tests__/agents-md.test.js.map +1 -1
- package/dist/utils/agents-md.d.ts +2 -0
- package/dist/utils/agents-md.d.ts.map +1 -1
- package/dist/utils/agents-md.js +19 -0
- package/dist/utils/agents-md.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +85 -10
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +1 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/package.json +4 -3
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +83 -0
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +1 -0
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
- package/prompts/researcher.md +15 -10
- package/skills/best-practice-research/SKILL.md +83 -0
- package/skills/deep-interview/SKILL.md +1 -0
- package/skills/ralplan/SKILL.md +1 -1
- package/src/scripts/__tests__/codex-native-hook.test.ts +810 -4
- package/src/scripts/__tests__/notify-dispatcher.test.ts +223 -1
- package/src/scripts/__tests__/smoke-packed-install.test.ts +8 -2
- package/src/scripts/build-api.ts +48 -0
- package/src/scripts/codex-native-hook.ts +262 -10
- package/src/scripts/codex-native-pre-post.ts +103 -24
- package/src/scripts/notify-dispatcher.ts +97 -0
- package/src/scripts/notify-hook/team-dispatch.ts +27 -8
- package/src/scripts/notify-hook/team-leader-nudge.ts +25 -11
- package/src/scripts/notify-hook/team-tmux-guard.ts +42 -0
- package/src/scripts/notify-hook/team-worker-stop.ts +24 -13
- package/src/scripts/run-provider-advisor.ts +11 -3
- package/src/scripts/smoke-packed-install.ts +2 -0
- package/templates/catalog-manifest.json +7 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
use omx_api::{http_request, http_request_with_bearer, read_daemon_state};
|
|
2
|
+
use serde_json::Value;
|
|
3
|
+
use std::io::{Read, Write};
|
|
4
|
+
use std::net::TcpListener;
|
|
5
|
+
use std::process::{Command, Stdio};
|
|
6
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
7
|
+
use std::thread;
|
|
8
|
+
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
|
9
|
+
|
|
10
|
+
fn temp_state_file(name: &str) -> std::path::PathBuf {
|
|
11
|
+
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
12
|
+
let nanos = SystemTime::now()
|
|
13
|
+
.duration_since(UNIX_EPOCH)
|
|
14
|
+
.unwrap()
|
|
15
|
+
.as_nanos();
|
|
16
|
+
let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
17
|
+
std::env::temp_dir().join(format!(
|
|
18
|
+
"omx-api-{name}-{}-{nanos}-{counter}.json",
|
|
19
|
+
std::process::id()
|
|
20
|
+
))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn read_http_request_raw(stream: &mut std::net::TcpStream) -> String {
|
|
24
|
+
let mut raw_bytes = Vec::new();
|
|
25
|
+
let mut buffer = [0_u8; 1024];
|
|
26
|
+
let header_end = loop {
|
|
27
|
+
let read = stream.read(&mut buffer).expect("read request");
|
|
28
|
+
assert!(read > 0, "request closed before headers");
|
|
29
|
+
raw_bytes.extend_from_slice(&buffer[..read]);
|
|
30
|
+
if let Some(index) = raw_bytes.windows(4).position(|chunk| chunk == b"\r\n\r\n") {
|
|
31
|
+
break index + 4;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
let head = String::from_utf8_lossy(&raw_bytes[..header_end]).to_string();
|
|
35
|
+
let content_length = head
|
|
36
|
+
.lines()
|
|
37
|
+
.find_map(|line| line.strip_prefix("Content-Length: "))
|
|
38
|
+
.and_then(|value| value.trim().parse::<usize>().ok())
|
|
39
|
+
.unwrap_or(0);
|
|
40
|
+
while raw_bytes.len() < header_end + content_length {
|
|
41
|
+
let read = stream.read(&mut buffer).expect("read body");
|
|
42
|
+
assert!(read > 0, "request closed before body");
|
|
43
|
+
raw_bytes.extend_from_slice(&buffer[..read]);
|
|
44
|
+
}
|
|
45
|
+
String::from_utf8_lossy(&raw_bytes).to_string()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn fake_sse_text_response(text: &str) -> String {
|
|
49
|
+
format!(
|
|
50
|
+
"event: response.output_text.delta\ndata: {{\"type\":\"response.output_text.delta\",\"delta\":\"{text}\"}}\n\ndata: [DONE]\n\n"
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn real_private_once_request(
|
|
55
|
+
path: &str,
|
|
56
|
+
body: Value,
|
|
57
|
+
upstream_sse_body: String,
|
|
58
|
+
) -> (String, String) {
|
|
59
|
+
let bin = env!("CARGO_BIN_EXE_omx-api");
|
|
60
|
+
let backend = TcpListener::bind(("127.0.0.1", 0)).expect("bind fake upstream");
|
|
61
|
+
let backend_port = backend.local_addr().expect("fake upstream addr").port();
|
|
62
|
+
let backend_handle = thread::spawn(move || {
|
|
63
|
+
let (mut stream, _) = backend.accept().expect("accept fake upstream request");
|
|
64
|
+
let raw = read_http_request_raw(&mut stream);
|
|
65
|
+
write!(
|
|
66
|
+
stream,
|
|
67
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
|
68
|
+
upstream_sse_body.len(),
|
|
69
|
+
upstream_sse_body,
|
|
70
|
+
)
|
|
71
|
+
.expect("write fake upstream response");
|
|
72
|
+
raw
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
let state_file = temp_state_file("real-private-e2e");
|
|
76
|
+
let mut child = Command::new(bin)
|
|
77
|
+
.args([
|
|
78
|
+
"serve",
|
|
79
|
+
"--backend",
|
|
80
|
+
"real-private",
|
|
81
|
+
"--port",
|
|
82
|
+
"0",
|
|
83
|
+
"--once",
|
|
84
|
+
"--state-file",
|
|
85
|
+
state_file.to_str().unwrap(),
|
|
86
|
+
])
|
|
87
|
+
.env("OMX_API_CODEX_OAUTH_TOKEN", "oauth-token-for-e2e")
|
|
88
|
+
.env("OMX_API_CODEX_ACCOUNT_ID", "account-e2e")
|
|
89
|
+
.env("OMX_API_CODEX_SESSION_ID", "session-e2e")
|
|
90
|
+
.env("OMX_API_CODEX_THREAD_ID", "thread-e2e")
|
|
91
|
+
.env(
|
|
92
|
+
"OMX_API_PRIVATE_BACKEND_URL",
|
|
93
|
+
format!("http://127.0.0.1:{backend_port}/backend-api/codex"),
|
|
94
|
+
)
|
|
95
|
+
.env(
|
|
96
|
+
"OMX_API_PRIVATE_IMAGE_BACKEND_URL",
|
|
97
|
+
format!("http://127.0.0.1:{backend_port}/backend-api/codex"),
|
|
98
|
+
)
|
|
99
|
+
.env("OMX_API_IMAGE_MODEL", "omx-private-image-test")
|
|
100
|
+
.stdout(Stdio::null())
|
|
101
|
+
.stderr(Stdio::piped())
|
|
102
|
+
.spawn()
|
|
103
|
+
.expect("spawn real-private omx-api serve");
|
|
104
|
+
|
|
105
|
+
let state = wait_for_daemon_state(&state_file, &mut child);
|
|
106
|
+
let token = state
|
|
107
|
+
.local_bearer_token_file
|
|
108
|
+
.as_ref()
|
|
109
|
+
.and_then(|path| std::fs::read_to_string(path).ok())
|
|
110
|
+
.expect("real-private serve should write a local bearer token");
|
|
111
|
+
let payload = serde_json::to_vec(&body).expect("request JSON");
|
|
112
|
+
let response = http_request_with_bearer(
|
|
113
|
+
&state.host,
|
|
114
|
+
state.port,
|
|
115
|
+
"POST",
|
|
116
|
+
path,
|
|
117
|
+
Some(&payload),
|
|
118
|
+
Some(token.trim()),
|
|
119
|
+
)
|
|
120
|
+
.unwrap();
|
|
121
|
+
|
|
122
|
+
let exit = child
|
|
123
|
+
.wait_timeout(Duration::from_secs(2))
|
|
124
|
+
.expect("wait for real-private child");
|
|
125
|
+
assert!(exit.is_some(), "real-private --once server did not exit");
|
|
126
|
+
let upstream_raw = backend_handle.join().expect("fake upstream thread");
|
|
127
|
+
(response, upstream_raw)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fn wait_for_daemon_state(
|
|
131
|
+
state_file: &std::path::Path,
|
|
132
|
+
child: &mut std::process::Child,
|
|
133
|
+
) -> omx_api::DaemonState {
|
|
134
|
+
let deadline = Instant::now() + Duration::from_secs(5);
|
|
135
|
+
loop {
|
|
136
|
+
if let Some(state) = read_daemon_state(state_file).ok().flatten() {
|
|
137
|
+
return state;
|
|
138
|
+
}
|
|
139
|
+
if let Ok(Some(status)) = child.try_wait() {
|
|
140
|
+
let mut stderr = String::new();
|
|
141
|
+
if let Some(mut pipe) = child.stderr.take() {
|
|
142
|
+
use std::io::Read;
|
|
143
|
+
let _ = pipe.read_to_string(&mut stderr);
|
|
144
|
+
}
|
|
145
|
+
panic!("server exited before writing daemon state: status={status}; stderr={stderr}");
|
|
146
|
+
}
|
|
147
|
+
if Instant::now() >= deadline {
|
|
148
|
+
let _ = child.kill();
|
|
149
|
+
let _ = child.wait();
|
|
150
|
+
panic!("server did not write daemon state within 5s at {state_file:?}");
|
|
151
|
+
}
|
|
152
|
+
thread::sleep(Duration::from_millis(20));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[test]
|
|
157
|
+
fn binary_system_dry_run_and_generate_emit_json() {
|
|
158
|
+
let bin = env!("CARGO_BIN_EXE_omx-api");
|
|
159
|
+
for action in ["dry-run", "generate"] {
|
|
160
|
+
let output = Command::new(bin)
|
|
161
|
+
.args(["system", action])
|
|
162
|
+
.output()
|
|
163
|
+
.expect("run omx-api system action");
|
|
164
|
+
assert!(
|
|
165
|
+
output.status.success(),
|
|
166
|
+
"stderr: {}",
|
|
167
|
+
String::from_utf8_lossy(&output.stderr)
|
|
168
|
+
);
|
|
169
|
+
let value: Value = serde_json::from_slice(&output.stdout).expect("json stdout");
|
|
170
|
+
assert_eq!(value["ok"], true);
|
|
171
|
+
assert_eq!(value["action"], format!("system.{action}"));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[test]
|
|
176
|
+
fn binary_real_private_image_json_uses_fake_upstream_e2e() {
|
|
177
|
+
let (response, upstream_raw) = real_private_once_request(
|
|
178
|
+
"/v1/images/generations",
|
|
179
|
+
serde_json::json!({
|
|
180
|
+
"prompt": "image through upstream",
|
|
181
|
+
"size": "1024x1024"
|
|
182
|
+
}),
|
|
183
|
+
"event: image_generation.completed\ndata: {\"type\":\"image_generation.completed\",\"b64_json\":\"ZmFrZS1pbWFnZQ==\",\"revised_prompt\":\"image through upstream\"}\n\ndata: [DONE]\n\n".to_string(),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
assert!(response.contains("200 OK"), "{response}");
|
|
187
|
+
let body = response.split("\r\n\r\n").nth(1).expect("response body");
|
|
188
|
+
let value: Value = serde_json::from_str(body).expect("image response JSON");
|
|
189
|
+
assert_eq!(value["backend"], "real-private");
|
|
190
|
+
assert_eq!(value["data"][0]["b64_json"], "ZmFrZS1pbWFnZQ==");
|
|
191
|
+
assert_eq!(value["data"][0]["revised_prompt"], "image through upstream");
|
|
192
|
+
|
|
193
|
+
assert!(upstream_raw.starts_with("POST /backend-api/codex/images/generations HTTP/1.1"));
|
|
194
|
+
assert!(upstream_raw.contains("Authorization: Bearer oauth-token-for-e2e\r\n"));
|
|
195
|
+
let forwarded_body = upstream_raw
|
|
196
|
+
.split("\r\n\r\n")
|
|
197
|
+
.nth(1)
|
|
198
|
+
.expect("upstream body");
|
|
199
|
+
let forwarded_json: Value = serde_json::from_str(forwarded_body).expect("upstream JSON");
|
|
200
|
+
assert_eq!(forwarded_json["model"], "omx-private-image-test");
|
|
201
|
+
assert_eq!(forwarded_json["prompt"], "image through upstream");
|
|
202
|
+
assert_eq!(forwarded_json["size"], "1024x1024");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#[test]
|
|
206
|
+
fn binary_real_private_responses_json_uses_fake_upstream_e2e() {
|
|
207
|
+
let (response, upstream_raw) = real_private_once_request(
|
|
208
|
+
"/v1/responses",
|
|
209
|
+
serde_json::json!({
|
|
210
|
+
"model": "gpt-5.3-codex",
|
|
211
|
+
"input": "hello from integration",
|
|
212
|
+
"reasoning": {"effort": "low"},
|
|
213
|
+
"instructions": "Use fake upstream."
|
|
214
|
+
}),
|
|
215
|
+
fake_sse_text_response("upstream-text-json"),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
assert!(response.contains("200 OK"), "{response}");
|
|
219
|
+
let body = response.split("\r\n\r\n").nth(1).expect("response body");
|
|
220
|
+
let value: Value = serde_json::from_str(body).expect("response JSON");
|
|
221
|
+
assert_eq!(value["object"], "response");
|
|
222
|
+
assert_eq!(value["backend"], "real-private");
|
|
223
|
+
assert_eq!(value["output_text"], "upstream-text-json");
|
|
224
|
+
assert_eq!(
|
|
225
|
+
value["choices"][0]["message"]["content"],
|
|
226
|
+
"upstream-text-json"
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
assert!(upstream_raw.starts_with("POST /backend-api/codex/responses HTTP/1.1"));
|
|
230
|
+
assert!(upstream_raw.contains("Accept: text/event-stream\r\n"));
|
|
231
|
+
assert!(upstream_raw.contains("Authorization: Bearer oauth-token-for-e2e\r\n"));
|
|
232
|
+
assert!(upstream_raw.contains("ChatGPT-Account-ID: account-e2e\r\n"));
|
|
233
|
+
assert!(upstream_raw.contains("originator: codex_cli_rs\r\n"));
|
|
234
|
+
let forwarded_body = upstream_raw
|
|
235
|
+
.split("\r\n\r\n")
|
|
236
|
+
.nth(1)
|
|
237
|
+
.expect("upstream body");
|
|
238
|
+
let forwarded_json: Value = serde_json::from_str(forwarded_body).expect("upstream JSON");
|
|
239
|
+
assert_eq!(forwarded_json["model"], "gpt-5.3-codex");
|
|
240
|
+
assert_eq!(forwarded_json["stream"], true);
|
|
241
|
+
assert_eq!(
|
|
242
|
+
forwarded_json["reasoning"],
|
|
243
|
+
serde_json::json!({"effort": "low"})
|
|
244
|
+
);
|
|
245
|
+
assert_eq!(forwarded_json["instructions"], "Use fake upstream.");
|
|
246
|
+
assert_eq!(forwarded_json["prompt_cache_key"], "thread-e2e");
|
|
247
|
+
assert_eq!(
|
|
248
|
+
forwarded_json["input"][0]["content"][0]["text"],
|
|
249
|
+
"hello from integration"
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#[test]
|
|
254
|
+
fn binary_real_private_responses_sse_uses_fake_upstream_e2e() {
|
|
255
|
+
let (response, upstream_raw) = real_private_once_request(
|
|
256
|
+
"/v1/responses",
|
|
257
|
+
serde_json::json!({
|
|
258
|
+
"model": "gpt-5.3-codex",
|
|
259
|
+
"input": "stream please",
|
|
260
|
+
"stream": true
|
|
261
|
+
}),
|
|
262
|
+
fake_sse_text_response("upstream-text-sse"),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
assert!(response.contains("200 OK"), "{response}");
|
|
266
|
+
assert!(
|
|
267
|
+
response.contains("Content-Type: text/event-stream"),
|
|
268
|
+
"{response}"
|
|
269
|
+
);
|
|
270
|
+
assert!(response.contains("event: response.created"), "{response}");
|
|
271
|
+
assert!(
|
|
272
|
+
response.contains("event: response.output_text.delta"),
|
|
273
|
+
"{response}"
|
|
274
|
+
);
|
|
275
|
+
assert!(response.contains("upstream-text-sse"), "{response}");
|
|
276
|
+
assert!(response.contains("data: [DONE]"), "{response}");
|
|
277
|
+
|
|
278
|
+
let forwarded_body = upstream_raw
|
|
279
|
+
.split("\r\n\r\n")
|
|
280
|
+
.nth(1)
|
|
281
|
+
.expect("upstream body");
|
|
282
|
+
let forwarded_json: Value = serde_json::from_str(forwarded_body).expect("upstream JSON");
|
|
283
|
+
assert_eq!(forwarded_json["stream"], true);
|
|
284
|
+
assert_eq!(
|
|
285
|
+
forwarded_json["input"][0]["content"][0]["text"],
|
|
286
|
+
"stream please"
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#[test]
|
|
291
|
+
fn binary_real_private_chat_sse_uses_chat_chunk_shape_with_fake_upstream_e2e() {
|
|
292
|
+
let (response, upstream_raw) = real_private_once_request(
|
|
293
|
+
"/v1/chat/completions",
|
|
294
|
+
serde_json::json!({
|
|
295
|
+
"model": "gpt-5.3-codex",
|
|
296
|
+
"messages": [{"role": "user", "content": "chat through upstream"}],
|
|
297
|
+
"stream": true
|
|
298
|
+
}),
|
|
299
|
+
fake_sse_text_response("chat-upstream-text"),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
assert!(response.contains("200 OK"), "{response}");
|
|
303
|
+
assert!(
|
|
304
|
+
response.contains("Content-Type: text/event-stream"),
|
|
305
|
+
"{response}"
|
|
306
|
+
);
|
|
307
|
+
assert!(
|
|
308
|
+
response.contains("\"object\":\"chat.completion.chunk\""),
|
|
309
|
+
"{response}"
|
|
310
|
+
);
|
|
311
|
+
assert!(response.contains("chat-upstream-text"), "{response}");
|
|
312
|
+
assert!(response.contains("data: [DONE]"), "{response}");
|
|
313
|
+
|
|
314
|
+
let forwarded_body = upstream_raw
|
|
315
|
+
.split("\r\n\r\n")
|
|
316
|
+
.nth(1)
|
|
317
|
+
.expect("upstream body");
|
|
318
|
+
let forwarded_json: Value = serde_json::from_str(forwarded_body).expect("upstream JSON");
|
|
319
|
+
assert_eq!(forwarded_json["stream"], true);
|
|
320
|
+
assert_eq!(
|
|
321
|
+
forwarded_json["input"][0]["content"][0]["text"],
|
|
322
|
+
"chat through upstream"
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#[test]
|
|
327
|
+
fn binary_serve_status_and_stop_work_together() {
|
|
328
|
+
let bin = env!("CARGO_BIN_EXE_omx-api");
|
|
329
|
+
let state_file = temp_state_file("serve-status-stop");
|
|
330
|
+
let mut child = Command::new(bin)
|
|
331
|
+
.args([
|
|
332
|
+
"serve",
|
|
333
|
+
"--port",
|
|
334
|
+
"0",
|
|
335
|
+
"--state-file",
|
|
336
|
+
state_file.to_str().unwrap(),
|
|
337
|
+
])
|
|
338
|
+
.stdout(Stdio::null())
|
|
339
|
+
.stderr(Stdio::piped())
|
|
340
|
+
.spawn()
|
|
341
|
+
.expect("spawn omx-api serve");
|
|
342
|
+
|
|
343
|
+
let state = wait_for_daemon_state(&state_file, &mut child);
|
|
344
|
+
|
|
345
|
+
let status = Command::new(bin)
|
|
346
|
+
.args(["status", "--state-file", state_file.to_str().unwrap()])
|
|
347
|
+
.output()
|
|
348
|
+
.expect("run status");
|
|
349
|
+
assert!(status.status.success());
|
|
350
|
+
let status_json: Value = serde_json::from_slice(&status.stdout).unwrap();
|
|
351
|
+
assert_eq!(status_json["status"], "running");
|
|
352
|
+
|
|
353
|
+
let health = http_request(&state.host, state.port, "GET", "/health", None).unwrap();
|
|
354
|
+
assert!(health.contains("200 OK"));
|
|
355
|
+
assert!(health.contains("\"status\":\"ok\""));
|
|
356
|
+
|
|
357
|
+
let stop = Command::new(bin)
|
|
358
|
+
.args(["stop", "--state-file", state_file.to_str().unwrap()])
|
|
359
|
+
.output()
|
|
360
|
+
.expect("run stop");
|
|
361
|
+
assert!(
|
|
362
|
+
stop.status.success(),
|
|
363
|
+
"stderr: {}",
|
|
364
|
+
String::from_utf8_lossy(&stop.stderr)
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
let exit = child
|
|
368
|
+
.wait_timeout(Duration::from_secs(2))
|
|
369
|
+
.expect("wait for child");
|
|
370
|
+
if exit.is_none() {
|
|
371
|
+
let _ = child.kill();
|
|
372
|
+
panic!("server did not stop after stop command");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
#[test]
|
|
377
|
+
fn binary_local_bearer_gate_rejects_missing_authorization() {
|
|
378
|
+
let bin = env!("CARGO_BIN_EXE_omx-api");
|
|
379
|
+
let state_file = temp_state_file("bearer-required");
|
|
380
|
+
let mut child = Command::new(bin)
|
|
381
|
+
.args([
|
|
382
|
+
"serve",
|
|
383
|
+
"--port",
|
|
384
|
+
"0",
|
|
385
|
+
"--once",
|
|
386
|
+
"--state-file",
|
|
387
|
+
state_file.to_str().unwrap(),
|
|
388
|
+
])
|
|
389
|
+
.env("OMX_API_REQUIRE_LOCAL_BEARER", "1")
|
|
390
|
+
.stdout(Stdio::null())
|
|
391
|
+
.stderr(Stdio::piped())
|
|
392
|
+
.spawn()
|
|
393
|
+
.expect("spawn omx-api serve");
|
|
394
|
+
|
|
395
|
+
let state = wait_for_daemon_state(&state_file, &mut child);
|
|
396
|
+
|
|
397
|
+
let response = http_request(&state.host, state.port, "GET", "/v1/models", None).unwrap();
|
|
398
|
+
assert!(response.contains("401 Unauthorized"), "{response}");
|
|
399
|
+
assert!(
|
|
400
|
+
response.contains("local bearer token required"),
|
|
401
|
+
"{response}"
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
let exit = child
|
|
405
|
+
.wait_timeout(Duration::from_secs(2))
|
|
406
|
+
.expect("wait for child");
|
|
407
|
+
assert!(exit.is_some(), "server did not exit after --once request");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
#[test]
|
|
411
|
+
fn binary_real_private_serve_generates_bearer_and_rejects_missing_authorization() {
|
|
412
|
+
let bin = env!("CARGO_BIN_EXE_omx-api");
|
|
413
|
+
let state_file = temp_state_file("real-private-bearer-default");
|
|
414
|
+
let mut child = Command::new(bin)
|
|
415
|
+
.args([
|
|
416
|
+
"serve",
|
|
417
|
+
"--backend",
|
|
418
|
+
"real-private",
|
|
419
|
+
"--port",
|
|
420
|
+
"0",
|
|
421
|
+
"--once",
|
|
422
|
+
"--state-file",
|
|
423
|
+
state_file.to_str().unwrap(),
|
|
424
|
+
])
|
|
425
|
+
.env("OMX_API_REAL_PRIVATE_RESPONSE_TEXT", "fixture")
|
|
426
|
+
.stdout(Stdio::null())
|
|
427
|
+
.stderr(Stdio::piped())
|
|
428
|
+
.spawn()
|
|
429
|
+
.expect("spawn omx-api real-private serve");
|
|
430
|
+
|
|
431
|
+
let state = wait_for_daemon_state(&state_file, &mut child);
|
|
432
|
+
assert!(
|
|
433
|
+
state.local_bearer_token_file.is_some(),
|
|
434
|
+
"real-private direct serve should persist a bearer token file"
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
let response = http_request(&state.host, state.port, "GET", "/v1/models", None).unwrap();
|
|
438
|
+
assert!(response.contains("401 Unauthorized"), "{response}");
|
|
439
|
+
assert!(
|
|
440
|
+
response.contains("matching local bearer token required"),
|
|
441
|
+
"{response}"
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
let exit = child
|
|
445
|
+
.wait_timeout(Duration::from_secs(2))
|
|
446
|
+
.expect("wait for child");
|
|
447
|
+
assert!(exit.is_some(), "server did not exit after --once request");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
#[test]
|
|
451
|
+
fn binary_daemon_token_is_not_printed_but_controls_generate_and_stop() {
|
|
452
|
+
let bin = env!("CARGO_BIN_EXE_omx-api");
|
|
453
|
+
let state_file = temp_state_file("daemon-token");
|
|
454
|
+
let start = Command::new(bin)
|
|
455
|
+
.args([
|
|
456
|
+
"serve",
|
|
457
|
+
"--port",
|
|
458
|
+
"0",
|
|
459
|
+
"--daemon",
|
|
460
|
+
"--state-file",
|
|
461
|
+
state_file.to_str().unwrap(),
|
|
462
|
+
])
|
|
463
|
+
.output()
|
|
464
|
+
.expect("start daemon");
|
|
465
|
+
assert!(
|
|
466
|
+
start.status.success(),
|
|
467
|
+
"stderr: {}",
|
|
468
|
+
String::from_utf8_lossy(&start.stderr)
|
|
469
|
+
);
|
|
470
|
+
let stdout = String::from_utf8_lossy(&start.stdout);
|
|
471
|
+
assert!(!stdout.contains("local_bearer_token\":"), "{stdout}");
|
|
472
|
+
|
|
473
|
+
let state = read_daemon_state(&state_file).unwrap().unwrap();
|
|
474
|
+
assert!(state.local_bearer_token.is_none());
|
|
475
|
+
let token_file = state
|
|
476
|
+
.local_bearer_token_file
|
|
477
|
+
.clone()
|
|
478
|
+
.expect("token file path");
|
|
479
|
+
let token = std::fs::read_to_string(&token_file).expect("token file");
|
|
480
|
+
assert!(!stdout.contains(token.trim()), "daemon stdout leaked token");
|
|
481
|
+
|
|
482
|
+
let unauthorized = http_request(&state.host, state.port, "GET", "/v1/models", None).unwrap();
|
|
483
|
+
assert!(unauthorized.contains("401 Unauthorized"), "{unauthorized}");
|
|
484
|
+
let authorized = http_request_with_bearer(
|
|
485
|
+
&state.host,
|
|
486
|
+
state.port,
|
|
487
|
+
"GET",
|
|
488
|
+
"/v1/models",
|
|
489
|
+
None,
|
|
490
|
+
Some(token.trim()),
|
|
491
|
+
)
|
|
492
|
+
.unwrap();
|
|
493
|
+
assert!(authorized.contains("200 OK"), "{authorized}");
|
|
494
|
+
|
|
495
|
+
let generated = Command::new(bin)
|
|
496
|
+
.args([
|
|
497
|
+
"generate",
|
|
498
|
+
"text",
|
|
499
|
+
"hello",
|
|
500
|
+
"--state-file",
|
|
501
|
+
state_file.to_str().unwrap(),
|
|
502
|
+
])
|
|
503
|
+
.output()
|
|
504
|
+
.expect("generate through daemon");
|
|
505
|
+
assert!(
|
|
506
|
+
generated.status.success(),
|
|
507
|
+
"stderr: {}",
|
|
508
|
+
String::from_utf8_lossy(&generated.stderr)
|
|
509
|
+
);
|
|
510
|
+
assert!(String::from_utf8_lossy(&generated.stdout).contains("omx mock response"));
|
|
511
|
+
|
|
512
|
+
let stop = Command::new(bin)
|
|
513
|
+
.args(["stop", "--state-file", state_file.to_str().unwrap()])
|
|
514
|
+
.output()
|
|
515
|
+
.expect("stop daemon");
|
|
516
|
+
assert!(
|
|
517
|
+
stop.status.success(),
|
|
518
|
+
"stderr: {}",
|
|
519
|
+
String::from_utf8_lossy(&stop.stderr)
|
|
520
|
+
);
|
|
521
|
+
assert!(!token_file.exists(), "token file should be removed on stop");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#[test]
|
|
525
|
+
fn binary_rejects_non_loopback_host() {
|
|
526
|
+
let bin = env!("CARGO_BIN_EXE_omx-api");
|
|
527
|
+
let output = Command::new(bin)
|
|
528
|
+
.args(["serve", "--host", "0.0.0.0", "--once"])
|
|
529
|
+
.output()
|
|
530
|
+
.expect("run omx-api serve");
|
|
531
|
+
assert!(!output.status.success());
|
|
532
|
+
assert!(String::from_utf8_lossy(&output.stderr).contains("localhost-only"));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
trait WaitTimeout {
|
|
536
|
+
fn wait_timeout(
|
|
537
|
+
&mut self,
|
|
538
|
+
timeout: Duration,
|
|
539
|
+
) -> std::io::Result<Option<std::process::ExitStatus>>;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
impl WaitTimeout for std::process::Child {
|
|
543
|
+
fn wait_timeout(
|
|
544
|
+
&mut self,
|
|
545
|
+
timeout: Duration,
|
|
546
|
+
) -> std::io::Result<Option<std::process::ExitStatus>> {
|
|
547
|
+
let start = std::time::Instant::now();
|
|
548
|
+
loop {
|
|
549
|
+
if let Some(status) = self.try_wait()? {
|
|
550
|
+
return Ok(Some(status));
|
|
551
|
+
}
|
|
552
|
+
if start.elapsed() >= timeout {
|
|
553
|
+
return Ok(None);
|
|
554
|
+
}
|
|
555
|
+
thread::sleep(Duration::from_millis(20));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
@@ -2721,6 +2721,10 @@ sleep 30
|
|
|
2721
2721
|
let TimedCommandOutput::TimedOut { .. } = result else {
|
|
2722
2722
|
panic!("expected timeout");
|
|
2723
2723
|
};
|
|
2724
|
+
let deadline = Instant::now() + Duration::from_secs(2);
|
|
2725
|
+
while !term_file.exists() && Instant::now() < deadline {
|
|
2726
|
+
std::thread::sleep(Duration::from_millis(10));
|
|
2727
|
+
}
|
|
2724
2728
|
assert_eq!(read_to_string(&term_file).unwrap_or_default(), "term");
|
|
2725
2729
|
}
|
|
2726
2730
|
|