@@ -384,6 +384,50 @@ func TestParseRAGServiceConfig_MissingRAGLLM(t *testing.T) {
384384 assert .Contains (t , errs [0 ].Error (), "rag_llm.provider" )
385385}
386386
387+ func TestParseRAGServiceConfig_PipelineNameAllowlist (t * testing.T ) {
388+ validNames := []string {
389+ "default" ,
390+ "my-pipeline" ,
391+ "my_pipeline" ,
392+ "pipeline-1" ,
393+ "a" ,
394+ "abc123" ,
395+ "a-b_c-1" ,
396+ }
397+ for _ , name := range validNames {
398+ t .Run ("valid/" + name , func (t * testing.T ) {
399+ config := minimalRAGConfig ()
400+ config ["pipelines" ].([]any )[0 ].(map [string ]any )["name" ] = name
401+ _ , errs := database .ParseRAGServiceConfig (config , false )
402+ assert .Empty (t , errs , "name %q should be valid" , name )
403+ })
404+ }
405+
406+ invalidNames := []string {
407+ "My Pipeline" , // uppercase + space
408+ "pipeline name" , // space
409+ "pipeline/name" , // slash
410+ "../etc/passwd" , // path traversal
411+ "UPPER" , // uppercase
412+ "pipe🔥line" , // unicode emoji
413+ "pipeline.name" , // dot
414+ "-pipeline" , // leading hyphen (could be misread as a CLI flag)
415+ "" , // empty (covered separately, but included for completeness)
416+ }
417+ for _ , name := range invalidNames {
418+ if name == "" {
419+ continue // empty name is a separate "required" error
420+ }
421+ t .Run ("invalid/" + name , func (t * testing.T ) {
422+ config := minimalRAGConfig ()
423+ config ["pipelines" ].([]any )[0 ].(map [string ]any )["name" ] = name
424+ _ , errs := database .ParseRAGServiceConfig (config , false )
425+ require .NotEmpty (t , errs , "name %q should be invalid" , name )
426+ assert .Contains (t , errs [0 ].Error (), "must match ^[a-z0-9_][a-z0-9_-]*$" )
427+ })
428+ }
429+ }
430+
387431func TestParseRAGServiceConfig_MultiplePipelines (t * testing.T ) {
388432 config := map [string ]any {
389433 "pipelines" : []any {
0 commit comments