deer-flow/tests/skills/test_chart_visualization.py
wannT-1 ec87cb9a21
fix(chart-visualization): wire generate_spreadsheet and fail CLI nonzero (#7395)
* fix(chart-visualization): wire generate_spreadsheet and fail CLI nonzero

SKILL.md already selects generate_spreadsheet and the reference file
documents it, but CHART_TYPE_MAP had no entry, so the CLI reported
Unknown tool. Map it to the AntV spreadsheet type and exit non-zero
after any unknown tool, missing tool, or HTTP failure. Successful chart
URLs stay on stdout.

Co-authored-by: wannT-1 <200890097+wannT-1@users.noreply.github.com>

* docs(agents): fold chart CLI failures into the shared stderr/stdout note

The sandbox guidance chain was over the AG002 hard limit. Share one
nonzero-exit rule for video, podcast, and chart-visualization CLIs.

Co-authored-by: wannT-1 <wannT-1@users.noreply.github.com>

---------

Co-authored-by: wannT-1 <200890097+wannT-1@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: wannT-1 <wannT-1@users.noreply.github.com>
2026-10-25 09:57:13 +08:00

167 lines
5.0 KiB
Python

"""Chart-visualization CLI regressions: spreadsheet mapping and nonzero failures.
The AntV request is mocked with a Node ``--require`` preload so the test stays
offline. Successful chart URLs stay on stdout; diagnostics stay on stderr.
"""
import json
import os
import shutil
import subprocess
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
SCRIPT = REPO_ROOT / "skills" / "public" / "chart-visualization" / "scripts" / "generate.js"
SKILL = REPO_ROOT / "skills" / "public" / "chart-visualization" / "SKILL.md"
NODE = shutil.which("node")
FETCH_PRELOAD = """\
const fs = require("fs");
global.fetch = async (url, options) => {
const capture = process.env.CHART_VIS_CAPTURE;
if (capture) {
fs.appendFileSync(
capture,
JSON.stringify({ url: String(url), body: options && options.body }) + "\\n",
);
}
const status = Number(process.env.CHART_VIS_HTTP_STATUS || 200);
if (status >= 400) {
return {
ok: false,
status,
text: async () => "upstream failed",
json: async () => ({}),
};
}
return {
ok: true,
status: 200,
text: async () => "",
json: async () => ({
success: true,
resultObj: "https://example.test/sheet.png",
}),
};
};
"""
pytestmark = pytest.mark.skipif(NODE is None, reason="node is required to run generate.js")
def _run(tmp_path, spec, *, http_status=None):
preload = tmp_path / "mock_fetch.js"
preload.write_text(FETCH_PRELOAD, encoding="utf-8")
capture = tmp_path / "capture.jsonl"
env = os.environ.copy()
env["CHART_VIS_CAPTURE"] = str(capture)
env["VIS_REQUEST_SERVER"] = "http://127.0.0.1:9/api/gpt-vis"
env.pop("SERVICE_ID", None)
if http_status is None:
env.pop("CHART_VIS_HTTP_STATUS", None)
else:
env["CHART_VIS_HTTP_STATUS"] = str(http_status)
result = subprocess.run(
[NODE, "--require", str(preload), str(SCRIPT), json.dumps(spec)],
capture_output=True,
text=True,
check=False,
timeout=15,
env=env,
)
calls = []
if capture.exists():
calls = [json.loads(line) for line in capture.read_text(encoding="utf-8").splitlines() if line]
return result, calls
def _chart_type_map():
probe = subprocess.run(
[
NODE,
"-e",
"const {CHART_TYPE_MAP}=require(process.argv[1]); process.stdout.write(JSON.stringify(CHART_TYPE_MAP));",
str(SCRIPT),
],
capture_output=True,
text=True,
check=False,
timeout=15,
)
assert probe.returncode == 0, probe.stderr
return json.loads(probe.stdout)
def test_generate_spreadsheet_is_mapped_and_prints_url(tmp_path):
chart_types = _chart_type_map()
skill = SKILL.read_text(encoding="utf-8-sig")
assert "26 available options" in skill
assert len(chart_types) == 26
assert chart_types["generate_spreadsheet"] == "spreadsheet"
spec = {
"tool": "generate_spreadsheet",
"args": {"data": [{"name": "Jane", "age": 25}], "title": "People"},
}
result, calls = _run(tmp_path, spec)
assert result.returncode == 0
assert result.stdout == "https://example.test/sheet.png\n"
assert result.stderr == ""
assert "Unknown tool" not in result.stderr
assert len(calls) == 1
body = json.loads(calls[0]["body"])
assert body["type"] == "spreadsheet"
assert body["source"] == "chart-visualization-creator"
assert body["data"] == [{"name": "Jane", "age": 25}]
assert body["title"] == "People"
assert calls[0]["url"] == "http://127.0.0.1:9/api/gpt-vis"
def test_unknown_tool_exits_nonzero_without_calling_the_service(tmp_path):
result, calls = _run(tmp_path, {"tool": "generate_not_a_chart", "args": {}})
assert result.returncode == 1
assert result.stdout == ""
assert "Unknown tool 'generate_not_a_chart'" in result.stderr
assert calls == []
def test_missing_tool_exits_nonzero(tmp_path):
result, calls = _run(tmp_path, {"args": {"data": []}})
assert result.returncode == 1
assert result.stdout == ""
assert "tool" in result.stderr
assert calls == []
def test_http_failure_exits_nonzero(tmp_path):
spec = {"tool": "generate_spreadsheet", "args": {"data": [{"name": "Jane"}]}}
result, calls = _run(tmp_path, spec, http_status=502)
assert result.returncode == 1
assert result.stdout == ""
assert "HTTP 502" in result.stderr
assert len(calls) == 1
assert json.loads(calls[0]["body"])["type"] == "spreadsheet"
def test_batch_keeps_successful_urls_and_still_exits_nonzero(tmp_path):
spec = [
{"tool": "generate_not_a_chart"},
{"tool": "generate_spreadsheet", "args": {"data": [{"name": "Ada"}]}},
]
result, calls = _run(tmp_path, spec)
assert result.returncode == 1
assert result.stdout == "https://example.test/sheet.png\n"
assert "Unknown tool 'generate_not_a_chart'" in result.stderr
assert len(calls) == 1
assert json.loads(calls[0]["body"])["type"] == "spreadsheet"