|
8 | 8 | import java.nio.file.Files; |
9 | 9 | import java.nio.file.Path; |
10 | 10 | import java.util.HashSet; |
| 11 | +import java.util.List; |
11 | 12 | import java.util.Map; |
12 | 13 | import java.util.Set; |
13 | 14 | import java.util.concurrent.ConcurrentHashMap; |
| 15 | +import java.util.function.BiFunction; |
14 | 16 | import java.util.logging.Logger; |
15 | 17 | import java.util.regex.Matcher; |
16 | 18 | import java.util.regex.Pattern; |
@@ -57,8 +59,19 @@ public class StubResponseRegistry { |
57 | 59 | private final Map<String, String> resourceCache = new ConcurrentHashMap<>(); |
58 | 60 | private volatile Path stubsDirectory; |
59 | 61 |
|
| 62 | + private final List<Lookup> userScenarioLadder; |
| 63 | + private final List<Lookup> defaultFallbackLadder; |
| 64 | + |
60 | 65 | private StubResponseRegistry() { |
61 | | - // Private constructor for singleton |
| 66 | + this.userScenarioLadder = |
| 67 | + List.of( |
| 68 | + new Lookup("registered", this::getRegisteredResponse), |
| 69 | + new Lookup("classpath resource", this::loadResourceResponse), |
| 70 | + new Lookup("filesystem", this::loadFilesystemResponse)); |
| 71 | + this.defaultFallbackLadder = |
| 72 | + List.of( |
| 73 | + new Lookup("default classpath resource", this::loadResourceResponse), |
| 74 | + new Lookup("default filesystem", this::loadFilesystemResponse)); |
62 | 75 | } |
63 | 76 |
|
64 | 77 | /// Returns the singleton registry instance. |
@@ -163,127 +176,54 @@ public void clearStubsDirectory() { |
163 | 176 | public String getResponse( |
164 | 177 | String nodeId, String agentId, Map<String, Object> context, String prompt) { |
165 | 178 | String scenario = getScenario(context); |
166 | | - String response; |
167 | | - |
168 | | - if (nodeId != null) { |
169 | | - response = getRegisteredResponse(scenario, nodeId); |
170 | | - if (response != null) { |
171 | | - logger.info( |
172 | | - "[STUB] Using registered response for node: " |
173 | | - + nodeId |
174 | | - + " (scenario: " |
175 | | - + scenario |
176 | | - + ")"); |
177 | | - return processResponse(response, context); |
178 | | - } |
179 | | - } |
180 | | - |
181 | | - if (agentId != null) { |
182 | | - response = getRegisteredResponse(scenario, agentId); |
183 | | - if (response != null) { |
184 | | - logger.info( |
185 | | - "[STUB] Using registered response for agent: " |
186 | | - + agentId |
187 | | - + " (scenario: " |
188 | | - + scenario |
189 | | - + ")"); |
190 | | - return processResponse(response, context); |
191 | | - } |
192 | | - } |
193 | | - |
194 | | - if (nodeId != null) { |
195 | | - response = loadResourceResponse(scenario, nodeId); |
196 | | - if (response != null) { |
197 | | - logger.info( |
198 | | - "[STUB] Loaded resource response for node: " |
199 | | - + nodeId |
200 | | - + " (scenario: " |
201 | | - + scenario |
202 | | - + ")"); |
203 | | - return processResponse(response, context); |
204 | | - } |
205 | | - } |
| 179 | + String[] identifiers = {nodeId, agentId}; |
206 | 180 |
|
207 | | - if (agentId != null) { |
208 | | - response = loadResourceResponse(scenario, agentId); |
209 | | - if (response != null) { |
210 | | - logger.info( |
211 | | - "[STUB] Loaded resource response for agent: " |
212 | | - + agentId |
213 | | - + " (scenario: " |
214 | | - + scenario |
215 | | - + ")"); |
216 | | - return processResponse(response, context); |
217 | | - } |
| 181 | + String response = firstHit(userScenarioLadder, scenario, identifiers); |
| 182 | + if (response == null && !DEFAULT_SCENARIO.equals(scenario)) { |
| 183 | + response = firstHit(defaultFallbackLadder, DEFAULT_SCENARIO, identifiers); |
218 | 184 | } |
219 | | - |
220 | | - // Filesystem lookup (working directory stubs) |
221 | | - if (nodeId != null) { |
222 | | - response = loadFilesystemResponse(scenario, nodeId); |
223 | | - if (response != null) { |
224 | | - logger.info( |
225 | | - "[STUB] Loaded filesystem response for node: " |
226 | | - + nodeId |
227 | | - + " (scenario: " |
228 | | - + scenario |
229 | | - + ")"); |
230 | | - return processResponse(response, context); |
231 | | - } |
| 185 | + if (response != null) { |
| 186 | + return processResponse(response, context); |
232 | 187 | } |
233 | 188 |
|
234 | | - if (agentId != null) { |
235 | | - response = loadFilesystemResponse(scenario, agentId); |
236 | | - if (response != null) { |
237 | | - logger.info( |
238 | | - "[STUB] Loaded filesystem response for agent: " |
239 | | - + agentId |
240 | | - + " (scenario: " |
241 | | - + scenario |
242 | | - + ")"); |
243 | | - return processResponse(response, context); |
244 | | - } |
245 | | - } |
246 | | - |
247 | | - if (!DEFAULT_SCENARIO.equals(scenario)) { |
248 | | - if (nodeId != null) { |
249 | | - response = loadResourceResponse(DEFAULT_SCENARIO, nodeId); |
250 | | - if (response != null) { |
251 | | - logger.info("[STUB] Loaded default resource for node: " + nodeId); |
252 | | - return processResponse(response, context); |
253 | | - } |
254 | | - } |
255 | | - |
256 | | - if (agentId != null) { |
257 | | - response = loadResourceResponse(DEFAULT_SCENARIO, agentId); |
258 | | - if (response != null) { |
259 | | - logger.info("[STUB] Loaded default resource for agent: " + agentId); |
260 | | - return processResponse(response, context); |
261 | | - } |
262 | | - } |
263 | | - |
264 | | - // Default scenario filesystem fallback |
265 | | - if (nodeId != null) { |
266 | | - response = loadFilesystemResponse(DEFAULT_SCENARIO, nodeId); |
267 | | - if (response != null) { |
268 | | - logger.info("[STUB] Loaded default filesystem response for node: " + nodeId); |
269 | | - return processResponse(response, context); |
270 | | - } |
271 | | - } |
| 189 | + logger.info( |
| 190 | + "[STUB] Generating fallback response for: " + (nodeId != null ? nodeId : agentId)); |
| 191 | + return null; |
| 192 | + } |
272 | 193 |
|
273 | | - if (agentId != null) { |
274 | | - response = loadFilesystemResponse(DEFAULT_SCENARIO, agentId); |
| 194 | + /// Walks the resolution ladder strategy-by-strategy, trying each identifier |
| 195 | + /// at each tier. Preserves strategy-major precedence: a registered stub for |
| 196 | + /// any identifier beats a filesystem stub for any identifier. |
| 197 | + /// |
| 198 | + /// @param ladder ordered list of lookup strategies |
| 199 | + /// @param scenario scenario name to query, not null |
| 200 | + /// @param identifiers candidate keys in specificity order (nulls skipped) |
| 201 | + /// @return the first non-null response, or null if none match |
| 202 | + private String firstHit(List<Lookup> ladder, String scenario, String[] identifiers) { |
| 203 | + for (Lookup lookup : ladder) { |
| 204 | + for (String id : identifiers) { |
| 205 | + if (id == null) continue; |
| 206 | + String response = lookup.fn.apply(scenario, id); |
275 | 207 | if (response != null) { |
276 | | - logger.info("[STUB] Loaded default filesystem response for agent: " + agentId); |
277 | | - return processResponse(response, context); |
| 208 | + logger.info( |
| 209 | + "[STUB] " |
| 210 | + + lookup.label |
| 211 | + + " matched for " |
| 212 | + + id |
| 213 | + + " (scenario: " |
| 214 | + + scenario |
| 215 | + + ")"); |
| 216 | + return response; |
278 | 217 | } |
279 | 218 | } |
280 | 219 | } |
281 | | - |
282 | | - logger.info( |
283 | | - "[STUB] Generating fallback response for: " + (nodeId != null ? nodeId : agentId)); |
284 | 220 | return null; |
285 | 221 | } |
286 | 222 |
|
| 223 | + /// Pairs a human-readable label with the lookup function for a single |
| 224 | + /// resolution tier. |
| 225 | + private record Lookup(String label, BiFunction<String, String, String> fn) {} |
| 226 | + |
287 | 227 | /// Determines the current scenario from context or system property. |
288 | 228 | /// |
289 | 229 | /// @param context execution context to check for `stub_scenario`, may be null |
|
0 commit comments