codeplain 0.3.10.dev3__py3-none-any.whl → 0.3.10.dev5__py3-none-any.whl

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.
@@ -54,14 +54,13 @@ class FixConformanceTest(BaseAction):
54
54
  )
55
55
  previous_conformance_tests_issue = previous_action_payload["previous_conformance_tests_issue"]
56
56
 
57
- render_context.conformance_tests_running_context.previous_conformance_tests_issue_old = (
58
- previous_conformance_tests_issue
59
- )
60
- render_context.conformance_tests_running_context.previous_conformance_tests_issue_frid = (
61
- render_context.conformance_tests_running_context.current_testing_frid
62
- )
63
- render_context.conformance_tests_running_context.previous_conformance_tests_issue_module = (
64
- render_context.conformance_tests_running_context.current_testing_module_name
57
+ journal = render_context.memory_manager.journal
58
+ journal.record_result(ctx.current_testing_module_name, ctx.current_testing_frid, passed=False)
59
+ journal.open_attempt(
60
+ ctx.current_testing_module_name,
61
+ ctx.current_testing_frid,
62
+ ctx.fix_attempts,
63
+ previous_conformance_tests_issue,
65
64
  )
66
65
 
67
66
  existing_files, existing_files_content = ImplementationCodeHelpers.fetch_existing_files(
@@ -114,25 +113,28 @@ class FixConformanceTest(BaseAction):
114
113
  style=console.INPUT_STYLE,
115
114
  )
116
115
 
117
- [issue_reason_code, response_files] = render_context.codeplain_api.fix_conformance_tests_issue(
118
- render_context.frid_context.frid,
119
- render_context.conformance_tests_running_context.current_testing_frid,
120
- render_context.plain_source_tree,
121
- render_context.frid_context.linked_resources,
122
- existing_files_content,
123
- memory_files_content,
124
- render_context.module_name,
125
- render_context.conformance_tests_running_context.current_testing_module_name,
126
- render_context.get_required_modules_functionalities(),
127
- previous_frid_code_diff,
128
- existing_conformance_test_files_content,
129
- render_context.conformance_tests_running_context.get_current_acceptance_tests(),
130
- previous_conformance_tests_issue,
131
- render_context.conformance_tests_running_context.fix_attempts,
132
- render_context.conformance_tests_running_context.get_current_conformance_test_folder_name(),
133
- render_context.conformance_tests_running_context.current_testing_frid_high_level_implementation_plan,
134
- render_context.conformance_tests_running_context.conflicting_requirement_count,
135
- run_state=render_context.run_state,
116
+ [issue_reason_code, response_files, fix_attempt_summary, prepared_conformance_tests_issue] = (
117
+ render_context.codeplain_api.fix_conformance_tests_issue(
118
+ render_context.frid_context.frid,
119
+ render_context.conformance_tests_running_context.current_testing_frid,
120
+ render_context.plain_source_tree,
121
+ render_context.frid_context.linked_resources,
122
+ existing_files_content,
123
+ memory_files_content,
124
+ render_context.module_name,
125
+ render_context.conformance_tests_running_context.current_testing_module_name,
126
+ render_context.get_required_modules_functionalities(),
127
+ previous_frid_code_diff,
128
+ existing_conformance_test_files_content,
129
+ render_context.conformance_tests_running_context.get_current_acceptance_tests(),
130
+ previous_conformance_tests_issue,
131
+ render_context.conformance_tests_running_context.fix_attempts,
132
+ render_context.conformance_tests_running_context.get_current_conformance_test_folder_name(),
133
+ render_context.conformance_tests_running_context.current_testing_frid_high_level_implementation_plan,
134
+ render_context.conformance_tests_running_context.conflicting_requirement_count,
135
+ journal.build_digest(ctx.current_testing_module_name, ctx.current_testing_frid),
136
+ run_state=render_context.run_state,
137
+ )
136
138
  )
137
139
  code_diff_files_content = {}
138
140
 
@@ -159,14 +161,21 @@ class FixConformanceTest(BaseAction):
159
161
  existing_conformance_test_files,
160
162
  )
161
163
  code_diff_files_content = diff_utils.get_code_diff(response_files, existing_conformance_test_files_content)
162
- render_context.conformance_tests_running_context.code_diff_files = code_diff_files_content
164
+ journal.record_fix(
165
+ ctx.current_testing_module_name,
166
+ ctx.current_testing_frid,
167
+ fix_attempt_summary,
168
+ list(response_files),
169
+ "CONFORMANCE_TESTS",
170
+ code_diff_files_content,
171
+ prepared_conformance_tests_issue,
172
+ )
163
173
 
164
174
  return self.IMPLEMENTATION_CODE_NOT_UPDATED, None
165
175
  else:
166
176
  if len(response_files) > 0:
167
177
  file_utils.store_response_files(render_context.build_folder, response_files, existing_files)
168
178
  code_diff_files_content = diff_utils.get_code_diff(response_files, existing_files_content)
169
- render_context.conformance_tests_running_context.code_diff_files = code_diff_files_content
170
179
  console.print_files(
171
180
  "Files fixed:",
172
181
  render_context.build_folder,
@@ -180,6 +189,26 @@ class FixConformanceTest(BaseAction):
180
189
  ctx.test_that_triggered_code_change = (ctx.current_testing_module_name, ctx.current_testing_frid)
181
190
  ctx.execution_phase = TestExecutionPhase.RETRYING_AFTER_CODE_CHANGE
182
191
 
192
+ journal.record_fix(
193
+ ctx.current_testing_module_name,
194
+ ctx.current_testing_frid,
195
+ fix_attempt_summary,
196
+ list(response_files),
197
+ "IMPLEMENTATION_CODE",
198
+ code_diff_files_content,
199
+ prepared_conformance_tests_issue,
200
+ )
201
+
183
202
  return self.IMPLEMENTATION_CODE_UPDATED, None
184
203
  else:
204
+ journal.record_fix(
205
+ ctx.current_testing_module_name,
206
+ ctx.current_testing_frid,
207
+ fix_attempt_summary,
208
+ [],
209
+ "IMPLEMENTATION_CODE",
210
+ None,
211
+ prepared_conformance_tests_issue,
212
+ )
213
+
185
214
  return self.IMPLEMENTATION_CODE_NOT_UPDATED, None
@@ -3,6 +3,7 @@ from typing import Any
3
3
  import file_utils
4
4
  import git_utils
5
5
  import plain_spec
6
+ from memory_management import MemoryManager
6
7
  from plain2code_console import console
7
8
  from render_machine.actions.base_action import BaseAction
8
9
  from render_machine.render_context import RenderContext
@@ -50,6 +51,13 @@ class PrepareRepositories(BaseAction):
50
51
  render_context.module_name,
51
52
  render_context.run_state.render_id,
52
53
  )
54
+
55
+ # Like the code repo, the distilled conformance test memories carry over from the
56
+ # previous module so its learnings are available while rendering this one.
57
+ MemoryManager.inherit_memories(
58
+ previous_module.module_memory_folder,
59
+ render_context.plain_module.module_memory_folder,
60
+ )
53
61
  else:
54
62
  console.debug("Initializing git repositories for the render folders.")
55
63
 
@@ -54,18 +54,12 @@ class RunConformanceTests(BaseAction):
54
54
  )
55
55
  render_context.script_execution_history.should_update_script_outputs = True
56
56
 
57
- render_context.memory_manager.create_conformance_tests_memory(
58
- render_context, exit_code, conformance_tests_issue
59
- )
60
-
61
57
  if exit_code == 0:
62
- if (
63
- render_context.conformance_tests_running_context.current_testing_module_name
64
- == render_context.module_name
65
- and render_context.conformance_tests_running_context.current_testing_frid
66
- == render_context.frid_context.frid
67
- ):
68
- render_context.memory_manager.delete_unresolved_memory_files()
58
+ render_context.memory_manager.journal.record_result(
59
+ render_context.conformance_tests_running_context.current_testing_module_name,
60
+ render_context.conformance_tests_running_context.current_testing_frid,
61
+ passed=True,
62
+ )
69
63
  return self.SUCCESSFUL_OUTCOME, None
70
64
 
71
65
  if exit_code in UNRECOVERABLE_ERROR_EXIT_CODES:
@@ -90,10 +90,6 @@ class ConformanceTestsRunningContext:
90
90
  self.regenerating_conformance_tests: bool = False
91
91
 
92
92
  self.current_testing_frid_high_level_implementation_plan: Optional[str] = None
93
- self.previous_conformance_tests_issue_old: Optional[str] = None
94
- self.previous_conformance_tests_issue_frid: Optional[str] = None
95
- self.previous_conformance_tests_issue_module: Optional[str] = None
96
- self.code_diff_files: Optional[dict[str, str]] = None
97
93
 
98
94
  def get_conformance_tests_json(self, module_name: str) -> dict:
99
95
  return self._conformance_tests_json[module_name]
@@ -13,6 +13,7 @@ from render_machine.actions.analyze_specification_ambiguity import AnalyzeSpecif
13
13
  from render_machine.actions.commit_conformance_tests_changes import CommitConformanceTestsChanges
14
14
  from render_machine.actions.commit_implementation_code_changes import CommitImplementationCodeChanges
15
15
  from render_machine.actions.create_dist import CreateDist
16
+ from render_machine.actions.distill_conformance_test_memory import DistillConformanceTestMemory
16
17
  from render_machine.actions.exit_with_error import ExitWithError
17
18
  from render_machine.actions.finish_functional_requirement import FinishFunctionalRequirement
18
19
  from render_machine.actions.fix_conformance_test import FixConformanceTest
@@ -52,6 +53,7 @@ class StateMachineConfig:
52
53
  f"{States.IMPLEMENTING_FRID.value}_{States.PROCESSING_CONFORMANCE_TESTS.value}_{States.CONFORMANCE_TEST_GENERATED.value}": PrepareTestingEnvironment(),
53
54
  f"{States.IMPLEMENTING_FRID.value}_{States.PROCESSING_CONFORMANCE_TESTS.value}_{States.CONFORMANCE_TEST_ENV_PREPARED.value}": RunConformanceTests(),
54
55
  f"{States.IMPLEMENTING_FRID.value}_{States.PROCESSING_CONFORMANCE_TESTS.value}_{States.CONFORMANCE_TEST_FAILED.value}": FixConformanceTest(),
56
+ f"{States.IMPLEMENTING_FRID.value}_{States.PROCESSING_CONFORMANCE_TESTS.value}_{States.POSTPROCESSING_CONFORMANCE_TESTS.value}_{States.CONFORMANCE_TESTS_READY_FOR_MEMORY_DISTILLATION.value}": DistillConformanceTestMemory(),
55
57
  f"{States.IMPLEMENTING_FRID.value}_{States.PROCESSING_CONFORMANCE_TESTS.value}_{States.POSTPROCESSING_CONFORMANCE_TESTS.value}_{States.CONFORMANCE_TESTS_READY_FOR_SUMMARY.value}": SummarizeConformanceTests(),
56
58
  f"{States.IMPLEMENTING_FRID.value}_{States.PROCESSING_CONFORMANCE_TESTS.value}_{States.POSTPROCESSING_CONFORMANCE_TESTS.value}_{States.CONFORMANCE_TESTS_READY_FOR_COMMIT.value}": CommitConformanceTestsChanges(
57
59
  git_utils.CONFORMANCE_TESTS_PASSED_COMMIT_MESSAGE,
@@ -96,6 +98,7 @@ class StateMachineConfig:
96
98
  FixConformanceTest.REGENERATE_CONFORMANCE_TESTS_OUTCOME: triggers.MARK_REGENERATION_OF_CONFORMANCE_TESTS,
97
99
  CommitConformanceTestsChanges.SUCCESSFUL_OUTCOME_IMPLEMENTATION_UPDATED: triggers.MARK_NEXT_CONFORMANCE_TESTS_POSTPROCESSING_STEP,
98
100
  CommitConformanceTestsChanges.SUCCESSFUL_OUTCOME_IMPLEMENTATION_NOT_UPDATED: triggers.PROCEED_FRID_PROCESSING,
101
+ DistillConformanceTestMemory.SUCCESSFUL_OUTCOME: triggers.MARK_NEXT_CONFORMANCE_TESTS_POSTPROCESSING_STEP,
99
102
  SummarizeConformanceTests.SUCCESSFUL_OUTCOME: triggers.MARK_NEXT_CONFORMANCE_TESTS_POSTPROCESSING_STEP,
100
103
  AnalyzeSpecificationAmbiguity.SUCCESSFUL_OUTCOME: triggers.PROCEED_FRID_PROCESSING,
101
104
  }
@@ -120,8 +123,9 @@ class StateMachineConfig:
120
123
  def get_postprocessing_conformance_tests_states(self) -> Dict[str, Any]:
121
124
  return {
122
125
  "name": States.POSTPROCESSING_CONFORMANCE_TESTS.value,
123
- "initial": States.CONFORMANCE_TESTS_READY_FOR_SUMMARY.value,
126
+ "initial": States.CONFORMANCE_TESTS_READY_FOR_MEMORY_DISTILLATION.value,
124
127
  "children": [
128
+ States.CONFORMANCE_TESTS_READY_FOR_MEMORY_DISTILLATION.value,
125
129
  States.CONFORMANCE_TESTS_READY_FOR_SUMMARY.value,
126
130
  States.CONFORMANCE_TESTS_READY_FOR_COMMIT.value,
127
131
  States.CONFORMANCE_TESTS_READY_FOR_AMBIGUITY_ANALYSIS.value,
@@ -278,6 +282,11 @@ class StateMachineConfig:
278
282
  "trigger": triggers.MARK_ALL_CONFORMANCE_TESTS_PASSED,
279
283
  "dest": f"{States.IMPLEMENTING_FRID.value}_{States.PROCESSING_CONFORMANCE_TESTS.value}_{States.POSTPROCESSING_CONFORMANCE_TESTS.value}",
280
284
  },
285
+ {
286
+ "source": f"{States.IMPLEMENTING_FRID.value}_{States.PROCESSING_CONFORMANCE_TESTS.value}_{States.POSTPROCESSING_CONFORMANCE_TESTS.value}_{States.CONFORMANCE_TESTS_READY_FOR_MEMORY_DISTILLATION.value}",
287
+ "trigger": triggers.MARK_NEXT_CONFORMANCE_TESTS_POSTPROCESSING_STEP,
288
+ "dest": f"{States.IMPLEMENTING_FRID.value}_{States.PROCESSING_CONFORMANCE_TESTS.value}_{States.POSTPROCESSING_CONFORMANCE_TESTS.value}_{States.CONFORMANCE_TESTS_READY_FOR_SUMMARY.value}",
289
+ },
281
290
  {
282
291
  "source": f"{States.IMPLEMENTING_FRID.value}_{States.PROCESSING_CONFORMANCE_TESTS.value}_{States.POSTPROCESSING_CONFORMANCE_TESTS.value}_{States.CONFORMANCE_TESTS_READY_FOR_SUMMARY.value}",
283
292
  "trigger": triggers.MARK_NEXT_CONFORMANCE_TESTS_POSTPROCESSING_STEP,
render_machine/states.py CHANGED
@@ -44,6 +44,7 @@ class States(Enum):
44
44
 
45
45
  # Postprocessing conformance tests states
46
46
  POSTPROCESSING_CONFORMANCE_TESTS = "postprocessingConformanceTests"
47
+ CONFORMANCE_TESTS_READY_FOR_MEMORY_DISTILLATION = "conformanceTestsReadyForMemoryDistillation"
47
48
  CONFORMANCE_TESTS_READY_FOR_SUMMARY = "conformanceTestsReadyForSummary"
48
49
  CONFORMANCE_TESTS_READY_FOR_COMMIT = "conformanceTestsReadyForCommit"
49
50
  CONFORMANCE_TESTS_READY_FOR_AMBIGUITY_ANALYSIS = "conformanceTestsReadyForAmbiguityAnalysis"
@@ -0,0 +1,259 @@
1
+ """Tests for the deterministic journal of conformance tests fix attempts."""
2
+
3
+ import json
4
+ import os
5
+ import tempfile
6
+
7
+ import pytest
8
+
9
+ from conformance_fix_journal import MAX_ISSUE_EXCERPT_CHARS, FixAttemptJournal, normalized_diff_hash
10
+
11
+
12
+ @pytest.fixture
13
+ def memory_folder():
14
+ with tempfile.TemporaryDirectory() as folder:
15
+ yield folder
16
+
17
+
18
+ @pytest.fixture
19
+ def journal(memory_folder):
20
+ return FixAttemptJournal(memory_folder)
21
+
22
+
23
+ def record_full_attempt(
24
+ journal,
25
+ module="mod",
26
+ frid="1",
27
+ attempt=1,
28
+ diff=None,
29
+ summary=None,
30
+ files=None,
31
+ prepared_issue="prepared issue",
32
+ ):
33
+ journal.open_attempt(module, frid, attempt, "tests failed")
34
+ journal.record_fix(
35
+ module,
36
+ frid,
37
+ summary or {"hypothesis": f"hypothesis {attempt}", "approach": f"approach {attempt}"},
38
+ files if files is not None else ["src/app.py"],
39
+ "IMPLEMENTATION_CODE",
40
+ diff if diff is not None else {"src/app.py": f"diff {attempt}"},
41
+ prepared_issue,
42
+ )
43
+
44
+
45
+ class TestNormalizedDiffHash:
46
+ def test_identical_diffs_hash_the_same(self):
47
+ diff = {"a.py": "+ line one\n+ line two"}
48
+
49
+ assert normalized_diff_hash(diff) == normalized_diff_hash(dict(diff))
50
+
51
+ def test_whitespace_only_differences_are_ignored(self):
52
+ assert normalized_diff_hash({"a.py": "+ line one\n\n + line two "}) == normalized_diff_hash(
53
+ {"a.py": "+ line one\n+ line two"}
54
+ )
55
+
56
+ def test_different_diffs_hash_differently(self):
57
+ assert normalized_diff_hash({"a.py": "+ line one"}) != normalized_diff_hash({"a.py": "+ line two"})
58
+
59
+ def test_empty_diff_has_no_hash(self):
60
+ assert normalized_diff_hash({}) is None
61
+ assert normalized_diff_hash(None) is None
62
+
63
+
64
+ class TestJournalEntries:
65
+ def test_attempt_lifecycle_is_recorded(self, journal):
66
+ journal.open_attempt("mod", "1", 1, "raw failure output")
67
+ journal.record_fix(
68
+ "mod",
69
+ "1",
70
+ {"hypothesis": "the root cause", "approach": "the fix"},
71
+ ["src/app.py"],
72
+ "IMPLEMENTATION_CODE",
73
+ {"src/app.py": "diff"},
74
+ "the issue as the fixer saw it",
75
+ )
76
+ journal.record_result("mod", "1", passed=False)
77
+
78
+ [entry] = journal._read_entries("mod", "1")
79
+ assert entry["attempt"] == 1
80
+ assert entry["issue_before_raw"] == "raw failure output"
81
+ assert entry["issue_before"] == "the issue as the fixer saw it"
82
+ assert entry["hypothesis"] == "the root cause"
83
+ assert entry["approach"] == "the fix"
84
+ assert entry["files_changed"] == ["src/app.py"]
85
+ assert entry["target"] == "IMPLEMENTATION_CODE"
86
+ assert entry["result"] == "conformance tests still failed"
87
+
88
+ def test_raw_issue_is_stored_untruncated(self, journal):
89
+ long_issue = "x" * (MAX_ISSUE_EXCERPT_CHARS * 3) + "THE END"
90
+ journal.open_attempt("mod", "1", 1, long_issue)
91
+
92
+ [entry] = journal._read_entries("mod", "1")
93
+ assert entry["issue_before_raw"] == long_issue
94
+
95
+ def test_a_passing_result_is_recorded(self, journal):
96
+ record_full_attempt(journal)
97
+ journal.record_result("mod", "1", passed=True)
98
+
99
+ [entry] = journal._read_entries("mod", "1")
100
+ assert entry["result"] == "conformance tests passed"
101
+
102
+ def test_record_result_without_a_pending_attempt_does_nothing(self, journal):
103
+ journal.record_result("mod", "1", passed=False)
104
+
105
+ assert journal._read_entries("mod", "1") == []
106
+
107
+ def test_record_result_does_not_overwrite_a_completed_attempt(self, journal):
108
+ record_full_attempt(journal)
109
+ journal.record_result("mod", "1", passed=False)
110
+ journal.record_result("mod", "1", passed=True)
111
+
112
+ [entry] = journal._read_entries("mod", "1")
113
+ assert entry["result"] == "conformance tests still failed"
114
+
115
+ def test_missing_summary_and_prepared_issue_leave_the_entry_without_them(self, journal):
116
+ journal.open_attempt("mod", "1", 1, "failure")
117
+ journal.record_fix("mod", "1", None, ["src/app.py"], "IMPLEMENTATION_CODE", {"src/app.py": "diff"}, None)
118
+
119
+ [entry] = journal._read_entries("mod", "1")
120
+ assert "hypothesis" not in entry
121
+ assert "approach" not in entry
122
+ assert "issue_before" not in entry
123
+
124
+
125
+ class TestDuplicateDetection:
126
+ def test_a_repeated_diff_is_marked_as_a_duplicate(self, journal):
127
+ diff = {"src/app.py": "+ same change"}
128
+ record_full_attempt(journal, attempt=1, diff=diff)
129
+ journal.record_result("mod", "1", passed=False)
130
+ record_full_attempt(journal, attempt=2, diff={"src/app.py": "+ same change "})
131
+
132
+ entries = journal._read_entries("mod", "1")
133
+ assert "duplicate_of" not in entries[0]
134
+ assert entries[1]["duplicate_of"] == 1
135
+
136
+ def test_a_different_diff_is_not_marked(self, journal):
137
+ record_full_attempt(journal, attempt=1, diff={"src/app.py": "+ change one"})
138
+ journal.record_result("mod", "1", passed=False)
139
+ record_full_attempt(journal, attempt=2, diff={"src/app.py": "+ change two"})
140
+
141
+ entries = journal._read_entries("mod", "1")
142
+ assert "duplicate_of" not in entries[1]
143
+
144
+ def test_attempts_without_a_diff_are_never_duplicates(self, journal):
145
+ record_full_attempt(journal, attempt=1, diff={}, files=[])
146
+ journal.record_result("mod", "1", passed=False)
147
+ record_full_attempt(journal, attempt=2, diff={}, files=[])
148
+
149
+ entries = journal._read_entries("mod", "1")
150
+ assert "duplicate_of" not in entries[1]
151
+
152
+
153
+ class TestDigest:
154
+ def test_digest_carries_the_compact_history_with_prepared_issues(self, journal):
155
+ record_full_attempt(journal, attempt=1, prepared_issue="issue as fixer saw it 1")
156
+ journal.record_result("mod", "1", passed=False)
157
+ record_full_attempt(journal, attempt=2, prepared_issue="issue as fixer saw it 2")
158
+
159
+ digest = journal.build_digest("mod", "1")
160
+
161
+ assert [entry["attempt"] for entry in digest] == [1, 2]
162
+ assert digest[0]["result"] == "conformance tests still failed"
163
+ assert digest[0]["hypothesis"] == "hypothesis 1"
164
+ assert digest[0]["issue"] == "issue as fixer saw it 1"
165
+ assert digest[1]["approach"] == "approach 2"
166
+ assert digest[1]["issue"] == "issue as fixer saw it 2"
167
+ for entry in digest:
168
+ assert "issue_before_raw" not in entry
169
+ assert "issue_before" not in entry
170
+ assert "diff_hash" not in entry
171
+
172
+ def test_digest_issue_is_capped(self, journal):
173
+ long_prepared = "x" * (MAX_ISSUE_EXCERPT_CHARS + 100) + "THE END"
174
+ record_full_attempt(journal, prepared_issue=long_prepared)
175
+
176
+ [entry] = journal.build_digest("mod", "1")
177
+ assert entry["issue"].endswith("THE END")
178
+ assert len(entry["issue"]) < len(long_prepared)
179
+
180
+ def test_digest_of_an_attempt_without_a_prepared_issue_has_no_issue(self, journal):
181
+ record_full_attempt(journal, prepared_issue=None)
182
+
183
+ [entry] = journal.build_digest("mod", "1")
184
+ assert "issue" not in entry
185
+
186
+ def test_the_open_attempt_is_excluded_from_the_digest(self, journal):
187
+ """The entry opened for the attempt about to be made carries nothing yet - the fixer must
188
+ not see an empty attempt in its history."""
189
+ record_full_attempt(journal, attempt=1)
190
+ journal.record_result("mod", "1", passed=False)
191
+ journal.open_attempt("mod", "1", 2, "current failure output")
192
+
193
+ digest = journal.build_digest("mod", "1")
194
+
195
+ assert [entry["attempt"] for entry in digest] == [1]
196
+
197
+ def test_a_dangling_attempt_without_a_fix_is_excluded_from_the_digest(self, journal):
198
+ """An interrupted render can leave an opened entry that later gets a result but never a fix."""
199
+ journal.open_attempt("mod", "1", 1, "failure output")
200
+ journal.record_result("mod", "1", passed=False)
201
+
202
+ assert journal.build_digest("mod", "1") == []
203
+
204
+ def test_digest_of_an_unknown_frid_is_empty(self, journal):
205
+ assert journal.build_digest("mod", "99") == []
206
+
207
+
208
+ class TestCollectAndClear:
209
+ def test_collect_all_groups_by_module_and_frid(self, journal):
210
+ record_full_attempt(journal, module="mod", frid="1")
211
+ record_full_attempt(journal, module="other_module", frid="2.1")
212
+
213
+ journals = journal.collect_all()
214
+
215
+ assert [(j["module"], j["frid"]) for j in journals] == [("mod", "1"), ("other_module", "2.1")]
216
+ assert journals[0]["initial_issue"] == "prepared issue"
217
+ assert journals[0]["attempts"][0]["hypothesis"] == "hypothesis 1"
218
+
219
+ def test_initial_issue_falls_back_to_the_trimmed_raw_output(self, journal):
220
+ long_raw = "x" * (MAX_ISSUE_EXCERPT_CHARS + 100) + "THE END"
221
+ journal.open_attempt("mod", "1", 1, long_raw)
222
+ journal.record_fix("mod", "1", None, [], "IMPLEMENTATION_CODE", None, None)
223
+
224
+ [collected] = journal.collect_all()
225
+ assert collected["initial_issue"].endswith("THE END")
226
+ assert len(collected["initial_issue"]) < len(long_raw)
227
+
228
+ def test_module_names_containing_double_underscores_are_parsed(self, journal):
229
+ record_full_attempt(journal, module="my__module", frid="3")
230
+
231
+ [collected] = journal.collect_all()
232
+ assert collected["module"] == "my__module"
233
+ assert collected["frid"] == "3"
234
+
235
+ def test_collect_all_without_journals_is_empty(self, journal):
236
+ assert journal.collect_all() == []
237
+
238
+ def test_collect_all_skips_a_journal_holding_only_an_open_attempt(self, journal):
239
+ journal.open_attempt("mod", "1", 1, "failure output")
240
+
241
+ assert journal.collect_all() == []
242
+
243
+ def test_clear_all_removes_every_journal(self, journal):
244
+ record_full_attempt(journal, module="mod", frid="1")
245
+ record_full_attempt(journal, module="mod", frid="2")
246
+
247
+ journal.clear_all()
248
+
249
+ assert journal.collect_all() == []
250
+ assert not os.path.exists(journal.journal_folder)
251
+
252
+ def test_malformed_lines_are_skipped(self, journal, memory_folder):
253
+ record_full_attempt(journal)
254
+ with open(journal._journal_path("mod", "1"), "a") as journal_file:
255
+ journal_file.write("not json\n")
256
+ journal_file.write(json.dumps({"attempt": 2, "issue_before_raw": "x"}) + "\n")
257
+
258
+ entries = journal._read_entries("mod", "1")
259
+ assert [entry["attempt"] for entry in entries] == [1, 2]
@@ -0,0 +1,145 @@
1
+ """Tests for the DistillConformanceTestMemory postprocessing action."""
2
+
3
+ import json
4
+ import os
5
+ import tempfile
6
+ from types import SimpleNamespace
7
+
8
+ import pytest
9
+
10
+ from memory_management import CONFORMANCE_TEST_MEMORY_SUBFOLDER, MemoryManager
11
+ from render_machine.actions.distill_conformance_test_memory import DistillConformanceTestMemory
12
+
13
+
14
+ class FakeCodeplainAPI:
15
+ def __init__(self, response_files=None, error=None):
16
+ self.response_files = response_files or {}
17
+ self.error = error
18
+ self.calls = []
19
+
20
+ def distill_conformance_test_memory(self, **kwargs):
21
+ self.calls.append(kwargs)
22
+ if self.error:
23
+ raise self.error
24
+ return self.response_files
25
+
26
+
27
+ @pytest.fixture
28
+ def build_folder():
29
+ with tempfile.TemporaryDirectory() as folder:
30
+ yield folder
31
+
32
+
33
+ @pytest.fixture
34
+ def memory_folder():
35
+ with tempfile.TemporaryDirectory() as folder:
36
+ yield folder
37
+
38
+
39
+ def make_render_context(api, memory_folder, build_folder):
40
+ memory_manager = MemoryManager(api, memory_folder)
41
+ return SimpleNamespace(
42
+ memory_manager=memory_manager,
43
+ codeplain_api=api,
44
+ build_folder=build_folder,
45
+ plain_source_tree={},
46
+ module_name="mod",
47
+ frid_context=SimpleNamespace(frid="1", linked_resources={}),
48
+ get_required_modules_functionalities=lambda: {},
49
+ run_state=SimpleNamespace(render_id="test-render-id"),
50
+ )
51
+
52
+
53
+ def seed_journal(memory_manager, module="mod", frid="1"):
54
+ journal = memory_manager.journal
55
+ journal.open_attempt(module, frid, 1, "tests failed")
56
+ journal.record_fix(
57
+ module,
58
+ frid,
59
+ {"hypothesis": "root cause", "approach": "the fix"},
60
+ ["src/app.py"],
61
+ "IMPLEMENTATION_CODE",
62
+ {"src/app.py": "diff"},
63
+ "the issue as the fixer saw it",
64
+ )
65
+ journal.record_result(module, frid, passed=True)
66
+
67
+
68
+ def test_no_journals_means_no_api_call(memory_folder, build_folder):
69
+ api = FakeCodeplainAPI()
70
+ render_context = make_render_context(api, memory_folder, build_folder)
71
+
72
+ outcome, payload = DistillConformanceTestMemory().execute(render_context, None)
73
+
74
+ assert outcome == DistillConformanceTestMemory.SUCCESSFUL_OUTCOME
75
+ assert payload is None
76
+ assert api.calls == []
77
+
78
+
79
+ def test_journals_are_distilled_stored_and_cleared(memory_folder, build_folder):
80
+ memory_content = json.dumps({"memory_id": "learning-1", "key_learnings": "start the server first"})
81
+ api = FakeCodeplainAPI(response_files={"learning-1.json": memory_content})
82
+ render_context = make_render_context(api, memory_folder, build_folder)
83
+ seed_journal(render_context.memory_manager)
84
+
85
+ outcome, _ = DistillConformanceTestMemory().execute(render_context, None)
86
+
87
+ assert outcome == DistillConformanceTestMemory.SUCCESSFUL_OUTCOME
88
+ [call] = api.calls
89
+ assert call["frid"] == "1"
90
+ assert call["module_name"] == "mod"
91
+ assert call["fix_journals"][0]["module"] == "mod"
92
+ assert call["fix_journals"][0]["attempts"][0]["hypothesis"] == "root cause"
93
+
94
+ memory_file = os.path.join(memory_folder, CONFORMANCE_TEST_MEMORY_SUBFOLDER, "learning-1.json")
95
+ assert os.path.isfile(memory_file)
96
+ assert render_context.memory_manager.journal.collect_all() == []
97
+
98
+
99
+ def test_a_delete_response_removes_an_existing_memory(memory_folder, build_folder):
100
+ memory_path = os.path.join(memory_folder, CONFORMANCE_TEST_MEMORY_SUBFOLDER)
101
+ os.makedirs(memory_path)
102
+ with open(os.path.join(memory_path, "stale.json"), "w") as memory_file:
103
+ memory_file.write(json.dumps({"memory_id": "stale"}))
104
+
105
+ api = FakeCodeplainAPI(response_files={"stale.json": None})
106
+ render_context = make_render_context(api, memory_folder, build_folder)
107
+ seed_journal(render_context.memory_manager)
108
+
109
+ DistillConformanceTestMemory().execute(render_context, None)
110
+
111
+ assert not os.path.exists(os.path.join(memory_path, "stale.json"))
112
+
113
+
114
+ def test_inherit_memories_copies_distilled_memories_but_not_journals(build_folder):
115
+ source_folder = os.path.join(build_folder, "previous_module", ".memory")
116
+ destination_folder = os.path.join(build_folder, "next_module", ".memory")
117
+ source_manager = MemoryManager(FakeCodeplainAPI(), source_folder)
118
+ seed_journal(source_manager)
119
+ source_manager.store_memory_files({"learning.json": json.dumps({"memory_id": "learning"})})
120
+
121
+ MemoryManager.inherit_memories(source_folder, destination_folder)
122
+
123
+ memory_files, _ = MemoryManager.fetch_memory_files(destination_folder)
124
+ assert memory_files == ["learning.json"]
125
+ assert MemoryManager(FakeCodeplainAPI(), destination_folder).journal.collect_all() == []
126
+
127
+
128
+ def test_inherit_memories_from_a_module_without_memories_is_a_no_op(build_folder):
129
+ missing_source = os.path.join(build_folder, "does_not_exist", ".memory")
130
+ destination_folder = os.path.join(build_folder, "next_module", ".memory")
131
+
132
+ MemoryManager.inherit_memories(missing_source, destination_folder)
133
+
134
+ assert not os.path.exists(destination_folder)
135
+
136
+
137
+ def test_an_api_failure_keeps_the_journals_and_does_not_raise(memory_folder, build_folder):
138
+ api = FakeCodeplainAPI(error=RuntimeError("api down"))
139
+ render_context = make_render_context(api, memory_folder, build_folder)
140
+ seed_journal(render_context.memory_manager)
141
+
142
+ outcome, _ = DistillConformanceTestMemory().execute(render_context, None)
143
+
144
+ assert outcome == DistillConformanceTestMemory.SUCCESSFUL_OUTCOME
145
+ assert len(render_context.memory_manager.journal.collect_all()) == 1