feat: complete production workflow migration

This commit is contained in:
2026-08-06 14:29:57 +08:00
parent 7f215e79c4
commit 8df5266abb
448 changed files with 56937 additions and 14619 deletions
+200
View File
@@ -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",
}