201 lines
5.7 KiB
Python
201 lines
5.7 KiB
Python
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",
|
|
}
|