feat(devkit): add resumable workspace automation and UI review tooling
Verified with the coordinated workspace changes by devkit full run 2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed). This shared UI pass does not mark the individual module reviews complete.
This commit is contained in:
Executable
+151
@@ -0,0 +1,151 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
|
||||
from govoplan_devkit.common import atomic_json, read_json, redact, safe_output
|
||||
from govoplan_devkit.context import build_context
|
||||
from govoplan_devkit.workspace import (
|
||||
load_project,
|
||||
selected_repositories,
|
||||
git_bytes,
|
||||
inspect_repository,
|
||||
Repository,
|
||||
)
|
||||
|
||||
|
||||
def test_portable_project_rejects_escape_alias_collisions_and_duplicate_json(tmp_path):
|
||||
config = tmp_path / "project.json"
|
||||
for repositories in (
|
||||
[{"name": "repo", "path": "../outside"}],
|
||||
[
|
||||
{"name": "repo", "path": "repo", "aliases": ["other"]},
|
||||
{"name": "other", "path": "other"},
|
||||
],
|
||||
):
|
||||
config.write_text(
|
||||
json.dumps({"schema_version": 1, "repositories": repositories})
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
load_project(tmp_path, config)
|
||||
config.write_text('{"schema_version":1,"schema_version":2}')
|
||||
with pytest.raises(ValueError, match="Duplicate"):
|
||||
read_json(config)
|
||||
|
||||
|
||||
def test_missing_repository_is_error_not_clean(tmp_path):
|
||||
config = tmp_path / "project.json"
|
||||
config.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"name": "Test",
|
||||
"repositories": [{"name": "missing", "path": "missing"}],
|
||||
}
|
||||
)
|
||||
)
|
||||
result = build_context(tmp_path, config, [], True)
|
||||
assert result["_exit_code"] == 1
|
||||
assert result["repositories"][0]["errors"]
|
||||
assert not result["remote_checked"]
|
||||
|
||||
|
||||
def test_unknown_repo_filter_is_not_silently_ignored(tmp_path):
|
||||
config = tmp_path / "project.json"
|
||||
config.write_text(
|
||||
json.dumps(
|
||||
{"schema_version": 1, "repositories": [{"name": "repo", "path": "repo"}]}
|
||||
)
|
||||
)
|
||||
with pytest.raises(ValueError, match="Unknown"):
|
||||
selected_repositories(load_project(tmp_path, config), ["typo"])
|
||||
|
||||
|
||||
def test_atomic_output_does_not_chmod_existing_parent(tmp_path):
|
||||
folder = tmp_path / "public"
|
||||
folder.mkdir(mode=0o755)
|
||||
atomic_json(folder / "private.json", {"safe": True})
|
||||
assert folder.stat().st_mode & 0o777 == 0o755
|
||||
assert (folder / "private.json").stat().st_mode & 0o777 == 0o600
|
||||
|
||||
|
||||
def test_read_rejects_fifo_and_symlink(tmp_path):
|
||||
fifo = tmp_path / "fifo"
|
||||
os.mkfifo(fifo)
|
||||
with pytest.raises(ValueError, match="regular"):
|
||||
read_json(fifo)
|
||||
target = tmp_path / "target.json"
|
||||
target.write_text("{}")
|
||||
alias = tmp_path / "alias.json"
|
||||
alias.symlink_to(target)
|
||||
with pytest.raises(ValueError, match="symlink"):
|
||||
read_json(alias)
|
||||
|
||||
|
||||
def test_generic_secret_display_hygiene():
|
||||
assert "topsecret" not in redact(
|
||||
"Authorization: Bearer topsecret\nhttps://user:topsecret@example.invalid/"
|
||||
)
|
||||
|
||||
|
||||
def test_separate_secret_arguments_are_redacted_in_nested_json():
|
||||
value = {
|
||||
"stages": [{"argv": ["check", "--token", "hidden-value"]}],
|
||||
"password": "hidden-password",
|
||||
}
|
||||
output = json.dumps(safe_output(value))
|
||||
assert "hidden-value" not in output and "hidden-password" not in output
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
[
|
||||
"GIT_DIR",
|
||||
"GIT_NAMESPACE",
|
||||
"GIT_COMMON_DIR",
|
||||
"GIT_CONFIG_PARAMETERS",
|
||||
"GIT_AUTHOR_NAME",
|
||||
],
|
||||
)
|
||||
def test_inherited_git_redirects_are_rejected(tmp_path, monkeypatch, name):
|
||||
monkeypatch.setenv(name, "fixture")
|
||||
assert inspect_repository(Repository("example", tmp_path))["errors"]
|
||||
with pytest.raises(ValueError, match="overrides"):
|
||||
git_bytes(tmp_path, "status")
|
||||
|
||||
|
||||
def test_deeply_nested_json_fails_as_controlled_input_error(tmp_path):
|
||||
path = tmp_path / "deep.json"
|
||||
path.write_text("[" * 10000 + "]" * 10000)
|
||||
with pytest.raises(ValueError, match="nesting"):
|
||||
read_json(path)
|
||||
|
||||
|
||||
def test_clean_commit_without_upstream_is_still_changed(tmp_path):
|
||||
subprocess.run(["git", "init", "-q", str(tmp_path)], check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(tmp_path),
|
||||
"-c",
|
||||
"user.name=Fixture",
|
||||
"-c",
|
||||
"user.email=fixture@example.invalid",
|
||||
"commit",
|
||||
"--allow-empty",
|
||||
"-qm",
|
||||
"fixture",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
from govoplan_devkit.workspace import Project
|
||||
|
||||
repo = Repository("example", tmp_path)
|
||||
assert selected_repositories(Project("Example", (repo,), {}), [], changed=True) == [
|
||||
repo
|
||||
]
|
||||
Reference in New Issue
Block a user