File size: 16,885 Bytes
2abcc30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
import json
import random
from pathlib import Path
from types import SimpleNamespace

from albedo_eval_service.shared.observation_format import MAX_CONSECUTIVE_BAD_TURNS
from local_eval.constants import MAX_NEW_TOKENS
from local_train.chain_eval import pick_free_gpus
from local_train.chain_heuristics import (
    ChainState,
    empty_submit_count_from_texts,
    evaluate_chain,
    mid_roll_fatal,
)
from albedo_eval_service.shared.observation_format import first_bash_block
from local_train.chain_gold import compile_session, micro_ok, path_aliases, session_passes
from local_train.chain_pack import _mix_stage_a, followup_edit_command, infer_micro
from local_train.reward import score_texts
from sanity_service.chain import empty_submit_count


def _state(**kwargs) -> ChainState:
    defaults = dict(
        sample_id="s1",
        submit_clause="echo SUBMIT_TASK_AAAA && cat patch.txt",
        submit_marker="SUBMIT_TASK_AAAA",
        micro={"file": "src/foo.py", "function": "bar", "request": "edit bar", "message": ""},
    )
    defaults.update(kwargs)
    return ChainState(**defaults)


def test_infer_micro_reads_path_and_function():
    micro = infer_micro("please look at src/util/foo.py\ndef bar():\n    pass")
    assert micro["file"] == "src/util/foo.py"
    assert micro["function"] == "bar"
    assert "foo.py" in followup_edit_command(micro["file"], micro["function"])


def test_infer_micro_skips_english_function_to():
    hay = (
        "Function: debounce<F extends (...args: any) => any>(func: F, options: {})\n"
        "Location: src/debounce.ts\n"
        "the function to debounce; its parameter and return types are preserved.\n"
    )
    micro = infer_micro(hay)
    assert micro["file"] == "src/debounce.ts"
    assert micro["function"] == "debounce"


def test_infer_micro_pairs_bold_symbol_with_matching_file():
    hay = (
        "Throws **SlackError** when parsing fails.\n"
        "Returns a parsed **Content** object.\n"
        "Edit src/content.js and src/errors.js.\n"
    )
    micro = infer_micro(hay)
    assert micro["file"] == "src/content.js"
    assert micro["function"] == "Content"


def test_infer_micro_prefers_cmakelists_on_cmake_pr():
    hay = (
        "# Build fails with GCC on macOS due to forced `-stdlib=libc++` flag\n"
        "CMakeLists.txt unconditionally adds the flag when APPLE.\n"
        "set(FlatBuffers_Library_SRCS\n"
        "  include/flatbuffers/base.h\n"
        "  include/flatbuffers/flatbuffers.h\n"
        ")\n"
        "Use CMAKE_CXX_FLAGS only for Clang.\n"
    )
    micro = infer_micro(hay)
    assert micro["file"].endswith("CMakeLists.txt")
    assert micro["function"] != "the helper"


def test_two_empty_submits_is_the_live_fault():
    texts = [
        "<think>x</think>\n\n```bash\necho SUBMIT_TASK_AAAA && cat patch.txt\n```",
        "<think>x</think>\n\n```bash\necho SUBMIT_TASK_AAAA && cat patch.txt\n```",
    ]
    assert empty_submit_count_from_texts(texts, "SUBMIT_TASK_AAAA") >= 2
    ns = SimpleNamespace(
        turns=[{"role": "assistant", "content": t, "score_target": True} for t in texts]
    )
    assert empty_submit_count(ns, "SUBMIT_TASK_AAAA") >= 2


def test_edit_between_submits_is_not_empty():
    texts = [
        "<think>e</think>\n\n```bash\nsed -i 's/a/b/' src/foo.py\n```",
        "<think>s</think>\n\n```bash\necho SUBMIT_TASK_AAAA && cat patch.txt\n```",
        "<think>e2</think>\n\n```bash\nsed -i 's/b/c/' src/foo.py\n```",
        "<think>s2</think>\n\n```bash\necho SUBMIT_TASK_AAAA && cat patch.txt\n```",
    ]
    assert empty_submit_count_from_texts(texts, "SUBMIT_TASK_AAAA") == 0


def test_evaluate_chain_flags_empty_double_submit():
    first = "<think>s</think>\n\n```bash\necho SUBMIT_TASK_AAAA && cat patch.txt\n```"
    second = "<think>s</think>\n\n```bash\necho SUBMIT_TASK_AAAA\n```"
    state = _state(
        turns=[
            {"role": "user", "content": "Please handle the missed call site.", "segment": "micro"},
            {"role": "assistant", "content": first, "score_target": True, "segment": "micro"},
            {"role": "user", "content": "Please handle the missed call site.", "injected": True},
            {"role": "assistant", "content": second, "score_target": True, "segment": "micro"},
        ],
        submits=[{"post_nudge": False, "has_edit": False}, {"post_nudge": False, "has_edit": False}],
        micro=None,
    )
    evaluate_chain([state], 8)
    assert state.heuristic_reason == "chain: repeated submissions without doing any work"


def test_evaluate_chain_passes_edit_then_submit_then_edit_then_submit():
    read = "<think>r</think>\n\n```bash\ncat src/foo.py\n```"
    edit = "<think>e</think>\n\n```bash\nsed -i 's/a/b/' src/foo.py\n```"
    edit2 = "<think>e2</think>\n\n```bash\nsed -i 's/b/c/' src/foo.py\n```"
    submit = "<think>s</think>\n\n```bash\necho SUBMIT_TASK_AAAA && cat patch.txt\n```"
    state = _state(
        turns=[
            {"role": "user", "content": "handle src/foo.py bar; echo SUBMIT_TASK_AAAA", "segment": "micro"},
            {"role": "assistant", "content": read, "score_target": True, "segment": "micro"},
            {"role": "user", "content": "ok"},
            {"role": "assistant", "content": edit, "score_target": True, "segment": "micro"},
            {"role": "user", "content": "ok"},
            {"role": "assistant", "content": submit, "score_target": True, "segment": "micro"},
            {"role": "user", "content": "not quite"},
            {"role": "assistant", "content": edit2, "score_target": True, "segment": "micro"},
            {"role": "user", "content": "ok"},
            {"role": "assistant", "content": submit, "score_target": True, "segment": "micro"},
        ],
        submits=[{"post_nudge": False, "has_edit": True}, {"post_nudge": False, "has_edit": True}],
    )
    evaluate_chain([state], 8)
    assert state.heuristic_reason == ""


def test_reward_zero_on_empty_double_submit():
    texts = [
        "<think>r</think>\n\n```bash\ncat src/foo.py\n```",
        "<think>x</think>\n\n```bash\necho ALBEDO_TASK_DONE_SUBMIT_NOW\n```",
        "<think>x</think>\n\n```bash\necho ALBEDO_TASK_DONE_SUBMIT_NOW\n```",
    ]
    got = score_texts(texts, submit_command="echo ALBEDO_TASK_DONE_SUBMIT_NOW")
    assert got.fatal and got.reward == 0.0
    assert "empty_double_submit" in got.reasons


def test_mix_stage_a_prefers_at_edit_and_post_edit_submit(tmp_path: Path):
    rows = [
        {"sample_id": "e1", "kind": "edit", "phase": "at_edit", "prompt": "p", "completion": "c"},
        {"sample_id": "e2", "kind": "edit", "phase": "explore", "prompt": "p", "completion": "c"},
        {"sample_id": "s1", "kind": "submit", "phase": "post_edit", "prompt": "p", "completion": "c"},
        {"sample_id": "s2", "kind": "submit", "phase": "cold", "prompt": "p", "completion": "c"},
        {"sample_id": "x1", "kind": "explore", "phase": "explore", "prompt": "p", "completion": "c"},
    ]
    pack = tmp_path / "mix.jsonl"
    pack.write_text("".join(json.dumps(row) + "\n" for row in rows))
    mixed = _mix_stage_a(pack, random.Random(0), edit_n=1, submit_n=1)
    assert {row.sample_id for row in mixed} == {"e1", "s1"}


def test_micro_ok_drops_weak_targets():
    assert not micro_ok({"file": "content.js", "function": "bar"})
    assert not micro_ok({"file": "src/foo.py", "function": "the helper"})
    assert not micro_ok({"file": "src/foo.py", "function": "to"})
    assert micro_ok({"file": "src/util/foo.py", "function": "bar"})
    assert micro_ok({"file": "CMakeLists.txt", "function": "GCC"})


def test_mid_roll_fatal_flags_loop_without_waiting_for_horizon():
    submit = "<think>s</think>\n\n```bash\necho SUBMIT_TASK_AAAA && cat patch.txt\n```"
    state = _state(
        turns=[
            {"role": "assistant", "content": submit, "score_target": True, "segment": "micro"}
            for _ in range(5)
        ],
        submits=[{"post_nudge": False, "has_edit": False} for _ in range(5)],
    )
    assert "same command repeated" in mid_roll_fatal(state)


def test_pick_free_gpus_skips_occupied_cards(monkeypatch):
    monkeypatch.setattr(
        "local_train.chain_eval.subprocess.check_output",
        lambda *args, **kwargs: "0, 20000\n1, 120000\n2, 130000\n3, 140000\n4, 5000\n",
    )
    assert pick_free_gpus(3, min_free_gib=100) == ["1", "2", "3"]


def test_pick_free_gpus_errors_when_not_enough(monkeypatch):
    monkeypatch.setattr(
        "local_train.chain_eval.subprocess.check_output",
        lambda *args, **kwargs: "0, 20000\n1, 21000\n",
    )
    try:
        pick_free_gpus(4, min_free_gib=100)
    except RuntimeError as exc:
        assert "need 4 GPUs" in str(exc)
    else:
        raise AssertionError("expected RuntimeError")


def test_kill_process_tree_reaps_child():
    import subprocess as sp

    from albedo_eval_service.remote.generation import _kill_process_tree

    proc = sp.Popen(["sleep", "60"])
    _kill_process_tree(proc.pid)
    proc.wait(timeout=3)
    assert proc.poll() is not None


def test_compiled_session_passes_evaluate_chain():
    micro = {
        "file": "src/util/foo.py",
        "function": "bar",
        "request": "edit bar in foo.py",
        "message": "",
    }
    clause = "echo SUBMIT_TASK_AAAA && cat patch.txt"
    completions = compile_session(micro, clause)
    assert completions is not None
    assert len(completions) >= 20
    assert session_passes(micro, clause, "SUBMIT_TASK_AAAA", completions) == ""
    cmds = [first_bash_block(text) or "" for text in completions]
    assert all("sed" not in cmd or "&&" not in cmd for cmd in cmds)
    for prev, cur in zip(cmds, cmds[1:]):
        assert not ("SUBMIT_TASK_AAAA" in prev and "SUBMIT_TASK_AAAA" in cur)


def test_live_token_cap_is_4k():
    assert MAX_NEW_TOKENS == 4096
    assert MAX_CONSECUTIVE_BAD_TURNS == 3


def test_is_submit_turn_accepts_marker_without_exact_tail():
    from local_train.chain_heuristics import is_submit_turn

    text = "<think>s</think>\n\n```bash\necho SUBMIT_TASK_AAAA\n```"
    assert is_submit_turn(text, "echo SUBMIT_TASK_AAAA && cat patch.txt", "SUBMIT_TASK_AAAA")


def test_path_aliases_include_testbed_prefix():
    aliases = path_aliases("CMakeLists.txt")
    assert "CMakeLists.txt" in aliases
    assert "testbed/CMakeLists.txt" in aliases
    assert "/testbed/CMakeLists.txt" in aliases
    completions = compile_session(
        {"file": "CMakeLists.txt", "function": "GCC", "request": "edit", "message": ""},
        "echo FINALIZE_AND_SUBMIT_TASK_OUTPUT",
        path="testbed/CMakeLists.txt",
    )
    assert completions is not None
    assert any("cat testbed/CMakeLists.txt" in text for text in completions)
    assert any("sed -i" in text and "testbed/CMakeLists.txt" in text for text in completions)


def test_live_nosa_trace_is_empty_double_submit():
    """Exact live v11/nosa pattern: sed, bare echo, grep/cat, bare echo, sed, echo, grep, echo."""
    marker = "COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT"
    texts = [
        "<think>e</think>\n\n```bash\nsed -i '/def test_wheel_tag_expand/,/^def / { /mac_ver/a\\    x' tests/test_builder.py\n```",
        "<think>s</think>\n\n```bash\necho COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT\n```",
        "<think>g</think>\n\n```bash\ngrep -n maxsize tests/test_builder.py\n```",
        "<think>c</think>\n\n```bash\ncat -n tests/test_builder.py | grep test_wheel_tag_expand\n```",
        "<think>s</think>\n\n```bash\necho COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT\n```",
        "<think>e</think>\n\n```bash\nsed -i '/def test_wheel_tag_expand_11/,/^def / { /mac_ver/a\\    x' tests/test_builder.py\n```",
        "<think>s</think>\n\n```bash\necho COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT\n```",
        "<think>g</think>\n\n```bash\ngrep -rn buildver src/scikit_build_core --include='*.py'\n```",
        "<think>s</think>\n\n```bash\necho COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT\n```",
    ]
    assert empty_submit_count_from_texts(texts, marker) >= 2


def test_reject_edit_thought_forbids_submit_now():
    from local_train.chain_gold import reject_edit_thought

    thought = reject_edit_thought("src/foo.py", "bar", "echo SUBMIT_TASK_AAAA && cat patch.txt")
    assert "not submit-now" in thought
    assert "sed -i" in thought
    assert "miner fault" in thought


def test_path_only_session_passes_on_live_micros():
    clause = "echo FINALIZE_AND_SUBMIT_TASK_OUTPUT"
    for micro in (
        {"file": "src/content.js", "function": "Content", "request": "edit Content", "message": ""},
        {"file": "CMakeLists.txt", "function": "GCC", "request": "edit CMakeLists", "message": ""},
        {"file": "src/debounce.ts", "function": "debounce", "request": "edit debounce", "message": ""},
    ):
        completions = compile_session(micro, clause)
        assert completions is not None
        assert session_passes(micro, clause, "FINALIZE_AND_SUBMIT_TASK_OUTPUT", completions) == ""
        assert all("sed -i" not in text or clause not in text for text in completions)
        cmds = [first_bash_block(text) or "" for text in completions]
        assert len(cmds) >= 20
        for prev, cur in zip(cmds, cmds[1:]):
            assert not ("FINALIZE_AND_SUBMIT_TASK_OUTPUT" in prev and "FINALIZE_AND_SUBMIT_TASK_OUTPUT" in cur)
        assert sum("sed -i" in cmd for cmd in cmds) >= 8
        assert sum(clause.split()[0] in cmd for cmd in cmds) == 2


def test_compiled_session_has_exactly_two_submits():
    micro = {
        "file": "src/util/foo.py",
        "function": "bar",
        "request": "edit bar",
        "message": "",
    }
    clause = "echo SUBMIT_TASK_AAAA && cat patch.txt"
    completions = compile_session(micro, clause)
    assert completions is not None
    cmds = [first_bash_block(text) or "" for text in completions]
    assert cmds[2] == clause
    assert cmds[4] == clause
    assert all("SUBMIT_TASK_AAAA" not in cmd for cmd in cmds[5:])
    assert all("sed -i" in cmd for i, cmd in enumerate(cmds) if i not in {0, 2, 4})


def test_dpo_pairs_prefer_sed_over_live_ban():
    from local_train.dpo_pack import CLAUSES, iter_prefixes, strip_open_think

    prefixes = iter_prefixes()
    assert {p.kind for p in prefixes} >= {"nosa", "mixed", "messy", "recat", "after_submit"}
    assert len({p.sample_id for p in prefixes}) == len(prefixes)
    assert any(p.file.startswith("tests/") for p in prefixes)
    assert any("cmake.org" in p.file for p in prefixes)
    nosa = next(p for p in prefixes if p.kind == "nosa")
    cmds = []
    for msg in nosa.messages:
        if msg["role"] == "assistant":
            cmds.append(first_bash_block(msg["content"]) or "")
    assert any(c.startswith("sed -i") for c in cmds)
    assert any("grep" in c for c in cmds)
    assert any(nosa.submit_marker in c for c in cmds)
    last_user = nosa.messages[-1]["content"]
    assert "Submit the same way when done" in last_user
    assert nosa.submit_command in last_user
    mixed = next(p for p in prefixes if p.kind == "mixed")
    last_asst = first_bash_block(mixed.messages[-2]["content"]) or ""
    assert "sed" in last_asst and "&&" in last_asst and mixed.submit_marker in last_asst
    for prefix in prefixes:
        assert "sed -i" in prefix.chosen_bash
        assert prefix.submit_marker not in prefix.chosen_bash
        assert any(prefix.submit_marker in bash for bash in prefix.rejected_bashes)
        assert prefix.chosen_bash not in prefix.rejected_bashes
    prompt = "assistant\n<think>\n"
    chosen = "<think>\nbody\n</think>\n\n```bash\nsed -i '1a\\x' a.py\n```\n"
    assert strip_open_think(prompt, chosen).startswith("body")
    assert all(marker for _, marker in CLAUSES)


def test_late_horizon_trap_is_sed_after_a_submit():
    from types import SimpleNamespace

    from local_eval.constants import TOKENIZER_DIR
    from local_train.chain_gold import _gold_rows

    micro = {
        "file": "src/util/foo.py",
        "function": "bar",
        "request": "edit bar",
        "message": "",
    }
    clause = "echo SUBMIT_TASK_AAAA && cat patch.txt"
    completions = compile_session(micro, clause)
    assert completions is not None
    rows = _gold_rows(
        sample_id="late-trap-test",
        prefix=[],
        micro=micro,
        completions=completions,
        clause=clause,
        marker="SUBMIT_TASK_AAAA",
        rewrite_mode="test",
        item=SimpleNamespace(source="test", gold="src/util/foo.py", family="chain", language="", repo=""),
        salt="late-test",
        tokenizer=str(TOKENIZER_DIR),
    )
    late = [row for row in rows if row.kind == "chain_late_trap"]
    assert late
    for row in late:
        bash = first_bash_block(row.completion) or ""
        assert "sed -i" in bash
        assert "SUBMIT_TASK_AAAA" not in bash
        assert "not submit-now" in row.completion
        assert "SUBMIT_TASK_AAAA" in row.prompt
        assert "chain-late" in bash