@@ -66,6 +66,110 @@ def stub_osv_hit(cve, fixed:)
6666 end
6767 end
6868
69+ it "only walks history for records not already present with --new-history" do
70+ stub_osv_hit ( "CVE-2024-1234" , fixed : "2.28.1" )
71+
72+ Dir . mktmpdir do |dir |
73+ path = File . join ( dir , "BREW-requests-CVE-2024-1234.json" )
74+ record = {
75+ "schema_version" => Homebrew ::Vulns ::OsvExport ::SCHEMA_VERSION ,
76+ "id" => "BREW-requests-CVE-2024-1234" ,
77+ "modified" => "2026-01-01T00:00:00Z" ,
78+ "affected" => [ {
79+ "package" => { "ecosystem" => "Homebrew" , "name" => "requests" } ,
80+ "ranges" => [ { "type" => "ECOSYSTEM" , "events" => [
81+ { "introduced" => "0" } , { "fixed" => "2.28.1" }
82+ ] } ] ,
83+ } ] ,
84+ "database_specific" => { "source" => "matched" } ,
85+ }
86+ File . write ( path , JSON . generate ( record ) )
87+
88+ matcher = Homebrew ::Vulns ::Match . new
89+ allow ( Homebrew ::Vulns ::Match ) . to receive ( :new ) . and_return ( matcher )
90+ expect ( matcher ) . not_to receive ( :first_fixed_version )
91+
92+ expect { cmd_for ( "requests" , "--output" , dir , "--new-history" ) . run }
93+ . to output ( /0 history walks/ ) . to_stdout
94+ expect ( JSON . parse ( File . read ( path ) ) . dig ( "affected" , 0 , "ranges" , 0 , "events" , 1 ) )
95+ . to eq ( "fixed" => "2.28.1" )
96+ end
97+ end
98+
99+ it "uses the historical boundary for a new record with --new-history" do
100+ stub_osv_hit ( "CVE-2024-1234" , fixed : "2.28.1" )
101+ matcher = Homebrew ::Vulns ::Match . new
102+ expect ( matcher ) . to receive ( :first_fixed_version ) . and_return ( "2.28.1" )
103+ allow ( Homebrew ::Vulns ::Match ) . to receive ( :new ) . and_return ( matcher )
104+
105+ Dir . mktmpdir do |dir |
106+ expect { cmd_for ( "requests" , "--output" , dir , "--new-history" ) . run }
107+ . to output ( /1 history walks/ ) . to_stdout
108+ path = File . join ( dir , "BREW-requests-CVE-2024-1234.json" )
109+ expect ( JSON . parse ( File . read ( path ) ) . dig ( "affected" , 0 , "ranges" , 0 , "events" , 1 ) )
110+ . to eq ( "fixed" => "2.28.1" )
111+ end
112+ end
113+
114+ it "walks history when an existing matched record has no ranges" do
115+ stub_osv_hit ( "CVE-2024-1234" , fixed : "2.28.1" )
116+ matcher = Homebrew ::Vulns ::Match . new
117+ expect ( matcher ) . to receive ( :first_fixed_version ) . and_return ( "2.28.1" )
118+ allow ( Homebrew ::Vulns ::Match ) . to receive ( :new ) . and_return ( matcher )
119+
120+ Dir . mktmpdir do |dir |
121+ path = File . join ( dir , "BREW-requests-CVE-2024-1234.json" )
122+ File . write ( path , JSON . generate ( {
123+ "id" => "BREW-requests-CVE-2024-1234" ,
124+ "affected" => [ { "package" => { "ecosystem" => "Homebrew" , "name" => "requests" } } ] ,
125+ "database_specific" => { "source" => "matched" } ,
126+ } ) )
127+
128+ cmd_for ( "requests" , "--output" , dir , "--new-history" ) . run
129+ expect ( JSON . parse ( File . read ( path ) ) . dig ( "affected" , 0 , "ranges" , 0 , "events" , 1 ) )
130+ . to eq ( "fixed" => "2.28.1" )
131+ end
132+ end
133+
134+ it "walks history when an existing record is malformed" do
135+ stub_osv_hit ( "CVE-2024-1234" , fixed : "2.28.1" )
136+ matcher = Homebrew ::Vulns ::Match . new
137+ expect ( matcher ) . to receive ( :first_fixed_version ) . and_return ( "2.28.1" )
138+ allow ( Homebrew ::Vulns ::Match ) . to receive ( :new ) . and_return ( matcher )
139+
140+ Dir . mktmpdir do |dir |
141+ path = File . join ( dir , "BREW-requests-CVE-2024-1234.json" )
142+ File . write ( path , "{" )
143+
144+ cmd_for ( "requests" , "--output" , dir , "--new-history" ) . run
145+ expect ( JSON . parse ( File . read ( path ) ) . dig ( "affected" , 0 , "ranges" , 0 , "events" , 1 ) )
146+ . to eq ( "fixed" => "2.28.1" )
147+ end
148+ end
149+
150+ it "does not walk history for a new record with --no-history" do
151+ stub_osv_hit ( "CVE-2024-1234" , fixed : "2.28.1" )
152+ matcher = Homebrew ::Vulns ::Match . new
153+ expect ( matcher ) . not_to receive ( :first_fixed_version )
154+ allow ( Homebrew ::Vulns ::Match ) . to receive ( :new ) . and_return ( matcher )
155+
156+ Dir . mktmpdir do |dir |
157+ cmd_for ( "requests" , "--output" , dir , "--no-history" ) . run
158+ end
159+ end
160+
161+ it "does not count a history walk for a new record that is still affected" do
162+ stub_osv_hit ( "CVE-2024-1234" , fixed : "2.32.0" )
163+ matcher = Homebrew ::Vulns ::Match . new
164+ expect ( matcher ) . not_to receive ( :first_fixed_version )
165+ allow ( Homebrew ::Vulns ::Match ) . to receive ( :new ) . and_return ( matcher )
166+
167+ Dir . mktmpdir do |dir |
168+ expect { cmd_for ( "requests" , "--output" , dir , "--new-history" ) . run }
169+ . to output ( /0 history walks/ ) . to_stdout
170+ end
171+ end
172+
69173 it "drops :not_applicable hits instead of emitting them as open ranges" do
70174 allow ( Homebrew ::Vulns ::OSV ) . to receive ( :query_batch ) . and_return ( [ [ { "id" => "CVE-2024-1234" } ] , [ ] ] )
71175 allow ( Homebrew ::Vulns ::OSV ) . to receive ( :vulnerability ) . with ( "CVE-2024-1234" ) . and_return (
@@ -88,7 +192,11 @@ def stub_osv_hit(cve, fixed:)
88192 "database_specific" => { "source" => "generated" } ,
89193 "affected" => [ { "ecosystem_specific" => { "fix" => "patch" } } ] } ) )
90194
91- expect { cmd_for ( "requests" , "--output" , dir , "--no-history" ) . run }
195+ matcher = Homebrew ::Vulns ::Match . new
196+ expect ( matcher ) . not_to receive ( :first_fixed_version )
197+ allow ( Homebrew ::Vulns ::Match ) . to receive ( :new ) . and_return ( matcher )
198+
199+ expect { cmd_for ( "requests" , "--output" , dir , "--new-history" ) . run }
92200 . to output ( /0 records written.*1 generated left as-is/ ) . to_stdout
93201 expect ( JSON . parse ( File . read ( path ) ) . dig ( "affected" , 0 , "ecosystem_specific" , "fix" ) ) . to eq "patch"
94202 end
@@ -160,6 +268,16 @@ def stub_osv_hit(cve, fixed:)
160268 expect { described_class . new ( [ "--all" , "--json" ] ) } . to raise_error ( UsageError , /mutually exclusive/ )
161269 end
162270
271+ it "requires --output with --new-history" do
272+ expect { described_class . new ( [ "requests" , "--new-history" ] ) }
273+ . to raise_error ( UsageError , /--new-history.*--output/ )
274+ end
275+
276+ it "rejects --new-history with --no-history" do
277+ expect { described_class . new ( [ "requests" , "--output" , "out" , "--new-history" , "--no-history" ] ) }
278+ . to raise_error ( UsageError , /mutually exclusive/ )
279+ end
280+
163281 it "emits the formula-identity index with --index" do
164282 requests
165283 core_tap = instance_double ( CoreTap , installed? : true , name : "homebrew/core" , formula_names : [ "requests" ] )
0 commit comments