oh-my-codex 0.17.3 → 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__/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__/setup-install-mode.test.js +138 -0
- package/dist/cli/__tests__/setup-install-mode.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/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/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 +96 -19
- 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 +34 -10
- package/dist/team/tmux-session.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
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
use std::env;
|
|
2
2
|
use std::fs;
|
|
3
|
+
use std::io::{Read, Write};
|
|
4
|
+
use std::net::{TcpListener, TcpStream};
|
|
3
5
|
use std::path::{Path, PathBuf};
|
|
4
6
|
use std::process::Command;
|
|
7
|
+
use std::sync::{Arc, Mutex};
|
|
8
|
+
use std::thread::{self, JoinHandle};
|
|
5
9
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
6
10
|
|
|
7
11
|
fn sparkshell_bin() -> &'static str {
|
|
@@ -32,6 +36,80 @@ fn write_executable(path: &Path, body: &str) {
|
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
fn start_api_server<F>(expected_requests: usize, mut handler: F) -> (String, JoinHandle<()>)
|
|
40
|
+
where
|
|
41
|
+
F: FnMut(String) -> (u16, String) + Send + 'static,
|
|
42
|
+
{
|
|
43
|
+
let listener = TcpListener::bind("127.0.0.1:0").expect("bind api server");
|
|
44
|
+
let address = listener.local_addr().expect("api address");
|
|
45
|
+
let handle = thread::spawn(move || {
|
|
46
|
+
for _ in 0..expected_requests {
|
|
47
|
+
let (stream, _) = listener.accept().expect("accept api request");
|
|
48
|
+
let request = read_http_request(stream.try_clone().expect("clone stream"));
|
|
49
|
+
let (status, body) = handler(request);
|
|
50
|
+
write_http_response(stream, status, &body);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
(format!("http://{}", address), handle)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn read_http_request(mut stream: TcpStream) -> String {
|
|
57
|
+
let mut buffer = Vec::new();
|
|
58
|
+
let mut scratch = [0; 1024];
|
|
59
|
+
loop {
|
|
60
|
+
let count = stream.read(&mut scratch).expect("read request");
|
|
61
|
+
assert!(count > 0, "connection closed before request headers");
|
|
62
|
+
buffer.extend_from_slice(&scratch[..count]);
|
|
63
|
+
if buffer.windows(4).any(|window| window == b"\r\n\r\n") {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
let request = String::from_utf8_lossy(&buffer).into_owned();
|
|
68
|
+
let content_length = request
|
|
69
|
+
.lines()
|
|
70
|
+
.find_map(|line| {
|
|
71
|
+
line.strip_prefix("Content-Length:")
|
|
72
|
+
.or_else(|| line.strip_prefix("content-length:"))
|
|
73
|
+
.and_then(|value| value.trim().parse::<usize>().ok())
|
|
74
|
+
})
|
|
75
|
+
.unwrap_or(0);
|
|
76
|
+
let body_start = buffer
|
|
77
|
+
.windows(4)
|
|
78
|
+
.position(|window| window == b"\r\n\r\n")
|
|
79
|
+
.expect("header boundary")
|
|
80
|
+
+ 4;
|
|
81
|
+
while buffer.len() - body_start < content_length {
|
|
82
|
+
let count = stream.read(&mut scratch).expect("read request body");
|
|
83
|
+
assert!(count > 0, "connection closed before request body");
|
|
84
|
+
buffer.extend_from_slice(&scratch[..count]);
|
|
85
|
+
}
|
|
86
|
+
String::from_utf8_lossy(&buffer).into_owned()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fn write_http_response(mut stream: TcpStream, status: u16, body: &str) {
|
|
90
|
+
let reason = if (200..300).contains(&status) {
|
|
91
|
+
"OK"
|
|
92
|
+
} else {
|
|
93
|
+
"ERROR"
|
|
94
|
+
};
|
|
95
|
+
let response = format!(
|
|
96
|
+
"HTTP/1.1 {status} {reason}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
|
|
97
|
+
body.len()
|
|
98
|
+
);
|
|
99
|
+
stream
|
|
100
|
+
.write_all(response.as_bytes())
|
|
101
|
+
.expect("write response");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn response_json(text: &str) -> String {
|
|
105
|
+
format!(
|
|
106
|
+
"{{\"object\":\"response\",\"output_text\":\"{}\"}}",
|
|
107
|
+
text.replace('\\', "\\\\")
|
|
108
|
+
.replace('"', "\\\"")
|
|
109
|
+
.replace('\n', "\\n")
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
35
113
|
#[test]
|
|
36
114
|
fn raw_mode_preserves_stdout_and_stderr() {
|
|
37
115
|
let output = Command::new(sparkshell_bin())
|
|
@@ -48,27 +126,19 @@ fn raw_mode_preserves_stdout_and_stderr() {
|
|
|
48
126
|
}
|
|
49
127
|
|
|
50
128
|
#[test]
|
|
51
|
-
fn
|
|
52
|
-
let
|
|
53
|
-
let
|
|
54
|
-
let
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
prompt_log.display()
|
|
62
|
-
),
|
|
63
|
-
);
|
|
129
|
+
fn summary_mode_uses_local_api_and_model_override() {
|
|
130
|
+
let request_log = Arc::new(Mutex::new(String::new()));
|
|
131
|
+
let request_log_for_server = Arc::clone(&request_log);
|
|
132
|
+
let (base_url, server) = start_api_server(1, move |request| {
|
|
133
|
+
*request_log_for_server.lock().expect("request log") = request;
|
|
134
|
+
(
|
|
135
|
+
200,
|
|
136
|
+
response_json("- summary: command produced long output\n- warnings: stderr was empty"),
|
|
137
|
+
)
|
|
138
|
+
});
|
|
64
139
|
|
|
65
|
-
let path = format!(
|
|
66
|
-
"{}:{}",
|
|
67
|
-
temp.display(),
|
|
68
|
-
env::var("PATH").unwrap_or_default()
|
|
69
|
-
);
|
|
70
140
|
let output = Command::new(sparkshell_bin())
|
|
71
|
-
.env("
|
|
141
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
72
142
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
73
143
|
.env("OMX_SPARKSHELL_MODEL", "spark-test-model")
|
|
74
144
|
.arg("sh")
|
|
@@ -76,6 +146,7 @@ fn summary_mode_uses_codex_exec_and_model_override() {
|
|
|
76
146
|
.arg("printf 'one\ntwo\n'")
|
|
77
147
|
.output()
|
|
78
148
|
.expect("run sparkshell");
|
|
149
|
+
server.join().expect("api server");
|
|
79
150
|
|
|
80
151
|
assert!(output.status.success());
|
|
81
152
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
@@ -83,42 +154,65 @@ fn summary_mode_uses_codex_exec_and_model_override() {
|
|
|
83
154
|
assert!(stdout.contains("- warnings: stderr was empty"));
|
|
84
155
|
assert!(String::from_utf8_lossy(&output.stderr).is_empty());
|
|
85
156
|
|
|
86
|
-
let
|
|
87
|
-
assert!(
|
|
88
|
-
assert!(
|
|
89
|
-
assert!(
|
|
90
|
-
assert!(
|
|
157
|
+
let request = request_log.lock().expect("request log");
|
|
158
|
+
assert!(request.starts_with("POST /v1/responses HTTP/1.1"));
|
|
159
|
+
assert!(request.contains("\"model\":\"spark-test-model\""));
|
|
160
|
+
assert!(request.contains("\"reasoning\":{\"effort\":\"low\"}"));
|
|
161
|
+
assert!(request.contains("Command family: generic-shell"));
|
|
162
|
+
assert!(request.contains("<<<STDOUT"));
|
|
163
|
+
assert!(request.contains("one\\ntwo"));
|
|
164
|
+
}
|
|
91
165
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
166
|
+
#[test]
|
|
167
|
+
fn summary_mode_redacts_secret_like_output_before_prompt_request() {
|
|
168
|
+
let request_log = Arc::new(Mutex::new(String::new()));
|
|
169
|
+
let request_log_for_server = Arc::clone(&request_log);
|
|
170
|
+
let (base_url, server) = start_api_server(1, move |request| {
|
|
171
|
+
*request_log_for_server.lock().expect("request log") = request;
|
|
172
|
+
(200, response_json("- summary: redacted output summarized"))
|
|
173
|
+
});
|
|
96
174
|
|
|
97
|
-
let
|
|
175
|
+
let output = Command::new(sparkshell_bin())
|
|
176
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
177
|
+
.env("OMX_SPARKSHELL_LINES", "1")
|
|
178
|
+
.env("CHILD_API_TOKEN", "super-secret-token")
|
|
179
|
+
.env("CHILD_BEARER", "bearer-secret-token")
|
|
180
|
+
.arg("sh")
|
|
181
|
+
.arg("-c")
|
|
182
|
+
.arg("printf 'API_TOKEN=%s\\nline-2\\n' \"$CHILD_API_TOKEN\"; printf 'Authorization: Bearer %s\\n' \"$CHILD_BEARER\" >&2")
|
|
183
|
+
.output()
|
|
184
|
+
.expect("run sparkshell");
|
|
185
|
+
server.join().expect("api server");
|
|
186
|
+
|
|
187
|
+
assert!(output.status.success());
|
|
188
|
+
assert!(String::from_utf8_lossy(&output.stdout).contains("redacted output summarized"));
|
|
189
|
+
|
|
190
|
+
let request = request_log.lock().expect("request log");
|
|
191
|
+
assert!(request.contains("API_TOKEN=[REDACTED]"));
|
|
192
|
+
assert!(request.contains("Authorization: Bearer [REDACTED]"));
|
|
193
|
+
assert!(request.contains("line-2"));
|
|
194
|
+
assert!(!request.contains("super-secret-token"));
|
|
195
|
+
assert!(!request.contains("bearer-secret-token"));
|
|
98
196
|
}
|
|
99
197
|
|
|
100
198
|
#[test]
|
|
101
199
|
fn summary_mode_injects_model_instructions_file_override() {
|
|
102
|
-
let temp = unique_temp_dir("
|
|
103
|
-
let codex = temp.join("codex");
|
|
104
|
-
let args_log = temp.join("args.log");
|
|
200
|
+
let temp = unique_temp_dir("api-instructions-file");
|
|
105
201
|
let instructions_file = temp.join("sparkshell-lightweight-AGENTS.md");
|
|
106
|
-
write_executable(
|
|
107
|
-
&codex,
|
|
108
|
-
&format!(
|
|
109
|
-
"#!/bin/sh\nprintf '%s\n' \"$@\" > '{}'\nprintf '%s\n' '- summary: command produced long output'\n",
|
|
110
|
-
args_log.display()
|
|
111
|
-
),
|
|
112
|
-
);
|
|
113
202
|
fs::write(&instructions_file, "# sparkshell instructions\n").expect("write instructions file");
|
|
114
203
|
|
|
115
|
-
let
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
204
|
+
let request_log = Arc::new(Mutex::new(String::new()));
|
|
205
|
+
let request_log_for_server = Arc::clone(&request_log);
|
|
206
|
+
let (base_url, server) = start_api_server(1, move |request| {
|
|
207
|
+
*request_log_for_server.lock().expect("request log") = request;
|
|
208
|
+
(
|
|
209
|
+
200,
|
|
210
|
+
response_json("- summary: command produced long output"),
|
|
211
|
+
)
|
|
212
|
+
});
|
|
213
|
+
|
|
120
214
|
let output = Command::new(sparkshell_bin())
|
|
121
|
-
.env("
|
|
215
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
122
216
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
123
217
|
.env(
|
|
124
218
|
"OMX_SPARKSHELL_MODEL_INSTRUCTIONS_FILE",
|
|
@@ -129,87 +223,63 @@ fn summary_mode_injects_model_instructions_file_override() {
|
|
|
129
223
|
.arg("printf 'one\ntwo\n'")
|
|
130
224
|
.output()
|
|
131
225
|
.expect("run sparkshell");
|
|
226
|
+
server.join().expect("api server");
|
|
132
227
|
|
|
133
228
|
assert!(output.status.success());
|
|
134
|
-
let
|
|
135
|
-
assert!(
|
|
136
|
-
assert!(
|
|
137
|
-
"model_instructions_file=\"{}\"",
|
|
138
|
-
instructions_file
|
|
139
|
-
.display()
|
|
140
|
-
.to_string()
|
|
141
|
-
.replace('\\', "\\\\")
|
|
142
|
-
.replace('"', "\\\"")
|
|
143
|
-
)));
|
|
229
|
+
let request = request_log.lock().expect("request log");
|
|
230
|
+
assert!(request.contains("\"reasoning\":{\"effort\":\"low\"}"));
|
|
231
|
+
assert!(request.contains("\"instructions\":\"# sparkshell instructions\\n\""));
|
|
144
232
|
|
|
145
233
|
let _ = fs::remove_dir_all(temp);
|
|
146
234
|
}
|
|
147
235
|
|
|
148
236
|
#[test]
|
|
149
237
|
fn summary_failure_falls_back_to_raw_output_with_notice() {
|
|
150
|
-
let
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
&codex,
|
|
154
|
-
"#!/bin/sh\nprintf '%s\n' 'bridge failed' >&2\nexit 9\n",
|
|
155
|
-
);
|
|
238
|
+
let (base_url, server) = start_api_server(1, |_request| {
|
|
239
|
+
(503, "{\"error\":\"bridge failed\"}".to_string())
|
|
240
|
+
});
|
|
156
241
|
|
|
157
|
-
let path = format!(
|
|
158
|
-
"{}:{}",
|
|
159
|
-
temp.display(),
|
|
160
|
-
env::var("PATH").unwrap_or_default()
|
|
161
|
-
);
|
|
162
242
|
let output = Command::new(sparkshell_bin())
|
|
163
|
-
.env("
|
|
243
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
164
244
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
165
245
|
.arg("/bin/sh")
|
|
166
246
|
.arg("-c")
|
|
167
247
|
.arg("printf 'one\ntwo\n'; printf 'child-err\n' >&2")
|
|
168
248
|
.output()
|
|
169
249
|
.expect("run sparkshell");
|
|
250
|
+
server.join().expect("api server");
|
|
170
251
|
|
|
171
252
|
assert!(output.status.success());
|
|
172
253
|
assert_eq!(String::from_utf8_lossy(&output.stdout), "one\ntwo\n");
|
|
173
254
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
174
255
|
assert!(stderr.contains("child-err"));
|
|
175
256
|
assert!(stderr.contains("summary unavailable"));
|
|
176
|
-
|
|
177
|
-
let _ = fs::remove_dir_all(temp);
|
|
178
257
|
}
|
|
179
258
|
|
|
180
259
|
#[test]
|
|
181
260
|
fn summary_mode_retries_with_fallback_model_when_spark_is_unavailable() {
|
|
182
|
-
let
|
|
183
|
-
let
|
|
184
|
-
let
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
for
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
",
|
|
202
|
-
args_log.display()
|
|
203
|
-
),
|
|
204
|
-
);
|
|
261
|
+
let request_log = Arc::new(Mutex::new(Vec::new()));
|
|
262
|
+
let request_log_for_server = Arc::clone(&request_log);
|
|
263
|
+
let (base_url, server) = start_api_server(2, move |request| {
|
|
264
|
+
request_log_for_server
|
|
265
|
+
.lock()
|
|
266
|
+
.expect("request log")
|
|
267
|
+
.push(request.clone());
|
|
268
|
+
if request.contains("\"model\":\"spark-test-model\"") {
|
|
269
|
+
(
|
|
270
|
+
429,
|
|
271
|
+
"{\"error\":\"rate limit exceeded for spark model\"}".to_string(),
|
|
272
|
+
)
|
|
273
|
+
} else {
|
|
274
|
+
(
|
|
275
|
+
200,
|
|
276
|
+
response_json("- summary: fallback model recovered summary"),
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
});
|
|
205
280
|
|
|
206
|
-
let path = format!(
|
|
207
|
-
"{}:{}",
|
|
208
|
-
temp.display(),
|
|
209
|
-
env::var("PATH").unwrap_or_default()
|
|
210
|
-
);
|
|
211
281
|
let output = Command::new(sparkshell_bin())
|
|
212
|
-
.env("
|
|
282
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
213
283
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
214
284
|
.env("OMX_SPARKSHELL_MODEL", "spark-test-model")
|
|
215
285
|
.env("OMX_SPARKSHELL_FALLBACK_MODEL", "frontier-test-model")
|
|
@@ -218,47 +288,36 @@ printf '%s\n' '- summary: fallback model recovered summary'
|
|
|
218
288
|
.arg("printf 'one\ntwo\n'")
|
|
219
289
|
.output()
|
|
220
290
|
.expect("run sparkshell");
|
|
291
|
+
server.join().expect("api server");
|
|
221
292
|
|
|
222
293
|
assert!(output.status.success());
|
|
223
294
|
assert!(String::from_utf8_lossy(&output.stdout).contains("fallback model recovered summary"));
|
|
224
295
|
assert!(String::from_utf8_lossy(&output.stderr).is_empty());
|
|
225
296
|
|
|
226
|
-
let
|
|
227
|
-
|
|
228
|
-
assert!(
|
|
229
|
-
|
|
230
|
-
let _ = fs::remove_dir_all(temp);
|
|
297
|
+
let requests = request_log.lock().expect("request log");
|
|
298
|
+
assert_eq!(requests.len(), 2);
|
|
299
|
+
assert!(requests[0].contains("\"model\":\"spark-test-model\""));
|
|
300
|
+
assert!(requests[1].contains("\"model\":\"frontier-test-model\""));
|
|
231
301
|
}
|
|
232
302
|
|
|
233
303
|
#[test]
|
|
234
304
|
fn summary_mode_reports_both_models_when_fallback_also_fails() {
|
|
235
|
-
let
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
exit 17
|
|
249
|
-
fi
|
|
250
|
-
printf '%s\n' 'fallback backend unavailable' >&2
|
|
251
|
-
exit 29
|
|
252
|
-
",
|
|
253
|
-
);
|
|
305
|
+
let (base_url, server) = start_api_server(2, |request| {
|
|
306
|
+
if request.contains("\"model\":\"spark-test-model\"") {
|
|
307
|
+
(
|
|
308
|
+
429,
|
|
309
|
+
"{\"error\":\"quota exhausted for spark model\"}".to_string(),
|
|
310
|
+
)
|
|
311
|
+
} else {
|
|
312
|
+
(
|
|
313
|
+
503,
|
|
314
|
+
"{\"error\":\"fallback backend unavailable\"}".to_string(),
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
});
|
|
254
318
|
|
|
255
|
-
let path = format!(
|
|
256
|
-
"{}:{}",
|
|
257
|
-
temp.display(),
|
|
258
|
-
env::var("PATH").unwrap_or_default()
|
|
259
|
-
);
|
|
260
319
|
let output = Command::new(sparkshell_bin())
|
|
261
|
-
.env("
|
|
320
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
262
321
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
263
322
|
.env("OMX_SPARKSHELL_MODEL", "spark-test-model")
|
|
264
323
|
.env("OMX_SPARKSHELL_FALLBACK_MODEL", "frontier-test-model")
|
|
@@ -267,6 +326,7 @@ exit 29
|
|
|
267
326
|
.arg("printf 'one\ntwo\n'; printf 'child-err\n' >&2")
|
|
268
327
|
.output()
|
|
269
328
|
.expect("run sparkshell");
|
|
329
|
+
server.join().expect("api server");
|
|
270
330
|
|
|
271
331
|
assert!(output.status.success());
|
|
272
332
|
assert_eq!(String::from_utf8_lossy(&output.stdout), "one\ntwo\n");
|
|
@@ -274,48 +334,34 @@ exit 29
|
|
|
274
334
|
assert!(stderr.contains("child-err"));
|
|
275
335
|
assert!(stderr.contains("primary model `spark-test-model`"));
|
|
276
336
|
assert!(stderr.contains("fallback model `frontier-test-model`"));
|
|
277
|
-
|
|
278
|
-
let _ = fs::remove_dir_all(temp);
|
|
279
337
|
}
|
|
280
338
|
|
|
281
339
|
#[test]
|
|
282
340
|
fn summary_mode_preserves_child_exit_code() {
|
|
283
|
-
let
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
&codex,
|
|
287
|
-
"#!/bin/sh\nprintf '%s\n' '- failures: command exited non-zero'\n",
|
|
288
|
-
);
|
|
341
|
+
let (base_url, server) = start_api_server(1, |_request| {
|
|
342
|
+
(200, response_json("- failures: command exited non-zero"))
|
|
343
|
+
});
|
|
289
344
|
|
|
290
|
-
let path = format!(
|
|
291
|
-
"{}:{}",
|
|
292
|
-
temp.display(),
|
|
293
|
-
env::var("PATH").unwrap_or_default()
|
|
294
|
-
);
|
|
295
345
|
let output = Command::new(sparkshell_bin())
|
|
296
|
-
.env("
|
|
346
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
297
347
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
298
348
|
.arg("sh")
|
|
299
349
|
.arg("-c")
|
|
300
350
|
.arg("printf 'one\ntwo\n'; exit 7")
|
|
301
351
|
.output()
|
|
302
352
|
.expect("run sparkshell");
|
|
353
|
+
server.join().expect("api server");
|
|
303
354
|
|
|
304
355
|
assert_eq!(output.status.code(), Some(7));
|
|
305
356
|
assert!(String::from_utf8_lossy(&output.stdout).contains("- failures: command exited non-zero"));
|
|
306
357
|
assert!(String::from_utf8_lossy(&output.stderr).is_empty());
|
|
307
|
-
|
|
308
|
-
let _ = fs::remove_dir_all(temp);
|
|
309
358
|
}
|
|
310
359
|
|
|
311
360
|
#[test]
|
|
312
361
|
fn tmux_pane_mode_captures_large_tail_and_summarizes() {
|
|
313
362
|
let temp = unique_temp_dir("tmux-pane-summary");
|
|
314
363
|
let tmux = temp.join("tmux");
|
|
315
|
-
let codex = temp.join("codex");
|
|
316
364
|
let args_log = temp.join("tmux-args.log");
|
|
317
|
-
let prompt_log = temp.join("pane-prompt.log");
|
|
318
|
-
|
|
319
365
|
write_executable(
|
|
320
366
|
&tmux,
|
|
321
367
|
&format!(
|
|
@@ -323,13 +369,16 @@ fn tmux_pane_mode_captures_large_tail_and_summarizes() {
|
|
|
323
369
|
args_log.display()
|
|
324
370
|
),
|
|
325
371
|
);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
372
|
+
|
|
373
|
+
let request_log = Arc::new(Mutex::new(String::new()));
|
|
374
|
+
let request_log_for_server = Arc::clone(&request_log);
|
|
375
|
+
let (base_url, server) = start_api_server(1, move |request| {
|
|
376
|
+
*request_log_for_server.lock().expect("request log") = request;
|
|
377
|
+
(
|
|
378
|
+
200,
|
|
379
|
+
response_json("- summary: tmux pane summarized\n- warnings: tail captured"),
|
|
380
|
+
)
|
|
381
|
+
});
|
|
333
382
|
|
|
334
383
|
let path = format!(
|
|
335
384
|
"{}:{}",
|
|
@@ -338,6 +387,7 @@ fn tmux_pane_mode_captures_large_tail_and_summarizes() {
|
|
|
338
387
|
);
|
|
339
388
|
let output = Command::new(sparkshell_bin())
|
|
340
389
|
.env("PATH", path)
|
|
390
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
341
391
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
342
392
|
.arg("--tmux-pane")
|
|
343
393
|
.arg("%17")
|
|
@@ -345,6 +395,7 @@ fn tmux_pane_mode_captures_large_tail_and_summarizes() {
|
|
|
345
395
|
.arg("400")
|
|
346
396
|
.output()
|
|
347
397
|
.expect("run sparkshell");
|
|
398
|
+
server.join().expect("api server");
|
|
348
399
|
|
|
349
400
|
assert!(output.status.success());
|
|
350
401
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
@@ -356,36 +407,16 @@ fn tmux_pane_mode_captures_large_tail_and_summarizes() {
|
|
|
356
407
|
assert!(tmux_args.contains("%17"));
|
|
357
408
|
assert!(tmux_args.contains("-400"));
|
|
358
409
|
|
|
359
|
-
let
|
|
360
|
-
assert!(
|
|
361
|
-
assert!(
|
|
410
|
+
let request = request_log.lock().expect("request log");
|
|
411
|
+
assert!(request.contains("Command: tmux capture-pane"));
|
|
412
|
+
assert!(request.contains("line-1"));
|
|
362
413
|
|
|
363
414
|
let _ = fs::remove_dir_all(temp);
|
|
364
415
|
}
|
|
365
416
|
|
|
366
417
|
#[test]
|
|
367
418
|
fn raw_mode_keeps_boundary_output_without_summary() {
|
|
368
|
-
let temp = unique_temp_dir("boundary-raw");
|
|
369
|
-
let codex = temp.join("codex");
|
|
370
|
-
let codex_log = temp.join("codex.log");
|
|
371
|
-
write_executable(
|
|
372
|
-
&codex,
|
|
373
|
-
&format!(
|
|
374
|
-
"#!/bin/sh
|
|
375
|
-
printf '%s\n' invoked > '{}'
|
|
376
|
-
exit 0
|
|
377
|
-
",
|
|
378
|
-
codex_log.display()
|
|
379
|
-
),
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
let path = format!(
|
|
383
|
-
"{}:{}",
|
|
384
|
-
temp.display(),
|
|
385
|
-
env::var("PATH").unwrap_or_default()
|
|
386
|
-
);
|
|
387
419
|
let output = Command::new(sparkshell_bin())
|
|
388
|
-
.env("PATH", path)
|
|
389
420
|
.env("OMX_SPARKSHELL_LINES", "2")
|
|
390
421
|
.arg("sh")
|
|
391
422
|
.arg("-c")
|
|
@@ -394,67 +425,47 @@ exit 0
|
|
|
394
425
|
.expect("run sparkshell");
|
|
395
426
|
|
|
396
427
|
assert!(output.status.success());
|
|
397
|
-
assert_eq!(
|
|
398
|
-
String::from_utf8_lossy(&output.stdout),
|
|
399
|
-
"one
|
|
400
|
-
two
|
|
401
|
-
"
|
|
402
|
-
);
|
|
403
|
-
assert!(
|
|
404
|
-
!codex_log.exists(),
|
|
405
|
-
"codex should not run at the raw/summary boundary"
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
let _ = fs::remove_dir_all(temp);
|
|
428
|
+
assert_eq!(String::from_utf8_lossy(&output.stdout), "one\ntwo\n");
|
|
409
429
|
}
|
|
410
430
|
|
|
411
431
|
#[test]
|
|
412
432
|
fn summary_mode_uses_combined_stdout_and_stderr_threshold() {
|
|
413
|
-
let
|
|
414
|
-
let
|
|
415
|
-
let
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
"
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
",
|
|
423
|
-
prompt_log.display()
|
|
424
|
-
),
|
|
425
|
-
);
|
|
433
|
+
let request_log = Arc::new(Mutex::new(String::new()));
|
|
434
|
+
let request_log_for_server = Arc::clone(&request_log);
|
|
435
|
+
let (base_url, server) = start_api_server(1, move |request| {
|
|
436
|
+
*request_log_for_server.lock().expect("request log") = request;
|
|
437
|
+
(
|
|
438
|
+
200,
|
|
439
|
+
response_json("- summary: combined output exceeded threshold"),
|
|
440
|
+
)
|
|
441
|
+
});
|
|
426
442
|
|
|
427
|
-
let path = format!(
|
|
428
|
-
"{}:{}",
|
|
429
|
-
temp.display(),
|
|
430
|
-
env::var("PATH").unwrap_or_default()
|
|
431
|
-
);
|
|
432
443
|
let output = Command::new(sparkshell_bin())
|
|
433
|
-
.env("
|
|
444
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
434
445
|
.env("OMX_SPARKSHELL_LINES", "2")
|
|
435
446
|
.arg("sh")
|
|
436
447
|
.arg("-c")
|
|
437
448
|
.arg("printf 'one\n' && printf 'warn\nextra\n' >&2")
|
|
438
449
|
.output()
|
|
439
450
|
.expect("run sparkshell");
|
|
451
|
+
server.join().expect("api server");
|
|
440
452
|
|
|
441
453
|
assert!(output.status.success());
|
|
442
454
|
assert!(String::from_utf8_lossy(&output.stdout).contains("combined output exceeded threshold"));
|
|
443
|
-
let
|
|
444
|
-
assert!(
|
|
445
|
-
assert!(
|
|
446
|
-
"warn
|
|
447
|
-
extra"
|
|
448
|
-
));
|
|
449
|
-
|
|
450
|
-
let _ = fs::remove_dir_all(temp);
|
|
455
|
+
let request = request_log.lock().expect("request log");
|
|
456
|
+
assert!(request.contains("<<<STDERR"));
|
|
457
|
+
assert!(request.contains("warn\\nextra"));
|
|
451
458
|
}
|
|
452
459
|
|
|
453
460
|
#[test]
|
|
454
|
-
fn
|
|
455
|
-
let
|
|
461
|
+
fn summary_failure_when_api_is_missing_falls_back_to_raw_output() {
|
|
462
|
+
let listener = TcpListener::bind("127.0.0.1:0").expect("reserve port");
|
|
463
|
+
let base_url = format!("http://{}", listener.local_addr().expect("address"));
|
|
464
|
+
drop(listener);
|
|
465
|
+
|
|
456
466
|
let output = Command::new(sparkshell_bin())
|
|
457
|
-
.env("
|
|
467
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
468
|
+
.env("OMX_SPARKSHELL_SUMMARY_TIMEOUT_MS", "500")
|
|
458
469
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
459
470
|
.arg("/bin/sh")
|
|
460
471
|
.arg("-c")
|
|
@@ -463,41 +474,27 @@ fn summary_failure_when_codex_is_missing_falls_back_to_raw_output() {
|
|
|
463
474
|
.expect("run sparkshell");
|
|
464
475
|
|
|
465
476
|
assert!(output.status.success());
|
|
466
|
-
assert_eq!(
|
|
467
|
-
String::from_utf8_lossy(&output.stdout),
|
|
468
|
-
"one
|
|
469
|
-
two
|
|
470
|
-
"
|
|
471
|
-
);
|
|
477
|
+
assert_eq!(String::from_utf8_lossy(&output.stdout), "one\ntwo\n");
|
|
472
478
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
473
479
|
assert!(stderr.contains("child-err"));
|
|
474
480
|
assert!(stderr.contains("summary unavailable"));
|
|
475
|
-
|
|
476
|
-
let _ = fs::remove_dir_all(empty_path);
|
|
477
481
|
}
|
|
478
482
|
|
|
479
483
|
#[test]
|
|
480
484
|
fn tmux_pane_mode_uses_default_tail_lines_when_not_overridden() {
|
|
481
485
|
let temp = unique_temp_dir("tmux-default-tail");
|
|
482
486
|
let tmux = temp.join("tmux");
|
|
483
|
-
let codex = temp.join("codex");
|
|
484
487
|
let args_log = temp.join("tmux-args.log");
|
|
485
488
|
write_executable(
|
|
486
489
|
&tmux,
|
|
487
490
|
&format!(
|
|
488
|
-
"#!/bin/sh
|
|
489
|
-
printf '%s\n' \"$@\" > '{}'
|
|
490
|
-
printf 'line-1\nline-2\nline-3\n'
|
|
491
|
-
",
|
|
491
|
+
"#!/bin/sh\nprintf '%s\n' \"$@\" > '{}'\nprintf 'line-1\nline-2\nline-3\n'\n",
|
|
492
492
|
args_log.display()
|
|
493
493
|
),
|
|
494
494
|
);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
printf '%s\n' '- summary: used default tmux tail'
|
|
499
|
-
",
|
|
500
|
-
);
|
|
495
|
+
let (base_url, server) = start_api_server(1, |_request| {
|
|
496
|
+
(200, response_json("- summary: used default tmux tail"))
|
|
497
|
+
});
|
|
501
498
|
|
|
502
499
|
let path = format!(
|
|
503
500
|
"{}:{}",
|
|
@@ -506,11 +503,13 @@ printf '%s\n' '- summary: used default tmux tail'
|
|
|
506
503
|
);
|
|
507
504
|
let output = Command::new(sparkshell_bin())
|
|
508
505
|
.env("PATH", path)
|
|
506
|
+
.env("OMX_API_BASE_URL", base_url)
|
|
509
507
|
.env("OMX_SPARKSHELL_LINES", "1")
|
|
510
508
|
.arg("--tmux-pane")
|
|
511
509
|
.arg("%21")
|
|
512
510
|
.output()
|
|
513
511
|
.expect("run sparkshell");
|
|
512
|
+
server.join().expect("api server");
|
|
514
513
|
|
|
515
514
|
assert!(output.status.success());
|
|
516
515
|
let tmux_args = fs::read_to_string(args_log).expect("tmux args");
|
|
@@ -519,3 +518,245 @@ printf '%s\n' '- summary: used default tmux tail'
|
|
|
519
518
|
|
|
520
519
|
let _ = fs::remove_dir_all(temp);
|
|
521
520
|
}
|
|
521
|
+
|
|
522
|
+
#[test]
|
|
523
|
+
fn summary_module_does_not_shell_out_to_codex() {
|
|
524
|
+
let source =
|
|
525
|
+
fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("src/codex_bridge.rs"))
|
|
526
|
+
.expect("source");
|
|
527
|
+
assert!(!source.contains("Command::new(\"codex\")"));
|
|
528
|
+
assert!(!source.contains(".arg(\"exec\")"));
|
|
529
|
+
assert!(!source.contains("codex exec"));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
#[test]
|
|
533
|
+
fn json_mode_emits_machine_readable_contract() {
|
|
534
|
+
let output = Command::new(sparkshell_bin())
|
|
535
|
+
.arg("--json")
|
|
536
|
+
.arg("sh")
|
|
537
|
+
.arg("-c")
|
|
538
|
+
.arg("printf 'ok\n'")
|
|
539
|
+
.output()
|
|
540
|
+
.expect("run sparkshell");
|
|
541
|
+
|
|
542
|
+
assert!(output.status.success());
|
|
543
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
544
|
+
assert!(stdout.contains("\"ok\": true"));
|
|
545
|
+
assert!(stdout.contains("\"mode\": \"command\""));
|
|
546
|
+
assert!(stdout.contains("\"status\": \"ok\""));
|
|
547
|
+
assert!(stdout.contains("\"summary\":"));
|
|
548
|
+
assert!(stdout.contains("\"evidence\":"));
|
|
549
|
+
assert!(stdout.contains("\"raw_hash\":"));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
#[test]
|
|
553
|
+
fn json_mode_reports_failed_command_details() {
|
|
554
|
+
let output = Command::new(sparkshell_bin())
|
|
555
|
+
.arg("--json")
|
|
556
|
+
.arg("sh")
|
|
557
|
+
.arg("-c")
|
|
558
|
+
.arg("printf 'bad\n' >&2; exit 9")
|
|
559
|
+
.output()
|
|
560
|
+
.expect("run sparkshell");
|
|
561
|
+
|
|
562
|
+
assert_eq!(output.status.code(), Some(9));
|
|
563
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
564
|
+
assert!(stdout.contains("\"ok\": false"));
|
|
565
|
+
assert!(stdout.contains("\"status\": \"failed\""));
|
|
566
|
+
assert!(stdout.contains("\"exit_code\": 9"));
|
|
567
|
+
assert!(stdout.contains("bad"));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
#[test]
|
|
571
|
+
fn json_mode_classifies_auth_errors() {
|
|
572
|
+
let output = Command::new(sparkshell_bin())
|
|
573
|
+
.arg("--json")
|
|
574
|
+
.arg("sh")
|
|
575
|
+
.arg("-c")
|
|
576
|
+
.arg("printf 'Authorization failed\n' >&2; exit 1")
|
|
577
|
+
.output()
|
|
578
|
+
.expect("run sparkshell");
|
|
579
|
+
|
|
580
|
+
assert_eq!(output.status.code(), Some(1));
|
|
581
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
582
|
+
assert!(stdout.contains("\"classification\": \"auth_error\""));
|
|
583
|
+
assert!(stdout.contains("authentication-like error"));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
#[test]
|
|
587
|
+
fn direct_command_preserves_child_json_flag() {
|
|
588
|
+
let temp = unique_temp_dir("child-json-flag");
|
|
589
|
+
let script = temp.join("echo-argv");
|
|
590
|
+
write_executable(
|
|
591
|
+
&script,
|
|
592
|
+
r#"#!/usr/bin/env bash
|
|
593
|
+
printf '%s\n' "$@"
|
|
594
|
+
"#,
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
let output = Command::new(sparkshell_bin())
|
|
598
|
+
.arg(script)
|
|
599
|
+
.arg("--json")
|
|
600
|
+
.arg("value")
|
|
601
|
+
.output()
|
|
602
|
+
.expect("run sparkshell");
|
|
603
|
+
|
|
604
|
+
assert!(output.status.success());
|
|
605
|
+
assert_eq!(String::from_utf8_lossy(&output.stdout), "--json\nvalue\n");
|
|
606
|
+
let _ = fs::remove_dir_all(temp);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
#[test]
|
|
610
|
+
fn team_diagnostics_reads_last_turn_at_heartbeat() {
|
|
611
|
+
let temp = unique_temp_dir("last-turn-heartbeat");
|
|
612
|
+
let worker_dir = temp.join("team/demo/workers/worker-1");
|
|
613
|
+
fs::create_dir_all(&worker_dir).expect("worker dir");
|
|
614
|
+
fs::write(
|
|
615
|
+
worker_dir.join("heartbeat.json"),
|
|
616
|
+
r#"{"last_turn_at":"1970-01-01T00:00:00.000Z"}"#,
|
|
617
|
+
)
|
|
618
|
+
.expect("heartbeat");
|
|
619
|
+
|
|
620
|
+
let output = Command::new(sparkshell_bin())
|
|
621
|
+
.env("OMX_TEAM_STATE_ROOT", temp.display().to_string())
|
|
622
|
+
.arg("--json")
|
|
623
|
+
.arg("--team")
|
|
624
|
+
.arg("demo")
|
|
625
|
+
.arg("--worker")
|
|
626
|
+
.arg("worker-1")
|
|
627
|
+
.arg("printf")
|
|
628
|
+
.arg("ok\n")
|
|
629
|
+
.output()
|
|
630
|
+
.expect("run sparkshell");
|
|
631
|
+
|
|
632
|
+
assert!(output.status.success());
|
|
633
|
+
assert!(
|
|
634
|
+
String::from_utf8_lossy(&output.stdout).contains("\"classification\": \"stale_heartbeat\"")
|
|
635
|
+
);
|
|
636
|
+
let _ = fs::remove_dir_all(temp);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
#[test]
|
|
640
|
+
fn json_mode_reads_team_state_from_env_root() {
|
|
641
|
+
let temp = unique_temp_dir("team-state");
|
|
642
|
+
let worker_dir = temp.join("team/demo/workers/worker-1");
|
|
643
|
+
fs::create_dir_all(&worker_dir).expect("worker dir");
|
|
644
|
+
fs::write(
|
|
645
|
+
worker_dir.join("status.json"),
|
|
646
|
+
r#"{"state":"busy","task":"in_progress"}"#,
|
|
647
|
+
)
|
|
648
|
+
.expect("status");
|
|
649
|
+
|
|
650
|
+
let output = Command::new(sparkshell_bin())
|
|
651
|
+
.env("OMX_TEAM_STATE_ROOT", temp.display().to_string())
|
|
652
|
+
.arg("--json")
|
|
653
|
+
.arg("--team")
|
|
654
|
+
.arg("demo")
|
|
655
|
+
.arg("--worker")
|
|
656
|
+
.arg("worker-1")
|
|
657
|
+
.arg("sh")
|
|
658
|
+
.arg("-c")
|
|
659
|
+
.arg("printf 'quiet\n'")
|
|
660
|
+
.output()
|
|
661
|
+
.expect("run sparkshell");
|
|
662
|
+
|
|
663
|
+
assert!(output.status.success());
|
|
664
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
665
|
+
assert!(stdout.contains("\"classification\": \"busy_processing\""));
|
|
666
|
+
assert!(stdout.contains("do not shutdown yet"));
|
|
667
|
+
|
|
668
|
+
let _ = fs::remove_dir_all(temp);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
#[test]
|
|
672
|
+
fn pane_json_cache_reports_hits_and_since_last_changes() {
|
|
673
|
+
let temp = unique_temp_dir("pane-cache");
|
|
674
|
+
let tmux = temp.join("tmux");
|
|
675
|
+
let cache = temp.join("cache");
|
|
676
|
+
let pane = temp.join("pane.txt");
|
|
677
|
+
fs::write(&pane, "line-1\nline-2\n").expect("pane");
|
|
678
|
+
write_executable(&tmux, &format!("#!/bin/sh\ncat {}\n", pane.display()));
|
|
679
|
+
let path = format!(
|
|
680
|
+
"{}:{}",
|
|
681
|
+
temp.display(),
|
|
682
|
+
env::var("PATH").unwrap_or_default()
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
let first = Command::new(sparkshell_bin())
|
|
686
|
+
.env("PATH", &path)
|
|
687
|
+
.env("OMX_SPARKSHELL_CACHE_DIR", cache.display().to_string())
|
|
688
|
+
.arg("--json")
|
|
689
|
+
.arg("--tmux-pane")
|
|
690
|
+
.arg("%31")
|
|
691
|
+
.output()
|
|
692
|
+
.expect("first");
|
|
693
|
+
assert!(first.status.success());
|
|
694
|
+
assert!(String::from_utf8_lossy(&first.stdout).contains("\"cache_hit\":false"));
|
|
695
|
+
|
|
696
|
+
let second = Command::new(sparkshell_bin())
|
|
697
|
+
.env("PATH", &path)
|
|
698
|
+
.env("OMX_SPARKSHELL_CACHE_DIR", cache.display().to_string())
|
|
699
|
+
.arg("--json")
|
|
700
|
+
.arg("--tmux-pane")
|
|
701
|
+
.arg("%31")
|
|
702
|
+
.output()
|
|
703
|
+
.expect("second");
|
|
704
|
+
assert!(second.status.success());
|
|
705
|
+
assert!(String::from_utf8_lossy(&second.stdout).contains("\"cache_hit\":true"));
|
|
706
|
+
|
|
707
|
+
fs::write(&pane, "line-1\nline-2\nline-3\n").expect("pane update");
|
|
708
|
+
let third = Command::new(sparkshell_bin())
|
|
709
|
+
.env("PATH", &path)
|
|
710
|
+
.env("OMX_SPARKSHELL_CACHE_DIR", cache.display().to_string())
|
|
711
|
+
.arg("--json")
|
|
712
|
+
.arg("--since-last")
|
|
713
|
+
.arg("--tmux-pane")
|
|
714
|
+
.arg("%31")
|
|
715
|
+
.output()
|
|
716
|
+
.expect("third");
|
|
717
|
+
assert!(third.status.success());
|
|
718
|
+
let stdout = String::from_utf8_lossy(&third.stdout);
|
|
719
|
+
assert!(stdout.contains("\"changed_line_ranges\":[\"3-3\"]"));
|
|
720
|
+
assert!(stdout.contains("new findings since last observation"));
|
|
721
|
+
assert!(stdout.contains("line-3"));
|
|
722
|
+
|
|
723
|
+
let _ = fs::remove_dir_all(temp);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
#[test]
|
|
727
|
+
fn raw_mode_preserves_non_utf8_bytes() {
|
|
728
|
+
let temp = unique_temp_dir("raw-non-utf8");
|
|
729
|
+
let script = temp.join("raw-bytes");
|
|
730
|
+
write_executable(
|
|
731
|
+
&script,
|
|
732
|
+
r#"#!/usr/bin/env bash
|
|
733
|
+
printf '\xff\xfe\n'
|
|
734
|
+
"#,
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
let output = Command::new(sparkshell_bin())
|
|
738
|
+
.arg(script)
|
|
739
|
+
.output()
|
|
740
|
+
.expect("run sparkshell");
|
|
741
|
+
|
|
742
|
+
assert!(output.status.success());
|
|
743
|
+
assert_eq!(output.stdout, vec![0xff, 0xfe, b'\n']);
|
|
744
|
+
let _ = fs::remove_dir_all(temp);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
#[test]
|
|
748
|
+
fn shell_mode_executes_explicit_shell_and_redacts_json_output() {
|
|
749
|
+
let output = Command::new(sparkshell_bin())
|
|
750
|
+
.arg("--json")
|
|
751
|
+
.arg("--shell")
|
|
752
|
+
.arg("printf 'left && right\n'; printf 'Authorization: Bearer secret-token\n' >&2")
|
|
753
|
+
.output()
|
|
754
|
+
.expect("run sparkshell");
|
|
755
|
+
|
|
756
|
+
assert!(output.status.success());
|
|
757
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
758
|
+
assert!(stdout.contains("\"mode\": \"shell\""));
|
|
759
|
+
assert!(stdout.contains("left && right"));
|
|
760
|
+
assert!(stdout.contains("Authorization: Bearer [REDACTED]"));
|
|
761
|
+
assert!(stdout.contains("\"redactions\": {\"count\": 1}"));
|
|
762
|
+
}
|