Skip to content

Commit d1d8ef3

Browse files
picklebentoclaude
andcommitted
fix(codex): handle empty eol when instruction is final line without newline
The hook-note injection regex allowed end-of-string matches via ``$``, which left the captured ``eol`` empty. When the matched indent was also empty, the substitution concatenated the note onto the same line as the instruction. Default ``eol`` to ``\n`` when the capture is empty. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8731f1a commit d1d8ef3

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

src/specify_cli/integrations/codex/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ def _inject_hook_command_note(content: str) -> str:
8484
def repl(m: re.Match[str]) -> str:
8585
indent = m.group(1)
8686
instruction = m.group(2)
87-
eol = m.group(3)
87+
# ``eol`` is empty when the regex matched via ``$`` because the
88+
# instruction was the final line of a file with no trailing
89+
# newline. Default to ``\n`` so the note never collapses onto
90+
# the same line as the instruction.
91+
eol = m.group(3) or "\n"
8892
return (
8993
indent
9094
+ _HOOK_COMMAND_NOTE.rstrip("\n")

tests/integrations/test_integration_codex.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,34 @@ def test_hook_note_preserves_indentation(self):
8383
lines = result.splitlines()
8484
note_line = [l for l in lines if "replace dots" in l][0]
8585
assert note_line.startswith(" "), "Note should preserve indentation"
86+
87+
def test_hook_note_when_instruction_is_final_line_without_newline(self):
88+
"""Note must not collapse onto the instruction line when the file
89+
ends without a trailing newline and the preceding line is not blank.
90+
"""
91+
from specify_cli.integrations.codex import CodexIntegration
92+
93+
# No blank line before the instruction and no trailing newline:
94+
# this is the case where the captured ``eol`` is empty and the
95+
# captured indent is also empty, so a missing line separator would
96+
# cause the note and instruction to collapse onto one line.
97+
content = (
98+
"---\nname: test\n---\n"
99+
"Body line\n"
100+
"- For each executable hook, output the following"
101+
)
102+
result = CodexIntegration._inject_hook_command_note(content)
103+
lines = result.splitlines()
104+
note_line_idx = next(
105+
i for i, l in enumerate(lines) if "replace dots" in l
106+
)
107+
instruction_line_idx = next(
108+
i for i, l in enumerate(lines)
109+
if l.lstrip().startswith("- For each executable hook")
110+
)
111+
assert note_line_idx < instruction_line_idx, (
112+
"Note must appear before the instruction"
113+
)
114+
assert "For each executable hook" not in lines[note_line_idx], (
115+
"Note and instruction must not be on the same line"
116+
)

0 commit comments

Comments
 (0)