feat: complete production workflow migration
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from gyxx_flow.cli import EXIT_CONFIGURATION, EXIT_SUCCESS, main
|
||||
from gyxx_flow.core.config import Settings
|
||||
from gyxx_flow.source_sync import SourceSyncService
|
||||
|
||||
|
||||
def _sha256(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def _project(tmp_path: Path) -> tuple[Settings, Path, Path]:
|
||||
repository = tmp_path / "repository"
|
||||
source = tmp_path / "source"
|
||||
target = repository / "src" / "module" / "job.py"
|
||||
manifest_path = repository / "config" / "source-manifests" / "module.json"
|
||||
source_file = source / "job.py"
|
||||
source_file.parent.mkdir(parents=True)
|
||||
target.parent.mkdir(parents=True)
|
||||
manifest_path.parent.mkdir(parents=True)
|
||||
old = b"old\n"
|
||||
new = b"new\n"
|
||||
source_file.write_bytes(new)
|
||||
target.write_bytes(old)
|
||||
manifest_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"module": "module",
|
||||
"source_project": "legacy",
|
||||
"files": [
|
||||
{
|
||||
"source_relative_path": "job.py",
|
||||
"target_relative_path": "src/module/job.py",
|
||||
"category": "source",
|
||||
"source_sha256": _sha256(old),
|
||||
"target_sha256": _sha256(old),
|
||||
"transformed": False,
|
||||
}
|
||||
],
|
||||
"intentionally_excluded": [],
|
||||
"snapshot": "test",
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(repository / "config" / "source-projects.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"projects": [
|
||||
{
|
||||
"module": "module",
|
||||
"source_project": "legacy",
|
||||
"manifest": "config/source-manifests/module.json",
|
||||
"root_env": "MODULE_SOURCE_ROOT",
|
||||
"target_root": "src/module",
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return Settings(project_root=repository, data_root=tmp_path / "data"), source, target
|
||||
|
||||
|
||||
def test_sources_status_is_read_only_and_does_not_build_workflow_registry(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
settings, source, target = _project(tmp_path)
|
||||
output = io.StringIO()
|
||||
|
||||
exit_code = main(
|
||||
[
|
||||
"sources",
|
||||
"status",
|
||||
"--project",
|
||||
"module",
|
||||
"--source-root",
|
||||
f"module={source}",
|
||||
"--json",
|
||||
],
|
||||
settings=settings,
|
||||
stdout=output,
|
||||
)
|
||||
|
||||
assert exit_code == EXIT_SUCCESS
|
||||
assert target.read_bytes() == b"old\n"
|
||||
payload = json.loads(output.getvalue())
|
||||
assert payload["summary"] == {"source_changed": 1}
|
||||
assert payload["files"][0]["state"] == "source_changed"
|
||||
|
||||
|
||||
def test_sources_apply_requires_execute_before_writing(tmp_path: Path) -> None:
|
||||
settings, source, target = _project(tmp_path)
|
||||
errors = io.StringIO()
|
||||
|
||||
exit_code = main(
|
||||
[
|
||||
"sources",
|
||||
"apply",
|
||||
"--project",
|
||||
"module",
|
||||
"--source-root",
|
||||
f"module={source}",
|
||||
],
|
||||
settings=settings,
|
||||
stderr=errors,
|
||||
)
|
||||
|
||||
assert exit_code == EXIT_CONFIGURATION
|
||||
assert "--execute" in errors.getvalue()
|
||||
assert target.read_bytes() == b"old\n"
|
||||
|
||||
|
||||
def test_sources_apply_copies_only_after_explicit_execute(tmp_path: Path) -> None:
|
||||
settings, source, target = _project(tmp_path)
|
||||
output = io.StringIO()
|
||||
|
||||
exit_code = main(
|
||||
[
|
||||
"sources",
|
||||
"apply",
|
||||
"--project",
|
||||
"module",
|
||||
"--source-root",
|
||||
f"module={source}",
|
||||
"--execute",
|
||||
"--json",
|
||||
],
|
||||
settings=settings,
|
||||
stdout=output,
|
||||
)
|
||||
|
||||
assert exit_code == EXIT_SUCCESS
|
||||
assert target.read_bytes() == b"new\n"
|
||||
payload = json.loads(output.getvalue())
|
||||
assert payload["applied"] == ["src/module/job.py"]
|
||||
|
||||
|
||||
def test_sources_status_reports_new_unmapped_source_candidate(tmp_path: Path) -> None:
|
||||
settings, source, target = _project(tmp_path)
|
||||
del target
|
||||
(source / "new_job.py").write_text("new candidate\n", encoding="utf-8")
|
||||
output = io.StringIO()
|
||||
|
||||
exit_code = main(
|
||||
[
|
||||
"sources",
|
||||
"status",
|
||||
"--project",
|
||||
"module",
|
||||
"--source-root",
|
||||
f"module={source}",
|
||||
"--json",
|
||||
],
|
||||
settings=settings,
|
||||
stdout=output,
|
||||
)
|
||||
|
||||
assert exit_code == EXIT_SUCCESS
|
||||
payload = json.loads(output.getvalue())
|
||||
assert payload["summary"] == {"new_source": 1, "source_changed": 1}
|
||||
candidate = next(
|
||||
item for item in payload["files"] if item["state"] == "new_source"
|
||||
)
|
||||
assert candidate["source_relative_path"] == "new_job.py"
|
||||
assert candidate["safe_to_apply"] is False
|
||||
|
||||
|
||||
def test_repository_source_project_config_declares_migration_target_roots() -> None:
|
||||
repository = Path(__file__).parents[1]
|
||||
service = SourceSyncService(
|
||||
repository,
|
||||
source_roots={
|
||||
"content_marketing": repository,
|
||||
"product_commerce": repository,
|
||||
"shop_intelligence": repository,
|
||||
"supply_chain": repository,
|
||||
},
|
||||
)
|
||||
|
||||
assert {
|
||||
project.module: project.target_root
|
||||
for project in service._projects
|
||||
} == {
|
||||
"content_marketing": "src/gyxx_flow/modules/content_marketing",
|
||||
"product_commerce": (
|
||||
"src/gyxx_flow/modules/product_commerce"
|
||||
),
|
||||
"shop_intelligence": (
|
||||
"src/gyxx_flow/modules/shop_intelligence"
|
||||
),
|
||||
"supply_chain": "src/gyxx_flow/modules/supply_chain",
|
||||
}
|
||||
Reference in New Issue
Block a user