deer-flow/tests/skills/test_data_analysis_export_paths.py
theater dcb3a70fbf
fix(skills): support basename data-analysis exports (#7132)
* fix(skills): support basename data-analysis exports

Use the current directory when export output has no parent directory.

* test(skills): cover data-analysis export paths

Exercise real DuckDB queries, filesystem exports and cached CLI execution across CSV, JSON and Markdown.

* docs(skills): record export path regression guidance

Document basename and nested output expectations and the regression test command.

* docs(skills): explain data-analysis export paths

Clarify bare filenames, relative parent directories and absolute output paths.

* docs: note supported data-analysis export paths

Add a concise README explanation while retaining existing upstream documentation.

* docs(skills): clarify export overwrite behavior

Document that existing export targets are overwritten without warning, as suggested in PR #7132. Verified real CSV, JSON and Markdown overwrites; 27 export regression tests pass.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-25 15:48:57 +08:00

107 lines
3.6 KiB
Python

"""Exercise output paths with real DuckDB queries and filesystem exports."""
import csv
import json
import os
import runpy
import subprocess
import sys
from contextlib import chdir
from pathlib import Path
import pytest
duckdb = pytest.importorskip("duckdb")
pytest.importorskip("openpyxl")
SCRIPT = (
Path(__file__).resolve().parents[2]
/ "skills/public/data-analysis/scripts/analyze.py"
)
def assert_export(path, extension, empty=False):
if extension == "csv":
with path.open(encoding="utf-8", newline="") as handle:
assert list(csv.reader(handle)) == (
[["answer", "label"]]
if empty
else [["answer", "label"], ["42", "分析"]]
)
elif extension == "json":
assert json.loads(path.read_text(encoding="utf-8")) == (
[] if empty else [{"answer": 42, "label": "分析"}]
)
else:
assert path.read_text(encoding="utf-8") == (
"| answer | label |\n| --- | --- |\n" + ("" if empty else "| 42 | 分析 |\n")
)
@pytest.mark.parametrize("extension", ["csv", "json", "md"])
@pytest.mark.parametrize("path_kind", ["basename", "dot", "nested", "absolute"])
@pytest.mark.parametrize("empty", [False, True])
def test_query_exports_to_requested_path(tmp_path, extension, path_kind, empty):
analysis = runpy.run_path(str(SCRIPT))
name = f"report.{extension}"
output = {
"basename": name,
"dot": f"./{name}",
"nested": f"new directory/reports/{name}",
"absolute": str(tmp_path / "absolute" / name),
}[path_kind]
sql = "SELECT 42 AS answer, '分析' AS label" + (" WHERE false" if empty else "")
with chdir(tmp_path), duckdb.connect() as con:
result = analysis["action_query"](con, sql, {}, output)
assert result == f"Results exported to {output} ({0 if empty else 1} rows)"
assert_export(Path(output), extension, empty)
@pytest.mark.parametrize("extension", ["csv", "json", "md"])
def test_cli_exports_basename_from_real_read_only_cache(tmp_path, extension):
# Build the real CSV cache without requiring the optional spatial extension.
# The subprocess then exercises main's normal cache-hit and export branches.
temp_dir = tmp_path / "runtime"
cache_dir = temp_dir / ".data-analysis-cache"
cache_dir.mkdir(parents=True)
source = tmp_path / "source.csv"
source.write_text("answer,label\n42,分析\n", encoding="utf-8")
analysis = runpy.run_path(str(SCRIPT))
key = analysis["compute_files_hash"]([str(source)])
table_map = {}
with duckdb.connect(str(cache_dir / f"{key}.duckdb")) as con:
analysis["_load_csv"](con, str(source), table_map)
(cache_dir / f"{key}.table_map.json").write_text(
json.dumps(table_map), encoding="utf-8"
)
output = f"report.{extension}"
completed = subprocess.run(
[
sys.executable,
"-B",
str(SCRIPT),
"--files",
str(source),
"--action",
"query",
"--sql",
"SELECT * FROM source",
"--output-file",
output,
],
cwd=tmp_path,
env={
**os.environ,
"TMPDIR": str(temp_dir),
"TEMP": str(temp_dir),
"TMP": str(temp_dir),
},
capture_output=True,
text=True,
encoding="utf-8",
check=False,
)
assert completed.returncode == 0, completed.stderr
assert "Cache hit!" in completed.stderr
assert f"Results exported to {output} (1 rows)" in completed.stdout
assert_export(tmp_path / output, extension)