1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
#![cfg(feature = "test-utils")]
//! Integration tests for --debug-packet flag behavior (FR-PKT-006, FR-PKT-007)
//!
//! **WHITE-BOX TEST**: This test uses internal module APIs (`orchestrator::{OrchestratorConfig,
//! PhaseOrchestrator}`, `paths`) and may break with internal refactors. These tests are
//! intentionally white-box to validate internal implementation details. See FR-TEST-4 for
//! white-box test policy.
//!
//! This test suite verifies that:
//! - Debug packet is written when --debug-packet flag is set and secret scan passes
//! - Debug packet is NOT written if secrets are detected
//! - Debug packet file is excluded from receipts
//! - Debug packet content is redacted if later reported
use anyhow::Result;
use std::collections::HashMap;
use std::fs;
use xchecker::orchestrator::{OrchestratorConfig, PhaseOrchestrator};
use xchecker::paths;
use xchecker::test_support;
/// Test that debug packet is written when --debug-packet flag is set (FR-PKT-006)
#[tokio::test]
async fn test_debug_packet_written_with_flag() -> Result<()> {
// Use thread-local isolated home to prevent cross-test contamination
let _home_guard = paths::with_isolated_home();
let spec_id = "test-debug-packet-enabled";
// Create a source file in the spec's context directory so the packet has content
let spec_context_dir = paths::spec_root(spec_id).join("context");
fs::create_dir_all(&spec_context_dir)?;
fs::write(
spec_context_dir.join("problem-statement.md"),
"# Problem Statement\n\nThis is a test problem statement for the debug packet test.",
)?;
// Create orchestrator
let orchestrator = PhaseOrchestrator::new(spec_id)?;
// Create config with debug_packet enabled
let mut config_map = HashMap::new();
config_map.insert("debug_packet".to_string(), "true".to_string());
let config = OrchestratorConfig {
dry_run: true, // Use dry-run to avoid actual Claude invocation
config: config_map,
full_config: None,
selectors: None,
strict_validation: false,
redactor: Default::default(),
hooks: None,
};
// Execute requirements phase
let result = orchestrator.execute_requirements_phase(&config).await;
// Verify execution succeeded
assert!(result.is_ok(), "Phase execution should succeed");
let execution_result = result.unwrap();
assert!(
execution_result.success,
"Phase should complete successfully"
);
// Verify debug packet file was written
let debug_packet_path = spec_context_dir.join("requirements-packet-debug.txt");
assert!(
debug_packet_path.exists(),
"Debug packet file should exist at: {debug_packet_path:?}"
);
// Verify debug packet contains content (at minimum the problem statement)
let debug_content = fs::read_to_string(&debug_packet_path)?;
assert!(
!debug_content.is_empty(),
"Debug packet should contain content"
);
Ok(())
}
/// Test that debug packet is NOT written when flag is not set (FR-PKT-006)
#[tokio::test]
async fn test_debug_packet_not_written_without_flag() -> Result<()> {
// Use thread-local isolated home to prevent cross-test contamination
let _home_guard = paths::with_isolated_home();
let spec_id = "test-debug-packet-disabled";
// Create orchestrator
let orchestrator = PhaseOrchestrator::new(spec_id)?;
// Create config WITHOUT debug_packet flag
let config = OrchestratorConfig {
dry_run: true,
config: HashMap::new(),
full_config: None,
selectors: None,
strict_validation: false,
redactor: Default::default(),
hooks: None,
};
// Execute requirements phase
let result = orchestrator.execute_requirements_phase(&config).await;
// Verify execution succeeded
assert!(result.is_ok(), "Phase execution should succeed");
// Verify debug packet file was NOT written
let debug_packet_path = paths::spec_root(spec_id)
.join("context")
.join("requirements-packet-debug.txt");
assert!(
!debug_packet_path.exists(),
"Debug packet file should NOT exist when flag is not set"
);
Ok(())
}
/// Test that debug packet is NOT written if secrets are detected (FR-PKT-007)
#[tokio::test]
async fn test_debug_packet_not_written_on_secret_detection() -> Result<()> {
// Use thread-local isolated home to prevent cross-test contamination
let _home_guard = paths::with_isolated_home();
let spec_id = "test-debug-packet-secret";
// Create a file containing a secret in the spec's context directory
// This is where the packet builder looks for files to include
let spec_context_dir = paths::spec_root(spec_id).join("context");
fs::create_dir_all(&spec_context_dir)?;
let token = test_support::github_pat();
fs::write(
spec_context_dir.join("problem-statement.md"),
format!(
"# Problem Statement\n\nThis file contains a secret: {}",
token
),
)?;
// Create orchestrator
let orchestrator = PhaseOrchestrator::new(spec_id)?;
// Create config with debug_packet enabled
let mut config_map = HashMap::new();
config_map.insert("debug_packet".to_string(), "true".to_string());
let config = OrchestratorConfig {
dry_run: true,
config: config_map,
full_config: None,
selectors: None,
strict_validation: false,
redactor: Default::default(),
hooks: None,
};
// Execute requirements phase (will fail due to secret detection)
let result = orchestrator.execute_requirements_phase(&config).await;
// Secret detection returns an error from the orchestrator
// The key assertion is that the debug packet file was NOT written
// before the secret was detected
// Verify debug packet file was NOT written (secret scan failed before debug packet could be written)
let debug_packet_path = spec_context_dir.join("requirements-packet-debug.txt");
assert!(
!debug_packet_path.exists(),
"Debug packet file should NOT exist when secrets are detected"
);
// The result should be an error (secret detection fails the phase)
// This is the expected behavior - secrets cause the phase to fail
assert!(
result.is_err(),
"Secret detection should cause the phase to fail with an error"
);
Ok(())
}
/// Test that debug packet file is not cross-linked in receipts (FR-PKT-007)
#[tokio::test]
async fn test_debug_packet_not_in_receipts() -> Result<()> {
// Use thread-local isolated home to prevent cross-test contamination
let _home_guard = paths::with_isolated_home();
let spec_id = "test-debug-packet-receipt";
// Create a source file in the spec's context directory so the packet has content
let spec_context_dir = paths::spec_root(spec_id).join("context");
fs::create_dir_all(&spec_context_dir)?;
fs::write(
spec_context_dir.join("problem-statement.md"),
"# Problem Statement\n\nThis is a test problem statement.",
)?;
// Create orchestrator
let orchestrator = PhaseOrchestrator::new(spec_id)?;
// Create config with debug_packet enabled
let mut config_map = HashMap::new();
config_map.insert("debug_packet".to_string(), "true".to_string());
let config = OrchestratorConfig {
dry_run: true,
config: config_map,
full_config: None,
selectors: None,
strict_validation: false,
redactor: Default::default(),
hooks: None,
};
// Execute requirements phase
let result = orchestrator.execute_requirements_phase(&config).await;
// Verify execution succeeded
assert!(result.is_ok(), "Phase execution should succeed");
let execution_result = result.unwrap();
assert!(
execution_result.success,
"Phase should complete successfully"
);
// Read the receipt
let receipt_path = execution_result.receipt_path.expect("Receipt should exist");
let receipt_content = fs::read_to_string(&receipt_path)?;
// Verify receipt does NOT mention the debug packet file specifically
// Note: We only check for the specific debug packet filename, not the word "debug"
// because the receipt may contain "debug" in other contexts (like dry_run extensions)
assert!(
!receipt_content.contains("packet-debug.txt"),
"Receipt should not reference debug packet file"
);
assert!(
!receipt_content.contains("requirements-packet-debug"),
"Receipt should not reference debug packet file path"
);
Ok(())
}
/// Test that regular packet preview is still written (FR-PKT)
#[tokio::test]
async fn test_packet_preview_always_written() -> Result<()> {
// Use thread-local isolated home to prevent cross-test contamination
let _home_guard = paths::with_isolated_home();
let spec_id = "test-packet-preview";
// Create orchestrator
let orchestrator = PhaseOrchestrator::new(spec_id)?;
// Create config WITHOUT debug_packet flag
let config = OrchestratorConfig {
dry_run: true,
config: HashMap::new(),
full_config: None,
selectors: None,
strict_validation: false,
redactor: Default::default(),
hooks: None,
};
// Execute requirements phase
let result = orchestrator.execute_requirements_phase(&config).await;
// Verify execution succeeded
assert!(result.is_ok(), "Phase execution should succeed");
// Verify regular packet preview file was written (not debug packet)
let preview_packet_path = paths::spec_root(spec_id)
.join("context")
.join("requirements-packet.txt");
assert!(
preview_packet_path.exists(),
"Regular packet preview should always be written"
);
// Verify debug packet was NOT written
let debug_packet_path = paths::spec_root(spec_id)
.join("context")
.join("requirements-packet-debug.txt");
assert!(
!debug_packet_path.exists(),
"Debug packet should not be written without flag"
);
Ok(())
}