@@ -336,6 +336,48 @@ def test_unresolved_policy_value_fails
336336 end
337337 end
338338
339+ def test_required_policy_values_reject_embedded_legacy_placeholders
340+ with_repo do |root |
341+ write_valid_binstub_contract ( root )
342+ write_policy (
343+ root ,
344+ POLICY . merge (
345+ "base_branch" => "Use <base branch>" ,
346+ "ci_parity_environment" => "Run in <runner image>"
347+ )
348+ )
349+ write_skill ( root , "No commands here.\n " )
350+
351+ out , status = run_doctor ( root )
352+
353+ refute status . success?
354+ assert_includes out , "unresolved policy value for key: base_branch"
355+ assert_includes out , "unresolved policy value for key: ci_parity_environment"
356+ end
357+ end
358+
359+ def test_optional_policy_values_recursively_reject_embedded_legacy_placeholders
360+ with_repo do |root |
361+ write_valid_binstub_contract ( root )
362+ write_policy (
363+ root ,
364+ POLICY . merge (
365+ "custom_scalar" => "Use <base branch>" ,
366+ "custom_array" => [ "Run in <runner image>" ] ,
367+ "custom_mapping" => { "nested" => "Use <main branch>" }
368+ )
369+ )
370+ write_skill ( root , "No commands here.\n " )
371+
372+ out , status = run_doctor ( root )
373+
374+ refute status . success?
375+ %w[ custom_scalar custom_array custom_mapping ] . each do |key |
376+ assert_includes out , "unresolved policy value for key: #{ key } "
377+ end
378+ end
379+ end
380+
339381 def test_optional_repo_prefix_accepts_valid_value_and_remains_optional
340382 with_repo do |root |
341383 write_valid_binstub_contract ( root )
@@ -413,6 +455,161 @@ def test_optional_autonomous_merge_policy_uses_the_shared_closed_schema
413455 end
414456 end
415457
458+ def test_optional_autonomous_merge_policy_accepts_an_exact_empty_mapping_seed
459+ with_repo do |root |
460+ write_valid_binstub_contract ( root )
461+ write_policy ( root , POLICY . merge ( "autonomous_merge" => { } ) )
462+ write_skill ( root , "No commands here.\n " )
463+
464+ policy_text = File . read ( File . join ( root , ".agents/agent-workflow.yml" ) , encoding : "UTF-8" )
465+ out , status = run_doctor ( root )
466+
467+ assert_includes policy_text , "autonomous_merge: {}\n "
468+ assert status . success? , out
469+ end
470+ end
471+
472+ def test_optional_autonomous_merge_policy_accepts_runtime_valid_empty_arrays
473+ with_repo do |root |
474+ write_valid_binstub_contract ( root )
475+ write_policy (
476+ root ,
477+ POLICY . merge (
478+ "autonomous_merge" => {
479+ "human_review_paths" => [ ] ,
480+ "policy_paths" => [ ] ,
481+ "generated_paths" => [ ]
482+ }
483+ )
484+ )
485+ write_skill ( root , "No commands here.\n " )
486+
487+ out , status = run_doctor ( root )
488+
489+ assert status . success? , out
490+ end
491+ end
492+
493+ def test_optional_autonomous_merge_policy_rejects_nested_unresolved_placeholders
494+ variants = {
495+ "legacy direct list scalar" => {
496+ "policy_paths" => [ "<base branch>" ]
497+ } ,
498+ "legacy nested mapping list scalar" => {
499+ "safe_path_groups" => {
500+ "documentation" => {
501+ "include" => [ "docs/**" ] ,
502+ "exclude" => [ "<base branch>" ]
503+ }
504+ }
505+ } ,
506+ "threshold relaxation rationale" => {
507+ "thresholds" => { "max_changed_files" => 30 } ,
508+ "threshold_relaxation" => {
509+ "rationale" => "<nonempty rationale covering all relaxed thresholds>"
510+ }
511+ } ,
512+ "policy path" => {
513+ "policy_paths" => [ "<repo-owned glob>" ]
514+ } ,
515+ "generated path" => {
516+ "generated_paths" => [ "<repo-owned generated glob>" ]
517+ } ,
518+ "human-review pattern" => {
519+ "human_review_paths" => [
520+ {
521+ "id" => "repo-owned-risk" ,
522+ "pattern" => "<repo-owned glob>" ,
523+ "reason" => "hot-path"
524+ }
525+ ]
526+ } ,
527+ "human-review other detail" => {
528+ "human_review_paths" => [
529+ {
530+ "id" => "repo-owned-risk" ,
531+ "pattern" => "app/**" ,
532+ "reason" => "other" ,
533+ "detail" => "<nonempty repo-owned reason>"
534+ }
535+ ]
536+ } ,
537+ "safe-path include" => {
538+ "safe_path_groups" => {
539+ "documentation" => {
540+ "include" => [ "<repo-owned glob>" ] ,
541+ "exclude" => [ ]
542+ }
543+ }
544+ } ,
545+ "safe-path exclude" => {
546+ "safe_path_groups" => {
547+ "documentation" => {
548+ "include" => [ "docs/**" ] ,
549+ "exclude" => [ "<repo-owned glob>" ]
550+ }
551+ }
552+ }
553+ }
554+ results = variants . transform_values do |autonomous_merge |
555+ with_repo do |root |
556+ write_valid_binstub_contract ( root )
557+ write_policy ( root , POLICY . merge ( "autonomous_merge" => autonomous_merge ) )
558+ write_skill ( root , "No commands here.\n " )
559+
560+ run_doctor ( root )
561+ end
562+ end
563+ failures = results . filter_map do |label , ( out , status ) |
564+ next unless status . success? || !out . include? ( "unresolved policy value for key: autonomous_merge" )
565+
566+ "#{ label } : status=#{ status . exitstatus } , output=#{ out . inspect } "
567+ end
568+
569+ assert_empty failures , failures . join ( "\n " )
570+ end
571+
572+ def test_optional_autonomous_merge_policy_allows_angle_bracket_text_inside_ordinary_prose
573+ rationales = [
574+ "Document <nonempty rationale covering all relaxed thresholds> after calibration." ,
575+ "Document <base branch> as historical context after calibration."
576+ ]
577+ rationales . each do |rationale |
578+ with_repo do |root |
579+ write_valid_binstub_contract ( root )
580+ write_policy (
581+ root ,
582+ POLICY . merge (
583+ "autonomous_merge" => {
584+ "thresholds" => { "max_changed_files" => 30 } ,
585+ "threshold_relaxation" => { "rationale" => rationale }
586+ }
587+ )
588+ )
589+ write_skill ( root , "No commands here.\n " )
590+
591+ out , status = run_doctor ( root )
592+
593+ assert status . success? , out
594+ end
595+ end
596+ end
597+
598+ def test_empty_collections_outside_autonomous_merge_remain_unresolved
599+ [ [ ] , { } ] . each do |empty_collection |
600+ with_repo do |root |
601+ write_valid_binstub_contract ( root )
602+ write_policy ( root , POLICY . merge ( "custom_runtime_paths" => empty_collection ) )
603+ write_skill ( root , "No commands here.\n " )
604+
605+ out , status = run_doctor ( root )
606+
607+ refute status . success? , empty_collection . class . name
608+ assert_includes out , "unresolved policy value for key: custom_runtime_paths" , empty_collection . class . name
609+ end
610+ end
611+ end
612+
416613 def test_invalid_autonomous_merge_policy_is_reported_by_the_seam_doctor
417614 with_repo do |root |
418615 write_valid_binstub_contract ( root )
0 commit comments