77from kiln_ai .datamodel .skill import (
88 Skill ,
99 _parse_skill_md_body ,
10- _validate_filename ,
1110)
1211
1312
@@ -343,42 +342,14 @@ def test_round_trip_description_with_dashes(mock_project):
343342 assert skill .body () == "The body"
344343
345344
346- # -- Filename validation tests --
347-
348-
349- class TestValidateFilename :
350- @pytest .mark .parametrize (
351- "filename" ,
352- [
353- "../etc/passwd" ,
354- "foo/../bar" ,
355- ".." ,
356- "." ,
357- "sub/dir.md" ,
358- "back\\ slash.md" ,
359- "" ,
360- " " ,
361- ],
362- )
363- def test_invalid_filenames (self , filename ):
364- with pytest .raises (ValueError ):
365- _validate_filename (filename )
366-
367- @pytest .mark .parametrize (
368- "filename" ,
369- ["REFERENCE.md" , "finance.md" , "schema.json" , "diagram.png" , "a" ],
370- )
371- def test_valid_filenames (self , filename ):
372- _validate_filename (filename )
373-
374-
375- # -- References tests --
345+ # -- References & assets tests --
376346
377347
378348class TestReferences :
379- def test_save_skill_md_creates_references_dir (self , mock_project ):
349+ def test_save_skill_md_creates_dirs (self , mock_project ):
380350 skill = save_skill_with_body (mock_project )
381351 assert skill .references_dir ().is_dir ()
352+ assert skill .assets_dir ().is_dir ()
382353
383354 def test_read_reference (self , mock_project ):
384355 skill = save_skill_with_body (mock_project )
@@ -388,24 +359,94 @@ def test_read_reference(self, mock_project):
388359
389360 def test_read_reference_not_found (self , mock_project ):
390361 skill = save_skill_with_body (mock_project )
391- with pytest .raises (FileNotFoundError , match = "Reference file not found" ):
362+ with pytest .raises (FileNotFoundError , match = "Resource file not found" ):
392363 skill .read_reference ("missing.md" )
393364
394- @pytest .mark .parametrize ("filename" , ["../etc/passwd" , "sub/dir.md" , ".." ])
395- def test_reference_path_traversal (self , mock_project , filename ):
365+ @pytest .mark .parametrize ("path" , ["../etc/passwd" , ".." , "../../secret.txt" ])
366+ def test_reference_path_traversal (self , mock_project , path ):
367+ skill = save_skill_with_body (mock_project )
368+ with pytest .raises (ValueError , match = "Path traversal" ):
369+ skill .read_reference (path )
370+
371+ def test_reference_empty_path (self , mock_project ):
372+ skill = save_skill_with_body (mock_project )
373+ with pytest .raises (ValueError , match = "Path cannot be empty" ):
374+ skill .read_reference ("" )
375+ with pytest .raises (ValueError , match = "Path cannot be empty" ):
376+ skill .read_reference (" " )
377+
378+ def test_read_reference_in_subdirectory (self , mock_project ):
396379 skill = save_skill_with_body (mock_project )
397- with pytest .raises (ValueError ):
398- skill .read_reference (filename )
380+ sub_dir = skill .references_dir () / "guides"
381+ sub_dir .mkdir (parents = True , exist_ok = True )
382+ (sub_dir / "style.md" ).write_text ("# Style Guide" , encoding = "utf-8" )
383+ assert skill .read_reference ("guides/style.md" ) == "# Style Guide"
384+
385+ def test_read_reference_in_deeply_nested_subdirectory (self , mock_project ):
386+ skill = save_skill_with_body (mock_project )
387+ nested_dir = skill .references_dir () / "a" / "b" / "c"
388+ nested_dir .mkdir (parents = True , exist_ok = True )
389+ (nested_dir / "deep.md" ).write_text ("Deep content" , encoding = "utf-8" )
390+ assert skill .read_reference ("a/b/c/deep.md" ) == "Deep content"
391+
392+ @pytest .mark .parametrize (
393+ "filename,content" ,
394+ [
395+ ("notes.txt" , "Plain text notes" ),
396+ ("data.json" , '{"key": "value"}' ),
397+ ("prices.csv" , "item,price\n widget,9.99" ),
398+ ("config.yaml" , "key: value" ),
399+ ],
400+ )
401+ def test_non_md_extensions_accepted (self , mock_project , filename , content ):
402+ skill = save_skill_with_body (mock_project )
403+ (skill .references_dir () / filename ).write_text (content , encoding = "utf-8" )
404+ assert skill .read_reference (filename ) == content
405+
406+ def test_binary_file_rejected (self , mock_project ):
407+ skill = save_skill_with_body (mock_project )
408+ (skill .references_dir () / "image.png" ).write_bytes (b"\x89 PNG\r \n \x1a \n \x00 " )
409+ with pytest .raises (ValueError , match = "not a readable text file" ):
410+ skill .read_reference ("image.png" )
399411
400412 def test_references_dir_requires_saved_skill (self ):
401413 skill = make_skill ()
402414 with pytest .raises (ValueError , match = "Skill must be saved" ):
403415 skill .references_dir ()
404416
405- @pytest .mark .parametrize (
406- "filename" , ["notes.txt" , "data.json" , "image.png" , "readme" ]
407- )
408- def test_non_md_extension_rejected (self , mock_project , filename ):
417+
418+ class TestAssets :
419+ def test_read_asset (self , mock_project ):
420+ skill = save_skill_with_body (mock_project )
421+ (skill .assets_dir () / "template.csv" ).write_text (
422+ "col1,col2\n a,b" , encoding = "utf-8"
423+ )
424+ assert skill .read_asset ("template.csv" ) == "col1,col2\n a,b"
425+
426+ def test_read_asset_in_subdirectory (self , mock_project ):
409427 skill = save_skill_with_body (mock_project )
410- with pytest .raises (ValueError , match = r"\.md extension" ):
411- skill .read_reference (filename )
428+ sub_dir = skill .assets_dir () / "data"
429+ sub_dir .mkdir (parents = True , exist_ok = True )
430+ (sub_dir / "prices.csv" ).write_text ("item,price" , encoding = "utf-8" )
431+ assert skill .read_asset ("data/prices.csv" ) == "item,price"
432+
433+ def test_read_asset_not_found (self , mock_project ):
434+ skill = save_skill_with_body (mock_project )
435+ with pytest .raises (FileNotFoundError , match = "Resource file not found" ):
436+ skill .read_asset ("missing.csv" )
437+
438+ def test_asset_path_traversal (self , mock_project ):
439+ skill = save_skill_with_body (mock_project )
440+ with pytest .raises (ValueError , match = "Path traversal" ):
441+ skill .read_asset ("../etc/passwd" )
442+
443+ def test_asset_binary_file_rejected (self , mock_project ):
444+ skill = save_skill_with_body (mock_project )
445+ (skill .assets_dir () / "photo.jpg" ).write_bytes (b"\xff \xd8 \xff \xe0 \x00 \x10 JFIF" )
446+ with pytest .raises (ValueError , match = "not a readable text file" ):
447+ skill .read_asset ("photo.jpg" )
448+
449+ def test_assets_dir_requires_saved_skill (self ):
450+ skill = make_skill ()
451+ with pytest .raises (ValueError , match = "Skill must be saved" ):
452+ skill .assets_dir ()
0 commit comments