mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(skills): reject non-finite JSON export values before writing (#7456)
This commit is contained in:
parent
481f8168d4
commit
afcb915de3
@ -1709,6 +1709,10 @@ before exporting. Duplicate names cause an error before the output file is opene
|
||||
preserving any existing file.
|
||||
The CLI names the repeated columns in a short stderr error and exits nonzero,
|
||||
so callers can correct their aliases without reading a traceback.
|
||||
JSON exports likewise reject non-finite numbers (`NaN` and infinities), including
|
||||
values nested in lists or structs, before opening the destination. Filter them
|
||||
or replace them with `NULL` in SQL; the CLI reports an error and preserves any
|
||||
existing output rather than writing invalid JSON.
|
||||
|
||||
The **data-analysis** skill preserves each CSV file or Excel sheet when names collide across inputs. [Prepare its Python environment](skills/public/data-analysis/SKILL.md#environment-setup) explicitly with DuckDB and openpyxl; missing packages report setup instructions instead of installing themselves during analysis. Use the SQL names printed by `inspect` (such as `report`, `report_1`) for queries and summaries. Its cache follows ordered input basenames and contents, so renaming or reordering inputs rebuilds their table map instead of silently selecting a different source. Quoted original names also resolve without rewriting query text; apostrophes in upload paths and double quotes in column names are preserved through loading, inspection, queries, and summaries.
|
||||
|
||||
|
||||
@ -101,6 +101,11 @@ Duplicate names cause an error and leave any existing output file untouched.
|
||||
The error lists the repeated names so you can alias them directly. The CLI prints
|
||||
a short `Error: ...` message to stderr and exits nonzero for this rejection.
|
||||
|
||||
JSON exports also reject non-finite numbers (`NaN`, `Infinity`, and `-Infinity`),
|
||||
including values inside lists or structs, before opening the output file. Filter
|
||||
or replace them in SQL, for example `CASE WHEN isfinite(ratio) THEN ratio ELSE
|
||||
NULL END AS ratio`. Finite numbers, nulls, and text such as `'NaN'` remain valid.
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|
||||
@ -383,8 +383,13 @@ def _export_results(columns: list[str], rows: list[tuple], output_file: str) ->
|
||||
val = val.hex()
|
||||
record[col] = val
|
||||
records.append(record)
|
||||
# Reject non-JSON numbers, including nested values, before touching the
|
||||
# destination so a failed export cannot overwrite an existing result.
|
||||
serialized = json.dumps(
|
||||
records, indent=2, ensure_ascii=False, default=str, allow_nan=False
|
||||
)
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
json.dump(records, f, indent=2, ensure_ascii=False, default=str)
|
||||
f.write(serialized)
|
||||
|
||||
elif ext == ".md":
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
|
||||
@ -128,6 +128,91 @@ def test_json_export_preserves_unique_and_case_distinct_keys(analysis, tmp_path)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expression",
|
||||
[
|
||||
"'NaN'::DOUBLE",
|
||||
"'Infinity'::DOUBLE",
|
||||
"'-Infinity'::DOUBLE",
|
||||
"[1.0, 'NaN'::DOUBLE]",
|
||||
"{'nested': 'Infinity'::DOUBLE}",
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("existing", [False, True])
|
||||
def test_json_export_rejects_nonfinite_values_before_writing(
|
||||
analysis, tmp_path, expression, existing
|
||||
):
|
||||
output = tmp_path / "result.json"
|
||||
original = b'[{"previous": "keep me"}]\n'
|
||||
if existing:
|
||||
output.write_bytes(original)
|
||||
|
||||
with duckdb.connect() as con:
|
||||
with pytest.raises(ValueError, match="Out of range float values"):
|
||||
analysis["action_query"](
|
||||
con, f"SELECT {expression} AS value", {}, str(output)
|
||||
)
|
||||
|
||||
if existing:
|
||||
assert output.read_bytes() == original
|
||||
else:
|
||||
assert not output.exists()
|
||||
|
||||
|
||||
def test_json_export_keeps_finite_numbers_nulls_and_nonfinite_text(analysis, tmp_path):
|
||||
output = tmp_path / "result.json"
|
||||
with duckdb.connect() as con:
|
||||
analysis["action_query"](
|
||||
con,
|
||||
"SELECT 1.25::DOUBLE AS amount, NULL AS missing, "
|
||||
"['NaN', 'Infinity'] AS labels",
|
||||
{},
|
||||
str(output),
|
||||
)
|
||||
assert json.loads(output.read_text(encoding="utf-8")) == [
|
||||
{"amount": 1.25, "missing": None, "labels": ["NaN", "Infinity"]}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("existing", [False, True])
|
||||
def test_cli_rejects_nonfinite_json_without_overwriting(
|
||||
cached_source, tmp_path, existing
|
||||
):
|
||||
source, temp_root = cached_source
|
||||
output = tmp_path / "result.json"
|
||||
original = b'[{"previous": "keep me"}]\n'
|
||||
if existing:
|
||||
output.write_bytes(original)
|
||||
completed = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
"--files",
|
||||
str(source),
|
||||
"--action",
|
||||
"query",
|
||||
"--sql",
|
||||
"SELECT 1.0 / 0.0 AS ratio FROM input",
|
||||
"--output-file",
|
||||
str(output),
|
||||
],
|
||||
env={**os.environ, "TMPDIR": str(temp_root)},
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
assert completed.returncode == 1
|
||||
assert "Error: Out of range float values" in completed.stderr
|
||||
assert "Traceback" not in completed.stderr
|
||||
assert "Results exported" not in completed.stdout
|
||||
if existing:
|
||||
assert output.read_bytes() == original
|
||||
else:
|
||||
assert not output.exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("extension", ["csv", "md"])
|
||||
def test_non_json_exports_keep_duplicate_columns(analysis, tmp_path, extension):
|
||||
output = tmp_path / f"result.{extension}"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user