diff --git a/src/specify_cli/workflows/overlays/_commands.py b/src/specify_cli/workflows/overlays/_commands.py index 06c4dca835..17e2fa4435 100644 --- a/src/specify_cli/workflows/overlays/_commands.py +++ b/src/specify_cli/workflows/overlays/_commands.py @@ -424,7 +424,17 @@ def workflow_resolve(project_root: Path, workflow_id: str) -> dict[str, Any] | N return None console.print(f"Resolved workflow '{workflow_id}':") - console.print("Layers (highest precedence first):") + # The list is sorted by (priority, source) ascending, which puts the + # winning layer first only while priorities DIFFER. On a tie the sort is + # alphabetical by source while the merge gives the conflict to the LAST id + # (docs/reference/workflows.md:126), so the plain "highest precedence + # first" label stated the opposite of the outcome printed directly beneath + # it in the same output -- in the one command whose job is explaining which + # overlay won. Spell the tie-break out rather than reorder the list, which + # `test_workflow_resolve_equal_priority_layers_sort_by_source` pins. + console.print( + "Layers (highest precedence first; on equal priority the last ID wins):" + ) for layer in layers: priority = ( "n/a" if layer.tier == "base" else str(normalize_priority(layer.priority)) diff --git a/tests/workflows/test_overlay_commands.py b/tests/workflows/test_overlay_commands.py index c28f53b050..ec28559144 100644 --- a/tests/workflows/test_overlay_commands.py +++ b/tests/workflows/test_overlay_commands.py @@ -698,6 +698,59 @@ def test_workflow_resolve_escapes_rich_markup_in_step_id( assert result.exit_code == 0, result.output assert step_id in result.output + def test_workflow_resolve_header_states_the_equal_priority_tiebreak( + self, project_dir, monkeypatch + ): + """The layer header must not contradict the attribution beneath it. + + The list is sorted by (priority, source) ascending, which puts the + winning layer first only while priorities DIFFER. On a tie the sort is + alphabetical while the merge gives the conflict to the LAST id, so a + bare "highest precedence first" label stated the opposite of the + outcome printed directly below it — in the one command whose job is + explaining which overlay won. + """ + monkeypatch.setattr("specify_cli._require_specify_project", lambda: project_dir) + _write_workflow( + project_dir, + "wf", + { + "schema_version": "1.0", + "workflow": {"id": "wf", "name": "WF", "version": "1.0.0"}, + "steps": [{"id": "build", "type": "command", "command": "echo BASE"}], + }, + ) + for overlay_id in ("alpha", "beta"): + _write_overlay( + project_dir, + "wf", + overlay_id, + { + "id": overlay_id, + "extends": "wf", + "priority": 10, + "edits": [ + { + "operation": "replace", + "anchor": "build", + "step": { + "id": "build", + "type": "command", + "command": f"echo FROM-{overlay_id.upper()}", + }, + } + ], + }, + ) + + result = runner.invoke(app, ["workflow", "resolve", "wf"]) + + assert result.exit_code == 0, result.output + output = " ".join(result.output.split()) + # alpha is listed first, but beta is the layer that actually wins. + assert "build: project:beta" in output, output + assert "on equal priority the last ID wins" in output, output + def test_workflow_resolve_equal_priority_layers_sort_by_source(self, project_dir, monkeypatch): """Equal-priority overlays are listed alphabetically by source.""" monkeypatch.setattr("specify_cli._require_specify_project", lambda: project_dir)