diff --git a/docs/features/export-viz-output.feature b/docs/features/export-viz-output.feature new file mode 100644 index 0000000..7ad3572 --- /dev/null +++ b/docs/features/export-viz-output.feature @@ -0,0 +1,80 @@ +Feature: Export Output Flag + + Add `--output` / `-o` flag to `flowr export` that writes export output to a file + instead of stdout, with automatic `.js` wrapping for `file://` compatibility + when combined with `--format json`. Wires `task regenerate-flowviz` to use + the native export command, eliminating the need for a separate generation script. + + Status: ELICITING + + Rules (Business): + - `--output` / `-o` flag writes export output to a file for all formats, creating parent directories as needed + - When `--format json` and `--output` path ends in `.js`, the content is wrapped as `window.FLOWVIZ_DATA = ;` for `file://` compatibility + - The `generate-flowviz-data.py` script is deleted; `app.js` handles the new JSON shape via its `transformFlow` function + - `regenerate-flowviz` task added to pyproject.toml + + Constraints: + - CLI exit codes: 0 = success, 1 = command failed, 2 = usage error (ADR_20260426_cli_io_convention) + - Zero new runtime dependencies introduced + - No changes to existing domain types (Flow, State, Transition, GuardCondition) + - `.js` wrapping applies ONLY to JSON format — mermaid output with `.js` extension is written as-is without wrapping + + ## Questions + + | ID | Question | Status | Answer / Assumption | + |----|----------|--------|---------------------| + | Q1 | Should `.js` wrapping apply to all formats or only JSON? | Resolved | Only JSON — wrapping non-JSON (e.g. mermaid) as `window.FLOWVIZ_DATA = ...;` would produce invalid JavaScript | + | Q2 | Should `--output` to an unwritable path produce exit code 1? | Resolved | Yes — OSError during write is caught and reported as exit code 1 | + | Q3 | Should `--output` overwrite existing files? | Resolved | Yes — consistent with shell `>` redirection behavior | + + ## Changes + + | Session | Q-IDs | Change | + |---------|-------|--------| + | 2026-05-07 planning | — | Created: split from export-viz-pipeline (INVEST: must_examples <= 8) | + + Rule: Output-to-file via --output flag + As a CLI user + I want to write export output to a file via `--output` / `-o` + So that scripted workflows (like `task regenerate-flowviz`) can produce output files directly without shell redirection + + @id:a7b9c1d3 + Example: --output writes to file instead of stdout + Given a flow definition file exists at `examples/simple.yaml` + When the user runs `flowr export --format json --output /tmp/flowr-out.json examples/simple.yaml` + Then the file `/tmp/flowr-out.json` contains valid JSON output and nothing is printed to stdout + + @id:b8c0d2e4 + Example: --output creates parent directories automatically + Given a flow definition file exists at `examples/simple.yaml` + When the user runs `flowr export --format json --output /tmp/flowr/deep/nested/out.json examples/simple.yaml` + Then the parent directories `/tmp/flowr/deep/nested/` are created and the file is written successfully + + @id:c9d1e3f5 + Example: --output works for all export formats + Given a flow definition file exists at `examples/simple.yaml` + When the user runs `flowr export --format mermaid --output /tmp/flowr-out.mmd examples/simple.yaml` + Then the file `/tmp/flowr-out.mmd` contains valid Mermaid stateDiagram-v2 output + + Rule: JavaScript wrapping for file:// compatibility + As a tool author + I want the `--output` flag to auto-wrap JSON output as a JavaScript variable assignment when the output file has a `.js` extension + So that the D3 visualizer can load flow data from local files via the `file://` protocol without CORS restrictions + + @id:d0e2f4a6 + Example: .js extension wraps JSON as window.FLOWVIZ_DATA assignment + Given a flow definition file exists at `examples/simple.yaml` + When the user runs `flowr export --format json --output .flowr/viz/data.js examples/simple.yaml` + Then the file content starts with `window.FLOWVIZ_DATA = ` followed by the JSON object and ending with `;\n` + + @id:e1f3a5b7 + Example: .js wrapping does not apply to non-JSON formats + Given a flow definition file exists at `examples/simple.yaml` + When the user runs `flowr export --format mermaid --output /tmp/out.js examples/simple.yaml` + Then the file contains plain Mermaid output without `window.FLOWVIZ_DATA` wrapping + + @id:f2a4b6c8 + Example: Non-.js extension writes JSON without wrapping + Given a flow definition file exists at `examples/simple.yaml` + When the user runs `flowr export --format json --output /tmp/out.json examples/simple.yaml` + Then the file contains raw JSON without any JavaScript wrapping diff --git a/docs/features/export-viz-pipeline.feature b/docs/features/export-viz-pipeline.feature new file mode 100644 index 0000000..ca40d66 --- /dev/null +++ b/docs/features/export-viz-pipeline.feature @@ -0,0 +1,74 @@ +Feature: Export Viz Pipeline + + Enrich JSON export with viz-required metadata (version, exits, subflow fields) + and restructure directory export as a named collection so that downstream tools + (like the D3 visualizer) can render complete flow information without additional + lookups or duplicated parsing. + + Status: ELICITING + + Rules (Business): + - JSON single-flow export must include `version` and `exits` from the Flow domain object, and subflow-state nodes must include `subflow` and `subflowVersion` fields + - JSON directory export must change from `[{...}]` array to `{"defaultFlow": "...", "flows": [...]}` object so downstream tools can identify the entry-point flow + + Constraints: + - This feature MODIFIES existing export-json behavior — existing tests (99a274dd, unit/export_test.py directory tests) must be updated to match the new output shape + - Zero new runtime dependencies introduced + - No changes to existing domain types (Flow, State, Transition, GuardCondition) + + ## Questions + + | ID | Question | Status | Answer / Assumption | + |----|----------|--------|---------------------| + + ## Changes + + | Session | Q-IDs | Change | + |---------|-------|--------| + | 2026-05-07 planning | — | Created: feature breakdown from stakeholder specification | + + Rule: JSON single-flow enrichment + As a tool author + I want the JSON single-flow export to include version, exits, and subflow metadata + So that downstream tools (like the D3 visualizer) can render complete flow information without additional lookups or duplicated parsing + + @id:a1c3e5f7 + Example: Single-flow export includes version and exits fields + Given a flow definition with version "1.0.20260507" and exits ["done", "failed"] + When the user runs `flowr export --format json examples/simple.yaml` + Then the JSON output contains a `version` field matching "1.0.20260507" and an `exits` field matching ["done", "failed"] + + @id:b2d4f6a8 + Example: Subflow-state nodes include subflow and subflowVersion + Given a flow where state "drill-down" has `flow: child-flow` and `flow_version: 2.0.0` + When the user runs `flowr export --format json main.yaml` + Then the node for "drill-down" includes `"subflow": "child-flow"` and `"subflowVersion": "2.0.0"` + + @id:c3e5f7a9 + Example: Non-subflow state nodes omit subflow fields + Given a flow with states that have no `flow` field + When the user runs `flowr export --format json examples/simple.yaml` + Then no node in the output contains a `subflow` or `subflowVersion` field + + Rule: JSON directory export restructured as named collection + As a tool author + I want the JSON directory export to produce a structured object with a `defaultFlow` key and a `flows` array + So that downstream tools can identify the entry-point flow without hardcoding assumptions + + @id:d4f6a8b0 + Example: Directory export produces object with defaultFlow and flows array + Given a directory `flows/` contains `alpha.yaml` and `beta.yaml` + When the user runs `flowr export --format json flows/` + Then the output is a JSON object (not array) with a `defaultFlow` key and a `flows` key containing an array of flow entries sorted alphabetically by filename + + @id:e5f7a9b1 + Example: defaultFlow selects main-flow when present + Given a directory contains `main-flow.yaml` and `other.yaml` + When the user runs `flowr export --format json flows/` + Then the `defaultFlow` value is `"main-flow"` + + @id:f6a8b0c2 + Example: defaultFlow falls back to alphabetically first flow name + Given a directory contains `beta.yaml` and `gamma.yaml` but no `main-flow.yaml` + When the user runs `flowr export --format json flows/` + Then the `defaultFlow` value is `"beta"` diff --git a/flowr/__main__.py b/flowr/__main__.py index 4505c3b..1652420 100644 --- a/flowr/__main__.py +++ b/flowr/__main__.py @@ -231,6 +231,12 @@ def _add_subcommands(parser: argparse.ArgumentParser) -> None: dest="export_format", help="Export format", ) + p_export.add_argument( + "--output", + "-o", + dest="output_path", + help="Write output to file instead of stdout", + ) from flowr.exporters.registry import EXPORTERS as EXPORTERS_FOR_ARGS for _name, adapter in EXPORTERS_FOR_ARGS.items(): @@ -542,7 +548,16 @@ def _cmd_export(args: argparse.Namespace) -> int: flow = load_flow_from_file(input_path) subflows = _load_subflows(flow, input_path.parent) output = adapter.export(flow, options, subflows=subflows) - print(output) # noqa: T201 + output_path = getattr(args, "output_path", None) + if output_path: + out = Path(output_path) + if out.suffix == ".js" and args.export_format == "json": + var_name = "window.FLOWVIZ_DATA" + output = f"{var_name} = {output};\n" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(output, encoding="utf-8") + else: + print(output) # noqa: T201 return 0 diff --git a/flowr/exporters/json_exporter.py b/flowr/exporters/json_exporter.py index 716fcd3..8de3f27 100644 --- a/flowr/exporters/json_exporter.py +++ b/flowr/exporters/json_exporter.py @@ -138,6 +138,10 @@ def _flow_to_dict( "id": s.id, "type": "subflow" if s.flow else "state", } + if s.flow: + node["subflow"] = s.flow + if s.flow_version: + node["subflowVersion"] = s.flow_version if include_attrs and s.attrs: node["attrs"] = s.attrs nodes.append(node) @@ -152,7 +156,13 @@ def _flow_to_dict( if transition.conditions: edge["conditions"] = dict(transition.conditions.conditions) edges.append(edge) - result = {"flow": flow.flow, "nodes": nodes, "edges": edges} + result = { + "flow": flow.flow, + "version": flow.version, + "exits": flow.exits, + "nodes": nodes, + "edges": edges, + } return result def export( @@ -172,4 +182,6 @@ def export_directory(self, flows: list[tuple[str, Flow]], options: dict) -> str: for _name, flow in flows: entry = self._flow_to_dict(flow, options) entries.append(entry) - return json.dumps(entries) + flow_names = {e["flow"] for e in entries} + default_flow = "main-flow" if "main-flow" in flow_names else min(flow_names) + return json.dumps({"defaultFlow": default_flow, "flows": entries}) diff --git a/tests/features/export/export_json_test.py b/tests/features/export/export_json_test.py index 0b28afa..c509efa 100644 --- a/tests/features/export/export_json_test.py +++ b/tests/features/export/export_json_test.py @@ -134,7 +134,9 @@ def test_export_json_99a274dd( captured = capsys.readouterr() data = json.loads(captured.out) - assert isinstance(data, list) - assert len(data) == 2 - assert data[0]["flow"] == "alpha" - assert data[1]["flow"] == "beta" + assert isinstance(data, dict) + assert data["defaultFlow"] == "alpha" + flows = data["flows"] + assert len(flows) == 2 + assert flows[0]["flow"] == "alpha" + assert flows[1]["flow"] == "beta" diff --git a/tests/features/export/export_viz_output_test.py b/tests/features/export/export_viz_output_test.py new file mode 100644 index 0000000..dcfda8c --- /dev/null +++ b/tests/features/export/export_viz_output_test.py @@ -0,0 +1,240 @@ +import json +import sys +from pathlib import Path +from unittest.mock import patch + +import pytest + +from flowr.__main__ import main + + +def test_export_viz_output_a7b9c1d3( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """ + Given a flow definition file exists at `examples/simple.yaml` + When the user runs `flowr export --format json --output /tmp/flowr-out.json examples/simple.yaml` + Then the file `/tmp/flowr-out.json` contains valid JSON output and nothing is printed to stdout + """ + flow_file = tmp_path / "simple.yaml" + flow_file.write_text( + "flow: test-flow\nversion: '1.0'\nexits:\n - exit_done\n" + "states:\n - id: idle\n next:\n" + " go:\n to: done\n - id: done\n next: {}\n" + ) + output_file = tmp_path / "flowr-out.json" + + with patch.object( + sys, + "argv", + [ + "flowr", + "export", + "--format", + "json", + "--output", + str(output_file), + str(flow_file), + ], + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 0 + + captured = capsys.readouterr() + assert captured.out == "" + data = json.loads(output_file.read_text()) + assert data["flow"] == "test-flow" + + +def test_export_viz_output_b8c0d2e4( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """ + Given a flow definition file exists at `examples/simple.yaml` + When the user runs `flowr export --format json --output /tmp/flowr/deep/nested/out.json examples/simple.yaml` + Then the parent directories `/tmp/flowr/deep/nested/` are created and the file is written successfully + """ + flow_file = tmp_path / "simple.yaml" + flow_file.write_text( + "flow: test-flow\nversion: '1.0'\nexits:\n - exit_done\n" + "states:\n - id: idle\n next:\n" + " go:\n to: done\n - id: done\n next: {}\n" + ) + output_file = tmp_path / "deep" / "nested" / "out.json" + + with patch.object( + sys, + "argv", + [ + "flowr", + "export", + "--format", + "json", + "--output", + str(output_file), + str(flow_file), + ], + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 0 + + assert output_file.exists() + data = json.loads(output_file.read_text()) + assert data["flow"] == "test-flow" + + +def test_export_viz_output_c9d1e3f5( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """ + Given a flow definition file exists at `examples/simple.yaml` + When the user runs `flowr export --format mermaid --output /tmp/flowr-out.mmd examples/simple.yaml` + Then the file `/tmp/flowr-out.mmd` contains valid Mermaid stateDiagram-v2 output + """ + flow_file = tmp_path / "simple.yaml" + flow_file.write_text( + "flow: test-flow\nversion: '1.0'\nexits:\n - exit_done\n" + "states:\n - id: idle\n next:\n" + " go:\n to: done\n - id: done\n next: {}\n" + ) + output_file = tmp_path / "flowr-out.mmd" + + with patch.object( + sys, + "argv", + [ + "flowr", + "export", + "--format", + "mermaid", + "--output", + str(output_file), + str(flow_file), + ], + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 0 + + content = output_file.read_text() + assert "stateDiagram-v2" in content + + +def test_export_viz_output_d0e2f4a6( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + r""" + Given a flow definition file exists at `examples/simple.yaml` + When the user runs `flowr export --format json --output .flowr/viz/data.js examples/simple.yaml` + Then the file content starts with `window.FLOWVIZ_DATA = ` followed by the JSON object and ending with `;\n` + """ + flow_file = tmp_path / "simple.yaml" + flow_file.write_text( + "flow: test-flow\nversion: '1.0'\nexits:\n - exit_done\n" + "states:\n - id: idle\n next:\n" + " go:\n to: done\n - id: done\n next: {}\n" + ) + output_file = tmp_path / "data.js" + + with patch.object( + sys, + "argv", + [ + "flowr", + "export", + "--format", + "json", + "--output", + str(output_file), + str(flow_file), + ], + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 0 + + content = output_file.read_text() + assert content.startswith("window.FLOWVIZ_DATA = ") + assert content.endswith(";\n") + json_part = content[len("window.FLOWVIZ_DATA = ") : -2] + data = json.loads(json_part) + assert data["flow"] == "test-flow" + + +def test_export_viz_output_e1f3a5b7( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """ + Given a flow definition file exists at `examples/simple.yaml` + When the user runs `flowr export --format mermaid --output /tmp/out.js examples/simple.yaml` + Then the file contains plain Mermaid output without `window.FLOWVIZ_DATA` wrapping + """ + flow_file = tmp_path / "simple.yaml" + flow_file.write_text( + "flow: test-flow\nversion: '1.0'\nexits:\n - exit_done\n" + "states:\n - id: idle\n next:\n" + " go:\n to: done\n - id: done\n next: {}\n" + ) + output_file = tmp_path / "out.js" + + with patch.object( + sys, + "argv", + [ + "flowr", + "export", + "--format", + "mermaid", + "--output", + str(output_file), + str(flow_file), + ], + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 0 + + content = output_file.read_text() + assert "window.FLOWVIZ_DATA" not in content + assert "stateDiagram-v2" in content + + +def test_export_viz_output_f2a4b6c8( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """ + Given a flow definition file exists at `examples/simple.yaml` + When the user runs `flowr export --format json --output /tmp/out.json examples/simple.yaml` + Then the file contains raw JSON without any JavaScript wrapping + """ + flow_file = tmp_path / "simple.yaml" + flow_file.write_text( + "flow: test-flow\nversion: '1.0'\nexits:\n - exit_done\n" + "states:\n - id: idle\n next:\n" + " go:\n to: done\n - id: done\n next: {}\n" + ) + output_file = tmp_path / "out.json" + + with patch.object( + sys, + "argv", + [ + "flowr", + "export", + "--format", + "json", + "--output", + str(output_file), + str(flow_file), + ], + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 0 + + content = output_file.read_text() + assert not content.startswith("window.FLOWVIZ_DATA") + data = json.loads(content) + assert data["flow"] == "test-flow" diff --git a/tests/features/export/export_viz_pipeline_test.py b/tests/features/export/export_viz_pipeline_test.py new file mode 100644 index 0000000..8c04131 --- /dev/null +++ b/tests/features/export/export_viz_pipeline_test.py @@ -0,0 +1,188 @@ +import json +import sys +from pathlib import Path +from unittest.mock import patch + +import pytest + +from flowr.__main__ import main + + +def test_export_viz_pipeline_a1c3e5f7( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """ + Given a flow definition with version "1.0.20260507" and exits ["done", "failed"] + When the user runs `flowr export --format json examples/simple.yaml` + Then the JSON output contains a `version` field matching "1.0.20260507" and an `exits` field matching ["done", "failed"] + """ + flow_file = tmp_path / "simple.yaml" + flow_file.write_text( + "flow: test-flow\nversion: '1.0.20260507'\nexits:\n - done\n - failed\n" + "states:\n - id: idle\n next:\n" + " go:\n to: finished\n - id: finished\n next: {}\n" + ) + + with patch.object( + sys, "argv", ["flowr", "export", "--format", "json", str(flow_file)] + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 0 + + captured = capsys.readouterr() + data = json.loads(captured.out) + assert data["version"] == "1.0.20260507" + assert data["exits"] == ["done", "failed"] + + +def test_export_viz_pipeline_b2d4f6a8( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """ + Given a flow where state "drill-down" has `flow: child-flow` and `flow_version: 2.0.0` + When the user runs `flowr export --format json main.yaml` + Then the node for "drill-down" includes `"subflow": "child-flow"` and `"subflowVersion": "2.0.0"` + """ + flow_file = tmp_path / "main.yaml" + flow_file.write_text( + "flow: parent\nversion: '1.0'\nexits:\n - exit_done\n" + "states:\n - id: idle\n next:\n" + " go:\n to: drill-down\n - id: drill-down\n flow: child-flow\n flow_version: '2.0.0'\n next:\n exit_child_done: done\n - id: done\n next: {}\n" + ) + + with patch.object( + sys, "argv", ["flowr", "export", "--format", "json", str(flow_file)] + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 0 + + captured = capsys.readouterr() + data = json.loads(captured.out) + drill_down_node = next(n for n in data["nodes"] if n["id"] == "drill-down") + assert drill_down_node["subflow"] == "child-flow" + assert drill_down_node["subflowVersion"] == "2.0.0" + + +def test_export_viz_pipeline_c3e5f7a9( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """ + Given a flow with states that have no `flow` field + When the user runs `flowr export --format json examples/simple.yaml` + Then no node in the output contains a `subflow` or `subflowVersion` field + """ + flow_file = tmp_path / "simple.yaml" + flow_file.write_text( + "flow: plain-flow\nversion: '1.0'\nexits:\n - exit_done\n" + "states:\n - id: idle\n next:\n" + " go:\n to: done\n - id: done\n next: {}\n" + ) + + with patch.object( + sys, "argv", ["flowr", "export", "--format", "json", str(flow_file)] + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 0 + + captured = capsys.readouterr() + data = json.loads(captured.out) + for node in data["nodes"]: + assert "subflow" not in node + assert "subflowVersion" not in node + + +def test_export_viz_pipeline_d4f6a8b0( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """ + Given a directory `flows/` contains `alpha.yaml` and `beta.yaml` + When the user runs `flowr export --format json flows/` + Then the output is a JSON object (not array) with a `defaultFlow` key and a `flows` key containing an array of flow entries sorted alphabetically by filename + """ + flows_dir = tmp_path / "flows" + flows_dir.mkdir() + (flows_dir / "beta.yaml").write_text( + "flow: beta\nversion: '1.0'\nexits:\n - exit_done\nstates:\n - id: idle\n next:\n go:\n to: done\n - id: done\n next: {}\n" + ) + (flows_dir / "alpha.yaml").write_text( + "flow: alpha\nversion: '1.0'\nexits:\n - exit_done\nstates:\n - id: idle\n next:\n go:\n to: done\n - id: done\n next: {}\n" + ) + + with patch.object( + sys, "argv", ["flowr", "export", "--format", "json", str(flows_dir)] + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 0 + + captured = capsys.readouterr() + data = json.loads(captured.out) + assert isinstance(data, dict) + assert "defaultFlow" in data + assert "flows" in data + flows = data["flows"] + assert isinstance(flows, list) + assert len(flows) == 2 + assert flows[0]["flow"] == "alpha" + assert flows[1]["flow"] == "beta" + + +def test_export_viz_pipeline_e5f7a9b1( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """ + Given a directory contains `main-flow.yaml` and `other.yaml` + When the user runs `flowr export --format json flows/` + Then the `defaultFlow` value is `"main-flow"` + """ + flows_dir = tmp_path / "flows" + flows_dir.mkdir() + (flows_dir / "main-flow.yaml").write_text( + "flow: main-flow\nversion: '1.0'\nexits:\n - exit_done\nstates:\n - id: idle\n next:\n go:\n to: done\n - id: done\n next: {}\n" + ) + (flows_dir / "other.yaml").write_text( + "flow: other\nversion: '1.0'\nexits:\n - exit_done\nstates:\n - id: idle\n next:\n go:\n to: done\n - id: done\n next: {}\n" + ) + + with patch.object( + sys, "argv", ["flowr", "export", "--format", "json", str(flows_dir)] + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 0 + + captured = capsys.readouterr() + data = json.loads(captured.out) + assert data["defaultFlow"] == "main-flow" + + +def test_export_viz_pipeline_f6a8b0c2( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """ + Given a directory contains `beta.yaml` and `gamma.yaml` but no `main-flow.yaml` + When the user runs `flowr export --format json flows/` + Then the `defaultFlow` value is `"beta"` + """ + flows_dir = tmp_path / "flows" + flows_dir.mkdir() + (flows_dir / "beta.yaml").write_text( + "flow: beta\nversion: '1.0'\nexits:\n - exit_done\nstates:\n - id: idle\n next:\n go:\n to: done\n - id: done\n next: {}\n" + ) + (flows_dir / "gamma.yaml").write_text( + "flow: gamma\nversion: '1.0'\nexits:\n - exit_done\nstates:\n - id: idle\n next:\n go:\n to: done\n - id: done\n next: {}\n" + ) + + with patch.object( + sys, "argv", ["flowr", "export", "--format", "json", str(flows_dir)] + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 0 + + captured = capsys.readouterr() + data = json.loads(captured.out) + assert data["defaultFlow"] == "beta" diff --git a/tests/unit/export_test.py b/tests/unit/export_test.py index 526cffa..d83f0a1 100644 --- a/tests/unit/export_test.py +++ b/tests/unit/export_test.py @@ -144,6 +144,8 @@ def test_json_export_directory_single( captured = capsys.readouterr() data = json.loads(captured.out) - assert isinstance(data, list) - assert len(data) == 1 - assert data[0]["flow"] == "a" + assert isinstance(data, dict) + assert data["defaultFlow"] == "a" + flows = data["flows"] + assert len(flows) == 1 + assert flows[0]["flow"] == "a"