@@ -95,6 +95,76 @@ function validateTerm(term) {
9595 create_test_file ( root_dir, "src/multi_term.js" , multi_term_content) ;
9696}
9797
98+ fn create_requirement_fixture ( root_dir : & TempDir ) {
99+ let service_ts = r#"export class PolicyService {
100+ // Implements: SYS-REQ-424
101+ async evaluatePolicy(input: string): Promise<boolean> {
102+ return input.length > 0 && input !== "deny";
103+ }
104+ }
105+
106+ // Implements: SYS-REQ-425
107+ export const normalizeDecision = (raw: string) => {
108+ return raw.trim().toLowerCase();
109+ };
110+ "# ;
111+ create_test_file ( root_dir, "web/src/service.ts" , service_ts) ;
112+
113+ let service_test_ts = r#"import { describe, it, expect, test } from "vitest";
114+
115+ // Verifies: SYS-REQ-424 [boundary]
116+ test("accepts valid policy", () => {
117+ expect(true && true).toBe(true);
118+ });
119+
120+ // MCDC SYS-REQ-424: input_valid=T, not_denied=T => TRUE
121+ it("records witness row", () => {
122+ expect(true).toBe(true);
123+ });
124+
125+ describe("normalization", () => {
126+ // Verifies: SYS-REQ-425
127+ it("normalizes decisions", () => {
128+ expect(" ALLOW ".trim().toLowerCase()).toBe("allow");
129+ });
130+ });
131+ "# ;
132+ create_test_file (
133+ root_dir,
134+ "web/src/__tests__/service.test.ts" ,
135+ service_test_ts,
136+ ) ;
137+
138+ let demo_go = r#"package demo
139+
140+ // Implements: SYS-REQ-426
141+ func RunDemo(flag bool) bool {
142+ return flag
143+ }
144+ "# ;
145+ create_test_file ( root_dir, "pkg/demo/demo.go" , demo_go) ;
146+
147+ let noise_ts = r#"export const literalOnly = "Implements: SYS-REQ-427";
148+
149+ export function unrelated() {
150+ return "SYS-REQ-428";
151+ }
152+
153+ // SYS-REQ-429 appears here without an annotation verb.
154+ export function looseComment() {
155+ return true;
156+ }
157+ "# ;
158+ create_test_file ( root_dir, "web/src/noise.ts" , noise_ts) ;
159+ }
160+
161+ fn find_result < ' a > ( results : & ' a [ Value ] , predicate : impl Fn ( & ' a Value ) -> bool ) -> & ' a Value {
162+ results
163+ . iter ( )
164+ . find ( |result| predicate ( result) )
165+ . expect ( "expected search result was not found" )
166+ }
167+
98168#[ test]
99169fn test_json_output_format_basic ( ) {
100170 let temp_dir = TempDir :: new ( ) . expect ( "Failed to create temp dir" ) ;
@@ -190,6 +260,138 @@ fn test_json_output_format_basic() {
190260 ) ;
191261}
192262
263+ #[ test]
264+ fn test_search_json_no_merge_reports_req_id_source_metadata ( ) {
265+ let temp_dir = TempDir :: new ( ) . expect ( "Failed to create temp dir" ) ;
266+ create_requirement_fixture ( & temp_dir) ;
267+
268+ let output = Command :: new ( "cargo" )
269+ . args ( [
270+ "run" ,
271+ "--quiet" ,
272+ "--" ,
273+ "search" ,
274+ "--allow-tests" ,
275+ "--strict-elastic-syntax" ,
276+ "--max-results" ,
277+ "20" ,
278+ "--no-merge" ,
279+ "--format" ,
280+ "json" ,
281+ r#""SYS-REQ-424" OR "SYS-REQ-425""# ,
282+ temp_dir. path ( ) . to_str ( ) . unwrap ( ) ,
283+ ] )
284+ . output ( )
285+ . expect ( "Failed to execute command" ) ;
286+
287+ assert ! (
288+ output. status. success( ) ,
289+ "search command failed: {}" ,
290+ String :: from_utf8_lossy( & output. stderr)
291+ ) ;
292+
293+ let stdout = String :: from_utf8_lossy ( & output. stdout ) ;
294+ let json_str = extract_json_from_output ( & stdout) ;
295+ let json_result: Value = serde_json:: from_str ( json_str) . expect ( "Failed to parse JSON output" ) ;
296+ let results = json_result
297+ . get ( "results" )
298+ . and_then ( Value :: as_array)
299+ . expect ( "results should be an array" ) ;
300+
301+ assert_eq ! (
302+ results. len( ) ,
303+ 5 ,
304+ "expected one result per semantic-ish block"
305+ ) ;
306+
307+ let method_result = find_result ( results, |result| {
308+ result
309+ . get ( "code" )
310+ . and_then ( Value :: as_str)
311+ . is_some_and ( |code| code. contains ( "evaluatePolicy" ) && code. contains ( "SYS-REQ-424" ) )
312+ } ) ;
313+ assert_eq ! (
314+ method_result. get( "language" ) . and_then( Value :: as_str) ,
315+ Some ( "typescript" )
316+ ) ;
317+ assert_eq ! (
318+ method_result. get( "node_type" ) . and_then( Value :: as_str) ,
319+ Some ( "method_definition" )
320+ ) ;
321+ assert_eq ! (
322+ method_result. get( "owner_symbol" ) . and_then( Value :: as_str) ,
323+ Some ( "evaluatePolicy" )
324+ ) ;
325+ assert_eq ! (
326+ method_result. get( "scope" ) . and_then( Value :: as_str) ,
327+ Some ( "function" )
328+ ) ;
329+
330+ let arrow_result = find_result ( results, |result| {
331+ result
332+ . get ( "code" )
333+ . and_then ( Value :: as_str)
334+ . is_some_and ( |code| code. contains ( "normalizeDecision" ) && code. contains ( "SYS-REQ-425" ) )
335+ } ) ;
336+ assert_eq ! (
337+ arrow_result. get( "node_type" ) . and_then( Value :: as_str) ,
338+ Some ( "export_statement" )
339+ ) ;
340+ assert_eq ! (
341+ arrow_result. get( "owner_symbol" ) . and_then( Value :: as_str) ,
342+ Some ( "normalizeDecision" )
343+ ) ;
344+ assert_eq ! (
345+ arrow_result. get( "scope" ) . and_then( Value :: as_str) ,
346+ Some ( "function" )
347+ ) ;
348+
349+ let callback_result = find_result ( results, |result| {
350+ result
351+ . get ( "code" )
352+ . and_then ( Value :: as_str)
353+ . is_some_and ( |code| code. contains ( "test(\" accepts valid policy\" " ) )
354+ } ) ;
355+ assert_eq ! (
356+ callback_result. get( "node_type" ) . and_then( Value :: as_str) ,
357+ Some ( "arrow_function" )
358+ ) ;
359+ assert_eq ! (
360+ callback_result. get( "scope" ) . and_then( Value :: as_str) ,
361+ Some ( "test" )
362+ ) ;
363+ assert_eq ! (
364+ callback_result. get( "is_test" ) . and_then( Value :: as_bool) ,
365+ Some ( true )
366+ ) ;
367+
368+ let leading_comments = callback_result
369+ . get ( "leading_comments" )
370+ . and_then ( Value :: as_array)
371+ . expect ( "callback result should expose leading comments" ) ;
372+ assert_eq ! (
373+ leading_comments[ 0 ] . get( "text" ) . and_then( Value :: as_str) ,
374+ Some ( "// Verifies: SYS-REQ-424 [boundary]" )
375+ ) ;
376+
377+ let matches = callback_result
378+ . get ( "matches" )
379+ . and_then ( Value :: as_array)
380+ . expect ( "callback result should expose classified matches" ) ;
381+ assert_eq ! (
382+ matches[ 0 ] . get( "text" ) . and_then( Value :: as_str) ,
383+ Some ( "SYS-REQ-424" )
384+ ) ;
385+ assert_eq ! (
386+ matches[ 0 ] . get( "kind" ) . and_then( Value :: as_str) ,
387+ Some ( "comment" )
388+ ) ;
389+ assert_eq ! (
390+ matches[ 0 ] . get( "comment_role" ) . and_then( Value :: as_str) ,
391+ Some ( "leading" )
392+ ) ;
393+ }
394+
193395#[ test]
194396fn test_json_output_with_special_characters ( ) {
195397 let temp_dir = TempDir :: new ( ) . expect ( "Failed to create temp dir" ) ;
0 commit comments