codeplain 0.3.10.dev3__py3-none-any.whl → 0.3.10.dev4__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.
- {codeplain-0.3.10.dev3.dist-info → codeplain-0.3.10.dev4.dist-info}/METADATA +1 -1
- {codeplain-0.3.10.dev3.dist-info → codeplain-0.3.10.dev4.dist-info}/RECORD +19 -15
- codeplain_REST_api.py +6 -14
- conformance_fix_journal.py +207 -0
- memory_management.py +24 -104
- render_machine/actions/distill_conformance_test_memory.py +56 -0
- render_machine/actions/fix_conformance_test.py +58 -29
- render_machine/actions/prepare_repositories.py +8 -0
- render_machine/actions/run_conformance_tests.py +5 -11
- render_machine/render_types.py +0 -4
- render_machine/state_machine_config.py +10 -1
- render_machine/states.py +1 -0
- tests/test_conformance_fix_journal.py +259 -0
- tests/test_distill_conformance_test_memory.py +145 -0
- tests/test_prepare_repositories_layout.py +38 -0
- tui/state_handlers.py +7 -1
- {codeplain-0.3.10.dev3.dist-info → codeplain-0.3.10.dev4.dist-info}/WHEEL +0 -0
- {codeplain-0.3.10.dev3.dist-info → codeplain-0.3.10.dev4.dist-info}/entry_points.txt +0 -0
- {codeplain-0.3.10.dev3.dist-info → codeplain-0.3.10.dev4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
change_detection.py,sha256=CazORZrezeZSw2I6VBvieN-c2SEjieZPo6pNrKwmMVA,6915
|
|
2
|
-
codeplain_REST_api.py,sha256=
|
|
2
|
+
codeplain_REST_api.py,sha256=Iu3htCE05k-8BOA6I2acz4Ezr74cHQIXmhBIsgnX4Ng,21232
|
|
3
3
|
concept_utils.py,sha256=TpyiQ4DfMtO231j-lPj65i0suRTPr8t9j3QNf9KiM4A,8080
|
|
4
|
+
conformance_fix_journal.py,sha256=7wL3UOc3BWaUo5MCmiFbb3kIwOQ6iEpdQSyEmedo8Vk,9488
|
|
4
5
|
diff_utils.py,sha256=AjiQlqo5pRos_8hVXZo5yBurl5BzSrTMGrQv4dCtRCg,1198
|
|
5
6
|
event_bus.py,sha256=g5XLwRfjrvTIcB6urjREuCJzZj5TA93hy8lKXLGXuCE,650
|
|
6
7
|
file_utils.py,sha256=MNZ5li1ofIMOYB6FCrMa_UkX6nkGCgSZuaqzhqVc1Ok,11682
|
|
7
8
|
git_utils.py,sha256=gCUA6_9zg19f6k_0aVcRnIVlAJSBD0Vt6takx-0pKME,16547
|
|
8
|
-
memory_management.py,sha256=
|
|
9
|
+
memory_management.py,sha256=CwIOQqF7Xo1sNfvP69zQRK_CpjinQD9dbolhRPl6lTk,2446
|
|
9
10
|
metadata_utils.py,sha256=YO17o7IAma5TT1ZN0pTwrLVr_KxFQ8APv02KqKPpI5k,1641
|
|
10
11
|
module_renderer.py,sha256=IJBle8mbBARn_xrekjSpsVID7719yy2rNPkM15-ONoI,6982
|
|
11
12
|
partial_rendering.py,sha256=uzZc2j4d3pAXzfiHw_y1TK4yLRsLOb1Mm21570AGxJ8,13837
|
|
@@ -39,26 +40,27 @@ render_machine/code_renderer.py,sha256=-vp_ltU7e3ssX9hbS8IY4ETSp-39JAuoJ3ZltKpdt
|
|
|
39
40
|
render_machine/conformance_tests.py,sha256=PNTCnJNd_xNIhpTPPS8vlx8qws3cTgQSTLdADRab2YM,9617
|
|
40
41
|
render_machine/implementation_code_helpers.py,sha256=Fxm-IInoZjAIZn41_-WXWj7VIYczpW4yH44Gt8CYMAQ,2378
|
|
41
42
|
render_machine/render_context.py,sha256=iaJWI_EMSn51LNF_b7KRvlir46ZgUkjbaEiVgSvMTCI,26314
|
|
42
|
-
render_machine/render_types.py,sha256=
|
|
43
|
+
render_machine/render_types.py,sha256=IY16mGnt5uoZlLKy16Wmpq0_M5PAitVDvt2khnDhKU0,8479
|
|
43
44
|
render_machine/render_utils.py,sha256=yA3X15E_Lebk_-MxkSz9rM_qy-Pes8EQ9Ye-smkHCDw,9320
|
|
44
|
-
render_machine/state_machine_config.py,sha256=
|
|
45
|
-
render_machine/states.py,sha256=
|
|
45
|
+
render_machine/state_machine_config.py,sha256=0pL4PyrZ5F2UumvHLMRfhyh64M4pDvf-9zEs-5OAEU8,27866
|
|
46
|
+
render_machine/states.py,sha256=Lu-7upbs9_bjJsy0P738qx5IiN0DzVcFdhF2-m6YjnQ,2067
|
|
46
47
|
render_machine/triggers.py,sha256=VekNhHGR-u5JXIboqc_Eyqf73LNH7wezLK7r3WWtodk,1269
|
|
47
48
|
render_machine/actions/analyze_specification_ambiguity.py,sha256=SykZAozGvr1jySxbpKCgt11nMpZhOK0sN-J66M42T5Y,2847
|
|
48
49
|
render_machine/actions/base_action.py,sha256=vVzMp-LanrZyq-jamHyWaQxpRl-pLqtTdtiEzUBhfyE,534
|
|
49
50
|
render_machine/actions/commit_conformance_tests_changes.py,sha256=08yxCPCpf5TlrzLPsEh5SJXVVvnLfwdNKSr7BlzlYxo,2589
|
|
50
51
|
render_machine/actions/commit_implementation_code_changes.py,sha256=yi51oAArLCgWcmFbynes-BK2IGnZwA7wygrTJGoL8dY,826
|
|
51
52
|
render_machine/actions/create_dist.py,sha256=ykER7i2OvWAXjrlAZnkQHpiNl00vSMF8WDxNhlWKfsA,1088
|
|
53
|
+
render_machine/actions/distill_conformance_test_memory.py,sha256=ENjfhTWqRLePHeRF4t8zNRhWKDV2rU4pLYuU_CZhhmo,2639
|
|
52
54
|
render_machine/actions/exit_with_error.py,sha256=W6TMULHJcHEaXE4WUnlELEu0_l7IZy9HWaPkXyqg7ck,1279
|
|
53
55
|
render_machine/actions/finish_functional_requirement.py,sha256=usQgPSg6PHpBOR1RhK7lPz9zjY-2VJJDqi3T0nlqdLw,841
|
|
54
|
-
render_machine/actions/fix_conformance_test.py,sha256=
|
|
56
|
+
render_machine/actions/fix_conformance_test.py,sha256=mHk-UJNEZ7Y65YLLUBljJ3Z5zBVA4jIYRSJ0VCd_LOw,11012
|
|
55
57
|
render_machine/actions/fix_unit_tests.py,sha256=hvwvCJDk_-8fHMpkNFyeBSG1IshIcufDpymF00ulPXY,2301
|
|
56
|
-
render_machine/actions/prepare_repositories.py,sha256=
|
|
58
|
+
render_machine/actions/prepare_repositories.py,sha256=HyZ1R6E3H5V-G4WDSGfhxl_arskmW9FcaxNdQr6JjVs,3867
|
|
57
59
|
render_machine/actions/prepare_testing_environment.py,sha256=kIsXgV6-jOlqp-04cimPgTQoHPrVIu0zW-H_Jj-jowE,2106
|
|
58
60
|
render_machine/actions/refactor_code.py,sha256=gyPsnqgHseL2bkENu4WeGCRyV0R18_GH3JOrPdoxKvg,2609
|
|
59
61
|
render_machine/actions/render_conformance_tests.py,sha256=5gOQn-G1T9UWEI8sDfALcVdArk0ln9DgUprOPjccS-U,9268
|
|
60
62
|
render_machine/actions/render_functional_requirement.py,sha256=53KzMVlcefoag1x73roCEQvQ7FbmH8FzhXMQau-s8tg,4655
|
|
61
|
-
render_machine/actions/run_conformance_tests.py,sha256=
|
|
63
|
+
render_machine/actions/run_conformance_tests.py,sha256=yUG6buqp75EOmJYxdM6MmCNF5_G4TUxA-QSTKx7EzxA,3760
|
|
62
64
|
render_machine/actions/run_unit_tests.py,sha256=A4WXtSzpCLDHYR8fQAbfxUf_E19GYflZSHZaSreoFPk,2101
|
|
63
65
|
render_machine/actions/summarize_conformance_tests.py,sha256=Vy1kRK8vyazK_BbsKuz1MLDN_TQWVxxzkwEEm-50byc,1639
|
|
64
66
|
standard_template_library/golang-console-app-template.plain,sha256=JOusEjCj7jagnXUzaI-SdIr3hj1wTo2uShK017aNtVM,949
|
|
@@ -70,7 +72,9 @@ tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
|
|
|
70
72
|
tests/test_arg_provenance.py,sha256=d_J7aCeHMXBpctBWTt60WU-I8pEKN7XRXRTJZZGCdo0,3441
|
|
71
73
|
tests/test_change_detection.py,sha256=jH8SEhndX400DNqIKmcYLzxHqq9WFdgmuvY3XPq-ZFc,13534
|
|
72
74
|
tests/test_cli_output.py,sha256=ZV2F1NNW4EiZvMNkhF9XUZH6bFJbddfkZof5qAFxe3Y,18885
|
|
75
|
+
tests/test_conformance_fix_journal.py,sha256=olTeAuxXgFwQ8LSQyt0FqtBy9_XY_gK-Ei_pUuJ0C3M,10389
|
|
73
76
|
tests/test_console_log_styles.py,sha256=63DQWiwWNhe_rMP70bW9DAB6-EPaqU3Wpwyh8eR09pc,4080
|
|
77
|
+
tests/test_distill_conformance_test_memory.py,sha256=Tftw9l-gLVwIU6QttY03h2VedvuFxpIsCmZIzvWfxMA,5481
|
|
74
78
|
tests/test_file_utils.py,sha256=R_ZIhrTtwlhHxgmbBtA8PFEU5-QB4Fi5skJ7PS2KzdU,3614
|
|
75
79
|
tests/test_folder_path_resolution.py,sha256=K9v684xkPE0HOAsovg8uDixpZmaIk7ho-Wd3rTjKLF0,6560
|
|
76
80
|
tests/test_git_utils.py,sha256=h3rWhhs4XVRYqzVzdmj51siF_AYD-GkbZiefK2iGfWo,15614
|
|
@@ -87,7 +91,7 @@ tests/test_plain_modules.py,sha256=Fn3FeH-M4exnyFJDZzc9YWaLEfBmiqPH5TpiBT9jgAs,3
|
|
|
87
91
|
tests/test_plainfile.py,sha256=wZ_PN4GULPWIpl_AK_r_6jkgtX0Ch8EHmN5IhX7huOE,11027
|
|
88
92
|
tests/test_plainfileparser.py,sha256=l8GfXkVaSzgH74fX45Wp5jX0q3QlO6t_TgDG72dc28o,23867
|
|
89
93
|
tests/test_plainspec.py,sha256=ng55E8ajrH2-gPC1p_QDIe_zXplPvoqHJ-d57UWKFTw,3577
|
|
90
|
-
tests/test_prepare_repositories_layout.py,sha256=
|
|
94
|
+
tests/test_prepare_repositories_layout.py,sha256=PhJQLnHfjfjo6CTWoicyEKgL0_GtjwB2OKYAWSBVHtk,14638
|
|
91
95
|
tests/test_requires.py,sha256=6tnHYaKudxN-RiQjSFoi9nHe_x0ze3QF-f-j67zAE74,1250
|
|
92
96
|
tests/test_resolve_config_file.py,sha256=b87tDOvyzbsnE-QRWbZ97ou7NuyaAgskv-dbYBum31c,2830
|
|
93
97
|
tests/test_script_path_resolution.py,sha256=tiEqMevQRXr9YpfHN0xldOOQpLAUgnSqTzeZWkKHaUY,4156
|
|
@@ -104,11 +108,11 @@ tui/models.py,sha256=KMjlWubmPD41GA-BjgiqkaV9v7tLJ0fuWKBDPmZj4JA,1493
|
|
|
104
108
|
tui/plain2code_tui.py,sha256=8F7ZYRpmTiWRdTeQ63LPf2LGrreYjIYXvf_9XNSPyHs,17104
|
|
105
109
|
tui/plain_module_render_choice_tui.py,sha256=XwBMMzyDat9FYqMkKdIV7kmIevddfwGA5Qk9kgy5sYY,12112
|
|
106
110
|
tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
|
|
107
|
-
tui/state_handlers.py,sha256=
|
|
111
|
+
tui/state_handlers.py,sha256=zbUS_D9eU8qIwVpaxPEp9zL9bpg8gXsWNoUaqg9TLrQ,16664
|
|
108
112
|
tui/styles.css,sha256=kiC7Og_2G1E7owoiZgV95-3_dSeQnsNj3qzaY04xkDg,6727
|
|
109
113
|
tui/widget_helpers.py,sha256=ToO9MV2bF2Rp-XMNcjIYclnB_qmgO5sLw4UtQ2CyTMg,6652
|
|
110
|
-
codeplain-0.3.10.
|
|
111
|
-
codeplain-0.3.10.
|
|
112
|
-
codeplain-0.3.10.
|
|
113
|
-
codeplain-0.3.10.
|
|
114
|
-
codeplain-0.3.10.
|
|
114
|
+
codeplain-0.3.10.dev4.dist-info/METADATA,sha256=AHNwzkViIw83FQTt9C5UKXgVVALnEwJ3li-gXTzaUxU,7256
|
|
115
|
+
codeplain-0.3.10.dev4.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
116
|
+
codeplain-0.3.10.dev4.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
|
|
117
|
+
codeplain-0.3.10.dev4.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
|
|
118
|
+
codeplain-0.3.10.dev4.dist-info/RECORD,,
|
codeplain_REST_api.py
CHANGED
|
@@ -254,7 +254,7 @@ class CodeplainAPI:
|
|
|
254
254
|
|
|
255
255
|
return self.post_request(endpoint_url, headers, payload, run_state)
|
|
256
256
|
|
|
257
|
-
def
|
|
257
|
+
def distill_conformance_test_memory(
|
|
258
258
|
self,
|
|
259
259
|
frid,
|
|
260
260
|
plain_source_tree,
|
|
@@ -263,15 +263,10 @@ class CodeplainAPI:
|
|
|
263
263
|
memory_files_content,
|
|
264
264
|
module_name,
|
|
265
265
|
required_modules,
|
|
266
|
-
|
|
267
|
-
conformance_tests_files,
|
|
268
|
-
acceptance_tests,
|
|
269
|
-
conformance_tests_issue,
|
|
270
|
-
conformance_tests_folder_name,
|
|
271
|
-
previous_conformance_tests_issue_old,
|
|
266
|
+
fix_journals,
|
|
272
267
|
run_state: RunState,
|
|
273
268
|
):
|
|
274
|
-
endpoint_url = f"{self.api_url}/
|
|
269
|
+
endpoint_url = f"{self.api_url}/distill_conformance_test_memory"
|
|
275
270
|
headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
|
|
276
271
|
|
|
277
272
|
payload = {
|
|
@@ -282,12 +277,7 @@ class CodeplainAPI:
|
|
|
282
277
|
"memory_files_content": memory_files_content,
|
|
283
278
|
"module_name": module_name,
|
|
284
279
|
"required_modules": required_modules,
|
|
285
|
-
"
|
|
286
|
-
"conformance_tests_files": conformance_tests_files,
|
|
287
|
-
"acceptance_tests": acceptance_tests,
|
|
288
|
-
"conformance_tests_issue": conformance_tests_issue,
|
|
289
|
-
"conformance_tests_folder_name": conformance_tests_folder_name,
|
|
290
|
-
"previous_conformance_tests_issue": previous_conformance_tests_issue_old,
|
|
280
|
+
"fix_journals": fix_journals,
|
|
291
281
|
}
|
|
292
282
|
|
|
293
283
|
return self.post_request(endpoint_url, headers, payload, run_state)
|
|
@@ -381,6 +371,7 @@ class CodeplainAPI:
|
|
|
381
371
|
conformance_tests_folder_name,
|
|
382
372
|
current_testing_frid_high_level_implementation_plan: Optional[str],
|
|
383
373
|
conflicting_requirements_count: int,
|
|
374
|
+
fix_attempts_journal: list,
|
|
384
375
|
run_state: RunState,
|
|
385
376
|
):
|
|
386
377
|
endpoint_url = f"{self.api_url}/fix_conformance_tests_issue"
|
|
@@ -403,6 +394,7 @@ class CodeplainAPI:
|
|
|
403
394
|
"conformance_tests_folder_name": conformance_tests_folder_name,
|
|
404
395
|
"current_testing_frid_high_level_implementation_plan": current_testing_frid_high_level_implementation_plan,
|
|
405
396
|
"conflicting_requirements_count": conflicting_requirements_count,
|
|
397
|
+
"fix_attempts_journal": fix_attempts_journal,
|
|
406
398
|
}
|
|
407
399
|
|
|
408
400
|
if acceptance_tests is not None:
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Deterministic journal of conformance tests fix attempts.
|
|
2
|
+
|
|
3
|
+
While a functionality's conformance tests are failing, every fix attempt is recorded here: the
|
|
4
|
+
issue that triggered it, what the fixer said it tried, which files it changed and whether the tests
|
|
5
|
+
passed afterwards. A digest of the journal is sent back with every fix request so the fixer does
|
|
6
|
+
not repeat attempts that already failed, and once the functionality's conformance tests all pass
|
|
7
|
+
the journal is distilled into durable memories and deleted.
|
|
8
|
+
|
|
9
|
+
Each entry carries the issue in two forms: the raw script output (kept untruncated, on disk only)
|
|
10
|
+
and the prepared form the server built for the fix prompt (trimmed, and summarized or truncated
|
|
11
|
+
when oversized) - the issue exactly the way the fixer saw it. The prepared form is what travels in
|
|
12
|
+
the fix digest and the distillation payload; the raw form exists for humans debugging a render.
|
|
13
|
+
An attempt's outcome text is not stored twice: the output produced after an attempt is the raw
|
|
14
|
+
issue of the next attempt.
|
|
15
|
+
|
|
16
|
+
Journals are keyed by the (module, frid) whose tests are being fixed - during the regression sweep
|
|
17
|
+
that can be a previously implemented functionality, possibly from a required module.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
|
|
25
|
+
from plain2code_console import console
|
|
26
|
+
|
|
27
|
+
CONFORMANCE_TEST_JOURNAL_SUBFOLDER = "conformance_test_journal"
|
|
28
|
+
|
|
29
|
+
# Cap for the issue excerpts included in payloads (the per-attempt issue in the fix digest, and the
|
|
30
|
+
# initial issue in the distillation payload). The server-prepared issue is already bounded (LLM
|
|
31
|
+
# summary or ~10k-char truncation), so this only trims the rare oversized case - it must not
|
|
32
|
+
# routinely cut into summaries the server already sized for a prompt.
|
|
33
|
+
MAX_ISSUE_EXCERPT_CHARS = 8000
|
|
34
|
+
|
|
35
|
+
# The keys of a journal entry that make up the digest sent to the fixer and the distiller.
|
|
36
|
+
DIGEST_KEYS = ["attempt", "hypothesis", "approach", "target", "files_changed", "duplicate_of", "result"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _trim_issue(issue: str | None, max_chars: int) -> str | None:
|
|
40
|
+
if issue is None or len(issue) <= max_chars:
|
|
41
|
+
return issue
|
|
42
|
+
return f"(TRUNCATED - showing the last {max_chars} characters)\n" + issue[-max_chars:]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def normalized_diff_hash(diff_files: dict[str, str] | None) -> str | None:
|
|
46
|
+
"""Hashes a fix's diff so that essentially identical retries can be detected.
|
|
47
|
+
|
|
48
|
+
Whitespace-only differences are ignored - a fix that reproduces an earlier failed change with
|
|
49
|
+
different formatting is still the same attempt.
|
|
50
|
+
"""
|
|
51
|
+
if not diff_files:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
normalized_parts = []
|
|
55
|
+
for file_name in sorted(diff_files):
|
|
56
|
+
content_lines = [line.strip() for line in (diff_files[file_name] or "").splitlines() if line.strip()]
|
|
57
|
+
normalized_parts.append(file_name + "\n" + "\n".join(content_lines))
|
|
58
|
+
|
|
59
|
+
return hashlib.sha256("\n".join(normalized_parts).encode("utf-8")).hexdigest()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FixAttemptJournal:
|
|
63
|
+
"""File-backed journal, one JSONL file per (module, frid) whose conformance tests are fixed."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, memory_folder: str):
|
|
66
|
+
self.journal_folder = os.path.join(memory_folder, CONFORMANCE_TEST_JOURNAL_SUBFOLDER)
|
|
67
|
+
|
|
68
|
+
def _journal_path(self, module_name: str, frid: str) -> str:
|
|
69
|
+
return os.path.join(self.journal_folder, f"{module_name}__{frid}.jsonl")
|
|
70
|
+
|
|
71
|
+
def _read_entries(self, module_name: str, frid: str) -> list[dict]:
|
|
72
|
+
journal_path = self._journal_path(module_name, frid)
|
|
73
|
+
if not os.path.exists(journal_path):
|
|
74
|
+
return []
|
|
75
|
+
|
|
76
|
+
entries = []
|
|
77
|
+
with open(journal_path, "r", encoding="utf-8") as journal_file:
|
|
78
|
+
for line in journal_file:
|
|
79
|
+
line = line.strip()
|
|
80
|
+
if not line:
|
|
81
|
+
continue
|
|
82
|
+
try:
|
|
83
|
+
entries.append(json.loads(line))
|
|
84
|
+
except json.JSONDecodeError:
|
|
85
|
+
console.error(f"Skipping malformed journal entry in {journal_path}.")
|
|
86
|
+
|
|
87
|
+
return entries
|
|
88
|
+
|
|
89
|
+
def _write_entries(self, module_name: str, frid: str, entries: list[dict]):
|
|
90
|
+
os.makedirs(self.journal_folder, exist_ok=True)
|
|
91
|
+
with open(self._journal_path(module_name, frid), "w", encoding="utf-8") as journal_file:
|
|
92
|
+
for entry in entries:
|
|
93
|
+
journal_file.write(json.dumps(entry) + "\n")
|
|
94
|
+
|
|
95
|
+
def open_attempt(self, module_name: str, frid: str, attempt_no: int, issue_before: str):
|
|
96
|
+
"""Starts a new journal entry for a fix attempt that is about to be made.
|
|
97
|
+
|
|
98
|
+
The raw issue is stored untruncated - it never travels in a payload, only its prepared form
|
|
99
|
+
(recorded later by record_fix) does.
|
|
100
|
+
"""
|
|
101
|
+
entries = self._read_entries(module_name, frid)
|
|
102
|
+
entries.append({"attempt": attempt_no, "issue_before_raw": issue_before})
|
|
103
|
+
self._write_entries(module_name, frid, entries)
|
|
104
|
+
|
|
105
|
+
def record_fix(
|
|
106
|
+
self,
|
|
107
|
+
module_name: str,
|
|
108
|
+
frid: str,
|
|
109
|
+
fix_attempt_summary: dict | None,
|
|
110
|
+
files_changed: list[str],
|
|
111
|
+
target: str,
|
|
112
|
+
diff_files: dict[str, str] | None,
|
|
113
|
+
prepared_issue: str | None,
|
|
114
|
+
):
|
|
115
|
+
"""Completes the open attempt with what the fix actually did.
|
|
116
|
+
|
|
117
|
+
The prepared issue is the form the server built for the fix prompt - the issue exactly the
|
|
118
|
+
way the fixer saw it. The diff hash marks the attempt as a duplicate when an earlier attempt
|
|
119
|
+
in the same journal produced essentially the same change - the strongest signal that the
|
|
120
|
+
fixer is going in circles.
|
|
121
|
+
"""
|
|
122
|
+
entries = self._read_entries(module_name, frid)
|
|
123
|
+
if not entries:
|
|
124
|
+
console.error("Cannot record a fix - no journal entry is open.")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
entry = entries[-1]
|
|
128
|
+
if isinstance(fix_attempt_summary, dict):
|
|
129
|
+
entry["hypothesis"] = fix_attempt_summary.get("hypothesis")
|
|
130
|
+
entry["approach"] = fix_attempt_summary.get("approach")
|
|
131
|
+
if prepared_issue:
|
|
132
|
+
entry["issue_before"] = prepared_issue
|
|
133
|
+
entry["files_changed"] = sorted(files_changed)
|
|
134
|
+
entry["target"] = target
|
|
135
|
+
|
|
136
|
+
diff_hash = normalized_diff_hash(diff_files)
|
|
137
|
+
entry["diff_hash"] = diff_hash
|
|
138
|
+
if diff_hash is not None:
|
|
139
|
+
for earlier_entry in entries[:-1]:
|
|
140
|
+
if earlier_entry.get("diff_hash") == diff_hash:
|
|
141
|
+
entry["duplicate_of"] = earlier_entry["attempt"]
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
self._write_entries(module_name, frid, entries)
|
|
145
|
+
|
|
146
|
+
def record_result(self, module_name: str, frid: str, passed: bool):
|
|
147
|
+
"""Records whether the conformance tests passed after the last fix attempt was applied.
|
|
148
|
+
|
|
149
|
+
A failed run's output is not stored here - it becomes the raw issue of the next attempt.
|
|
150
|
+
Does nothing when no attempt is pending: the first test run of a functionality has no fix
|
|
151
|
+
attempt behind it.
|
|
152
|
+
"""
|
|
153
|
+
entries = self._read_entries(module_name, frid)
|
|
154
|
+
if not entries or "result" in entries[-1]:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
entries[-1]["result"] = "conformance tests passed" if passed else "conformance tests still failed"
|
|
158
|
+
self._write_entries(module_name, frid, entries)
|
|
159
|
+
|
|
160
|
+
def build_digest(self, module_name: str, frid: str) -> list[dict]:
|
|
161
|
+
"""Builds the compact attempt history sent with a fix request.
|
|
162
|
+
|
|
163
|
+
Only attempts whose fix has been recorded are included - the entry just opened for the
|
|
164
|
+
attempt being made (and any dangling entry an interrupted render left behind) carries no
|
|
165
|
+
information for the fixer. Each included attempt carries the prepared issue it addressed,
|
|
166
|
+
so the fixer can see how the issue evolved across attempts. Raw outputs never travel - the
|
|
167
|
+
current issue is sent with the request separately.
|
|
168
|
+
"""
|
|
169
|
+
return [self._digest_entry(entry) for entry in self._read_entries(module_name, frid) if "target" in entry]
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _digest_entry(entry: dict) -> dict:
|
|
173
|
+
digest_entry = {key: entry[key] for key in DIGEST_KEYS if key in entry}
|
|
174
|
+
if entry.get("issue_before"):
|
|
175
|
+
digest_entry["issue"] = _trim_issue(entry["issue_before"], MAX_ISSUE_EXCERPT_CHARS)
|
|
176
|
+
return digest_entry
|
|
177
|
+
|
|
178
|
+
def collect_all(self) -> list[dict]:
|
|
179
|
+
"""Collects the digests of every journal, keyed by the tested module and frid, for distillation."""
|
|
180
|
+
if not os.path.exists(self.journal_folder):
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
journals = []
|
|
184
|
+
for file_name in sorted(os.listdir(self.journal_folder)):
|
|
185
|
+
if not file_name.endswith(".jsonl") or "__" not in file_name:
|
|
186
|
+
continue
|
|
187
|
+
module_name, _, frid = file_name[: -len(".jsonl")].rpartition("__")
|
|
188
|
+
entries = [entry for entry in self._read_entries(module_name, frid) if "target" in entry]
|
|
189
|
+
if not entries:
|
|
190
|
+
continue
|
|
191
|
+
initial_issue = entries[0].get("issue_before") or _trim_issue(
|
|
192
|
+
entries[0].get("issue_before_raw"), MAX_ISSUE_EXCERPT_CHARS
|
|
193
|
+
)
|
|
194
|
+
journal = {
|
|
195
|
+
"module": module_name,
|
|
196
|
+
"frid": frid,
|
|
197
|
+
"initial_issue": initial_issue,
|
|
198
|
+
"attempts": [self._digest_entry(entry) for entry in entries],
|
|
199
|
+
}
|
|
200
|
+
journals.append(journal)
|
|
201
|
+
|
|
202
|
+
return journals
|
|
203
|
+
|
|
204
|
+
def clear_all(self):
|
|
205
|
+
"""Deletes every journal. Called after the journals have been distilled into memories."""
|
|
206
|
+
if os.path.exists(self.journal_folder):
|
|
207
|
+
shutil.rmtree(self.journal_folder)
|
memory_management.py
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import os
|
|
3
2
|
|
|
4
3
|
import file_utils
|
|
4
|
+
from conformance_fix_journal import FixAttemptJournal
|
|
5
5
|
from plain2code_console import console
|
|
6
|
-
from render_machine.implementation_code_helpers import ImplementationCodeHelpers
|
|
7
|
-
from render_machine.render_context import RenderContext
|
|
8
6
|
|
|
9
|
-
CONFORMANCE_TESTS_SUCCESS_EXIT_CODE = 0
|
|
10
7
|
CONFORMANCE_TEST_MEMORY_SUBFOLDER = "conformance_test_memory"
|
|
11
8
|
|
|
12
9
|
|
|
13
10
|
class MemoryManager:
|
|
11
|
+
"""Manages a module's memory store at <build>/<module>/.memory.
|
|
12
|
+
|
|
13
|
+
The distilled memories are inherited by the next module in the chain (copied over when its
|
|
14
|
+
render starts, like the code repo), so learnings travel across modules.
|
|
15
|
+
"""
|
|
14
16
|
|
|
15
17
|
@staticmethod
|
|
16
18
|
def fetch_memory_files(memory_folder: str) -> tuple[list[str], dict[str, str]]:
|
|
@@ -26,107 +28,25 @@ class MemoryManager:
|
|
|
26
28
|
def __init__(self, codeplain_api, memory_folder: str):
|
|
27
29
|
self.codeplain_api = codeplain_api
|
|
28
30
|
self.memory_folder = memory_folder
|
|
31
|
+
self.journal = FixAttemptJournal(memory_folder)
|
|
29
32
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
current_conformance_tests_issue_module = (
|
|
36
|
-
render_context.conformance_tests_running_context.current_testing_module_name
|
|
37
|
-
)
|
|
38
|
-
old_conformance_tests_issue_frid = (
|
|
39
|
-
render_context.conformance_tests_running_context.previous_conformance_tests_issue_frid
|
|
40
|
-
)
|
|
41
|
-
old_conformance_tests_issue_module = (
|
|
42
|
-
render_context.conformance_tests_running_context.previous_conformance_tests_issue_module
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
old_conformance_tests_issue = (
|
|
46
|
-
render_context.conformance_tests_running_context.previous_conformance_tests_issue_old
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
is_first_time_running_conformance_tests = (
|
|
50
|
-
old_conformance_tests_issue_frid is None
|
|
51
|
-
or old_conformance_tests_issue_frid == ""
|
|
52
|
-
or old_conformance_tests_issue_module != current_conformance_tests_issue_module
|
|
53
|
-
)
|
|
54
|
-
is_same_frid_as_previous_failing_test = (
|
|
55
|
-
current_conformance_tests_issue_frid == old_conformance_tests_issue_frid
|
|
56
|
-
and current_conformance_tests_issue_module == old_conformance_tests_issue_module
|
|
57
|
-
)
|
|
58
|
-
is_conformance_test_failed = exit_code != CONFORMANCE_TESTS_SUCCESS_EXIT_CODE
|
|
59
|
-
|
|
60
|
-
should_create_memory = not is_first_time_running_conformance_tests and (
|
|
61
|
-
is_same_frid_as_previous_failing_test or is_conformance_test_failed
|
|
62
|
-
)
|
|
63
|
-
code_diff_files = render_context.conformance_tests_running_context.code_diff_files
|
|
33
|
+
def store_memory_files(self, response_files: dict[str, str]):
|
|
34
|
+
"""Stores distilled memory files, applying deletions (None content) to existing memories."""
|
|
35
|
+
memory_path = os.path.join(self.memory_folder, CONFORMANCE_TEST_MEMORY_SUBFOLDER)
|
|
36
|
+
memory_files, _ = MemoryManager.fetch_memory_files(self.memory_folder)
|
|
37
|
+
file_utils.store_response_files(memory_path, response_files, memory_files)
|
|
64
38
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
39
|
+
@staticmethod
|
|
40
|
+
def inherit_memories(source_memory_folder: str, destination_memory_folder: str):
|
|
41
|
+
"""Copies the distilled memories of a previously rendered module into this module's store.
|
|
42
|
+
|
|
43
|
+
Only the distilled memories travel - fix attempt journals are transient state of one
|
|
44
|
+
module's render. A previous module without memories (e.g. shipped as an archive that
|
|
45
|
+
carries only code) is a no-op.
|
|
46
|
+
"""
|
|
47
|
+
source_path = os.path.join(source_memory_folder, CONFORMANCE_TEST_MEMORY_SUBFOLDER)
|
|
48
|
+
if not os.path.isdir(source_path):
|
|
69
49
|
return
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
render_context.build_folder
|
|
73
|
-
)
|
|
74
|
-
memory_files, memory_files_content = MemoryManager.fetch_memory_files(self.memory_folder)
|
|
75
|
-
|
|
76
|
-
conformance_tests_folder_name = (
|
|
77
|
-
render_context.conformance_tests_running_context.get_current_conformance_test_folder_name()
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
(
|
|
81
|
-
_,
|
|
82
|
-
existing_conformance_test_files_content,
|
|
83
|
-
) = render_context.conformance_tests.fetch_existing_conformance_test_files(
|
|
84
|
-
render_context.module_name,
|
|
85
|
-
render_context.required_modules,
|
|
86
|
-
render_context.conformance_tests_running_context.current_testing_module_name,
|
|
87
|
-
conformance_tests_folder_name,
|
|
88
|
-
)
|
|
89
|
-
acceptance_tests = render_context.conformance_tests_running_context.get_current_acceptance_tests()
|
|
90
|
-
|
|
91
|
-
response_files = render_context.codeplain_api.create_conformance_test_memory(
|
|
92
|
-
render_context.frid_context.frid,
|
|
93
|
-
render_context.plain_source_tree,
|
|
94
|
-
render_context.frid_context.linked_resources,
|
|
95
|
-
existing_files_content,
|
|
96
|
-
memory_files_content,
|
|
97
|
-
render_context.module_name,
|
|
98
|
-
render_context.get_required_modules_functionalities(),
|
|
99
|
-
code_diff_files,
|
|
100
|
-
existing_conformance_test_files_content,
|
|
101
|
-
acceptance_tests,
|
|
102
|
-
conformance_tests_issue,
|
|
103
|
-
conformance_tests_folder_name,
|
|
104
|
-
old_conformance_tests_issue,
|
|
105
|
-
run_state=render_context.run_state,
|
|
50
|
+
file_utils.copy_folder_content(
|
|
51
|
+
source_path, os.path.join(destination_memory_folder, CONFORMANCE_TEST_MEMORY_SUBFOLDER)
|
|
106
52
|
)
|
|
107
|
-
if len(response_files) > 0:
|
|
108
|
-
memory_folder_path = os.path.join(self.memory_folder, CONFORMANCE_TEST_MEMORY_SUBFOLDER)
|
|
109
|
-
file_utils.store_response_files(memory_folder_path, response_files, memory_files)
|
|
110
|
-
|
|
111
|
-
def delete_unresolved_memory_files(self):
|
|
112
|
-
"""Delete memory files whose resolution_status is not 'RESOLVED'."""
|
|
113
|
-
memory_path = os.path.join(self.memory_folder, CONFORMANCE_TEST_MEMORY_SUBFOLDER)
|
|
114
|
-
if not os.path.exists(memory_path):
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
memory_files = file_utils.list_all_text_files(memory_path)
|
|
118
|
-
for file_name in memory_files:
|
|
119
|
-
file_path = os.path.join(memory_path, file_name)
|
|
120
|
-
try:
|
|
121
|
-
with open(file_path, "r") as f:
|
|
122
|
-
content = json.load(f)
|
|
123
|
-
if content.get("resolution_status") == "RESOLVED":
|
|
124
|
-
continue
|
|
125
|
-
else:
|
|
126
|
-
os.remove(file_path)
|
|
127
|
-
except (json.JSONDecodeError, OSError):
|
|
128
|
-
# Not a valid JSON file, unlikely to be a valid memory file, delete it
|
|
129
|
-
console.error(f"Memory file is not a valid JSON file: {file_name}. Deleting it.")
|
|
130
|
-
os.remove(file_path)
|
|
131
|
-
|
|
132
|
-
console.debug(f"Deleted temporary memory file: {file_name}")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from plain2code_console import console
|
|
4
|
+
from render_machine.actions.base_action import BaseAction
|
|
5
|
+
from render_machine.implementation_code_helpers import ImplementationCodeHelpers
|
|
6
|
+
from render_machine.render_context import RenderContext
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DistillConformanceTestMemory(BaseAction):
|
|
10
|
+
"""Distills the conformance tests fix journals into durable memories once all tests pass.
|
|
11
|
+
|
|
12
|
+
Runs exactly once per functional requirement, as the first postprocessing step. When no fix
|
|
13
|
+
attempts were needed there is nothing to distill and no API call is made. Distillation is best
|
|
14
|
+
effort - a failure must never fail the render, so the journals are kept for the next
|
|
15
|
+
distillation instead.
|
|
16
|
+
|
|
17
|
+
The memories land in the module's own memory store and are inherited by the next module in the
|
|
18
|
+
chain when its render starts.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
SUCCESSFUL_OUTCOME = "conformance_test_memory_distilled"
|
|
22
|
+
|
|
23
|
+
def execute(self, render_context: RenderContext, _previous_action_payload: Any | None):
|
|
24
|
+
journal = render_context.memory_manager.journal
|
|
25
|
+
fix_journals = journal.collect_all()
|
|
26
|
+
if not fix_journals:
|
|
27
|
+
return self.SUCCESSFUL_OUTCOME, None
|
|
28
|
+
|
|
29
|
+
console.info(f"Distilling conformance test fix learnings for functionality {render_context.frid_context.frid}.")
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
_, existing_files_content = ImplementationCodeHelpers.fetch_existing_files(render_context.build_folder)
|
|
33
|
+
_, memory_files_content = render_context.memory_manager.fetch_memory_files(
|
|
34
|
+
render_context.memory_manager.memory_folder
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
response_files = render_context.codeplain_api.distill_conformance_test_memory(
|
|
38
|
+
frid=render_context.frid_context.frid,
|
|
39
|
+
plain_source_tree=render_context.plain_source_tree,
|
|
40
|
+
linked_resources=render_context.frid_context.linked_resources,
|
|
41
|
+
existing_files_content=existing_files_content,
|
|
42
|
+
memory_files_content=memory_files_content,
|
|
43
|
+
module_name=render_context.module_name,
|
|
44
|
+
required_modules=render_context.get_required_modules_functionalities(),
|
|
45
|
+
fix_journals=fix_journals,
|
|
46
|
+
run_state=render_context.run_state,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if response_files:
|
|
50
|
+
render_context.memory_manager.store_memory_files(response_files)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
console.error(f"Failed to distill conformance test memory (keeping the journals): {e}")
|
|
53
|
+
return self.SUCCESSFUL_OUTCOME, None
|
|
54
|
+
|
|
55
|
+
journal.clear_all()
|
|
56
|
+
return self.SUCCESSFUL_OUTCOME, None
|
|
@@ -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.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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] =
|
|
118
|
-
render_context.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
render_context.conformance_tests_running_context.current_testing_module_name
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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:
|
render_machine/render_types.py
CHANGED
|
@@ -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.
|
|
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
|
|
@@ -18,7 +18,9 @@ from types import SimpleNamespace
|
|
|
18
18
|
|
|
19
19
|
import pytest
|
|
20
20
|
|
|
21
|
+
from conformance_fix_journal import CONFORMANCE_TEST_JOURNAL_SUBFOLDER
|
|
21
22
|
from git_utils import FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE, add_all_files_and_commit, init_git_repo
|
|
23
|
+
from memory_management import CONFORMANCE_TEST_MEMORY_SUBFOLDER
|
|
22
24
|
from partial_rendering import archive_missing_conformance_tests, get_plain_module_render_state, get_render_choices
|
|
23
25
|
from plain_modules import PlainModule
|
|
24
26
|
from render_machine.actions.prepare_repositories import PrepareRepositories
|
|
@@ -115,6 +117,42 @@ def test_fresh_render_without_conformance_tests_does_not_create_tests_folder(sol
|
|
|
115
117
|
assert not os.path.exists(solo_module.module_conformance_tests_folder)
|
|
116
118
|
|
|
117
119
|
|
|
120
|
+
def test_fresh_render_inherits_memories_from_the_previous_module(root_module):
|
|
121
|
+
"""The previous module's distilled memories are copied into the new module's memory store
|
|
122
|
+
(replacing any stale store), while its fix attempt journals stay behind."""
|
|
123
|
+
previous = root_module.required_modules[-1]
|
|
124
|
+
_init_repo_with_finished_frid(previous.module_build_folder, previous.module_name)
|
|
125
|
+
memory_path = Path(previous.module_memory_folder) / CONFORMANCE_TEST_MEMORY_SUBFOLDER
|
|
126
|
+
memory_path.mkdir(parents=True)
|
|
127
|
+
(memory_path / "learning.json").write_text('{"memory_id": "learning"}')
|
|
128
|
+
journal_path = Path(previous.module_memory_folder) / CONFORMANCE_TEST_JOURNAL_SUBFOLDER
|
|
129
|
+
journal_path.mkdir(parents=True)
|
|
130
|
+
(journal_path / "pr_middle__1.jsonl").write_text('{"attempt": 1}\n')
|
|
131
|
+
|
|
132
|
+
stale_memory = Path(root_module.module_memory_folder) / CONFORMANCE_TEST_MEMORY_SUBFOLDER / "stale.json"
|
|
133
|
+
stale_memory.parent.mkdir(parents=True)
|
|
134
|
+
stale_memory.write_text('{"memory_id": "stale"}')
|
|
135
|
+
|
|
136
|
+
render_context = _make_render_context(root_module, render_conformance_tests=False)
|
|
137
|
+
PrepareRepositories().execute(render_context, None)
|
|
138
|
+
|
|
139
|
+
inherited_memory_folder = Path(root_module.module_memory_folder) / CONFORMANCE_TEST_MEMORY_SUBFOLDER
|
|
140
|
+
assert sorted(os.listdir(inherited_memory_folder)) == ["learning.json"]
|
|
141
|
+
assert not stale_memory.exists()
|
|
142
|
+
assert not (Path(root_module.module_memory_folder) / CONFORMANCE_TEST_JOURNAL_SUBFOLDER).exists()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_fresh_render_with_a_memoryless_previous_module_starts_with_an_empty_store(root_module):
|
|
146
|
+
previous = root_module.required_modules[-1]
|
|
147
|
+
_init_repo_with_finished_frid(previous.module_build_folder, previous.module_name)
|
|
148
|
+
|
|
149
|
+
render_context = _make_render_context(root_module, render_conformance_tests=False)
|
|
150
|
+
PrepareRepositories().execute(render_context, None)
|
|
151
|
+
|
|
152
|
+
assert os.path.isdir(os.path.join(root_module.module_build_folder, ".git"))
|
|
153
|
+
assert not os.path.exists(root_module.module_memory_folder)
|
|
154
|
+
|
|
155
|
+
|
|
118
156
|
# --------------------------------------------------------------------------
|
|
119
157
|
# ConformanceTests — tests-folder paths
|
|
120
158
|
# --------------------------------------------------------------------------
|
tui/state_handlers.py
CHANGED
|
@@ -241,7 +241,13 @@ class ConformanceTestsHandler(StateHandler):
|
|
|
241
241
|
self.tui, TUIComponents.FRID_PROGRESS_CONFORMANCE_TEST.value, [Substate(fixing_text)]
|
|
242
242
|
)
|
|
243
243
|
else:
|
|
244
|
-
if segments[3] == States.
|
|
244
|
+
if segments[3] == States.CONFORMANCE_TESTS_READY_FOR_MEMORY_DISTILLATION.value:
|
|
245
|
+
update_progress_item_substates(
|
|
246
|
+
self.tui,
|
|
247
|
+
TUIComponents.FRID_PROGRESS_CONFORMANCE_TEST.value,
|
|
248
|
+
[Substate("Distilling conformance test fix learnings")],
|
|
249
|
+
)
|
|
250
|
+
elif segments[3] == States.CONFORMANCE_TESTS_READY_FOR_SUMMARY.value:
|
|
245
251
|
update_progress_item_substates(
|
|
246
252
|
self.tui,
|
|
247
253
|
TUIComponents.FRID_PROGRESS_CONFORMANCE_TEST.value,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|