@@ -67,6 +67,91 @@ void checkUsesTheSameResultsAsThePublicJavaApi() {
6767 assertTrue (error .isEmpty ());
6868 }
6969
70+ @ Test
71+ void dottedRuleIdsAreAcceptedAndUnknownFieldsAreRejected () throws IOException {
72+ String base = Files .readString (configuration );
73+ String dotted = base .replace ("boundary" , "api.boundary" );
74+ configuration = writeConfiguration (dotted );
75+ assertEquals (CliExitCode .SUCCESS .code (), run ("validate-config" ).exit ());
76+ assertEquals (CliExitCode .POLICY_VIOLATION .code (), run ("check" ).exit ());
77+
78+ configuration = writeConfiguration (dotted + "rule.api.boundary.unknown=true\n " );
79+ assertEquals (CliExitCode .INVALID_CONFIGURATION .code (), run ("validate-config" ).exit ());
80+
81+ configuration = writeConfiguration (dotted + "rule.api.other.domain=types\n " );
82+ assertEquals (CliExitCode .INVALID_CONFIGURATION .code (), run ("validate-config" ).exit ());
83+ }
84+
85+ @ Test
86+ void graphRejectsIncompleteBytecodeUnlessExplicitlyAllowed () throws IOException {
87+ Files .write (classes .resolve ("Broken.class" ), new byte [] {0 , 1 , 2 , 3 });
88+ for (String format : new String [] {"dot" , "mermaid" , "json" , "csv" , "d2" , "html" }) {
89+ Invocation graph = run ("graph" , "--graph-format" , format );
90+ assertEquals (CliExitCode .ANALYSIS_ERROR .code (), graph .exit (), format );
91+ assertTrue (graph .out ().isEmpty (), format );
92+ assertTrue (graph .error ().contains ("incomplete" ), format );
93+ }
94+ configuration = writeConfiguration (Files .readString (configuration )
95+ .replace ("allowIncompleteAnalysis=false" , "allowIncompleteAnalysis=true" ));
96+ Invocation allowed = run ("graph" , "--graph-format" , "json" );
97+ assertEquals (CliExitCode .SUCCESS .code (), allowed .exit ());
98+ assertTrue (allowed .out ().contains ("api.A" ));
99+ }
100+
101+ @ Test
102+ void corruptInputArchiveCannotProduceAPassingPartialAnalysis () throws IOException {
103+ Files .write (root .resolve ("broken.jar" ), new byte [] {0 , 1 , 2 , 3 });
104+ configuration = writeConfiguration (Files .readString (configuration )
105+ .replace ("inputs=classes" , "inputs=classes,broken.jar" )
106+ .replace ("origins=exact:api.A" , "origins=exact:internal.B" ));
107+ Invocation check = run ("check" , "--result-format" , "json" );
108+ assertEquals (CliExitCode .ANALYSIS_ERROR .code (), check .exit ());
109+ assertTrue (check .out ().contains ("INCOMPLETE" ));
110+ assertEquals (CliExitCode .ANALYSIS_ERROR .code (), run ("graph" ).exit ());
111+ assertEquals (dev .archunitjava .result .RuleStatus .INCOMPLETE ,
112+ new CliAnalyzer ().analyze (CliConfigurationLoader .load (configuration , root ))
113+ .results ().results ().getFirst ().status ());
114+ configuration = writeConfiguration (Files .readString (configuration )
115+ .replace ("allowIncompleteAnalysis=false" , "allowIncompleteAnalysis=true" ));
116+ Invocation allowed = run ("check" , "--result-format" , "json" );
117+ assertEquals (CliExitCode .SUCCESS .code (), allowed .exit ());
118+ assertTrue (allowed .out ().contains ("cli.input.IO_FAILURE" ));
119+ assertTrue (allowed .out ().contains ("WARNING" ));
120+ }
121+
122+ @ Test
123+ void malformedClassCannotProduceAPassingPartialAnalysis () throws IOException {
124+ Files .write (classes .resolve ("Broken.class" ), new byte [] {0 , 1 , 2 , 3 });
125+ configuration = writeConfiguration (Files .readString (configuration )
126+ .replace ("origins=exact:api.A" , "origins=exact:internal.B" ));
127+ Invocation check = run ("check" , "--result-format" , "json" );
128+ assertEquals (CliExitCode .ANALYSIS_ERROR .code (), check .exit ());
129+ assertTrue (check .out ().contains ("cli.classfile." ));
130+ assertTrue (check .out ().contains ("INCOMPLETE" ));
131+ }
132+
133+ @ Test
134+ void incompleteChecksRetainObservedViolations () throws IOException {
135+ Files .write (classes .resolve ("Broken.class" ), new byte [] {0 , 1 , 2 , 3 });
136+ var result = new CliAnalyzer ().analyze (CliConfigurationLoader .load (configuration , root ))
137+ .results ().results ().getFirst ();
138+ assertEquals (dev .archunitjava .result .RuleStatus .INCOMPLETE , result .status ());
139+ assertFalse (result .violations ().isEmpty ());
140+ assertTrue (result .diagnostics ().stream ()
141+ .anyMatch (value -> value .code ().startsWith ("cli.classfile." )));
142+ }
143+
144+ @ Test
145+ void graphIgnoresRuleSelectionFailuresAndHarmlessDuplicateInputs () throws IOException {
146+ configuration = writeConfiguration (Files .readString (configuration )
147+ .replace ("inputs=classes" , "inputs=classes,./classes" )
148+ .replace ("targets=exact:internal.B" , "targets=exact:absent.C" ));
149+ Invocation graph = run ("graph" , "--graph-format" , "json" );
150+ assertEquals (CliExitCode .SUCCESS .code (), graph .exit ());
151+ assertTrue (graph .out ().contains ("api.A" ));
152+ assertTrue (graph .error ().isEmpty ());
153+ }
154+
70155 @ Test
71156 void graphExplainAndValidationCommandsAreStableAndBounded () {
72157 Invocation validation = run ("validate-config" );
0 commit comments