Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a049a4-ee9f-78e1-9d66-2cb0f9bea3e3
390 lines
12 KiB
Python
390 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import stat
|
|
import sys
|
|
import uuid
|
|
|
|
import pytest
|
|
|
|
import custodian_cli
|
|
import repository_rename_cli as rename_cli
|
|
|
|
|
|
REPO_ID = "11111111-1111-4111-8111-111111111111"
|
|
OPERATION_ID = "22222222-2222-4222-8222-222222222222"
|
|
CONFIRMATION = f"rename:{REPO_ID}:flex-auth:access-engine"
|
|
|
|
|
|
def _repo() -> dict:
|
|
return {
|
|
"id": REPO_ID,
|
|
"slug": "flex-auth",
|
|
"canonical_slug": "flex-auth",
|
|
"requested_slug": "flex-auth",
|
|
"slug_status": "canonical",
|
|
}
|
|
|
|
|
|
def _operation(phase: str, *, no_op: bool = False) -> dict:
|
|
return {
|
|
"id": OPERATION_ID,
|
|
"repo_id": REPO_ID,
|
|
"phase": phase,
|
|
"old_slug": "flex-auth",
|
|
"new_slug": "access-engine",
|
|
"actor": "helixforge",
|
|
"error_code": None,
|
|
"error_message": None,
|
|
"no_op": no_op,
|
|
}
|
|
|
|
|
|
def _preflight() -> dict:
|
|
return {
|
|
"schema_version": "state-hub.repository-rename-preflight.v1",
|
|
"repo_id": REPO_ID,
|
|
"old_slug": "flex-auth",
|
|
"new_slug": "access-engine",
|
|
"safe_to_apply": True,
|
|
"blockers": [],
|
|
"warnings": [],
|
|
"queued_edge_writes": [],
|
|
"preflight_token": "signed-preflight-token",
|
|
}
|
|
|
|
|
|
def _run(monkeypatch, *args: str) -> None:
|
|
monkeypatch.setattr(sys, "argv", ["statehub", *args])
|
|
custodian_cli.main()
|
|
|
|
|
|
def test_preflight_writes_private_token_file_and_redacts_stdout(
|
|
monkeypatch, tmp_path, capsys
|
|
):
|
|
calls = []
|
|
|
|
def request(_api_base, method, path, body=None):
|
|
calls.append((method, path, body))
|
|
if method == "GET":
|
|
return _repo()
|
|
return _preflight()
|
|
|
|
monkeypatch.setattr(rename_cli, "_api_request", request)
|
|
output = tmp_path / "preflight.json"
|
|
_run(
|
|
monkeypatch,
|
|
"repo", "rename", "preflight", "flex-auth", "access-engine",
|
|
"--operation-id", OPERATION_ID,
|
|
"--output", str(output),
|
|
"--json",
|
|
)
|
|
|
|
public = json.loads(capsys.readouterr().out)
|
|
private = json.loads(output.read_text())
|
|
assert public["schema_version"] == rename_cli.CLI_SCHEMA_VERSION
|
|
assert public["operation_id"] == OPERATION_ID
|
|
assert public["next_safe_action"]["action"] == "start-operation"
|
|
assert public["result"]["preflight_token"].startswith("[REDACTED")
|
|
assert "signed-preflight-token" not in json.dumps(public)
|
|
assert private["result"]["preflight_token"] == "signed-preflight-token"
|
|
assert stat.S_IMODE(output.stat().st_mode) == 0o600
|
|
assert calls[1][2] == {
|
|
"new_slug": "access-engine",
|
|
"queued_edge_writes": [],
|
|
}
|
|
|
|
|
|
def test_start_uses_client_operation_id_and_retry_safe_api_body(
|
|
monkeypatch, tmp_path, capsys
|
|
):
|
|
preflight_file = tmp_path / "preflight.json"
|
|
preflight_file.write_text(
|
|
json.dumps(
|
|
{
|
|
"schema_version": rename_cli.CLI_SCHEMA_VERSION,
|
|
"command": "preflight",
|
|
"operation_id": OPERATION_ID,
|
|
"repo_id": REPO_ID,
|
|
"result": _preflight(),
|
|
}
|
|
)
|
|
)
|
|
preflight_file.chmod(0o600)
|
|
calls = []
|
|
|
|
def request(_api_base, method, path, body=None):
|
|
calls.append((method, path, body))
|
|
if path.startswith("/repository-renames/"):
|
|
raise rename_cli.RenameCLIError(
|
|
"not found", code="repository_rename_not_found", status_code=404
|
|
)
|
|
return _repo() if method == "GET" else _operation("preflighted")
|
|
|
|
monkeypatch.setattr(rename_cli, "_api_request", request)
|
|
_run(
|
|
monkeypatch,
|
|
"repo", "rename", "start", "flex-auth", "access-engine",
|
|
"--operation-id", OPERATION_ID,
|
|
"--preflight-file", str(preflight_file),
|
|
"--actor", "helixforge",
|
|
"--confirm", CONFIRMATION,
|
|
"--json",
|
|
)
|
|
|
|
result = json.loads(capsys.readouterr().out)
|
|
assert result["phase"] == "preflighted"
|
|
assert result["next_safe_action"]["phase"] == "forge-renamed"
|
|
create_body = calls[2][2]
|
|
assert create_body["operation_id"] == OPERATION_ID
|
|
assert create_body["preflight_token"] == "signed-preflight-token"
|
|
assert create_body["actor"] == "helixforge"
|
|
assert "signed-preflight-token" not in json.dumps(result)
|
|
|
|
|
|
def test_start_retry_uses_operation_journal_after_old_slug_becomes_alias(
|
|
monkeypatch, capsys
|
|
):
|
|
calls = []
|
|
|
|
def request(_api_base, method, path, body=None):
|
|
calls.append((method, path, body))
|
|
return _operation("statehub-rebound")
|
|
|
|
monkeypatch.setattr(rename_cli, "_api_request", request)
|
|
_run(
|
|
monkeypatch,
|
|
"repo",
|
|
"rename",
|
|
"start",
|
|
"flex-auth",
|
|
"access-engine",
|
|
"--operation-id",
|
|
OPERATION_ID,
|
|
"--preflight-file",
|
|
"/already-consumed/preflight.json",
|
|
"--actor",
|
|
"helixforge",
|
|
"--confirm",
|
|
CONFIRMATION,
|
|
"--json",
|
|
)
|
|
|
|
result = json.loads(capsys.readouterr().out)
|
|
assert result["phase"] == "statehub-rebound"
|
|
assert result["result"]["no_op"] is True
|
|
assert calls == [
|
|
(
|
|
"GET",
|
|
f"/repository-renames/operations/{OPERATION_ID}",
|
|
None,
|
|
)
|
|
]
|
|
|
|
|
|
def test_apply_uses_files_and_names_the_next_safe_phase(
|
|
monkeypatch, tmp_path, capsys
|
|
):
|
|
evidence = tmp_path / "evidence.json"
|
|
evidence.write_text('{"fresh_clone": true}')
|
|
evidence.chmod(0o600)
|
|
calls = []
|
|
|
|
def request(_api_base, method, path, body=None):
|
|
calls.append((method, path, body))
|
|
if method == "GET":
|
|
return _operation("statehub-rebound")
|
|
return _operation("source-synced")
|
|
|
|
monkeypatch.setattr(rename_cli, "_api_request", request)
|
|
_run(
|
|
monkeypatch,
|
|
"repo", "rename", "apply", OPERATION_ID,
|
|
"--phase", "source-synced",
|
|
"--confirm", CONFIRMATION,
|
|
"--evidence-file", str(evidence),
|
|
"--json",
|
|
)
|
|
|
|
result = json.loads(capsys.readouterr().out)
|
|
assert result["state"] == "planned"
|
|
assert result["phase"] == "source-synced"
|
|
assert result["next_safe_action"]["phase"] == "consumers-verified"
|
|
apply_body = calls[1][2]
|
|
assert apply_body["expected_phase"] == "statehub-rebound"
|
|
assert apply_body["evidence"] == {"fresh_clone": True}
|
|
|
|
|
|
def test_rollback_requires_separate_execute_flag(monkeypatch, capsys):
|
|
calls = []
|
|
|
|
def request(_api_base, method, path, body=None):
|
|
calls.append((method, path, body))
|
|
if method == "GET":
|
|
return _operation("source-synced")
|
|
return {
|
|
"safe_to_rollback": True,
|
|
"operation": _operation("rollback-preflight"),
|
|
}
|
|
|
|
monkeypatch.setattr(rename_cli, "_api_request", request)
|
|
_run(
|
|
monkeypatch,
|
|
"repo", "rename", "rollback", OPERATION_ID,
|
|
"--confirm", f"rollback:{OPERATION_ID}",
|
|
"--json",
|
|
)
|
|
result = json.loads(capsys.readouterr().out)
|
|
assert result["phase"] == "rollback-preflight"
|
|
assert result["next_safe_action"]["action"] == "execute-rollback"
|
|
assert [method for method, _path, _body in calls] == ["GET", "POST"]
|
|
|
|
|
|
def test_rollback_execute_reaches_terminal_state(monkeypatch, capsys):
|
|
calls = []
|
|
|
|
def request(_api_base, method, path, body=None):
|
|
calls.append((method, path, body))
|
|
return (
|
|
_operation("rollback-preflight")
|
|
if method == "GET"
|
|
else _operation("rolled-back")
|
|
)
|
|
|
|
monkeypatch.setattr(rename_cli, "_api_request", request)
|
|
_run(
|
|
monkeypatch,
|
|
"repo", "rename", "rollback", OPERATION_ID,
|
|
"--confirm", f"rollback:{OPERATION_ID}",
|
|
"--execute",
|
|
"--json",
|
|
)
|
|
result = json.loads(capsys.readouterr().out)
|
|
assert result["state"] == "rolled-back"
|
|
assert result["next_safe_action"]["action"] == "none"
|
|
assert [method for method, _path, _body in calls] == ["GET", "POST"]
|
|
|
|
|
|
def test_json_error_output_redacts_credentials_and_userinfo(monkeypatch, capsys):
|
|
def request(*_args, **_kwargs):
|
|
raise rename_cli.RenameCLIError(
|
|
"failed via https://user:password@forge.example/repo"
|
|
"?X-Amz-Credential=url-credential&X-Amz-Signature=url-secret",
|
|
code="simulated",
|
|
details={"authorization": "Bearer top-secret", "password": "hidden"},
|
|
)
|
|
|
|
monkeypatch.setattr(rename_cli, "_api_request", request)
|
|
with pytest.raises(SystemExit) as exc:
|
|
_run(
|
|
monkeypatch,
|
|
"repo", "rename", "status", OPERATION_ID, "--json",
|
|
)
|
|
assert exc.value.code == 1
|
|
output = capsys.readouterr().out
|
|
assert "user:password" not in output
|
|
assert "top-secret" not in output
|
|
assert "hidden" not in output
|
|
assert "url-secret" not in output
|
|
assert "url-credential" not in output
|
|
assert "[REDACTED]" in output
|
|
|
|
|
|
def test_invalid_confirmation_stops_before_mutating_request(monkeypatch, capsys):
|
|
calls = []
|
|
|
|
def request(_api_base, method, path, body=None):
|
|
calls.append((method, path, body))
|
|
return _operation("preflighted")
|
|
|
|
monkeypatch.setattr(rename_cli, "_api_request", request)
|
|
with pytest.raises(SystemExit) as exc:
|
|
_run(
|
|
monkeypatch,
|
|
"repo", "rename", "apply", OPERATION_ID,
|
|
"--phase", "forge-renamed",
|
|
"--confirm", "yes",
|
|
"--json",
|
|
)
|
|
assert exc.value.code == 1
|
|
assert [method for method, _path, _body in calls] == ["GET"]
|
|
result = json.loads(capsys.readouterr().out)
|
|
assert result["error"]["code"] == "confirmation_mismatch"
|
|
|
|
|
|
def test_unsafe_preflight_is_machine_detectable(monkeypatch, capsys):
|
|
def request(_api_base, method, _path, body=None):
|
|
del body
|
|
if method == "GET":
|
|
return _repo()
|
|
return {
|
|
**_preflight(),
|
|
"safe_to_apply": False,
|
|
"blockers": [{"code": "target_exists"}],
|
|
"preflight_token": None,
|
|
}
|
|
|
|
monkeypatch.setattr(rename_cli, "_api_request", request)
|
|
with pytest.raises(SystemExit) as exc:
|
|
_run(
|
|
monkeypatch,
|
|
"repo",
|
|
"rename",
|
|
"preflight",
|
|
"flex-auth",
|
|
"access-engine",
|
|
"--json",
|
|
)
|
|
assert exc.value.code == 2
|
|
result = json.loads(capsys.readouterr().out)
|
|
assert result["state"] == "failed"
|
|
assert result["next_safe_action"]["action"] == "resolve-blockers"
|
|
|
|
|
|
def test_invalid_operation_id_is_stable_json(monkeypatch, capsys):
|
|
monkeypatch.setattr(
|
|
rename_cli,
|
|
"_api_request",
|
|
lambda *_args, **_kwargs: pytest.fail("API should not be called"),
|
|
)
|
|
with pytest.raises(SystemExit) as exc:
|
|
_run(
|
|
monkeypatch,
|
|
"repo",
|
|
"rename",
|
|
"status",
|
|
"not-a-uuid",
|
|
"--json",
|
|
)
|
|
assert exc.value.code == 1
|
|
result = json.loads(capsys.readouterr().out)
|
|
assert result["schema_version"] == rename_cli.CLI_SCHEMA_VERSION
|
|
assert result["state"] == "failed"
|
|
assert result["error"]["code"] == "invalid_operation_id"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("operation", "label"),
|
|
[
|
|
(_operation("preflighted"), "PLANNED"),
|
|
(_operation("completed"), "ACHIEVED"),
|
|
({**_operation("source-synced"), "error_code": "failed"}, "FAILED"),
|
|
(_operation("rolled-back"), "ROLLED-BACK"),
|
|
],
|
|
)
|
|
def test_human_status_distinguishes_lifecycle_states(
|
|
monkeypatch, capsys, operation, label
|
|
):
|
|
monkeypatch.setattr(
|
|
rename_cli,
|
|
"_api_request",
|
|
lambda *_args, **_kwargs: operation,
|
|
)
|
|
if label == "FAILED":
|
|
with pytest.raises(SystemExit) as exc:
|
|
_run(monkeypatch, "repo", "rename", "status", OPERATION_ID)
|
|
assert exc.value.code == 2
|
|
else:
|
|
_run(monkeypatch, "repo", "rename", "status", OPERATION_ID)
|
|
captured = capsys.readouterr()
|
|
assert f"{label}: repository rename status" in captured.out + captured.err
|