@@ -478,3 +478,161 @@ def test_fix_command_in_commands_directory(self):
478478 assert fix_cmd .is_file (), (
479479 "templates/commands/fix.md must exist for scaffold loop inclusion"
480480 )
481+
482+
483+ # ---------------------------------------------------------------------------
484+ # 5. ask.md — grounded Q&A command
485+ # ---------------------------------------------------------------------------
486+
487+ _ASK_CMD = _REPO_ROOT / "templates" / "commands" / "ask.md"
488+
489+
490+ class TestAskCommand :
491+ """Validate the /speckit.ask command template."""
492+
493+ @pytest .fixture (scope = "class" )
494+ def content (self ) -> str :
495+ return _ASK_CMD .read_text (encoding = "utf-8" )
496+
497+ @pytest .fixture (scope = "class" )
498+ def frontmatter (self , content ) -> dict :
499+ fm , _ = _parse_frontmatter (content )
500+ return fm
501+
502+ @pytest .fixture (scope = "class" )
503+ def body (self , content ) -> str :
504+ _ , b = _parse_frontmatter (content )
505+ return b
506+
507+ # --- File & frontmatter ---
508+
509+ def test_file_exists (self ):
510+ assert _ASK_CMD .is_file (), "templates/commands/ask.md is missing"
511+
512+ def test_frontmatter_parseable (self , content ):
513+ assert content .startswith ("---" ), "ask.md must start with YAML frontmatter"
514+ fm , _ = _parse_frontmatter (content )
515+ assert isinstance (fm , dict )
516+
517+ def test_has_nonempty_description (self , frontmatter ):
518+ assert frontmatter .get ("description" ), "ask.md frontmatter must have a non-empty 'description'"
519+
520+ def test_has_scripts_sh_and_ps (self , frontmatter ):
521+ scripts = frontmatter .get ("scripts" , {}) or {}
522+ assert "sh" in scripts , "ask.md frontmatter must have 'scripts.sh'"
523+ assert "ps" in scripts , "ask.md frontmatter must have 'scripts.ps'"
524+
525+ def test_scripts_reference_check_prerequisites (self , frontmatter ):
526+ scripts = frontmatter .get ("scripts" , {}) or {}
527+ assert "check-prerequisites.sh" in scripts .get ("sh" , "" )
528+ assert "check-prerequisites.ps1" in scripts .get ("ps" , "" )
529+
530+ def test_has_arguments_placeholder (self , body ):
531+ assert "$ARGUMENTS" in body , "ask.md must contain $ARGUMENTS placeholder"
532+
533+ def test_no_toml_double_brace_leak (self , body ):
534+ assert "{{args}}" not in body
535+
536+ # --- Phase 0: question classification ---
537+
538+ def test_phase_zero_present (self , body ):
539+ assert "Phase 0" in body , "ask.md must contain Phase 0 (question classification)"
540+
541+ def test_classification_table_covers_workflow (self , body ):
542+ assert "workflow" in body , "ask.md Phase 0 must cover 'workflow' question category"
543+
544+ def test_classification_table_covers_spec (self , body ):
545+ assert "spec" in body .lower (), "ask.md Phase 0 must cover 'spec' question category"
546+
547+ def test_classification_table_covers_constitution (self , body ):
548+ assert "constitution" in body , "ask.md Phase 0 must cover 'constitution' question category"
549+
550+ def test_fast_redirect_to_fix (self , body ):
551+ """Error questions must be immediately redirected to /speckit.fix."""
552+ assert "speckit.fix" in body , "ask.md must redirect error questions to /speckit.fix"
553+
554+ def test_fast_redirect_to_specify (self , body ):
555+ """Feature-gap questions must be immediately redirected to /speckit.specify."""
556+ assert "speckit.specify" in body , "ask.md must redirect feature requests to /speckit.specify"
557+
558+ # --- Phase 2: structured answer block ---
559+
560+ def test_answer_block_has_question_field (self , body ):
561+ assert "QUESTION" in body , "ask.md Phase 2 answer block must contain QUESTION field"
562+
563+ def test_answer_block_has_category_field (self , body ):
564+ assert "CATEGORY" in body , "ask.md Phase 2 answer block must contain CATEGORY field"
565+
566+ def test_answer_block_has_grounded_in_field (self , body ):
567+ assert "GROUNDED IN" in body , "ask.md Phase 2 answer block must contain GROUNDED IN field"
568+
569+ def test_answer_block_has_confidence_field (self , body ):
570+ assert "CONFIDENCE" in body , "ask.md Phase 2 answer block must contain CONFIDENCE field"
571+
572+ def test_constitution_read_when_decision_touched (self , body ):
573+ """constitution.md must be loaded when the answer touches a project principle."""
574+ assert "constitution" in body .lower (), (
575+ "ask.md must instruct loading constitution.md when architectural decisions are involved"
576+ )
577+
578+ # --- Phase 3: routing ---
579+
580+ def test_routing_section_present (self , body ):
581+ assert "SUGGESTED NEXT" in body or "Phase 3" in body , (
582+ "ask.md must contain a routing phase (Phase 3 / SUGGESTED NEXT)"
583+ )
584+
585+ def test_routing_covers_clarify (self , body ):
586+ assert "speckit.clarify" in body
587+
588+ def test_routing_covers_plan (self , body ):
589+ assert "speckit.plan" in body
590+
591+ def test_routing_covers_analyze (self , body ):
592+ assert "speckit.analyze" in body
593+
594+ def test_routing_covers_tasks (self , body ):
595+ assert "speckit.tasks" in body
596+
597+ def test_routing_covers_implement (self , body ):
598+ assert "speckit.implement" in body , (
599+ "ask.md routing must include /speckit.implement for when tasks are ready to execute"
600+ )
601+
602+ def test_routing_covers_taskstoissues (self , body ):
603+ assert "speckit.taskstoissues" in body , (
604+ "ask.md routing must include /speckit.taskstoissues for edge-case tracking"
605+ )
606+
607+ def test_routing_requires_reason_per_suggestion (self , body ):
608+ """Each routing suggestion must be accompanied by a reason (no blind suggestions)."""
609+ assert "reason" in body .lower () or "why" in body .lower () or "warranted" in body .lower (), (
610+ "ask.md must require that every routing suggestion includes a reason"
611+ )
612+
613+ # --- Phase 4: confidence check ---
614+
615+ def test_low_confidence_triggers_clarification (self , body ):
616+ assert "low" in body .lower () and "CONFIDENCE" in body , (
617+ "ask.md must handle low-confidence answers with a clarification block"
618+ )
619+
620+ def test_max_two_clarifying_questions (self , body ):
621+ assert "2" in body or "two" in body .lower (), (
622+ "ask.md must cap clarifying questions at 2 when confidence is low"
623+ )
624+
625+ # --- Bundle inclusion ---
626+
627+ def test_ask_stem_in_commands_directory (self ):
628+ commands_dir = _REPO_ROOT / "templates" / "commands"
629+ assert (commands_dir / "ask.md" ).is_file (), (
630+ "templates/commands/ask.md must exist for scaffold loop inclusion"
631+ )
632+
633+ def test_fix_command_map_includes_ask (self ):
634+ """fix.md command map must list speckit.ask so agents know it exists."""
635+ fix_text = _FIX_CMD .read_text (encoding = "utf-8" )
636+ assert "speckit.ask" in fix_text , (
637+ "fix.md command map must reference /speckit.ask"
638+ )
0 commit comments