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
//! gh #892 — end to end, attaching the debugger must not change the solve.
//!
//! `crates/pounce-convex/tests/issue892_debug_routes_like_the_plain_solve.rs`
//! pins the library-level routing. This file pins what the reporter actually
//! ran: the `pounce` binary, with and without a `--debug-script` containing
//! nothing but `continue`, which is a pure no-op on the trajectory.
//!
//! It exists because the CLI carried the *second* half of the defect, and the
//! library legs cannot see it. Until this issue the convex entry points
//! skipped presolve while a debugger was attached — deliberately, so the
//! inspected blocks were the user's rows rather than a reduced set — which
//! meant the debugged run solved a different, smaller problem. Both halves
//! have to be closed for "attaching the debugger changes nothing" to be true
//! of the command the user types, and only a process-level test says so.
//!
//! What each check reaches, in the terms `CLAUDE.md` sets for branch coverage:
//!
//! | check | path | the half it covers |
//! | --- | --- | --- |
//! | `conic_qcqp_is_unmoved_by_the_debugger` | `solver_selection=socp` | driver substitution (the reported symptom) |
//! | `conic_qcqp_is_unmoved_under_auto` | `solver_selection=auto` | the same, plus the NLP fallback that masked it |
//! | `presolve_still_runs_under_the_debugger` | conic, presolve reduces | the CLI carve-out |
//! | `convex_qp_is_unmoved_by_the_debugger` | `solver_selection=qp-ipm` | the LP/QP twin of the same carve-out |
//!
//! **Mutation table** — measured, not asserted:
//!
//! | mutation | red |
//! | --- | --- |
//! | reinstate the CLI presolve carve-out (a `debug_hook` arm ahead of `presolve_on` in both `run_convex_qp` and `run_convex_socp`) | `presolve_still_runs_under_the_debugger`, `convex_qp_is_unmoved_by_the_debugger` |
//! | `solve_socp_ipm_debug` builds its own iteration for symmetric cones | `conic_qcqp_is_unmoved_by_the_debugger`, `conic_qcqp_is_unmoved_under_auto` |
//!
//! The two halves are independent, and each mutation leaves the other half's
//! checks green — which is the argument for keeping both files.
//!
//! Not evidence about: the NLP filter-IPM (which never had the carve-out),
//! the active-set engine (where the debugger deliberately does not engage and
//! the caller says so), or `--debug-json`.
use Write;
use PathBuf;
use Command;
use ;
/// A `--debug-script` holding the issue's script: `continue`, and nothing
/// else. Written under the temp dir so the test needs no fixture of its own.
///
/// The name carries a per-call counter as well as the pid, because the four
/// `#[test]` functions below run as parallel threads of **one** process and
/// [`assert_unmoved`] deletes its script when it is done. On a pid-only name
/// all nine calls share one path, and one thread's `remove_file` lands between
/// another's create and its `--debug-script` read — a race whose window is
/// wide enough to matter at nine calls, and which green CI is no evidence
/// against.
/// The verdict line the CLI prints, reduced to the three fields a trajectory
/// change shows up in: status, objective and iteration count. The wall-clock
/// suffix is stripped — it moves run to run and says nothing about the solve.
///
/// e.g. `POUNCE (convex QCQP conic IPM, pounce-convex): Optimal Solution
/// Found. obj=0.46851408 iters=38 (0.049s)`
/// Run the binary on `model` with `opts`, optionally with the debugger
/// attached. Returns `(verdict line, exit code)`.
/// Plain and debugged runs must agree on status, objective, iteration count
/// **and exit code**. The iteration count is the sensitive field: gh #892's
/// two `Optimal` instances carried the same substituted driver as its two
/// failing ones, and only the count showed it.
/// The reported symptom. On `d32204e` the plain run returned `Optimal` and the
/// debugged one `Numerical failure (no verified KKT point)` with exit 1,
/// because the debug entry point ran the direct IPM where the plain one ran
/// the HSDE embedding.
/// Under `auto` the same conic failure was *masked* by the NLP fallback, at
/// the cost of a silent second solve — so the run disagreed with the plain one
/// in engine and iteration count even where the objective survived.
/// The CLI half. `qcqp_shared_linear_rows.nl` is the fixture whose rows
/// `presolve_conic` is the correct entry point for, so presolve has something
/// to do on it; before this issue the debugged run skipped the reduction and
/// solved a different problem. Checked both ways round, because
/// `qp_presolve=no` is the documented escape hatch for inspecting unreduced
/// blocks and it has to agree too.
/// The LP/QP entry point, which carried the same CLI carve-out. The library
/// legs report this path as already agreeing on the *driver*; presolve is the
/// part only a process-level run can see.