abapgit-agent 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@ This folder contains detailed ABAP coding guidelines that can be searched using
14
14
  | `06_objects.md` | Object Naming Conventions |
15
15
  | `07_json.md` | JSON Handling |
16
16
  | `08_abapgit.md` | abapGit XML Metadata Templates |
17
+ | `09_unit_testable_code.md` | Unit Testable Code Guidelines (Dependency Injection) |
17
18
 
18
19
  ## Usage
19
20
 
@@ -1,7 +1,16 @@
1
1
  # ABAP SQL Best Practices
2
2
 
3
+ **Searchable keywords**: SELECT, FROM, WHERE, ABAP SQL, Open SQL, host variable, @ prefix, range table, INTO, UP TO, OFFSET, GROUP BY, JOIN
4
+
3
5
  When writing ABAP SQL (Open SQL) queries, follow these rules:
4
6
 
7
+ ## TOPICS IN THIS FILE
8
+ 1. Host Variables (@ prefix) - line 5
9
+ 2. Range Tables (IN clause) - line 17
10
+ 3. SELECT Clause Order - line 35
11
+ 4. Fixed Point Arithmetic - line 52
12
+ 5. Field Separation - line 62
13
+
5
14
  ## 1. Host Variables - Use @ Prefix
6
15
 
7
16
  Use `@` prefix for host variables in ABAP SQL:
@@ -70,3 +79,10 @@ SELECT object, obj_name FROM tadir ...
70
79
  " Wrong - missing comma
71
80
  SELECT object obj_name FROM tadir ...
72
81
  ```
82
+
83
+ ---
84
+
85
+ ## See Also
86
+ - **Constructor Expressions** (05_classes.md) - for VALUE #(), FILTER, FOR loops
87
+ - **Internal Tables** - for filtering and iteration patterns
88
+ - **abapGit** (08_abapgit.md) - for XML metadata templates
@@ -1,5 +1,15 @@
1
1
  # Exception Handling - Classical vs Class-Based
2
2
 
3
+ **Searchable keywords**: exception, RAISING, TRY, CATCH, cx_static_check, cx_dynamic_check, EXCEPTIONS, sy-subrc, class-based exception, classical exception
4
+
5
+ ## TOPICS IN THIS FILE
6
+ 1. Quick Identification - line 5
7
+ 2. Classical Exceptions - line 12
8
+ 3. Class-Based Exceptions - line 25
9
+ 4. Method Signatures - line 50
10
+ 5. Best Practices - line 80
11
+ 6. Interface vs Class Methods - line 118
12
+
3
13
  ABAP has two exception handling mechanisms. Using the wrong one causes silent failures.
4
14
 
5
15
  ## Quick Identification
@@ -106,3 +116,61 @@ Or search the cheat sheets:
106
116
  ```bash
107
117
  abapgit-agent ref --topic exceptions
108
118
  ```
119
+
120
+ ## Interface vs Class Methods
121
+
122
+ ### Interface Methods
123
+
124
+ Cannot add RAISING clause in the implementing class. Options:
125
+
126
+ 1. Add RAISING to interface definition
127
+ 2. Use TRY-CATCH in the implementation
128
+
129
+ ```abap
130
+ " Interface definition - can add RAISING here
131
+ INTERFACE zif_example.
132
+ methods execute
133
+ importing is_param type data optional
134
+ returning value(rv_result) type string
135
+ raising cx_static_check.
136
+ ENDINTERFACE.
137
+
138
+ " Implementation - CANNOT add RAISING here
139
+ CLASS zcl_example DEFINITION.
140
+ INTERFACES zif_example.
141
+ ENDCLASS.
142
+
143
+ CLASS zcl_example IMPLEMENTATION.
144
+ METHOD zif_example~execute.
145
+ " Must handle cx_static_check here with TRY-CATCH
146
+ " or declare it in interface, not here
147
+ ENDMETHOD.
148
+ ENDCLASS.
149
+ ```
150
+
151
+ ### Class Methods
152
+
153
+ Can add RAISING clause to declare exceptions, allowing caller to handle in one place:
154
+
155
+ ```abap
156
+ " Class method with RAISING clause
157
+ METHODS process_data
158
+ importing iv_data type string
159
+ returning value(rv_result) type string
160
+ raising cx_static_check.
161
+
162
+ " Caller can handle in one place
163
+ TRY.
164
+ lv_result = lo_obj->process_data( iv_data = 'test' ).
165
+ CATCH cx_static_check.
166
+ " Handle exception
167
+ ENDTRY.
168
+ ```
169
+
170
+ ### When to Use Each
171
+
172
+ | Scenario | Recommendation |
173
+ |----------|----------------|
174
+ | Multiple callers need to handle exception | Add RAISING to method definition |
175
+ | Exception should be handled internally | Use TRY-CATCH in implementation |
176
+ | Interface method | Add RAISING to interface, or use TRY-CATCH in class |
@@ -1,5 +1,15 @@
1
1
  # Unit Testing
2
2
 
3
+ **Searchable keywords**: unit test, AUnit, test class, cl_abap_unit_assert, FOR TESTING, setup, teardown, RISK LEVEL, DURATION, CDS test double, CL_CDS_TEST_ENVIRONMENT
4
+
5
+ ## TOPICS IN THIS FILE
6
+ 1. Local Test Classes - line 3
7
+ 2. File Structure - line 5
8
+ 3. Required Elements - line 16
9
+ 4. Naming Conventions - line 48
10
+ 5. CDS Test Doubles - line 94
11
+ 6. CDS with Aggregations - line 178
12
+
3
13
  ## Unit Testing with Local Test Classes
4
14
 
5
15
  ### File Structure
@@ -250,3 +260,10 @@ ENDMETHOD.
250
260
  abapgit-agent ref "cl_cds_test_environment"
251
261
  abapgit-agent ref --topic unit-tests
252
262
  ```
263
+
264
+ ---
265
+
266
+ ## See Also
267
+ - **CDS Views** (04_cds.md) - for CDS view definitions and syntax
268
+ - **abapGit** (08_abapgit.md) - for WITH_UNIT_TESTS in XML metadata
269
+ - **ABAP SQL** (01_sql.md) - for SELECT statements in tests
@@ -1,5 +1,14 @@
1
1
  # Creating CDS Views
2
2
 
3
+ **Searchable keywords**: CDS, DDL, DDLS, CDS view, @AbapCatalog, @AccessControl, association, projection, consumption
4
+
5
+ ## TOPICS IN THIS FILE
6
+ 1. File Naming - line 7
7
+ 2. DDL Source (.ddls.asddls) - line 18
8
+ 3. Annotations - line 50
9
+ 4. Associations - line 75
10
+ 5. CDS Test Doubles - see 03_testing.md
11
+
3
12
  ## Creating CDS Views (DDLS)
4
13
 
5
14
  CDS views (Data Definition Language Source) require specific file naming and structure for abapGit.
@@ -118,3 +127,10 @@ When working with CDS view syntax (arithmetic, built-in functions, aggregations,
118
127
  - `zdemo_abap_cds_ve_assoc.ddls.asddls` - Associations
119
128
 
120
129
  **Note**: This requires `abap-cheat-sheets` to be in the reference folder (configured in `.abapGitAgent`).
130
+
131
+ ---
132
+
133
+ ## See Also
134
+ - **Unit Testing** (03_testing.md) - for CDS Test Double Framework
135
+ - **abapGit** (08_abapgit.md) - for CDS XML metadata templates
136
+ - **ABAP SQL** (01_sql.md) - for SQL functions used in CDS
@@ -1,5 +1,13 @@
1
1
  # ABAP Classes and Objects
2
2
 
3
+ **Searchable keywords**: CLASS, DEFINITION, PUBLIC, CREATE OBJECT, NEW, METHOD, INTERFACES, inheritance, FINAL, ABSTRACT
4
+
5
+ ## TOPICS IN THIS FILE
6
+ 1. Class Definition (PUBLIC) - line 3
7
+ 2. Constructor - line 20
8
+ 3. Interfaces - line 35
9
+ 4. Inline Declaration - line 50
10
+
3
11
  ## ABAP Class Definition - Must Use PUBLIC
4
12
 
5
13
  **CRITICAL**: Global ABAP classes MUST use `PUBLIC` in the class definition:
@@ -1,5 +1,12 @@
1
1
  # ABAP Objects
2
2
 
3
+ **Searchable keywords**: naming convention, Z prefix, namespace, object type, CLAS, INTF, PROG, TABL, DDLS, XML metadata, .abapGit.xml
4
+
5
+ ## TOPICS IN THIS FILE
6
+ 1. XML Metadata Required - line 3
7
+ 2. Naming Conventions - line 30
8
+ 3. Object Types - line 60
9
+
3
10
  ## Creating New ABAP Objects - XML Metadata Required
4
11
 
5
12
  **CRITICAL CHECKLIST - Never Forget!**
@@ -1,5 +1,7 @@
1
1
  # JSON Handling
2
2
 
3
+ **Searchable keywords**: JSON, serialize, deserialize, /ui2/cl_json, REST API, API response
4
+
3
5
  **CRITICAL**: Always use `/ui2/cl_json` for JSON serialization and deserialization.
4
6
 
5
7
  ## Correct Usage
@@ -2,6 +2,35 @@
2
2
 
3
3
  Each ABAP object requires an XML metadata file for abapGit to understand how to serialize/deserialize it. This guide provides templates for common object types.
4
4
 
5
+ ## QUICK REFERENCE
6
+
7
+ ```
8
+ File Type | ABAP File | XML File
9
+ -------------------|------------------------------|-------------------
10
+ Class | zcl_*.clas.abap | zcl_*.clas.xml
11
+ Test Class | zcl_*.clas.testclasses.abap | zcl_*.clas.xml
12
+ Interface | zif_*.intf.abap | zif_*.intf.xml
13
+ Program | z*.prog.abap | z*.prog.xml
14
+ Table | z*.tabl.abap | z*.tabl.xml
15
+ CDS View | zc_*.ddls.asddls | zc_*.ddls.xml
16
+ CDS Entity | ze_*.ddlx.asddlx | ze_*.ddlx.xml
17
+ Data Element | z*.dtel.abap | z*.dtel.xml
18
+ Structure | z*.stru.abap | z*.stru.xml
19
+ Table Type | z*.ttyp.abap | z*.ttyp.xml
20
+ ```
21
+
22
+ ```
23
+ Key XML Settings:
24
+ Class EXPOSURE: 2=Public, 3=Protected, 4=Private
25
+ Class STATE: 1=Active
26
+ Table TABCLASS: TRANSP, POOL, CLUSTER
27
+ Table DELIVERY: A=Application, C=Customizing
28
+ CDS SOURCE_TYPE: V=View, C=Consumption
29
+ Test Class XML: <WITH_UNIT_TESTS>X</WITH_UNIT_TESTS>
30
+ ```
31
+
32
+ **Searchable keywords**: class xml, interface xml, table xml, cds xml, test class, exposure, serializer, abapgit
33
+
5
34
  ## Why XML Metadata?
6
35
 
7
36
  abapGit needs XML files to:
@@ -0,0 +1,568 @@
1
+ # ABAP Unit Testable Code Guidelines
2
+
3
+ This document provides guidelines for creating ABAP OO classes/interfaces that can be easily unit tested with test doubles. These guidelines help AI coding tools understand how to design classes that are testable without requiring real dependencies.
4
+
5
+ ## The Problem
6
+
7
+ When ABAP classes are not designed for testability, unit tests cannot mock dependencies. This leads to:
8
+
9
+ - Tests that depend on real external systems (databases, APIs, file systems)
10
+ - Tests that fail in different environments
11
+ - Tests that are slow and unreliable
12
+ - Impossible to test error conditions
13
+
14
+ **Example of untestable code:**
15
+
16
+ ```abap
17
+ " BAD - Hardcoded dependency, cannot be replaced in tests
18
+ CLASS zcl_abgagt_command_pull DEFINITION PUBLIC.
19
+ METHOD execute.
20
+ lo_agent = NEW zcl_abgagt_agent( ). " Hardcoded!
21
+ ls_result = lo_agent->pull( ... ). " Calls real system
22
+ ENDMETHOD.
23
+ ENDCLASS.
24
+ ```
25
+
26
+ The unit test will instantiate the REAL `zcl_abgagt_agent` which tries to connect to abapGit and a real git repository, causing test failures.
27
+
28
+ ---
29
+
30
+ ## Core Principles
31
+
32
+ ### 1. Dependency Inversion (Dependency Injection)
33
+
34
+ **Pass dependencies through constructor instead of creating them internally.**
35
+
36
+ ```abap
37
+ " GOOD - Dependency injected via constructor
38
+ CLASS zcl_abgagt_command_pull DEFINITION PUBLIC.
39
+ PUBLIC SECTION.
40
+ INTERFACES zif_abgagt_command.
41
+
42
+ " Constructor injection
43
+ METHODS constructor
44
+ IMPORTING
45
+ io_agent TYPE REF TO zif_abgagt_agent.
46
+
47
+ PRIVATE SECTION.
48
+ DATA mo_agent TYPE REF TO zif_abgagt_agent.
49
+
50
+ ENDCLASS.
51
+
52
+ CLASS zcl_abgagt_command_pull IMPLEMENTATION.
53
+
54
+ METHOD constructor.
55
+ super->constructor( ).
56
+ mo_agent = io_agent.
57
+ ENDMETHOD.
58
+
59
+ METHOD execute.
60
+ " Use injected dependency
61
+ ls_result = mo_agent->pull( ... ).
62
+ ENDMETHOD.
63
+
64
+ ENDCLASS.
65
+ ```
66
+
67
+ **In production code:**
68
+ ```abap
69
+ DATA(lo_command) = NEW zcl_abgagt_command_pull(
70
+ io_agent = NEW zcl_abgagt_agent( ) ).
71
+ ```
72
+
73
+ **In test code:**
74
+ ```abap
75
+ " Create test double
76
+ CLASS ltd_mock_agent DEFINITION FOR TESTING.
77
+ PUBLIC SECTION.
78
+ INTERFACES zif_abgagt_agent PARTIALLY IMPLEMENTED.
79
+ ENDCLASS.
80
+
81
+ CLASS ltd_mock_agent IMPLEMENTATION.
82
+ METHOD zif_abgagt_agent~pull.
83
+ " Return test data instead of calling real system
84
+ rs_result-success = abap_true.
85
+ rs_result-message = 'Test success'.
86
+ ENDMETHOD.
87
+ ENDCLASS.
88
+
89
+ " Test uses test double
90
+ CLASS ltcl_test DEFINITION FOR TESTING.
91
+ METHOD test_execute.
92
+ DATA(lo_mock) = NEW ltd_mock_agent( ).
93
+ DATA(lo_cut) = NEW zcl_abgagt_command_pull( io_agent = lo_mock ).
94
+
95
+ DATA(lv_result) = lo_cut->execute( ... ).
96
+
97
+ " Assert expected results
98
+ ENDMETHOD.
99
+ ENDCLASS.
100
+ ```
101
+
102
+ ### 2. Always Use Interfaces for Dependencies
103
+
104
+ **Never depend on concrete classes - depend on interfaces.**
105
+
106
+ ```abap
107
+ " GOOD - Depend on interface
108
+ DATA mo_agent TYPE REF TO zif_abgagt_agent. " Interface!
109
+
110
+ " BAD - Depends on concrete class
111
+ DATA mo_agent TYPE REF TO zcl_abgagt_agent. " Concrete class!
112
+ ```
113
+
114
+ This allows you to replace the implementation with test doubles.
115
+
116
+ ### 3. Make Dependencies Injectable via Constructor
117
+
118
+ **Use constructor injection, not setter injection.**
119
+
120
+ ```abap
121
+ " GOOD - Constructor injection (required dependency)
122
+ METHODS constructor
123
+ IMPORTING
124
+ io_agent TYPE REF TO zif_abgagt_agent.
125
+
126
+ " BAD - Setter injection (optional, can be forgotten)
127
+ METHODS set_agent
128
+ IMPORTING
129
+ io_agent TYPE REF TO zif_abgagt_agent.
130
+ ```
131
+
132
+ Constructor injection:
133
+ - Makes dependency explicit
134
+ - Ensures object is always in valid state
135
+ - Cannot forget to inject
136
+
137
+ ### 4. Avoid Static Calls
138
+
139
+ **Static method calls cannot be mocked/test-doubled.**
140
+
141
+ ```abap
142
+ " BAD - Static call cannot be replaced
143
+ DATA(li_repo) = zcl_abapgit_repo_srv=>get_instance( )->get_repo_from_url( ... ).
144
+
145
+ " GOOD - Instance method via injected dependency
146
+ DATA(li_repo) = mo_repo_srv->get_repo_from_url( ... ).
147
+ ```
148
+
149
+ If you must call static methods, wrap them in an instance method of an injected class.
150
+
151
+ ### 5. Keep Constructor Simple
152
+
153
+ **Constructor should only assign dependencies, not perform complex logic.**
154
+
155
+ ```abap
156
+ " GOOD - Simple constructor
157
+ METHOD constructor.
158
+ mo_agent = io_agent.
159
+ mo_logger = io_logger.
160
+ ENDMETHOD.
161
+
162
+ " BAD - Complex logic in constructor
163
+ METHOD constructor.
164
+ mo_agent = io_agent.
165
+ " Don't do this here:
166
+ mo_agent->connect( ). " Network call in constructor!
167
+ DATA(ls_config) = read_config( ). " File I/O in constructor!
168
+ ENDMETHOD.
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Injection Techniques
174
+
175
+ ### Constructor Injection (Recommended)
176
+
177
+ ```abap
178
+ CLASS zcl_my_class DEFINITION PUBLIC.
179
+ PUBLIC SECTION.
180
+ METHODS constructor
181
+ IMPORTING
182
+ io_dependency TYPE REF TO zif_my_interface.
183
+ PRIVATE SECTION.
184
+ DATA mo_dependency TYPE REF TO zif_my_interface.
185
+ ENDCLASS.
186
+ ```
187
+
188
+ ### Back Door Injection (for existing code)
189
+
190
+ When you cannot modify the constructor, use friendship:
191
+
192
+ ```abap
193
+ " In test class
194
+ CLASS zcl_my_class DEFINITION LOCAL FRIENDS ltcl_test.
195
+
196
+ CLASS ltcl_test IMPLEMENTATION.
197
+ METHOD test_with_mock.
198
+ " Directly set private attribute via friendship
199
+ CREATE OBJECT mo_cut.
200
+ mo_cut->mo_dependency = lo_mock. " Access private attribute
201
+ ENDMETHOD.
202
+ ENDCLASS.
203
+ ```
204
+
205
+ ### Test Seams (last resort)
206
+
207
+ For legacy code that cannot be refactored:
208
+
209
+ ```abap
210
+ " In production code
211
+ METHOD get_data.
212
+ TEST-SEAM db_select.
213
+ SELECT * FROM dbtab INTO TABLE @DATA(lt_data).
214
+ END-TEST-SEAM.
215
+ ENDMETHOD.
216
+
217
+ " In test class
218
+ METHOD test_get_data.
219
+ TEST-INJECTION db_select.
220
+ lt_data = VALUE #( ( id = '1' ) ( id = '2' ) ).
221
+ END-TEST-INJECTION.
222
+
223
+ DATA(lt_result) = mo_cut->get_data( ).
224
+ ENDMETHOD.
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Test Double Patterns
230
+
231
+ ### Manual Test Double (Local Class)
232
+
233
+ ```abap
234
+ " Create test double class
235
+ CLASS ltd_mock_reader DEFINITION FOR TESTING.
236
+ PUBLIC SECTION.
237
+ INTERFACES zif_data_reader PARTIALLY IMPLEMENTED.
238
+ METHODS set_result_data
239
+ IMPORTING it_data TYPE ANY TABLE.
240
+ PRIVATE SECTION.
241
+ DATA mt_data TYPE ANY TABLE.
242
+ ENDCLASS.
243
+
244
+ CLASS ltd_mock_reader IMPLEMENTATION.
245
+ METHOD set_result_data.
246
+ mt_data = it_data.
247
+ ENDMETHOD.
248
+
249
+ METHOD zif_data_reader~read_all.
250
+ rt_data = mt_data.
251
+ ENDMETHOD.
252
+ ENDCLASS.
253
+ ```
254
+
255
+ ### Using ABAP Test Double Framework
256
+
257
+ ```abap
258
+ " Step 1: Declare with correct interface type, then assign
259
+ DATA lo_mock TYPE REF TO zif_my_interface.
260
+ lo_mock ?= cl_abap_testdouble=>create( 'ZIF_MY_INTERFACE' ).
261
+
262
+ " Step 2: Configure return value - use returning() not IMPORTING
263
+ cl_abap_testdouble=>configure_call( lo_mock )->returning( lo_mock_result ).
264
+
265
+ " Step 3: Call method to register configuration (MUST use same params in test)
266
+ lo_mock->my_method(
267
+ EXPORTING
268
+ iv_param1 = 'value1'
269
+ iv_param2 = 'value2' ).
270
+
271
+ " Step 4: In test, call with SAME parameters as registered above
272
+ DATA(ls_result) = lo_mock->my_method(
273
+ EXPORTING
274
+ iv_param1 = 'value1'
275
+ iv_param2 = 'value2' ).
276
+
277
+ " To raise exception:
278
+ DATA(lx_error) = NEW zcx_my_exception( ).
279
+ cl_abap_testdouble=>configure_call( lo_mock )->raise_exception( lx_error ).
280
+ lo_mock->my_method( ... ).
281
+ ```
282
+
283
+ **Important Notes:**
284
+ - Parameters in configure_call registration MUST match parameters in test execution
285
+ - Always declare variable with interface type first: `DATA lo_mock TYPE REF TO zif_xxx`
286
+ - Use `returning(value = ...)` not `IMPORTING`
287
+ - Call method after configure_call to register the configuration
288
+
289
+ ### Mocking EXPORT Parameters
290
+
291
+ Some methods use EXPORT parameters instead of returning values. Use `set_parameter`:
292
+
293
+ ```abap
294
+ " Mock EXPORT parameter EI_REPO
295
+ cl_abap_testdouble=>configure_call( lo_repo_srv )->set_parameter(
296
+ EXPORTING
297
+ name = 'EI_REPO'
298
+ value = lo_repo_double ).
299
+
300
+ " Register the method call
301
+ lo_repo_srv->get_repo_from_url(
302
+ EXPORTING iv_url = 'https://github.com/test/repo.git' ).
303
+ ```
304
+
305
+ ### Mocking Inherited Methods
306
+
307
+ When an interface extends another interface, use the parent interface prefix:
308
+
309
+ ```abap
310
+ " zif_abapgit_repo_online extends zif_abapgit_repo
311
+ " Call inherited method with prefix
312
+ lo_repo->zif_abapgit_repo~get_package( ).
313
+ lo_repo->zif_abapgit_repo~refresh( ).
314
+ lo_repo->zif_abapgit_repo~get_files_local( ).
315
+ ```
316
+
317
+ ### Mocking Methods with No Parameters
318
+
319
+ When source code calls a method with no parameters:
320
+
321
+ ```abap
322
+ " Configure returning (no method name)
323
+ cl_abap_testdouble=>configure_call( lo_mock )->returning( lt_data ).
324
+
325
+ " Register with no parameters (matches source code)
326
+ lo_mock->get_files_local( ).
327
+ ```
328
+
329
+ ### Common Mistakes
330
+
331
+ | Mistake | Correction |
332
+ |---------|------------|
333
+ | Using `IMPORTING` in configure_call | Use `returning()` or `set_parameter()` |
334
+ | Calling method inside configure_call | Call method separately after configure_call |
335
+ | Wrong parameter count | Match exactly what source code calls |
336
+ | Forgot to mock a method | Mock ALL methods the code under test calls |
337
+ | Interface prefix not used | Use `zif_parent~method()` for inherited methods |
338
+ | Didn't check source code first | ALWAYS read source code to see how method is called |
339
+ | Cannot add RAISING to interface method | Use TRY..CATCH to handle exceptions in implementation |
340
+
341
+ ### Handling Exceptions in Interface Implementation
342
+
343
+ When implementing an interface method that calls other methods raising exceptions:
344
+
345
+ - **DO NOT** add RAISING to the interface method - you cannot change the interface
346
+ - **USE** TRY..CATCH to catch and handle exceptions within the implementation
347
+
348
+ ```abap
349
+ " Interface method does NOT declare RAISING
350
+ METHOD zif_abgagt_command~execute.
351
+
352
+ " Method being called can raise exception
353
+ TRY.
354
+ get_user( )->set_repo_git_user_name( ... ).
355
+ CATCH zcx_abapgit_exception INTO DATA(lx_error).
356
+ rv_result = '{"error":"' && lx_error->get_text( ) && '"}'.
357
+ RETURN.
358
+ ENDTRY.
359
+
360
+ ENDMETHOD.
361
+ ```
362
+
363
+ ### Important: Read Source Code First
364
+
365
+ **ALWAYS check the source code to see HOW a method is called before writing tests:**
366
+
367
+ 1. Check what parameters are passed (none, some, or all)
368
+ 2. Check if optional parameters are used
369
+ 3. Check if parameters have default values
370
+ 4. Check for type casts (e.g., `li_repo_online ?= li_repo`)
371
+
372
+ ```abap
373
+ " Source code line 122:
374
+ lt_files = li_repo->get_files_local( ).
375
+
376
+ " Test MUST match - no parameters!
377
+ cl_abap_testdouble=>configure_call( lo_repo )->returning( lt_empty_files ).
378
+ lo_repo->get_files_local( ). " No parameters!
379
+ ```
380
+
381
+ If the source code calls `get_files_local( )` with no parameters, your test registration must also have no parameters. Even if the method signature has optional parameters, if the source doesn't pass them, your mock registration must not pass them either.
382
+
383
+ ---
384
+
385
+ ## Guidelines for AI Coding Tools
386
+
387
+ When creating new ABAP classes, ALWAYS follow these rules:
388
+
389
+ ### DO:
390
+
391
+ 1. **Use interfaces for ALL external dependencies**
392
+ - Database access → interface
393
+ - External API calls → interface
394
+ - File I/O → interface
395
+ - Other services → interface
396
+
397
+ 2. **Pass dependencies via constructor**
398
+ ```abap
399
+ METHODS constructor
400
+ IMPORTING
401
+ io_agent TYPE REF TO zif_abgagt_agent
402
+ io_logger TYPE REF TO zif_logger.
403
+ ```
404
+
405
+ 3. **Define interfaces for all service classes**
406
+ ```abap
407
+ " Instead of using class directly
408
+ DATA mo_agent TYPE REF TO zcl_abgagt_agent. " BAD
409
+
410
+ " Use interface
411
+ DATA mo_agent TYPE REF TO zif_abgagt_agent. " GOOD
412
+ ```
413
+
414
+ 4. **Keep classes FINAL if they don't need mocking**
415
+ - If a class has no dependencies and doesn't need test doubles, make it FINAL
416
+ - If a class needs to be mocked in tests, don't make it FINAL
417
+
418
+ 5. **Use dependency injection in command classes**
419
+ ```abap
420
+ " Good pattern for command classes
421
+ CLASS zcl_abgagt_command_pull DEFINITION PUBLIC.
422
+ PUBLIC SECTION.
423
+ INTERFACES zif_abgagt_command.
424
+ METHODS constructor
425
+ IMPORTING io_agent TYPE REF TO zif_abgagt_agent.
426
+ ENDCLASS.
427
+ ```
428
+
429
+ ### DON'T:
430
+
431
+ 1. **Never create dependencies inside methods**
432
+ ```abap
433
+ " BAD
434
+ METHOD execute.
435
+ lo_agent = NEW zcl_abgagt_agent( ). " Hardcoded!
436
+ ENDMETHOD.
437
+
438
+ " GOOD
439
+ METHOD execute.
440
+ ls_result = mo_agent->pull( ... ). " Use injected
441
+ ENDMETHOD.
442
+ ```
443
+
444
+ 2. **Don't use static method calls for testable code**
445
+ ```abap
446
+ " BAD
447
+ DATA(lo_srv) = zcl_some_srv=>get_instance( ).
448
+
449
+ " GOOD - inject the service
450
+ DATA(lo_srv) = mo_service_provider.
451
+ ```
452
+
453
+ 3. **Don't make classes FINAL if they need test doubles**
454
+ - If you need to mock a class in tests, don't declare it FINAL
455
+
456
+ ---
457
+
458
+ ## Example: Refactoring for Testability
459
+
460
+ ### Before (Not Testable)
461
+
462
+ ```abap
463
+ CLASS zcl_abgagt_command_pull DEFINITION PUBLIC.
464
+ METHOD execute.
465
+ DATA lo_agent TYPE REF TO zcl_abgagt_agent.
466
+ lo_agent = NEW zcl_abgagt_agent( ). " Hardcoded!
467
+
468
+ ls_result = lo_agent->pull(
469
+ iv_url = ls_params-url
470
+ iv_branch = ls_params-branch ).
471
+ ENDMETHOD.
472
+ ENDCLASS.
473
+ ```
474
+
475
+ ### After (Testable)
476
+
477
+ ```abap
478
+ " Interface for agent
479
+ INTERFACE zif_abgagt_agent PUBLIC.
480
+ METHODS pull ... RAISING zcx_abapgit_exception.
481
+ ENDINTERFACE.
482
+
483
+ " Command class with constructor injection
484
+ CLASS zcl_abgagt_command_pull DEFINITION PUBLIC.
485
+ PUBLIC SECTION.
486
+ INTERFACES zif_abgagt_command.
487
+
488
+ METHODS constructor
489
+ IMPORTING
490
+ io_agent TYPE REF TO zif_abgagt_agent OPTIONAL. " Optional for backward compat
491
+
492
+ PRIVATE SECTION.
493
+ DATA mo_agent TYPE REF TO zif_abgagt_agent.
494
+
495
+ METHODS get_agent
496
+ RETURNING VALUE(ro_agent) TYPE REF TO zif_abgagt_agent.
497
+ ENDCLASS.
498
+
499
+ CLASS zcl_abgagt_command_pull IMPLEMENTATION.
500
+
501
+ METHOD constructor.
502
+ mo_agent = io_agent.
503
+ ENDMETHOD.
504
+
505
+ METHOD get_agent.
506
+ " Lazy creation if not injected (for production)
507
+ IF mo_agent IS NOT BOUND.
508
+ mo_agent = NEW zcl_abgagt_agent( ).
509
+ ENDIF.
510
+ ro_agent = mo_agent.
511
+ ENDMETHOD.
512
+
513
+ METHOD execute.
514
+ DATA(lo_agent) = get_agent( ).
515
+ ls_result = lo_agent->pull( ... ).
516
+ ENDMETHOD.
517
+
518
+ ENDCLASS.
519
+ ```
520
+
521
+ **Production usage:**
522
+ ```abap
523
+ DATA(lo_command) = NEW zcl_abgagt_command_pull(
524
+ io_agent = NEW zcl_abgagt_agent( ) ).
525
+ ```
526
+
527
+ **Test usage:**
528
+ ```abap
529
+ CLASS ltd_mock_agent DEFINITION FOR TESTING.
530
+ PUBLIC SECTION.
531
+ INTERFACES zif_abgagt_agent PARTIALLY IMPLEMENTED.
532
+ ENDCLASS.
533
+
534
+ CLASS ltd_mock_agent IMPLEMENTATION.
535
+ METHOD zif_abgagt_agent~pull.
536
+ rs_result-success = abap_true.
537
+ rs_result-message = 'Mocked success'.
538
+ ENDMETHOD.
539
+ ENDCLASS.
540
+
541
+ CLASS ltcl_test DEFINITION FOR TESTING.
542
+ METHOD test_pull_success.
543
+ DATA(lo_mock) = NEW ltd_mock_agent( ).
544
+ DATA(lo_cut) = NEW zcl_abgagt_command_pull( io_agent = lo_mock ).
545
+
546
+ DATA(lv_result) = lo_cut->execute( ... ).
547
+
548
+ " Assert mocked behavior
549
+ ENDMETHOD.
550
+ ENDCLASS.
551
+ ```
552
+
553
+ ---
554
+
555
+ ## Key Takeaways
556
+
557
+ 1. **Always use interfaces** for dependencies
558
+ 2. **Use constructor injection** to pass dependencies
559
+ 3. **Never hardcode `NEW` for dependencies** - pass them in
560
+ 4. **Avoid static calls** - use instance methods with injected dependencies
561
+ 5. **Keep constructors simple** - only assign dependencies
562
+
563
+ Following these guidelines ensures that:
564
+ - Unit tests can mock all dependencies
565
+ - Tests run fast without external systems
566
+ - Tests are reliable and repeatable
567
+ - Error conditions can be tested easily
568
+ - Code is modular and loosely coupled
package/bin/abapgit-agent CHANGED
@@ -538,12 +538,14 @@ async function processInspectResult(res) {
538
538
  const errorCount = res.ERROR_COUNT !== undefined ? res.ERROR_COUNT : (res.error_count || 0);
539
539
  const errors = res.ERRORS !== undefined ? res.ERRORS : (res.errors || []);
540
540
  const warnings = res.WARNINGS !== undefined ? res.WARNINGS : (res.warnings || []);
541
+ const infos = res.INFOS !== undefined ? res.INFOS : (res.infos || []);
541
542
 
542
- if (errorCount > 0 || warnings.length > 0) {
543
+ if (errorCount > 0 || warnings.length > 0 || infos.length > 0) {
543
544
  if (errorCount > 0) {
544
545
  console.log(`❌ ${objectType} ${objectName} - Syntax check failed (${errorCount} error(s)):`);
545
546
  } else {
546
- console.log(`⚠️ ${objectType} ${objectName} - Syntax check passed with warnings (${warnings.length}):`);
547
+ const total = warnings.length + infos.length;
548
+ console.log(`⚠️ ${objectType} ${objectName} - Syntax check passed with warnings (${total}):`);
547
549
  }
548
550
  console.log('\nErrors:');
549
551
  console.log('─'.repeat(60));
@@ -552,8 +554,16 @@ async function processInspectResult(res) {
552
554
  const line = err.LINE || err.line || '?';
553
555
  const column = err.COLUMN || err.column || '?';
554
556
  const text = err.TEXT || err.text || 'Unknown error';
557
+ const methodName = err.METHOD_NAME || err.method_name;
558
+ const sobjname = err.SOBJNAME || err.sobjname;
555
559
 
560
+ if (methodName) {
561
+ console.log(` Method: ${methodName}`);
562
+ }
556
563
  console.log(` Line ${line}, Column ${column}:`);
564
+ if (sobjname && sobjname.includes('====')) {
565
+ console.log(` Include: ${sobjname}`);
566
+ }
557
567
  console.log(` ${text}`);
558
568
  console.log('');
559
569
  }
@@ -565,7 +575,38 @@ async function processInspectResult(res) {
565
575
  for (const warn of warnings) {
566
576
  const line = warn.LINE || warn.line || '?';
567
577
  const text = warn.MESSAGE || warn.message || 'Unknown warning';
568
- console.log(` Line ${line}: ${text}`);
578
+ const methodName = warn.METHOD_NAME || warn.method_name;
579
+ const sobjname = warn.SOBJNAME || warn.sobjname;
580
+
581
+ if (methodName) {
582
+ console.log(` Method: ${methodName}`);
583
+ }
584
+ console.log(` Line ${line}:`);
585
+ if (sobjname && sobjname.includes('====')) {
586
+ console.log(` Include: ${sobjname}`);
587
+ }
588
+ console.log(` ${text}`);
589
+ }
590
+ }
591
+
592
+ // Show infos if any
593
+ if (infos.length > 0) {
594
+ console.log('Info:');
595
+ console.log('─'.repeat(60));
596
+ for (const info of infos) {
597
+ const line = info.LINE || info.line || '?';
598
+ const text = info.MESSAGE || info.message || 'Unknown info';
599
+ const methodName = info.METHOD_NAME || info.method_name;
600
+ const sobjname = info.SOBJNAME || info.sobjname;
601
+
602
+ if (methodName) {
603
+ console.log(` Method: ${methodName}`);
604
+ }
605
+ console.log(` Line ${line}:`);
606
+ if (sobjname && sobjname.includes('====')) {
607
+ console.log(` Include: ${sobjname}`);
608
+ }
609
+ console.log(` ${text}`);
569
610
  }
570
611
  }
571
612
  } else if (success === true || success === 'X') {
@@ -652,7 +693,7 @@ async function runUnitTests(options) {
652
693
  /**
653
694
  * Run unit test for a single file
654
695
  */
655
- async function runUnitTestForFile(sourceFile, csrfToken, config) {
696
+ async function runUnitTestForFile(sourceFile, csrfToken, config, coverage = false) {
656
697
  console.log(` Running unit test for: ${sourceFile}`);
657
698
 
658
699
  try {
@@ -687,7 +728,8 @@ async function runUnitTestForFile(sourceFile, csrfToken, config) {
687
728
 
688
729
  // Send files array to unit endpoint (ABAP expects string_table of file names)
689
730
  const data = {
690
- files: [sourceFile]
731
+ files: [sourceFile],
732
+ coverage: coverage
691
733
  };
692
734
 
693
735
  const result = await request('POST', '/sap/bc/z_abapgit_agent/unit', data, { csrfToken });
@@ -700,6 +742,9 @@ async function runUnitTestForFile(sourceFile, csrfToken, config) {
700
742
  const message = result.MESSAGE || result.message || '';
701
743
  const errors = result.ERRORS || result.errors || [];
702
744
 
745
+ // Handle coverage data
746
+ const coverageStats = result.COVERAGE_STATS || result.coverage_stats;
747
+
703
748
  if (testCount === 0) {
704
749
  console.log(` ➖ ${objName} - No unit tests`);
705
750
  } else if (success === 'X' || success === true) {
@@ -710,6 +755,17 @@ async function runUnitTestForFile(sourceFile, csrfToken, config) {
710
755
 
711
756
  console.log(` Tests: ${testCount} | Passed: ${passedCount} | Failed: ${failedCount}`);
712
757
 
758
+ // Display coverage if available
759
+ if (coverage && coverageStats) {
760
+ const totalLines = coverageStats.TOTAL_LINES || coverageStats.total_lines || 0;
761
+ const coveredLines = coverageStats.COVERED_LINES || coverageStats.covered_lines || 0;
762
+ const coverageRate = coverageStats.COVERAGE_RATE || coverageStats.coverage_rate || 0;
763
+
764
+ console.log(` 📊 Coverage: ${coverageRate}%`);
765
+ console.log(` Total Lines: ${totalLines}`);
766
+ console.log(` Covered Lines: ${coveredLines}`);
767
+ }
768
+
713
769
  if (failedCount > 0 && errors.length > 0) {
714
770
  for (const err of errors) {
715
771
  const className = err.CLASS_NAME || err.class_name || '?';
@@ -1727,21 +1783,25 @@ Examples:
1727
1783
  const filesArgIndex = args.indexOf('--files');
1728
1784
  if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
1729
1785
  console.error('Error: --files parameter required');
1730
- console.error('Usage: abapgit-agent unit --files <file1>,<file2>,...');
1786
+ console.error('Usage: abapgit-agent unit --files <file1>,<file2>,... [--coverage]');
1731
1787
  console.error('Example: abapgit-agent unit --files zcl_my_test.clas.abap');
1788
+ console.error('Example: abapgit-agent unit --files zcl_my_test.clas.abap --coverage');
1732
1789
  process.exit(1);
1733
1790
  }
1734
1791
 
1735
1792
  const files = args[filesArgIndex + 1].split(',').map(f => f.trim());
1736
1793
 
1737
- console.log(`\n Running unit tests for ${files.length} file(s)`);
1794
+ // Check for coverage option
1795
+ const coverage = args.includes('--coverage');
1796
+
1797
+ console.log(`\n Running unit tests for ${files.length} file(s)${coverage ? ' (with coverage)' : ''}`);
1738
1798
  console.log('');
1739
1799
 
1740
1800
  const config = loadConfig();
1741
1801
  const csrfToken = await fetchCsrfToken(config);
1742
1802
 
1743
1803
  for (const sourceFile of files) {
1744
- await runUnitTestForFile(sourceFile, csrfToken, config);
1804
+ await runUnitTestForFile(sourceFile, csrfToken, config, coverage);
1745
1805
  }
1746
1806
  break;
1747
1807
  }
@@ -2452,32 +2512,10 @@ Examples:
2452
2512
  const pattern = args[patternIndex];
2453
2513
  const result = await refSearch.searchPattern(pattern);
2454
2514
 
2455
- // Also search guidelines if available
2456
- const guidelinesResult = await refSearch.searchGuidelines(pattern);
2457
- if (guidelinesResult && guidelinesResult.guidelinesFound && guidelinesResult.matches.length > 0) {
2458
- result.guidelines = guidelinesResult;
2459
- }
2460
-
2461
2515
  if (jsonOutput) {
2462
2516
  console.log(JSON.stringify(result, null, 2));
2463
2517
  } else {
2464
2518
  refSearch.displaySearchResults(result);
2465
- // Display guidelines results if found
2466
- if (guidelinesResult && guidelinesResult.guidelinesFound && guidelinesResult.matches.length > 0) {
2467
- console.log('\n 📋 Custom Guidelines:');
2468
- for (const match of guidelinesResult.matches.slice(0, 5)) {
2469
- console.log(` 📄 ${match.file} (line ${match.line}):`);
2470
- const lines = match.context.split('\n');
2471
- lines.forEach((line, idx) => {
2472
- const prefix = idx === 1 ? ' → ' : ' ';
2473
- const trimmed = line.slice(0, 80);
2474
- console.log(`${prefix}${trimmed}`);
2475
- });
2476
- }
2477
- if (guidelinesResult.matches.length > 5) {
2478
- console.log(` ... and ${guidelinesResult.matches.length - 5} more matches`);
2479
- }
2480
- }
2481
2519
  }
2482
2520
  break;
2483
2521
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abapgit-agent",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
5
5
  "main": "src/index.js",
6
6
  "files": [
package/src/config.js CHANGED
@@ -24,7 +24,7 @@ function loadConfig() {
24
24
  if (fs.existsSync(configPath)) {
25
25
  config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
26
26
  } else {
27
- // Load from environment variables
27
+ // Load from environment variables only when no config file exists
28
28
  config = {
29
29
  host: process.env.ABAP_HOST,
30
30
  sapport: parseInt(process.env.ABAP_PORT, 10) || 443,
package/src/ref-search.js CHANGED
@@ -257,79 +257,117 @@ async function getSearchableFiles(repoPath, repoName, extensions = ['.md', '.aba
257
257
  }
258
258
 
259
259
  /**
260
- * Search for a pattern across all reference repositories
260
+ * Search for a pattern across all reference repositories and local guidelines
261
261
  * @param {string} pattern - Pattern to search for
262
262
  * @returns {Promise<Object>} Search results
263
263
  */
264
264
  async function searchPattern(pattern) {
265
265
  const refFolder = detectReferenceFolder();
266
266
  const repos = await getReferenceRepositories();
267
+ const guidelinesFolder = detectGuidelinesFolder();
267
268
 
268
- if (!refFolder) {
269
+ // If neither reference folder nor guidelines exist, return error
270
+ if (!refFolder && !guidelinesFolder) {
269
271
  return {
270
272
  error: 'Reference folder not found',
271
- hint: 'Configure referenceFolder in .abapGitAgent or clone to ~/abap-reference'
272
- };
273
- }
274
-
275
- if (repos.length === 0) {
276
- return {
277
- error: 'No ABAP repositories found in reference folder',
278
- hint: 'Clone ABAP repositories to the reference folder to enable searching'
273
+ hint: 'Configure referenceFolder in .abapGitAgent, clone to ~/abap-reference, or create abap/guidelines/ folder'
279
274
  };
280
275
  }
281
276
 
282
277
  const results = {
283
278
  pattern,
284
279
  referenceFolder: refFolder,
280
+ guidelinesFolder: guidelinesFolder,
285
281
  repositories: repos.map(r => r.name),
286
282
  files: [],
287
283
  matches: []
288
284
  };
289
285
 
290
286
  try {
291
- // Search across all repositories
292
- for (const repo of repos) {
293
- const searchableFiles = await getSearchableFiles(repo.path, repo.name);
287
+ // Search reference repositories if available
288
+ if (repos.length > 0) {
289
+ for (const repo of repos) {
290
+ const searchableFiles = await getSearchableFiles(repo.path, repo.name);
291
+
292
+ for (const fileInfo of searchableFiles) {
293
+ try {
294
+ const content = await readFile(fileInfo.path, 'utf8');
295
+
296
+ if (content.toLowerCase().includes(pattern.toLowerCase())) {
297
+ results.files.push({
298
+ repo: repo.name,
299
+ file: fileInfo.relativePath
300
+ });
301
+
302
+ // Find matching lines with context
303
+ const lines = content.split('\n');
304
+ let matchCount = 0;
305
+
306
+ for (let i = 0; i < lines.length; i++) {
307
+ if (lines[i].toLowerCase().includes(pattern.toLowerCase())) {
308
+ const start = Math.max(0, i - 1);
309
+ const end = Math.min(lines.length, i + 2);
310
+ const context = lines.slice(start, end).join('\n');
311
+
312
+ results.matches.push({
313
+ repo: repo.name,
314
+ file: fileInfo.relativePath,
315
+ line: i + 1,
316
+ context
317
+ });
318
+
319
+ matchCount++;
320
+
321
+ // Limit matches per file to avoid overwhelming output
322
+ if (matchCount >= 3) {
323
+ break;
324
+ }
325
+ }
326
+ }
327
+ }
328
+ } catch (error) {
329
+ // Skip files we can't read
330
+ }
331
+ }
332
+ }
333
+ }
294
334
 
295
- for (const fileInfo of searchableFiles) {
296
- try {
297
- const content = await readFile(fileInfo.path, 'utf8');
335
+ // Search local guidelines folder if available
336
+ if (guidelinesFolder) {
337
+ const guidelineFiles = await getGuidelineFiles();
298
338
 
299
- if (content.toLowerCase().includes(pattern.toLowerCase())) {
300
- results.files.push({
301
- repo: repo.name,
302
- file: fileInfo.relativePath
303
- });
339
+ for (const file of guidelineFiles) {
340
+ if (file.content.toLowerCase().includes(pattern.toLowerCase())) {
341
+ results.files.push({
342
+ repo: 'guidelines',
343
+ file: file.relativePath
344
+ });
304
345
 
305
- // Find matching lines with context
306
- const lines = content.split('\n');
307
- let matchCount = 0;
346
+ // Find matching lines with context
347
+ const lines = file.content.split('\n');
348
+ let matchCount = 0;
308
349
 
309
- for (let i = 0; i < lines.length; i++) {
310
- if (lines[i].toLowerCase().includes(pattern.toLowerCase())) {
311
- const start = Math.max(0, i - 1);
312
- const end = Math.min(lines.length, i + 2);
313
- const context = lines.slice(start, end).join('\n');
350
+ for (let i = 0; i < lines.length; i++) {
351
+ if (lines[i].toLowerCase().includes(pattern.toLowerCase())) {
352
+ const start = Math.max(0, i - 1);
353
+ const end = Math.min(lines.length, i + 2);
354
+ const context = lines.slice(start, end).join('\n');
314
355
 
315
- results.matches.push({
316
- repo: repo.name,
317
- file: fileInfo.relativePath,
318
- line: i + 1,
319
- context
320
- });
356
+ results.matches.push({
357
+ repo: 'guidelines',
358
+ file: file.relativePath,
359
+ line: i + 1,
360
+ context
361
+ });
321
362
 
322
- matchCount++;
363
+ matchCount++;
323
364
 
324
- // Limit matches per file to avoid overwhelming output
325
- if (matchCount >= 3) {
326
- break;
327
- }
365
+ // Limit matches per file to avoid overwhelming output
366
+ if (matchCount >= 3) {
367
+ break;
328
368
  }
329
369
  }
330
370
  }
331
- } catch (error) {
332
- // Skip files we can't read
333
371
  }
334
372
  }
335
373
  }
@@ -469,7 +507,16 @@ function displaySearchResults(results) {
469
507
  }
470
508
 
471
509
  console.log(`\n 🔍 Searching for: '${results.pattern}'`);
472
- console.log(` 📁 Reference folder: ${results.referenceFolder}`);
510
+
511
+ // Show which sources were searched
512
+ const sources = [];
513
+ if (results.referenceFolder) {
514
+ sources.push('reference repositories');
515
+ }
516
+ if (results.guidelinesFolder) {
517
+ sources.push('local guidelines');
518
+ }
519
+ console.log(` 📁 Sources searched: ${sources.join(', ') || 'none'}`);
473
520
 
474
521
  if (results.repositories && results.repositories.length > 0) {
475
522
  console.log(` 📚 Repositories (${results.repositories.length}): ${results.repositories.join(', ')}`);
@@ -491,7 +538,8 @@ function displaySearchResults(results) {
491
538
 
492
539
  console.log(` ✅ Found in ${results.files.length} file(s):`);
493
540
  for (const [repo, files] of Object.entries(filesByRepo)) {
494
- console.log(`\n 📦 ${repo}/`);
541
+ const icon = repo === 'guidelines' ? '📋' : '📦';
542
+ console.log(`\n ${icon} ${repo}/`);
495
543
  files.forEach(file => {
496
544
  console.log(` • ${file}`);
497
545
  });