claude-dev-env 2.19.0 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +31 -1
  2. package/.agents/skills/e-code-review/SKILL.md +12 -1
  3. package/.agents/skills/e-code-review/reference/fix.md +5 -1
  4. package/.agents/skills/e-code-review/reference/loop.md +4 -0
  5. package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +66 -0
  6. package/.agents/skills/e-code-review/reference/preflight-proposal.md +40 -0
  7. package/.agents/skills/e-code-review/reference/runner-selection.md +1 -0
  8. package/.agents/skills/pr-cleanup/SKILL.md +109 -11
  9. package/_shared/pr-loop/scripts/code_rules_gate.py +29 -6
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +15 -3
  11. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +4 -0
  12. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -0
  13. package/docs/CODE_RULES.md +2 -0
  14. package/hooks/advisory/conftest.py +10 -0
  15. package/hooks/advisory/refactor_guard.py +250 -144
  16. package/hooks/advisory/refactor_guard_test_support.py +46 -0
  17. package/hooks/advisory/test_refactor_guard_advisory.py +171 -0
  18. package/hooks/advisory/test_refactor_guard_eligibility.py +166 -0
  19. package/hooks/blocking/block_main_commit.py +66 -33
  20. package/hooks/blocking/code_rules_blast_radius.py +194 -0
  21. package/hooks/blocking/code_rules_enforcer.py +95 -0
  22. package/hooks/blocking/codex_apply_patch.py +238 -0
  23. package/hooks/blocking/test_block_main_commit.py +145 -0
  24. package/hooks/blocking/test_code_rules_blast_radius.py +161 -0
  25. package/hooks/blocking/test_code_rules_enforcer_codex_apply_patch.py +148 -0
  26. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +1 -0
  27. package/hooks/blocking/test_destructive_command_blocker.py +154 -138
  28. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +52 -9
  29. package/hooks/blocking/test_destructive_command_blocker_patterns.py +133 -0
  30. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +71 -5
  31. package/hooks/git-hooks/AGENTS.md +1 -1
  32. package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -0
  33. package/hooks/git-hooks/post_commit.py +160 -51
  34. package/hooks/git-hooks/pre_commit.py +3 -3
  35. package/hooks/git-hooks/test_post_commit.py +203 -0
  36. package/hooks/git-hooks/test_pre_commit.py +2 -2
  37. package/hooks/hooks_constants/blast_radius_constants.py +14 -0
  38. package/hooks/hooks_constants/code_rules_enforcer_constants.py +1 -0
  39. package/hooks/hooks_constants/refactor_guard_constants.py +75 -0
  40. package/hooks/hooks_constants/test_refactor_guard_constants.py +21 -0
  41. package/hooks/observability/test_instructions_loaded_logger.py +54 -0
  42. package/hooks/session/test_plugin_data_dir_cleanup.py +70 -0
  43. package/hooks/session/test_session_edit_tracker_cleanup.py +16 -3
  44. package/hooks/validation/mypy_validator.py +213 -80
  45. package/hooks/validation/test_mypy_validator.py +288 -13
  46. package/hooks/workflow/auto_formatter.py +225 -93
  47. package/hooks/workflow/investigation_tracker_reset.py +2 -0
  48. package/hooks/workflow/test_auto_formatter.py +261 -12
  49. package/hooks/workflow/test_investigation_tracker_reset.py +90 -0
  50. package/package.json +1 -1
  51. package/rules/failure-blast-radius.md +126 -0
  52. package/scripts/codex_compat_materializer.py +395 -17
  53. package/scripts/tests/test_codex_compat_materializer.py +17 -3
@@ -0,0 +1,203 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ import post_commit
8
+ import pytest
9
+ from git_hooks_constants import GIT_EXECUTABLE_NAME
10
+
11
+
12
+ def build_fixture_git_environment() -> dict[str, str]:
13
+ """Copy the process environment without inherited Git state overrides."""
14
+ return {
15
+ each_name: each_value
16
+ for each_name, each_value in os.environ.items()
17
+ if not each_name.upper().startswith("GIT_")
18
+ }
19
+
20
+
21
+ def clear_inherited_git_environment(monkeypatch: pytest.MonkeyPatch) -> None:
22
+ """Remove inherited Git state overrides from the current test process."""
23
+ for each_name in list(os.environ):
24
+ if each_name.upper().startswith("GIT_"):
25
+ monkeypatch.delenv(each_name, raising=False)
26
+
27
+
28
+ def test_fixture_git_environment_preserves_process_execution_path(
29
+ monkeypatch: pytest.MonkeyPatch,
30
+ ) -> None:
31
+ """The fixture environment keeps PATH while removing Git overrides."""
32
+ monkeypatch.setenv("PATH", "fixture-execution-path")
33
+ monkeypatch.setenv("GIT_DIR", "unrelated-repository")
34
+
35
+ fixture_environment = build_fixture_git_environment()
36
+
37
+ assert fixture_environment["PATH"] == "fixture-execution-path"
38
+ assert all(not each_name.upper().startswith("GIT_") for each_name in fixture_environment)
39
+
40
+
41
+ def run_fixture_git(repository_path: Path, *arguments: str) -> str:
42
+ """Run Git in a fixture repository and return standard output."""
43
+ completed_process = subprocess.run(
44
+ [GIT_EXECUTABLE_NAME, *arguments],
45
+ cwd=repository_path,
46
+ check=False,
47
+ capture_output=True,
48
+ text=True,
49
+ env=build_fixture_git_environment(),
50
+ )
51
+ assert completed_process.returncode == 0, completed_process.stderr
52
+ return completed_process.stdout.strip()
53
+
54
+
55
+ def configure_fixture_identity(repository_path: Path) -> None:
56
+ """Configure a stable identity for a fixture repository."""
57
+ run_fixture_git(repository_path, "config", "user.name", "Fixture User")
58
+ run_fixture_git(
59
+ repository_path,
60
+ "config",
61
+ "user.email",
62
+ "fixture@example.invalid",
63
+ )
64
+ run_fixture_git(repository_path, "config", "commit.gpgsign", "false")
65
+ fixture_hooks_directory = repository_path / ".fixture-hooks"
66
+ fixture_hooks_directory.mkdir(exist_ok=True)
67
+ run_fixture_git(
68
+ repository_path,
69
+ "config",
70
+ "core.hooksPath",
71
+ str(fixture_hooks_directory),
72
+ )
73
+
74
+
75
+ def initialize_fixture_repository(repository_path: Path) -> None:
76
+ """Create and configure a fixture Git repository."""
77
+ repository_path.mkdir()
78
+ run_fixture_git(repository_path, "init", "-b", "main")
79
+ configure_fixture_identity(repository_path)
80
+
81
+
82
+ def test_native_submodule_commit_records_parent_pointer(
83
+ tmp_path: Path,
84
+ monkeypatch: pytest.MonkeyPatch,
85
+ ) -> None:
86
+ """A real submodule commit creates the matching parent pointer commit."""
87
+ unrelated_repository = tmp_path / "unrelated-repository"
88
+ initialize_fixture_repository(unrelated_repository)
89
+ monkeypatch.setenv("GIT_DIR", str(unrelated_repository / ".git"))
90
+
91
+ source_repository = tmp_path / "source-repository"
92
+ parent_repository = tmp_path / "parent-repository"
93
+ initialize_fixture_repository(source_repository)
94
+ (source_repository / "README.md").write_text("fixture source\n", encoding="utf-8")
95
+ run_fixture_git(source_repository, "add", "README.md")
96
+ run_fixture_git(source_repository, "commit", "-m", "Create fixture source")
97
+
98
+ initialize_fixture_repository(parent_repository)
99
+ (parent_repository / "README.md").write_text("fixture parent\n", encoding="utf-8")
100
+ run_fixture_git(parent_repository, "add", "README.md")
101
+ run_fixture_git(parent_repository, "commit", "-m", "Create fixture parent")
102
+ run_fixture_git(
103
+ parent_repository,
104
+ "-c",
105
+ "protocol.file.allow=always",
106
+ "submodule",
107
+ "add",
108
+ str(source_repository),
109
+ "nested/sub[module]",
110
+ )
111
+ run_fixture_git(parent_repository, "commit", "-am", "Add fixture submodule")
112
+
113
+ submodule_repository = parent_repository / "nested" / "sub[module]"
114
+ configure_fixture_identity(submodule_repository)
115
+ (parent_repository / "staged-note.txt").write_text("keep staged\n", encoding="utf-8")
116
+ run_fixture_git(parent_repository, "add", "staged-note.txt")
117
+ (submodule_repository / "change.txt").write_text("fixture change\n", encoding="utf-8")
118
+ run_fixture_git(submodule_repository, "add", "change.txt")
119
+ run_fixture_git(submodule_repository, "commit", "-m", "Record fixture change")
120
+ clear_inherited_git_environment(monkeypatch)
121
+ monkeypatch.chdir(submodule_repository)
122
+ assert post_commit.main() == 0
123
+
124
+ submodule_commit_hash = run_fixture_git(submodule_repository, "rev-parse", "HEAD")
125
+ parent_commit_subject = run_fixture_git(parent_repository, "log", "-1", "--pretty=%s")
126
+ parent_commit_body = run_fixture_git(parent_repository, "log", "-1", "--pretty=%B")
127
+ recorded_pointer_hash = run_fixture_git(
128
+ parent_repository,
129
+ "rev-parse",
130
+ "HEAD:nested/sub[module]",
131
+ )
132
+
133
+ assert recorded_pointer_hash == submodule_commit_hash
134
+ assert parent_commit_subject == (
135
+ f"chore: update sub[module] submodule to {submodule_commit_hash}"
136
+ )
137
+ assert "Submodule commit: Record fixture change" in parent_commit_body
138
+ assert run_fixture_git(parent_repository, "status", "--short") == "A staged-note.txt"
139
+
140
+
141
+ def test_parent_update_returns_git_diagnostic_for_failed_add(
142
+ tmp_path: Path,
143
+ monkeypatch: pytest.MonkeyPatch,
144
+ ) -> None:
145
+ """A failed parent add returns the Git diagnostic and literal pathspec."""
146
+ parent_repository = tmp_path / "parent-repository"
147
+ submodule_repository = parent_repository / "sub[module]"
148
+ submodule_repository.mkdir(parents=True)
149
+ recorded_commands: list[tuple[str, ...]] = []
150
+
151
+ def fail_git_add(*arguments: str, cwd: Path) -> subprocess.CompletedProcess[str]:
152
+ recorded_commands.append(arguments)
153
+ return subprocess.CompletedProcess(
154
+ ["git", *arguments],
155
+ 1,
156
+ "",
157
+ "fatal: fixture add failure",
158
+ )
159
+
160
+ monkeypatch.setattr(post_commit, "execute_git", fail_git_add)
161
+
162
+ parent_update = post_commit.update_parent_pointer(
163
+ parent_repository,
164
+ submodule_repository,
165
+ "a" * 40,
166
+ "Fixture commit",
167
+ )
168
+
169
+ assert parent_update == (
170
+ post_commit.ParentPointerStatus.FAILED,
171
+ "fatal: fixture add failure",
172
+ )
173
+ assert recorded_commands == [("add", "--", ":(literal)sub[module]")]
174
+
175
+
176
+ def test_main_prints_git_failure_diagnostic(
177
+ tmp_path: Path,
178
+ monkeypatch: pytest.MonkeyPatch,
179
+ capsys: pytest.CaptureFixture[str],
180
+ ) -> None:
181
+ """The hook prints the Git diagnostic when the parent update fails."""
182
+ submodule_repository = tmp_path / "submodule"
183
+ parent_repository = tmp_path / "parent"
184
+ submodule_repository.mkdir()
185
+ parent_repository.mkdir()
186
+ monkeypatch.setattr(
187
+ post_commit,
188
+ "run_git_from_current_directory",
189
+ lambda *arguments: str(submodule_repository),
190
+ )
191
+ monkeypatch.setattr(post_commit, "find_parent_repo", lambda repo: parent_repository)
192
+ monkeypatch.setattr(post_commit, "run_git", lambda *arguments, cwd: "a" * 40)
193
+ monkeypatch.setattr(
194
+ post_commit,
195
+ "update_parent_pointer",
196
+ lambda *arguments: (
197
+ post_commit.ParentPointerStatus.FAILED,
198
+ "fatal: fixture commit failure",
199
+ ),
200
+ )
201
+
202
+ assert post_commit.main() == 0
203
+ assert "Git diagnostic: fatal: fixture commit failure" in capsys.readouterr().out
@@ -60,7 +60,7 @@ def test_main_propagates_passing_exit_code_from_gate(
60
60
  assert exit_code == 0
61
61
 
62
62
 
63
- def test_main_invokes_gate_with_staged_flag(
63
+ def test_main_invokes_gate_with_immediate_flag(
64
64
  tmp_path: Path,
65
65
  monkeypatch: pytest.MonkeyPatch,
66
66
  ) -> None:
@@ -84,7 +84,7 @@ def test_main_invokes_gate_with_staged_flag(
84
84
  recorded_arguments = recorded_arguments_path.read_text(
85
85
  encoding="utf-8"
86
86
  ).splitlines()
87
- assert recorded_arguments == ["--staged"]
87
+ assert recorded_arguments == ["--immediate"]
88
88
 
89
89
 
90
90
  def test_main_exits_two_when_invoke_gate_raises_oserror(
@@ -0,0 +1,14 @@
1
+ """Constants for the blast-radius declaration check."""
2
+
3
+ BLAST_RADIUS_RUN_SUFFIX = "RunFatal"
4
+ BLAST_RADIUS_ITEM_SUFFIX = "ItemBlocked"
5
+ ALL_BLAST_RADIUS_SUFFIXES = (BLAST_RADIUS_RUN_SUFFIX, BLAST_RADIUS_ITEM_SUFFIX)
6
+
7
+ MAX_BLAST_RADIUS_ISSUES = 20
8
+
9
+ BLAST_RADIUS_MESSAGE_SUFFIX = (
10
+ "raises inside per-item work; name its blast radius by ending the type in "
11
+ f"{BLAST_RADIUS_RUN_SUFFIX} when the whole run stops, or {BLAST_RADIUS_ITEM_SUFFIX} "
12
+ "when this one item stops and the batch carries on. See "
13
+ "rules/failure-blast-radius.md."
14
+ )
@@ -36,6 +36,7 @@ ADVISORY_LINE_THRESHOLD_SOFT = 400
36
36
  ADVISORY_LINE_THRESHOLD_HARD = 1000
37
37
 
38
38
  DENY_REASON_ISSUE_PREVIEW_COUNT = 10
39
+ VIOLATION_SEPARATOR = "; "
39
40
 
40
41
  ALL_BOOLEAN_NAME_PREFIXES: tuple[str, ...] = ("is_", "has_", "should_", "can_", "was_", "did_")
41
42
  UPPER_SNAKE_CONSTANT_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$")
@@ -0,0 +1,75 @@
1
+ """Constants for the changed-surface refactor advisory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ MAXIMUM_REFACTOR_LINE_DELTA: int = 3
6
+ REFACTOR_LINE_DELTA_DIVISOR: int = 2
7
+ CHANGED_SURFACE_MATCH_RATIO: float = 0.5
8
+ ALL_PYTHON_KEYWORDS: frozenset[str] = frozenset(
9
+ {
10
+ "def",
11
+ "class",
12
+ "return",
13
+ "import",
14
+ "from",
15
+ "if",
16
+ "elif",
17
+ "else",
18
+ "for",
19
+ "while",
20
+ "try",
21
+ "except",
22
+ "finally",
23
+ "with",
24
+ "as",
25
+ "yield",
26
+ "raise",
27
+ "pass",
28
+ "break",
29
+ "continue",
30
+ "and",
31
+ "or",
32
+ "not",
33
+ "in",
34
+ "is",
35
+ "lambda",
36
+ "None",
37
+ "True",
38
+ "False",
39
+ "self",
40
+ "cls",
41
+ "async",
42
+ "await",
43
+ "global",
44
+ "nonlocal",
45
+ "assert",
46
+ "del",
47
+ "print",
48
+ "len",
49
+ "range",
50
+ "list",
51
+ "dict",
52
+ "set",
53
+ "str",
54
+ "int",
55
+ "float",
56
+ "bool",
57
+ "type",
58
+ "isinstance",
59
+ "hasattr",
60
+ "getattr",
61
+ "setattr",
62
+ "super",
63
+ "property",
64
+ "staticmethod",
65
+ "classmethod",
66
+ "abstractmethod",
67
+ "Optional",
68
+ "Union",
69
+ "List",
70
+ "Dict",
71
+ "Set",
72
+ "Tuple",
73
+ "Any",
74
+ }
75
+ )
@@ -0,0 +1,21 @@
1
+ """Tests for refactor advisory constants."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ _HOOKS_ROOT = Path(__file__).resolve().parent.parent
7
+ if str(_HOOKS_ROOT) not in sys.path:
8
+ sys.path.insert(0, str(_HOOKS_ROOT))
9
+
10
+ from hooks_constants.refactor_guard_constants import (
11
+ ALL_PYTHON_KEYWORDS,
12
+ CHANGED_SURFACE_MATCH_RATIO,
13
+ MAXIMUM_REFACTOR_LINE_DELTA,
14
+ REFACTOR_LINE_DELTA_DIVISOR,
15
+ )
16
+
17
+
18
+ def test_refactor_threshold_constants_define_a_half_boundary() -> None:
19
+ assert CHANGED_SURFACE_MATCH_RATIO * REFACTOR_LINE_DELTA_DIVISOR == 1
20
+ assert MAXIMUM_REFACTOR_LINE_DELTA > REFACTOR_LINE_DELTA_DIVISOR
21
+ assert "return" in ALL_PYTHON_KEYWORDS
@@ -48,6 +48,60 @@ def test_should_write_record_with_known_payload_fields_to_jsonl_log() -> None:
48
48
  assert "timestamp" in record
49
49
 
50
50
 
51
+ def test_should_exclude_instruction_body_and_unrelated_secret_like_fields(
52
+ tmp_path: Path,
53
+ ) -> None:
54
+ fake_home = tmp_path / "home"
55
+ fake_home.mkdir()
56
+ payload = {
57
+ "file_path": "/tmp/CLAUDE.md",
58
+ "instruction_body": "private instruction text",
59
+ "api_key": "secret-api-key",
60
+ "authorization": "Bearer secret-token",
61
+ "metadata": {
62
+ "instruction_body": "nested private instruction text",
63
+ "api_key": "nested-secret-api-key",
64
+ "authorization": "Nested bearer secret-token",
65
+ "unrelated_secret_like_field": "nested private payload",
66
+ },
67
+ }
68
+
69
+ completed = _run_hook(payload, fake_home)
70
+
71
+ assert completed.returncode == 0, completed.stderr
72
+ log_path = fake_home / ".claude" / "logs" / "instructions_loaded.jsonl"
73
+ serialized_record = log_path.read_text(encoding="utf-8").strip()
74
+ record = json.loads(serialized_record)
75
+ assert set(record) == {
76
+ "timestamp",
77
+ "file_path",
78
+ "load_reason",
79
+ "memory_type",
80
+ "trigger_file_path",
81
+ "parent_file_path",
82
+ "globs",
83
+ "session_id",
84
+ }
85
+ sensitive_field_names = {
86
+ "instruction_body",
87
+ "api_key",
88
+ "authorization",
89
+ "metadata",
90
+ "unrelated_secret_like_field",
91
+ }
92
+ sensitive_field_values = {
93
+ "private instruction text",
94
+ "secret-api-key",
95
+ "Bearer secret-token",
96
+ "nested private instruction text",
97
+ "nested-secret-api-key",
98
+ "Nested bearer secret-token",
99
+ "nested private payload",
100
+ }
101
+ for each_sensitive_text in sensitive_field_names | sensitive_field_values:
102
+ assert each_sensitive_text not in serialized_record
103
+
104
+
51
105
  def test_should_exit_zero_and_record_error_when_stdin_payload_is_invalid_json() -> None:
52
106
  with tempfile.TemporaryDirectory() as fake_home_string:
53
107
  fake_home = Path(fake_home_string)
@@ -0,0 +1,70 @@
1
+ """Behavior tests for plugin data directory cleanup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import stat
7
+ from pathlib import Path
8
+
9
+
10
+ HOOK_DIRECTORY = Path(__file__).parent
11
+ HOOK_SPEC = importlib.util.spec_from_file_location(
12
+ "plugin_data_dir_cleanup",
13
+ HOOK_DIRECTORY / "plugin_data_dir_cleanup.py",
14
+ )
15
+ assert HOOK_SPEC is not None
16
+ assert HOOK_SPEC.loader is not None
17
+ HOOK_MODULE = importlib.util.module_from_spec(HOOK_SPEC)
18
+ HOOK_SPEC.loader.exec_module(HOOK_MODULE)
19
+
20
+
21
+ def _configure_plugin_data_directory(plugin_data_directory: Path) -> None:
22
+ HOOK_MODULE.__dict__["PLUGINS_DATA_DIRECTORY"] = str(plugin_data_directory)
23
+ HOOK_MODULE.__dict__["AFFECTED_PLUGIN_DIRECTORIES"] = ["sample-plugin"]
24
+
25
+
26
+ def test_main_removes_empty_plugin_data_directory(tmp_path: Path) -> None:
27
+ plugin_data_directory = tmp_path / "plugin-data"
28
+ plugin_data_directory.mkdir()
29
+ empty_plugin_directory = plugin_data_directory / "sample-plugin"
30
+ empty_plugin_directory.mkdir()
31
+ _configure_plugin_data_directory(plugin_data_directory)
32
+
33
+ HOOK_MODULE.main()
34
+
35
+ assert not empty_plugin_directory.exists()
36
+
37
+
38
+ def test_main_keeps_plugin_data_directory_with_files(tmp_path: Path) -> None:
39
+ plugin_data_directory = tmp_path / "plugin-data"
40
+ plugin_data_directory.mkdir()
41
+ retained_plugin_directory = plugin_data_directory / "sample-plugin"
42
+ retained_plugin_directory.mkdir()
43
+ (retained_plugin_directory / "state.json").write_text("{}", encoding="utf-8")
44
+ _configure_plugin_data_directory(plugin_data_directory)
45
+
46
+ HOOK_MODULE.main()
47
+
48
+ assert retained_plugin_directory.is_dir()
49
+
50
+
51
+ def test_main_keeps_read_only_plugin_data_directory_with_files(
52
+ tmp_path: Path,
53
+ ) -> None:
54
+ plugin_data_directory = tmp_path / "plugin-data"
55
+ plugin_data_directory.mkdir()
56
+ retained_plugin_directory = plugin_data_directory / "sample-plugin"
57
+ retained_plugin_directory.mkdir()
58
+ state_file = retained_plugin_directory / "state.json"
59
+ state_file.write_text("{}", encoding="utf-8")
60
+ state_file.chmod(stat.S_IREAD)
61
+ _configure_plugin_data_directory(plugin_data_directory)
62
+
63
+ try:
64
+ HOOK_MODULE.main()
65
+
66
+ assert retained_plugin_directory.is_dir()
67
+ assert state_file.is_file()
68
+ assert state_file.read_text(encoding="utf-8") == "{}"
69
+ finally:
70
+ state_file.chmod(stat.S_IREAD | stat.S_IWRITE)
@@ -127,12 +127,25 @@ def test_should_keep_current_session_tracker_on_continuation(
127
127
  redirected_temp_directory: pathlib.Path, continuation_source: str
128
128
  ) -> None:
129
129
  current_file = _seed_edit_file(redirected_temp_directory, "currentsession")
130
- _run_main_with_stdin(
131
- _session_continuation_payload("currentsession", continuation_source)
132
- )
130
+ _run_main_with_stdin(_session_continuation_payload("currentsession", continuation_source))
133
131
  assert current_file.exists()
134
132
 
135
133
 
134
+ def test_resume_preserves_edit_record(
135
+ redirected_temp_directory: pathlib.Path,
136
+ ) -> None:
137
+ current_file = _seed_edit_file(redirected_temp_directory, "currentsession")
138
+ current_file.write_text(
139
+ json.dumps({ALL_EDITED_FILE_PATHS_KEY: ["packages/example.py"]}),
140
+ encoding="utf-8",
141
+ )
142
+ _run_main_with_stdin(_session_continuation_payload("currentsession", "resume"))
143
+
144
+ assert json.loads(current_file.read_text(encoding="utf-8")) == {
145
+ ALL_EDITED_FILE_PATHS_KEY: ["packages/example.py"]
146
+ }
147
+
148
+
136
149
  def test_should_remove_current_session_tracker_on_session_end(
137
150
  redirected_temp_directory: pathlib.Path,
138
151
  ) -> None: