Skip to content

Commit 03e901d

Browse files
committed
fix(harness): preserve artifact values and write exits
1 parent dcc43e2 commit 03e901d

4 files changed

Lines changed: 119 additions & 29 deletions

File tree

nemo_retriever/src/nemo_retriever/harness/artifact_writer.py

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import json
1111
import os
1212
from pathlib import Path
13+
import re
1314
import shutil
1415
import sys
1516
import tempfile
@@ -50,15 +51,17 @@ def append_jsonl(path: Path, payload: Mapping[str, Any]) -> None:
5051

5152

5253
def append_text(path: Path, text: str) -> None:
53-
path.parent.mkdir(parents=True, exist_ok=True)
54-
with path.open("a", encoding="utf-8") as handle:
55-
handle.write(text)
54+
try:
55+
path.parent.mkdir(parents=True, exist_ok=True)
56+
with path.open("a", encoding="utf-8") as handle:
57+
handle.write(text)
58+
except OSError as exc:
59+
raise artifact_write_error(exc) from exc
5660

5761

5862
@contextlib.contextmanager
5963
def capture_output_to_log(path: Path, *, label: str):
6064
"""Capture noisy stdout/stderr to a persistent run log."""
61-
path.parent.mkdir(parents=True, exist_ok=True)
6265
append_text(path, f"\n## {utc_now()} {label} start\n")
6366
try:
6467
stdout_fd, stderr_fd = sys.stdout.fileno(), sys.stderr.fileno()
@@ -99,16 +102,19 @@ def capture_output_to_log(path: Path, *, label: str):
99102
if buf is not None:
100103
buf.seek(0)
101104
captured = buf.read()
102-
with path.open("ab") as handle:
103-
if captured:
104-
handle.write(captured)
105-
if not captured.endswith(b"\n"):
106-
handle.write(b"\n")
107-
if failure_traceback:
108-
handle.write(failure_traceback.encode("utf-8", errors="replace"))
109-
if not failure_traceback.endswith("\n"):
110-
handle.write(b"\n")
111-
handle.write(f"## {utc_now()} {label} {'failed' if failed else 'complete'}\n".encode("utf-8"))
105+
try:
106+
with path.open("ab") as handle:
107+
if captured:
108+
handle.write(captured)
109+
if not captured.endswith(b"\n"):
110+
handle.write(b"\n")
111+
if failure_traceback:
112+
handle.write(failure_traceback.encode("utf-8", errors="replace"))
113+
if not failure_traceback.endswith("\n"):
114+
handle.write(b"\n")
115+
handle.write(f"## {utc_now()} {label} {'failed' if failed else 'complete'}\n".encode("utf-8"))
116+
except OSError as exc:
117+
raise artifact_write_error(exc) from exc
112118
if failed and captured:
113119
sys.stderr.buffer.write(captured)
114120
sys.stderr.flush()
@@ -121,12 +127,22 @@ def capture_output_to_log(path: Path, *, label: str):
121127
os.close(saved_stdout)
122128

123129

124-
_SENSITIVE_KEY_PARTS = ("api_key", "password", "secret", "credential", "token", "webhook")
130+
_SENSITIVE_KEY_MARKERS = ("api_key", "password", "secret", "credential", "webhook")
131+
_TOKEN_CREDENTIAL_QUALIFIERS = {"access", "api", "auth", "bearer", "oauth", "refresh", "service", "session"}
125132

126133

127134
def _is_sensitive_key(value: Any) -> bool:
128-
normalized = str(value).lower()
129-
return any(token in normalized for token in _SENSITIVE_KEY_PARTS)
135+
snake_case = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", str(value))
136+
normalized = re.sub(r"[^a-z0-9]+", "_", snake_case.lower()).strip("_")
137+
if any(marker in normalized for marker in _SENSITIVE_KEY_MARKERS):
138+
return True
139+
parts = normalized.split("_")
140+
if "token" in parts:
141+
return True
142+
return any(
143+
part == "tokens" and index > 0 and parts[index - 1] in _TOKEN_CREDENTIAL_QUALIFIERS
144+
for index, part in enumerate(parts)
145+
)
130146

131147

132148
def redact(value: Any) -> Any:

nemo_retriever/src/nemo_retriever/harness/beir_runner.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
FailurePayload,
1818
HarnessRunError,
1919
)
20-
from nemo_retriever.harness.json_io import write_json
20+
from nemo_retriever.harness.json_io import artifact_write_error, write_json
2121
from nemo_retriever.query.options import QueryRequest
2222
from nemo_retriever.query.workflow import ResolvedQueryPlan
2323
from nemo_retriever.tools.recall.beir import (
@@ -30,12 +30,15 @@
3030

3131

3232
def _write_trec_run(path: Path, run: Mapping[str, Mapping[str, float]]) -> None:
33-
path.parent.mkdir(parents=True, exist_ok=True)
34-
with path.open("w", encoding="utf-8") as handle:
35-
for query_id, docs in run.items():
36-
ordered = sorted(docs.items(), key=lambda item: (-item[1], item[0]))
37-
for rank, (doc_id, score) in enumerate(ordered, start=1):
38-
handle.write(f"{query_id} Q0 {doc_id} {rank} {float(score):.6f} retriever-harness\n")
33+
try:
34+
path.parent.mkdir(parents=True, exist_ok=True)
35+
with path.open("w", encoding="utf-8") as handle:
36+
for query_id, docs in run.items():
37+
ordered = sorted(docs.items(), key=lambda item: (-item[1], item[0]))
38+
for rank, (doc_id, score) in enumerate(ordered, start=1):
39+
handle.write(f"{query_id} Q0 {doc_id} {rank} {float(score):.6f} retriever-harness\n")
40+
except OSError as exc:
41+
raise artifact_write_error(exc) from exc
3942

4043

4144
def _write_query_result(
@@ -165,8 +168,6 @@ def _dense_retrieve(
165168
raw_hits: list[list[dict[str, Any]]] = []
166169
latencies_ms: list[float] = []
167170
query_results_path = writer.path("query_results.jsonl")
168-
if query_results_path.exists():
169-
query_results_path.unlink()
170171
for query_id, query_text in zip(dataset.query_ids, dataset.queries):
171172
start = time.perf_counter()
172173
try:
@@ -251,8 +252,6 @@ def _agentic_retrieve(
251252
query_count = len(dataset.queries)
252253
per_query_ms = total_ms / query_count if query_count else 0.0
253254
query_results_path = writer.path("query_results.jsonl")
254-
if query_results_path.exists():
255-
query_results_path.unlink()
256255
for query_id, query_text, doc_ids in zip(dataset.query_ids, dataset.queries, ranked_doc_ids):
257256
_write_query_result(
258257
query_results_path,

nemo_retriever/src/nemo_retriever/harness/execution.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,8 @@ def run_prepared_benchmark(
373373
with capture_output_to_log(writer.path("run.log"), label="ingest"):
374374
ingest_summary = run_ingest_workflow(ingest_plan, dry_run=False)
375375
except Exception as exc:
376+
if isinstance(exc, HarnessRunError) and exc.exit_code == EXIT_ARTIFACT_WRITE_FAILURE:
377+
raise
376378
raise HarnessRunError(
377379
EXIT_INGEST_FAILURE,
378380
FailurePayload(

nemo_retriever/tests/test_harness_artifacts.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@
22
# All rights reserved.
33
# SPDX-License-Identifier: Apache-2.0
44

5+
from contextlib import contextmanager
56
import json
67

78
import pytest
89

9-
from nemo_retriever.harness.artifact_writer import artifact_paths, ArtifactWriter, capture_output_to_log, redact
10+
from nemo_retriever.harness.artifact_writer import (
11+
append_text,
12+
artifact_paths,
13+
ArtifactWriter,
14+
capture_output_to_log,
15+
redact,
16+
)
17+
from nemo_retriever.harness.beir_runner import _write_trec_run
1018
from nemo_retriever.harness.contracts import (
1119
EXIT_ARTIFACT_WRITE_FAILURE,
1220
EXIT_INGEST_FAILURE,
@@ -91,6 +99,42 @@ def test_redact_recurses_into_structured_override_values():
9199
assert redact(override) == 'query={"reranker_api_key": "<redacted>", "top_k": 10}'
92100

93101

102+
def test_redact_preserves_token_limits():
103+
payload = {
104+
"reranker_api_key": "secret-value",
105+
"webhookUrl": "https://hooks.example.invalid/secret",
106+
"webhooks": ["https://hooks.example.invalid/one"],
107+
"passwords": ["password-value"],
108+
"client_secrets": ["client-secret"],
109+
"access_tokens": ["access-token"],
110+
"caption_max_tokens": 77,
111+
"text_chunk_overlap_tokens": 12,
112+
"tokenizer_model": "model-name",
113+
}
114+
115+
assert redact(payload) == {
116+
"reranker_api_key": "<redacted>",
117+
"webhookUrl": "<redacted>",
118+
"webhooks": "<redacted>",
119+
"passwords": "<redacted>",
120+
"client_secrets": "<redacted>",
121+
"access_tokens": "<redacted>",
122+
"caption_max_tokens": 77,
123+
"text_chunk_overlap_tokens": 12,
124+
"tokenizer_model": "model-name",
125+
}
126+
127+
128+
def test_text_artifact_writes_are_classified(tmp_path):
129+
with pytest.raises(HarnessRunError) as log_error:
130+
append_text(tmp_path, "log line")
131+
with pytest.raises(HarnessRunError) as trec_error:
132+
_write_trec_run(tmp_path, {"query": {"document": 1.0}})
133+
134+
assert log_error.value.exit_code == EXIT_ARTIFACT_WRITE_FAILURE
135+
assert trec_error.value.exit_code == EXIT_ARTIFACT_WRITE_FAILURE
136+
137+
94138
def test_invalid_run_config_preserves_existing_artifacts(tmp_path):
95139
(tmp_path / "results.json").write_text('{"old": true}', encoding="utf-8")
96140
(tmp_path / "lancedb").mkdir()
@@ -222,6 +266,35 @@ def fail_writer(**kwargs):
222266
assert error.value.failure.message == "OSError: artifact directory unavailable"
223267

224268

269+
def test_run_benchmark_preserves_run_log_write_failure(monkeypatch, tmp_path):
270+
import nemo_retriever.harness.execution as execution
271+
from nemo_retriever.harness.json_io import artifact_write_error
272+
273+
class FakeIngestPlan:
274+
documents = ()
275+
276+
@contextmanager
277+
def fail_log_capture(*args, **kwargs):
278+
raise artifact_write_error(OSError("run log unavailable"))
279+
yield
280+
281+
monkeypatch.setattr(execution, "resolve_ingest_plan", lambda request: FakeIngestPlan())
282+
monkeypatch.setattr(execution, "run_ingest_workflow", lambda plan, dry_run: {})
283+
monkeypatch.setattr(execution, "resolve_query_plan", lambda request: object())
284+
monkeypatch.setattr(execution, "query_plan_payload", lambda plan: {})
285+
monkeypatch.setattr(execution, "capture_output_to_log", fail_log_capture)
286+
287+
outcome = run_benchmark(
288+
"jp20_smoke",
289+
output_dir=str(tmp_path / "run"),
290+
overrides=(f'dataset.path="{tmp_path}"',),
291+
)
292+
293+
assert outcome.exit_code == EXIT_ARTIFACT_WRITE_FAILURE
294+
assert outcome.results["failure"]["failure_reason"] == "artifact_write_failed"
295+
assert outcome.results["failure"]["message"] == "OSError: run log unavailable"
296+
297+
225298
def test_run_benchmark_keeps_non_write_failures_internal(monkeypatch, tmp_path):
226299
import nemo_retriever.harness.execution as execution
227300

0 commit comments

Comments
 (0)