feat(devkit): add resumable workspace automation and UI review tooling
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m30s

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:
2026-09-09 02:03:17 +02:00
parent 14b19fbead
commit 2ffdb23f69
67 changed files with 17306 additions and 94 deletions
+393
View File
@@ -0,0 +1,393 @@
"""Repository input identities use disposable Git fixtures, never remote effects."""
from copy import deepcopy
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 import inputs
from govoplan_devkit.inputs import InputSnapshotter, validate_input_declaration
from govoplan_devkit.workspace import Project, Repository, source_fingerprint
def git(repo, *arguments):
return subprocess.run(
["git", "-C", str(repo), *arguments], capture_output=True, check=True
).stdout
@pytest.fixture
def fixture(tmp_path, monkeypatch):
repositories = []
for name in ("alpha", "beta"):
path = tmp_path / name
path.mkdir()
git(path, "init", "-q")
(path / "source.txt").write_text("initial\n")
git(path, "add", "source.txt")
git(
path,
"-c",
"user.name=Fixture",
"-c",
"user.email=fixture@example.invalid",
"commit",
"-qm",
"fixture",
)
repositories.append(Repository(name, path, ("alias-" + name,)))
project = Project(
"Fixture",
tuple(repositories),
{"tools": {}, "profiles": {"quick": ["one"]}, "checks": []},
)
# Avoid incidental edits by other agents affecting these source-input tests.
# The actual tool-source hashing implementation is checked separately.
monkeypatch.setattr(
InputSnapshotter, "_tooling_identity", lambda *_: "fixture-devkit-source"
)
return tmp_path, project, InputSnapshotter(project, workspace_root=tmp_path)
def stage(identity="one", repos=None, **extra):
return {
"id": identity,
"argv": ["true"],
**({"inputs": {"repos": repos}} if repos is not None else {}),
**extra,
}
def fingerprint(snapshot, name="one"):
return snapshot["stages"][name]["fingerprint"]
def test_undeclared_inputs_remain_whole_workspace(fixture):
root, _, engine = fixture
result = engine.snapshot([stage()])
assert result["complete_workspace"] is True
assert result["observed_scope"]["repos"] == ["alpha", "beta"]
assert result["stages"]["one"]["scope"] == {
"version": 1,
"kind": "workspace",
"declared": False,
"repos": ["alpha", "beta"],
}
(root / "beta/source.txt").write_text("changed\n")
assert fingerprint(engine.snapshot([stage()])) != fingerprint(result)
def test_scoped_input_does_not_read_unrelated_repository_bytes(fixture, monkeypatch):
root, _, engine = fixture
original = inputs.os.open
def guarded(path, *args, **kwargs):
if Path(path).is_relative_to(root / "beta"):
raise AssertionError("Unrelated repository input was opened")
return original(path, *args, **kwargs)
monkeypatch.setattr(inputs.os, "open", guarded)
first = engine.snapshot([stage(repos=["alpha"])])
(root / "beta/source.txt").write_text("unrelated change")
second = engine.snapshot([stage(repos=["alpha"])])
assert fingerprint(first) == fingerprint(second)
assert second["scan_stats"]["repositories"] == 1
assert second["scan_stats"]["git_calls"] == 2
assert second["complete_workspace"] is False
def test_repository_union_scans_once_per_snapshot(fixture):
_, _, engine = fixture
result = engine.snapshot(
[
stage("one", ["alpha"]),
stage("two", ["alpha"]),
stage("three", ["beta", "alpha"]),
]
)
assert result["scan_stats"]["repositories"] == 2
assert result["scan_stats"]["git_calls"] == 4
assert result["stages"]["three"]["scope"]["repos"] == ["alpha", "beta"]
def test_session_rechecks_metadata_but_reuses_stable_file_content(fixture):
_, _, engine = fixture
plan = [stage(repos=["alpha"])]
before = engine.snapshot(plan)
after = engine.snapshot(plan)
assert fingerprint(before) == fingerprint(after)
assert before["scan_stats"]["bytes"] == len("initial\n")
assert after["scan_stats"]["bytes"] == 0
assert after["scan_stats"]["cache_hits"] == 1
assert after["scan_stats"]["git_calls"] == 2
def test_changed_bytes_with_restored_mtime_are_not_reused(fixture):
root, _, engine = fixture
plan = [stage(repos=["alpha"])]
before = engine.snapshot(plan)
path = root / "alpha/source.txt"
metadata = path.stat()
path.write_text("changed\n")
os.utime(path, ns=(metadata.st_atime_ns, metadata.st_mtime_ns))
after = engine.snapshot(plan)
assert fingerprint(after) != fingerprint(before)
assert after["scan_stats"]["bytes"] == len("changed\n")
def test_inode_replacement_with_same_size_and_mtime_is_not_reused(fixture):
root, _, engine = fixture
plan = [stage(repos=["alpha"])]
before = engine.snapshot(plan)
path = root / "alpha/source.txt"
metadata = path.stat()
replacement = root / "replacement.txt"
replacement.write_text("changed\n")
os.utime(replacement, ns=(metadata.st_atime_ns, metadata.st_mtime_ns))
replacement.replace(path)
after = engine.snapshot(plan)
assert fingerprint(before) != fingerprint(after)
assert after["scan_stats"]["cache_hits"] == 0
@pytest.mark.parametrize("flag", ["assume-unchanged", "skip-worktree"])
def test_hidden_index_flags_do_not_hide_worktree_changes(fixture, flag):
root, _, engine = fixture
git(root / "alpha", "update-index", "--" + flag, "source.txt")
before = engine.source_snapshot(["alpha"])
(root / "alpha/source.txt").write_text("hidden change\n")
after = engine.source_snapshot(["alpha"])
assert before["observed_source_fingerprint"] != after["observed_source_fingerprint"]
def test_index_head_and_new_deleted_files_are_bound(fixture):
root, _, engine = fixture
repo = root / "alpha"
identities = []
def record():
identities.append(
engine.source_snapshot(["alpha"])["observed_source_fingerprint"]
)
record()
(repo / "source.txt").write_text("changed\n")
record()
git(repo, "add", "source.txt")
record()
git(
repo,
"-c",
"user.name=Fixture",
"-c",
"user.email=fixture@example.invalid",
"commit",
"-qm",
"change",
)
record()
(repo / "new.txt").write_text("new")
record()
(repo / "source.txt").unlink()
record()
assert len(set(identities)) == len(identities)
def test_missing_registered_repository_becoming_present_invalidates(fixture):
root, project, _ = fixture
extra = Repository("missing", root / "missing")
extended = Project("Fixture", (*project.repositories, extra), project.config)
engine = InputSnapshotter(extended, workspace_root=root)
before = engine.source_snapshot(["missing"])
extra.path.mkdir()
git(extra.path, "init", "-q")
after = engine.source_snapshot(["missing"])
assert before["observed_source_fingerprint"] != after["observed_source_fingerprint"]
def test_unrelated_plan_config_edits_do_not_invalidate_scoped_stage(fixture):
root, project, engine = fixture
plan = [stage(repos=["alpha"])]
before = engine.snapshot(plan)
updated = deepcopy(project.config)
updated["profiles"]["full"] = ["unrelated"]
updated["checks"].append({"id": "unrelated", "argv": ["false"]})
other = InputSnapshotter(
Project("Changed label", project.repositories, updated), workspace_root=root
)
assert fingerprint(other.snapshot(plan)) == fingerprint(before)
def test_scope_command_tools_and_tooling_change_invalidate(fixture):
root, project, engine = fixture
first = engine.snapshot([stage(repos=["alpha"])])
assert fingerprint(engine.snapshot([stage(repos=["beta"])])) != fingerprint(first)
assert fingerprint(
engine.snapshot([stage(repos=["alpha"], argv=["false"])])
) != fingerprint(first)
assert fingerprint(
engine.snapshot([stage(repos=["alpha"])], tooling_fingerprint="new-env")
) != fingerprint(first)
changed = deepcopy(project.config)
changed["tools"] = {"node": "/another/node"}
other = InputSnapshotter(
Project("Fixture", project.repositories, changed), workspace_root=root
)
assert fingerprint(other.snapshot([stage(repos=["alpha"])])) != fingerprint(first)
def test_source_only_attestation_is_independent_of_plan_and_tooling(fixture):
_, _, engine = fixture
one = engine.snapshot([stage(repos=["alpha"])], tooling_fingerprint="env-one")
two = engine.snapshot(
[stage("different", ["alpha"], argv=["false"])], tooling_fingerprint="env-two"
)
source = engine.source_snapshot(["alpha"])
assert (
one["observed_source_fingerprint"]
== two["observed_source_fingerprint"]
== source["observed_source_fingerprint"]
)
assert (
one["stages"]["one"]["source_fingerprint"]
== source["observed_source_fingerprint"]
)
assert source["fingerprint_version"] == inputs.FINGERPRINT_VERSION
@pytest.mark.parametrize(
"value",
[
None,
{},
{"paths": ["src/**"]},
{"repos": []},
{"repos": "alpha"},
{"repos": ["unknown"]},
{"repos": ["alias-alpha"]},
{"repos": ["alpha", "alpha"]},
{"repos": [""]},
{"repos": ["alpha"], "extra": True},
],
)
def test_synthetic_stage_scopes_fail_closed(fixture, value):
_, _, engine = fixture
with pytest.raises(ValueError):
engine.snapshot([stage(inputs=value)])
def test_duplicate_stage_ids_and_escaped_repository_paths_fail(fixture):
root, project, engine = fixture
with pytest.raises(ValueError, match="Duplicate"):
engine.snapshot([stage(), stage()])
bad = Project("Bad", (Repository("outside", root.parent),), project.config)
with pytest.raises(ValueError, match="escapes"):
InputSnapshotter(bad, workspace_root=root)
def test_declaration_validation_is_pure_before_dry_run(monkeypatch):
monkeypatch.setattr(
inputs,
"git_bytes",
lambda *_a, **_k: pytest.fail("Planning validation ran Git"),
)
assert validate_input_declaration(
{"repos": ["beta", "alpha"]}, {"alpha", "beta"}
) == {"repos": ["alpha", "beta"]}
with pytest.raises(ValueError):
validate_input_declaration({"repos": ["unknown"]}, {"alpha", "beta"})
def test_runtime_records_are_not_hashed_as_execution_plans(fixture):
_, _, engine = fixture
with pytest.raises(ValueError, match="freshly planned"):
engine.snapshot([stage(repos=["alpha"], status="passed")])
def test_membership_count_is_bounded_before_entry_hashing(fixture, monkeypatch):
root, _, engine = fixture
(root / "alpha/new.txt").write_text("extra")
monkeypatch.setattr(inputs, "MAX_REPOSITORY_ENTRIES", 1)
with pytest.raises(ValueError, match="entry count"):
engine.source_snapshot(["alpha"])
def test_symlink_to_ignored_file_inside_repository_binds_target(fixture):
root, _, engine = fixture
repo = root / "alpha"
(repo / ".gitignore").write_text("ignored.txt\n")
(repo / "ignored.txt").write_text("first")
(repo / "linked.txt").symlink_to("ignored.txt")
before = engine.source_snapshot(["alpha"])
(repo / "ignored.txt").write_text("other")
assert (
engine.source_snapshot(["alpha"])["observed_source_fingerprint"]
!= before["observed_source_fingerprint"]
)
def test_dangling_symlink_target_creation_is_observed(fixture):
root, _, engine = fixture
repo = root / "alpha"
(repo / "linked.txt").symlink_to("future.txt")
before = engine.source_snapshot(["alpha"])
(repo / "future.txt").write_text("created")
assert (
engine.source_snapshot(["alpha"])["observed_source_fingerprint"]
!= before["observed_source_fingerprint"]
)
def test_cross_repository_and_directory_symlinks_are_not_silently_reused(fixture):
root, _, engine = fixture
link = root / "alpha/linked.txt"
link.symlink_to(root / "beta/source.txt")
with pytest.raises(ValueError, match="escapes"):
engine.source_snapshot(["alpha"])
link.unlink()
link.symlink_to(".")
with pytest.raises(ValueError, match="regular file"):
engine.source_snapshot(["alpha"])
def test_file_change_during_hash_is_rejected(fixture, monkeypatch):
root, _, engine = fixture
path = root / "alpha/source.txt"
original = inputs.os.fstat
changed = False
def mutate(descriptor):
nonlocal changed
metadata = original(descriptor)
if not changed:
changed = True
path.write_text("changed during read")
return metadata
monkeypatch.setattr(inputs.os, "fstat", mutate)
with pytest.raises(ValueError, match="changed"):
engine._file_hash(path, inputs._stats())
def test_legacy_whole_project_api_remains_independent(fixture):
_, project, engine = fixture
legacy = source_fingerprint(project)
engine.snapshot([stage(repos=["alpha"])])
assert source_fingerprint(project) == legacy
def test_real_tooling_inventory_is_bound_and_memoized(tmp_path):
repo = Repository("example", tmp_path)
project = Project("Fixture", (repo,), {"tools": {}})
engine = InputSnapshotter(project, workspace_root=tmp_path)
before_stats, after_stats = inputs._stats(), inputs._stats()
before = engine._tooling_identity(before_stats)
after = engine._tooling_identity(after_stats)
assert before == after
assert before_stats["tooling_files"] > 15 and before_stats["tooling_bytes"] > 0
assert after_stats["tooling_files"] == after_stats["tooling_cache_hits"]
assert after_stats["tooling_bytes"] == 0