diff --git a/docs/reqstream/modeling/requirements.yaml b/docs/reqstream/modeling/requirements.yaml index a5d8ae1..cf88da1 100644 --- a/docs/reqstream/modeling/requirements.yaml +++ b/docs/reqstream/modeling/requirements.yaml @@ -153,7 +153,7 @@ sections: tags: - lint tests: - - Linter_Lint_WithMalformedYaml_ReportsError + - RequirementsLoader_Load_WithMalformedYaml_ReportsError - id: ReqStream-Lint-UnknownDocumentField title: >- @@ -165,7 +165,7 @@ sections: tags: - lint tests: - - Linter_Lint_WithUnknownDocumentField_ReportsError + - RequirementsLoader_Load_WithUnknownDocumentField_ReportsError - id: ReqStream-Lint-UnknownSectionField title: The requirements loader shall report an error when a section contains an unknown field. @@ -175,7 +175,7 @@ sections: tags: - lint tests: - - Linter_Lint_WithUnknownSectionField_ReportsError + - RequirementsLoader_Load_WithUnknownSectionField_ReportsError - id: ReqStream-Lint-MissingSectionTitle title: The requirements loader shall report an error when a section is missing the required title field. @@ -185,8 +185,8 @@ sections: tags: - lint tests: - - Linter_Lint_WithSectionMissingTitle_ReportsError - - Linter_Lint_WithBlankSectionTitle_ReportsError + - RequirementsLoader_Load_WithSectionMissingTitle_ReportsError + - RequirementsLoader_Load_WithBlankSectionTitle_ReportsError - id: ReqStream-Lint-UnknownRequirementField title: The requirements loader shall report an error when a requirement contains an unknown field. @@ -196,8 +196,8 @@ sections: tags: - lint tests: - - Linter_Lint_WithUnknownRequirementField_ReportsError - - Linter_Lint_WithNestedSectionIssues_ReportsError + - RequirementsLoader_Load_WithUnknownRequirementField_ReportsError + - RequirementsLoader_Load_WithNestedSectionIssues_ReportsError - id: ReqStream-Lint-MissingRequirementFields title: >- @@ -209,10 +209,10 @@ sections: tags: - lint tests: - - Linter_Lint_WithRequirementMissingId_ReportsError - - Linter_Lint_WithRequirementMissingTitle_ReportsError - - Linter_Lint_WithBlankRequirementId_ReportsError - - Linter_Lint_WithBlankRequirementTitle_ReportsError + - RequirementsLoader_Load_WithRequirementMissingId_ReportsError + - RequirementsLoader_Load_WithRequirementMissingTitle_ReportsError + - RequirementsLoader_Load_WithBlankRequirementId_ReportsError + - RequirementsLoader_Load_WithBlankRequirementTitle_ReportsError - id: ReqStream-Lint-DuplicateIds title: The requirements loader shall report an error when duplicate requirement IDs are found. @@ -222,8 +222,8 @@ sections: tags: - lint tests: - - Linter_Lint_WithDuplicateIds_ReportsError - - Linter_Lint_WithDuplicateIdsAcrossFiles_ReportsError + - RequirementsLoader_Load_WithDuplicateIds_ReportsError + - RequirementsLoader_Load_WithDuplicateIdsAcrossFiles_ReportsError - id: ReqStream-Lint-MultipleIssues title: The requirements loader shall report all issues found rather than stopping at the first issue. @@ -233,7 +233,7 @@ sections: tags: - lint tests: - - Linter_Lint_WithMultipleIssues_ReportsAllIssues + - RequirementsLoader_Load_WithMultipleIssues_ReportsAllIssues - id: ReqStream-Lint-FollowsIncludes title: The requirements loader shall follow include directives and load all included files. @@ -243,7 +243,7 @@ sections: tags: - lint tests: - - Linter_Lint_WithIncludes_LintsIncludedFiles + - RequirementsLoader_Load_WithIncludes_LintsIncludedFiles - id: ReqStream-Lint-NoIssuesMessage title: The requirements loader shall report a no-issues result when no problems are found. @@ -253,8 +253,8 @@ sections: tags: - lint tests: - - Linter_Lint_WithValidFile_ReportsNoIssues - - Linter_Lint_WithEmptyFile_ReportsNoIssues + - RequirementsLoader_Load_WithValidFile_ReportsNoIssues + - RequirementsLoader_Load_WithEmptyFile_ReportsNoIssues - id: ReqStream-Lint-ErrorFormat title: "The requirements loader shall format errors as \"[location]: [severity]: [description]\"." @@ -264,7 +264,7 @@ sections: tags: - lint tests: - - Linter_Lint_ErrorFormat_IncludesFileAndLocation + - RequirementsLoader_Load_ErrorFormat_IncludesFileAndLocation - id: ReqStream-Lint-UnknownMappingField title: The requirements loader shall report an error when a test mapping contains an unknown field. @@ -274,7 +274,7 @@ sections: tags: - lint tests: - - Linter_Lint_WithUnknownMappingField_ReportsError + - RequirementsLoader_Load_WithUnknownMappingField_ReportsError - id: ReqStream-Lint-MissingMappingId title: The requirements loader shall report an error when a test mapping is missing or has a blank id field. @@ -284,8 +284,8 @@ sections: tags: - lint tests: - - Linter_Lint_WithMappingMissingId_ReportsError - - Linter_Lint_WithBlankMappingId_ReportsError + - RequirementsLoader_Load_WithMappingMissingId_ReportsError + - RequirementsLoader_Load_WithBlankMappingId_ReportsError - id: ReqStream-Lint-BlankTestName title: The requirements loader shall report an error when a test name in a requirement or mapping is blank. @@ -295,8 +295,8 @@ sections: tags: - lint tests: - - Linter_Lint_WithBlankTestName_ReportsError - - Linter_Lint_WithBlankMappingTestName_ReportsError + - RequirementsLoader_Load_WithBlankTestName_ReportsError + - RequirementsLoader_Load_WithBlankMappingTestName_ReportsError - id: ReqStream-Lint-BlankTagName title: The requirements loader shall report an error when a tag name in a requirement is blank. @@ -306,7 +306,7 @@ sections: tags: - lint tests: - - Linter_Lint_WithBlankTagName_ReportsError + - RequirementsLoader_Load_WithBlankTagName_ReportsError - id: ReqStream-Lint-IssueType title: The requirements loader shall represent each issue with a location, severity, and description. @@ -328,11 +328,11 @@ sections: tags: - lint tests: - - Linter_Lint_WithNonScalarTestEntry_ReportsError - - Linter_Lint_WithNonScalarChildEntry_ReportsError - - Linter_Lint_WithNonScalarTagEntry_ReportsError - - Linter_Lint_WithNonScalarMappingTestEntry_ReportsError - - Linter_Lint_WithNonScalarIncludeEntry_ReportsError + - RequirementsLoader_Load_WithNonScalarTestEntry_ReportsError + - RequirementsLoader_Load_WithNonScalarChildEntry_ReportsError + - RequirementsLoader_Load_WithNonScalarTagEntry_ReportsError + - RequirementsLoader_Load_WithNonScalarMappingTestEntry_ReportsError + - RequirementsLoader_Load_WithNonScalarIncludeEntry_ReportsError - id: ReqStream-Lint-CircularReferences title: >- @@ -344,4 +344,17 @@ sections: tags: - lint tests: - - Linter_Lint_WithMultipleCycles_ReportsAllCycles + - RequirementsLoader_Load_WithMultipleCycles_ReportsAllCycles + + - id: ReqStream-Lint-UnknownChildReference + title: >- + The requirements loader shall report an error when a requirement references a child + requirement ID that does not exist. + justification: | + References to non-existent child requirement IDs indicate typos or stale references. + Reporting these as errors ensures that requirement hierarchies remain consistent and + prevents silent failures in trace matrix generation. + tags: + - lint + tests: + - RequirementsLoader_Load_WithUnknownChildReference_ReportsError diff --git a/docs/reqstream/program.yaml b/docs/reqstream/program.yaml index e3a9c30..d405a67 100644 --- a/docs/reqstream/program.yaml +++ b/docs/reqstream/program.yaml @@ -71,5 +71,5 @@ sections: - cli - requirements tests: - - Linter_ProgramRun_WithLintFlag_RunsLinter + - Program_Run_WithLintFlag_RunsLinter - ReqStream_Lint diff --git a/src/DemaConsulting.ReqStream/Modeling/RequirementsLoader.cs b/src/DemaConsulting.ReqStream/Modeling/RequirementsLoader.cs index 5528198..1fe53ff 100644 --- a/src/DemaConsulting.ReqStream/Modeling/RequirementsLoader.cs +++ b/src/DemaConsulting.ReqStream/Modeling/RequirementsLoader.cs @@ -707,13 +707,20 @@ private static void ValidateCyclesFrom( { foreach (var childId in requirement.Children) { + if (!allRequirements.TryGetValue(childId, out var childReq)) + { + issues.Add(new LintIssue( + requirement.Location ?? reqId, + LintSeverity.Error, + $"Requirement '{reqId}' references unknown child '{childId}'")); + continue; + } + if (visiting.Contains(childId)) { var cycleStart = currentPath.IndexOf(childId); var cyclePath = string.Join(" -> ", currentPath.Skip(cycleStart).Append(childId)); - var location = allRequirements.TryGetValue(childId, out var cycleReq) && cycleReq.Location != null - ? cycleReq.Location - : childId; + var location = childReq.Location ?? childId; issues.Add(new LintIssue( location, LintSeverity.Error, diff --git a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadParsingTests.cs b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadParsingTests.cs index cbf78aa..132b129 100644 --- a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadParsingTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoadParsingTests.cs @@ -128,6 +128,10 @@ public void Requirements_Load_RequirementWithChildren_ParsesChildrenCorrectly() children: - ""AUTH-001"" - ""AUTH-002"" + - id: ""AUTH-001"" + title: ""The system shall validate user credentials."" + - id: ""AUTH-002"" + title: ""The system shall reject invalid credentials."" "; var filePath = Path.Combine(_testDirectory, "requirements.yaml"); File.WriteAllText(filePath, yamlContent); diff --git a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoaderTests.cs b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoaderTests.cs index 1429e34..2e2d600 100644 --- a/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoaderTests.cs +++ b/test/DemaConsulting.ReqStream.Tests/Modeling/RequirementsLoaderTests.cs @@ -82,7 +82,7 @@ private static (int exitCode, string output, string errors) RunLintWithOutput(pa /// Test that loading with no files produces no issues (empty list). /// [TestMethod] - public void Linter_Lint_WithNoFiles_PrintsMessage() + public void RequirementsLoader_Load_WithNoFiles_PrintsMessage() { var originalOut = Console.Out; using var output = new StringWriter(); @@ -106,7 +106,7 @@ public void Linter_Lint_WithNoFiles_PrintsMessage() /// Test that a valid requirements file produces no issues. /// [TestMethod] - public void Linter_Lint_WithValidFile_ReportsNoIssues() + public void RequirementsLoader_Load_WithValidFile_ReportsNoIssues() { var reqFile = Path.Combine(_testDirectory, "valid.yaml"); File.WriteAllText(reqFile, @"sections: @@ -131,7 +131,7 @@ public void Linter_Lint_WithValidFile_ReportsNoIssues() /// Test that a file that doesn't exist reports an error. /// [TestMethod] - public void Linter_Lint_WithMissingFile_ReportsError() + public void RequirementsLoader_Load_WithMissingFile_ReportsError() { var (exitCode, errors) = RunLint("/nonexistent/path/missing.yaml"); @@ -144,7 +144,7 @@ public void Linter_Lint_WithMissingFile_ReportsError() /// Test that malformed YAML reports an error. /// [TestMethod] - public void Linter_Lint_WithMalformedYaml_ReportsError() + public void RequirementsLoader_Load_WithMalformedYaml_ReportsError() { var reqFile = Path.Combine(_testDirectory, "malformed.yaml"); File.WriteAllText(reqFile, @"sections: @@ -164,7 +164,7 @@ invalid yaml here /// Test that an empty YAML file produces no issues. /// [TestMethod] - public void Linter_Lint_WithEmptyFile_ReportsNoIssues() + public void RequirementsLoader_Load_WithEmptyFile_ReportsNoIssues() { var reqFile = Path.Combine(_testDirectory, "empty.yaml"); File.WriteAllText(reqFile, string.Empty); @@ -180,7 +180,7 @@ public void Linter_Lint_WithEmptyFile_ReportsNoIssues() /// Test that an unknown field at document root reports an error. /// [TestMethod] - public void Linter_Lint_WithUnknownDocumentField_ReportsError() + public void RequirementsLoader_Load_WithUnknownDocumentField_ReportsError() { var reqFile = Path.Combine(_testDirectory, "unknown-field.yaml"); File.WriteAllText(reqFile, @"sections: @@ -198,7 +198,7 @@ public void Linter_Lint_WithUnknownDocumentField_ReportsError() /// Test that a section missing the title field reports an error. /// [TestMethod] - public void Linter_Lint_WithSectionMissingTitle_ReportsError() + public void RequirementsLoader_Load_WithSectionMissingTitle_ReportsError() { var reqFile = Path.Combine(_testDirectory, "missing-title.yaml"); File.WriteAllText(reqFile, @"sections: @@ -217,7 +217,7 @@ public void Linter_Lint_WithSectionMissingTitle_ReportsError() /// Test that a section with a blank title reports an error. /// [TestMethod] - public void Linter_Lint_WithBlankSectionTitle_ReportsError() + public void RequirementsLoader_Load_WithBlankSectionTitle_ReportsError() { var reqFile = Path.Combine(_testDirectory, "blank-title.yaml"); File.WriteAllText(reqFile, @"sections: @@ -237,7 +237,7 @@ public void Linter_Lint_WithBlankSectionTitle_ReportsError() /// Test that a section with an unknown field reports an error. /// [TestMethod] - public void Linter_Lint_WithUnknownSectionField_ReportsError() + public void RequirementsLoader_Load_WithUnknownSectionField_ReportsError() { var reqFile = Path.Combine(_testDirectory, "unknown-section-field.yaml"); File.WriteAllText(reqFile, @"sections: @@ -255,7 +255,7 @@ public void Linter_Lint_WithUnknownSectionField_ReportsError() /// Test that a requirement missing the id field reports an error. /// [TestMethod] - public void Linter_Lint_WithRequirementMissingId_ReportsError() + public void RequirementsLoader_Load_WithRequirementMissingId_ReportsError() { var reqFile = Path.Combine(_testDirectory, "missing-id.yaml"); File.WriteAllText(reqFile, @"sections: @@ -274,7 +274,7 @@ public void Linter_Lint_WithRequirementMissingId_ReportsError() /// Test that a requirement missing the title field reports an error. /// [TestMethod] - public void Linter_Lint_WithRequirementMissingTitle_ReportsError() + public void RequirementsLoader_Load_WithRequirementMissingTitle_ReportsError() { var reqFile = Path.Combine(_testDirectory, "missing-req-title.yaml"); File.WriteAllText(reqFile, @"sections: @@ -293,7 +293,7 @@ public void Linter_Lint_WithRequirementMissingTitle_ReportsError() /// Test that a requirement with an unknown field reports an error. /// [TestMethod] - public void Linter_Lint_WithUnknownRequirementField_ReportsError() + public void RequirementsLoader_Load_WithUnknownRequirementField_ReportsError() { var reqFile = Path.Combine(_testDirectory, "unknown-req-field.yaml"); File.WriteAllText(reqFile, @"sections: @@ -314,7 +314,7 @@ public void Linter_Lint_WithUnknownRequirementField_ReportsError() /// Test that duplicate requirement IDs report an error. /// [TestMethod] - public void Linter_Lint_WithDuplicateIds_ReportsError() + public void RequirementsLoader_Load_WithDuplicateIds_ReportsError() { var reqFile = Path.Combine(_testDirectory, "duplicates.yaml"); File.WriteAllText(reqFile, @"sections: @@ -336,7 +336,7 @@ public void Linter_Lint_WithDuplicateIds_ReportsError() /// Test that duplicate IDs across multiple files report an error. /// [TestMethod] - public void Linter_Lint_WithDuplicateIdsAcrossFiles_ReportsError() + public void RequirementsLoader_Load_WithDuplicateIdsAcrossFiles_ReportsError() { var reqFile1 = Path.Combine(_testDirectory, "file1.yaml"); File.WriteAllText(reqFile1, @"sections: @@ -364,7 +364,7 @@ public void Linter_Lint_WithDuplicateIdsAcrossFiles_ReportsError() /// Test that multiple issues are all reported. /// [TestMethod] - public void Linter_Lint_WithMultipleIssues_ReportsAllIssues() + public void RequirementsLoader_Load_WithMultipleIssues_ReportsAllIssues() { var reqFile = Path.Combine(_testDirectory, "multiple-issues.yaml"); File.WriteAllText(reqFile, @"sections: @@ -392,7 +392,7 @@ public void Linter_Lint_WithMultipleIssues_ReportsAllIssues() /// Test that loading follows includes and lints included files. /// [TestMethod] - public void Linter_Lint_WithIncludes_LintsIncludedFiles() + public void RequirementsLoader_Load_WithIncludes_LintsIncludedFiles() { var includedFile = Path.Combine(_testDirectory, "included.yaml"); File.WriteAllText(includedFile, @"sections: @@ -423,7 +423,7 @@ public void Linter_Lint_WithIncludes_LintsIncludedFiles() /// Test that --lint via Program.Run works correctly. /// [TestMethod] - public void Linter_ProgramRun_WithLintFlag_RunsLinter() + public void Program_Run_WithLintFlag_RunsLinter() { var reqFile = Path.Combine(_testDirectory, "valid.yaml"); File.WriteAllText(reqFile, @"sections: @@ -459,7 +459,7 @@ public void Linter_ProgramRun_WithLintFlag_RunsLinter() /// Test that a mapping with an unknown field reports an error. /// [TestMethod] - public void Linter_Lint_WithUnknownMappingField_ReportsError() + public void RequirementsLoader_Load_WithUnknownMappingField_ReportsError() { var reqFile = Path.Combine(_testDirectory, "unknown-mapping-field.yaml"); File.WriteAllText(reqFile, @"sections: @@ -484,7 +484,7 @@ public void Linter_Lint_WithUnknownMappingField_ReportsError() /// Test that a mapping missing id reports an error. /// [TestMethod] - public void Linter_Lint_WithMappingMissingId_ReportsError() + public void RequirementsLoader_Load_WithMappingMissingId_ReportsError() { var reqFile = Path.Combine(_testDirectory, "mapping-missing-id.yaml"); File.WriteAllText(reqFile, @"sections: @@ -507,7 +507,7 @@ public void Linter_Lint_WithMappingMissingId_ReportsError() /// Test that a nested section with issues is linted. /// [TestMethod] - public void Linter_Lint_WithNestedSectionIssues_ReportsError() + public void RequirementsLoader_Load_WithNestedSectionIssues_ReportsError() { var reqFile = Path.Combine(_testDirectory, "nested.yaml"); File.WriteAllText(reqFile, @"sections: @@ -532,7 +532,7 @@ public void Linter_Lint_WithNestedSectionIssues_ReportsError() /// Test that error format includes file path and line/column info. /// [TestMethod] - public void Linter_Lint_ErrorFormat_IncludesFileAndLocation() + public void RequirementsLoader_Load_ErrorFormat_IncludesFileAndLocation() { var reqFile = Path.Combine(_testDirectory, "format-test.yaml"); File.WriteAllText(reqFile, @"unknown_field: value @@ -552,7 +552,7 @@ public void Linter_Lint_ErrorFormat_IncludesFileAndLocation() /// Test that a requirement with a blank id reports an error. /// [TestMethod] - public void Linter_Lint_WithBlankRequirementId_ReportsError() + public void RequirementsLoader_Load_WithBlankRequirementId_ReportsError() { var reqFile = Path.Combine(_testDirectory, "blank-req-id.yaml"); File.WriteAllText(reqFile, @"sections: @@ -572,7 +572,7 @@ public void Linter_Lint_WithBlankRequirementId_ReportsError() /// Test that a requirement with a blank title reports an error. /// [TestMethod] - public void Linter_Lint_WithBlankRequirementTitle_ReportsError() + public void RequirementsLoader_Load_WithBlankRequirementTitle_ReportsError() { var reqFile = Path.Combine(_testDirectory, "blank-req-title.yaml"); File.WriteAllText(reqFile, @"sections: @@ -592,7 +592,7 @@ public void Linter_Lint_WithBlankRequirementTitle_ReportsError() /// Test that a mapping with a blank id reports an error. /// [TestMethod] - public void Linter_Lint_WithBlankMappingId_ReportsError() + public void RequirementsLoader_Load_WithBlankMappingId_ReportsError() { var reqFile = Path.Combine(_testDirectory, "blank-mapping-id.yaml"); File.WriteAllText(reqFile, @"sections: @@ -616,7 +616,7 @@ public void Linter_Lint_WithBlankMappingId_ReportsError() /// Test that a blank test name in a requirement reports an error. /// [TestMethod] - public void Linter_Lint_WithBlankTestName_ReportsError() + public void RequirementsLoader_Load_WithBlankTestName_ReportsError() { var reqFile = Path.Combine(_testDirectory, "blank-test-name.yaml"); File.WriteAllText(reqFile, @"sections: @@ -638,7 +638,7 @@ public void Linter_Lint_WithBlankTestName_ReportsError() /// Test that a blank tag name in a requirement reports an error. /// [TestMethod] - public void Linter_Lint_WithBlankTagName_ReportsError() + public void RequirementsLoader_Load_WithBlankTagName_ReportsError() { var reqFile = Path.Combine(_testDirectory, "blank-tag-name.yaml"); File.WriteAllText(reqFile, @"sections: @@ -660,7 +660,7 @@ public void Linter_Lint_WithBlankTagName_ReportsError() /// Test that a mapping with a blank test name reports an error. /// [TestMethod] - public void Linter_Lint_WithBlankMappingTestName_ReportsError() + public void RequirementsLoader_Load_WithBlankMappingTestName_ReportsError() { var reqFile = Path.Combine(_testDirectory, "blank-mapping-test-name.yaml"); File.WriteAllText(reqFile, @"sections: @@ -684,7 +684,7 @@ public void Linter_Lint_WithBlankMappingTestName_ReportsError() /// Test that a requirements file with a non-mapping root (e.g. a top-level sequence) reports an error. /// [TestMethod] - public void Linter_Lint_WithNonMappingRoot_ReportsError() + public void RequirementsLoader_Load_WithNonMappingRoot_ReportsError() { var reqFile = Path.Combine(_testDirectory, "non-mapping-root.yaml"); File.WriteAllText(reqFile, @"- item1 @@ -701,7 +701,7 @@ public void Linter_Lint_WithNonMappingRoot_ReportsError() /// Test that a non-scalar entry in the tests list of a requirement reports an error. /// [TestMethod] - public void Linter_Lint_WithNonScalarTestEntry_ReportsError() + public void RequirementsLoader_Load_WithNonScalarTestEntry_ReportsError() { var reqFile = Path.Combine(_testDirectory, "non-scalar-test.yaml"); File.WriteAllText(reqFile, @"sections: @@ -723,7 +723,7 @@ public void Linter_Lint_WithNonScalarTestEntry_ReportsError() /// Test that a non-scalar entry in the children list of a requirement reports an error. /// [TestMethod] - public void Linter_Lint_WithNonScalarChildEntry_ReportsError() + public void RequirementsLoader_Load_WithNonScalarChildEntry_ReportsError() { var reqFile = Path.Combine(_testDirectory, "non-scalar-child.yaml"); File.WriteAllText(reqFile, @"sections: @@ -745,7 +745,7 @@ public void Linter_Lint_WithNonScalarChildEntry_ReportsError() /// Test that a non-scalar entry in the tags list of a requirement reports an error. /// [TestMethod] - public void Linter_Lint_WithNonScalarTagEntry_ReportsError() + public void RequirementsLoader_Load_WithNonScalarTagEntry_ReportsError() { var reqFile = Path.Combine(_testDirectory, "non-scalar-tag.yaml"); File.WriteAllText(reqFile, @"sections: @@ -767,7 +767,7 @@ public void Linter_Lint_WithNonScalarTagEntry_ReportsError() /// Test that a non-scalar entry in the tests list of a mapping reports an error. /// [TestMethod] - public void Linter_Lint_WithNonScalarMappingTestEntry_ReportsError() + public void RequirementsLoader_Load_WithNonScalarMappingTestEntry_ReportsError() { var reqFile = Path.Combine(_testDirectory, "non-scalar-mapping-test.yaml"); File.WriteAllText(reqFile, @"sections: @@ -791,7 +791,7 @@ public void Linter_Lint_WithNonScalarMappingTestEntry_ReportsError() /// Test that a non-scalar entry in the includes list reports an error. /// [TestMethod] - public void Linter_Lint_WithNonScalarIncludeEntry_ReportsError() + public void RequirementsLoader_Load_WithNonScalarIncludeEntry_ReportsError() { var reqFile = Path.Combine(_testDirectory, "non-scalar-include.yaml"); File.WriteAllText(reqFile, @"includes: @@ -808,7 +808,7 @@ public void Linter_Lint_WithNonScalarIncludeEntry_ReportsError() /// Test that multiple cycles in the requirement children graph are all reported. /// [TestMethod] - public void Linter_Lint_WithMultipleCycles_ReportsAllCycles() + public void RequirementsLoader_Load_WithMultipleCycles_ReportsAllCycles() { var reqFile = Path.Combine(_testDirectory, "multiple-cycles.yaml"); File.WriteAllText(reqFile, @"sections: @@ -838,4 +838,28 @@ public void Linter_Lint_WithMultipleCycles_ReportsAllCycles() .Count(line => line.Contains("Circular requirement reference detected")); Assert.AreEqual(2, cycleCount, $"Expected exactly 2 cycle errors, got {cycleCount}: {errors}"); } + + /// + /// Test that a child reference to a non-existent requirement ID is reported as an error. + /// + [TestMethod] + public void RequirementsLoader_Load_WithUnknownChildReference_ReportsError() + { + var reqFile = Path.Combine(_testDirectory, "unknown-child.yaml"); + File.WriteAllText(reqFile, @"sections: + - title: Test Section + requirements: + - id: PARENT + title: Parent Requirement + children: + - NONEXISTENT +"); + + var (exitCode, errors) = RunLint(reqFile); + + Assert.AreEqual(1, exitCode); + Assert.IsTrue(errors.Contains("PARENT"), $"Expected 'PARENT' in errors: {errors}"); + Assert.IsTrue(errors.Contains("NONEXISTENT"), $"Expected 'NONEXISTENT' in errors: {errors}"); + Assert.IsTrue(errors.Contains("unknown child"), $"Expected 'unknown child' in errors: {errors}"); + } }