KSvend Claude Happy commited on
Commit
cb89291
·
1 Parent(s): c6c1ec2

fix: NDVI harvest baseline_path never assigned after download

Browse files

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

app/indicators/ndvi.py CHANGED
@@ -109,6 +109,7 @@ class NdviIndicator(BaseIndicator):
109
  baseline_dir = os.path.join(results_dir, "baseline")
110
  os.makedirs(baseline_dir, exist_ok=True)
111
  paths = baseline_job.download_results(baseline_dir)
 
112
  except Exception as exc:
113
  logger.warning("NDVI baseline batch download failed, degrading: %s", exc)
114
 
 
109
  baseline_dir = os.path.join(results_dir, "baseline")
110
  os.makedirs(baseline_dir, exist_ok=True)
111
  paths = baseline_job.download_results(baseline_dir)
112
+ baseline_path = self._find_tif(paths, baseline_dir)
113
  except Exception as exc:
114
  logger.warning("NDVI baseline batch download failed, degrading: %s", exc)
115
 
tests/test_indicator_ndvi.py CHANGED
@@ -263,3 +263,67 @@ async def test_ndvi_harvest_falls_back_when_current_fails(test_aoi, test_time_ra
263
  )
264
 
265
  assert result.data_source == "placeholder"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  )
264
 
265
  assert result.data_source == "placeholder"
266
+
267
+
268
+ @pytest.mark.asyncio
269
+ async def test_ndvi_harvest_uses_baseline_when_available(test_aoi, test_time_range):
270
+ """harvest() computes change vs baseline when baseline download succeeds."""
271
+ from app.indicators.ndvi import NdviIndicator
272
+
273
+ indicator = NdviIndicator()
274
+
275
+ with tempfile.TemporaryDirectory() as tmpdir:
276
+ # Current: higher NDVI
277
+ current_path = os.path.join(tmpdir, "current.tif")
278
+ _mock_ndvi_tif(current_path, n_months=12)
279
+
280
+ # Baseline: lower NDVI (shift down by 0.1)
281
+ baseline_path = os.path.join(tmpdir, "baseline.tif")
282
+ import rasterio
283
+ from rasterio.transform import from_bounds
284
+ rng = np.random.default_rng(99)
285
+ data = np.zeros((12, 10, 10), dtype=np.float32)
286
+ for m in range(12):
287
+ data[m] = 0.2 + 0.1 * np.sin(np.pi * (m - 3) / 6) + rng.normal(0, 0.02, (10, 10))
288
+ with rasterio.open(
289
+ baseline_path, "w", driver="GTiff", height=10, width=10, count=12,
290
+ dtype="float32", crs="EPSG:4326",
291
+ transform=from_bounds(32.45, 15.65, 32.65, 15.8, 10, 10),
292
+ nodata=-9999.0,
293
+ ) as dst:
294
+ for i in range(12):
295
+ dst.write(data[i], i + 1)
296
+
297
+ rgb_path = os.path.join(tmpdir, "rgb.tif")
298
+ _mock_true_color_tif(rgb_path)
299
+
300
+ def make_mock_job(src_path):
301
+ job = MagicMock()
302
+ job.job_id = "j-test"
303
+ def fake_download_results(target):
304
+ import shutil
305
+ os.makedirs(target, exist_ok=True)
306
+ dest = os.path.join(target, "result.tif")
307
+ shutil.copy(src_path, dest)
308
+ from pathlib import Path
309
+ return {Path(dest): {"type": "image/tiff"}}
310
+ job.download_results.side_effect = fake_download_results
311
+ job.status.return_value = "finished"
312
+ return job
313
+
314
+ current_job = make_mock_job(current_path)
315
+ baseline_job = make_mock_job(baseline_path)
316
+ true_color_job = make_mock_job(rgb_path)
317
+
318
+ result = await indicator.harvest(
319
+ test_aoi, test_time_range,
320
+ batch_jobs=[current_job, baseline_job, true_color_job],
321
+ )
322
+
323
+ assert result.data_source == "satellite"
324
+ # With distinct current vs baseline, confidence should NOT be LOW
325
+ # (it should be HIGH with 12 valid months)
326
+ assert result.confidence == ConfidenceLevel.HIGH
327
+ # Baseline band data should be present in chart
328
+ assert "baseline_mean" in result.chart_data
329
+ assert len(result.chart_data["baseline_mean"]) > 0