@@ -211,6 +211,100 @@ def test_autofix_hook(cookies, context):
211211 pytest .fail (f"stdout: { error .stdout .decode ('utf-8' )} , stderr: { error .stderr .decode ('utf-8' )} " )
212212
213213
214+ @pytest .mark .unit
215+ def test_gitattributes_exists (cookies ):
216+ """
217+ Test that generated projects include a .gitattributes file
218+ to enforce LF line endings for shell scripts and Dockerfiles.
219+ """
220+ os .environ ["RUN_POST_HOOK" ] = "false"
221+
222+ result = cookies .bake ()
223+
224+ assert result .exit_code == 0
225+ assert result .exception is None
226+
227+ gitattributes = result .project_path / ".gitattributes"
228+ assert gitattributes .is_file (), ".gitattributes file must exist in generated project"
229+
230+ content = gitattributes .read_text (encoding = "utf-8" )
231+ assert "*.sh" in content , ".gitattributes must enforce line endings for shell scripts"
232+ assert "Dockerfile" in content , ".gitattributes must enforce line endings for Dockerfiles"
233+
234+
235+ @pytest .mark .unit
236+ def test_shell_scripts_have_lf_line_endings (cookies ):
237+ """
238+ Test that all shell scripts in generated projects have LF line endings,
239+ not CRLF. CRLF line endings break bash on Windows with errors like:
240+ ': invalid option namesh: line 2: set: pipefail'
241+ """
242+ os .environ ["RUN_POST_HOOK" ] = "false"
243+
244+ result = cookies .bake ()
245+
246+ assert result .exit_code == 0
247+ assert result .exception is None
248+
249+ sh_files = list (result .project_path .glob ("**/*.sh" ))
250+ assert sh_files , "Expected at least one .sh file in generated project"
251+
252+ for sh_file in sh_files :
253+ raw_content = sh_file .read_bytes ()
254+ assert b"\r \n " not in raw_content , f"{ sh_file .name } contains CRLF line endings — this breaks bash on Windows"
255+
256+
257+ @pytest .mark .unit
258+ def test_dockerfile_has_lf_line_endings (cookies ):
259+ """
260+ Test that the Dockerfile in generated projects has LF line endings.
261+ CRLF line endings cause Docker build failures.
262+ """
263+ os .environ ["RUN_POST_HOOK" ] = "false"
264+
265+ result = cookies .bake ()
266+
267+ assert result .exit_code == 0
268+ assert result .exception is None
269+
270+ dockerfile = result .project_path / "Dockerfile"
271+ assert dockerfile .is_file (), "Dockerfile must exist in generated project"
272+
273+ raw_content = dockerfile .read_bytes ()
274+ assert b"\r \n " not in raw_content , "Dockerfile contains CRLF line endings — this breaks Docker builds"
275+
276+
277+ @pytest .mark .unit
278+ def test_no_dead_shell_scripts (cookies ):
279+ """
280+ Test that all shell scripts in the generated project are referenced
281+ by at least one other file (Taskfile.yml, CI workflows, etc.).
282+ """
283+ os .environ ["RUN_POST_HOOK" ] = "false"
284+
285+ result = cookies .bake ()
286+
287+ assert result .exit_code == 0
288+ assert result .exception is None
289+
290+ sh_files = list (result .project_path .glob ("scripts/*.sh" ))
291+ assert sh_files , "Expected at least one .sh file in generated project"
292+
293+ # Collect all non-.sh file content to search for references
294+ all_content = ""
295+ for f in result .project_path .rglob ("*" ):
296+ if f .is_file () and f .suffix != ".sh" and ".git/" not in str (f ):
297+ try :
298+ all_content += f .read_text (encoding = "utf-8" , errors = "ignore" )
299+ except (IsADirectoryError , PermissionError ):
300+ pass
301+
302+ for sh_file in sh_files :
303+ assert sh_file .name in all_content , (
304+ f"scripts/{ sh_file .name } is dead code — not referenced by any other file in the project"
305+ )
306+
307+
214308@pytest .mark .unit
215309@pytest .mark .parametrize (
216310 "invalid_name" ,
0 commit comments