From 0ae059da3c68ed90eb2bd3741d8f8bae4256bc7f Mon Sep 17 00:00:00 2001 From: Andrew Hundt Date: Wed, 1 Jul 2026 01:02:00 -0400 Subject: [PATCH] fix(pipeline): reject generationless delta apply Keep plan-only exact-delta candidates usable with provisional generation zero, but make apply_file_delta_batch fail closed with missing_generation before calling the store publish path. Add a focused regression for generation-zero apply attempts while preserving the existing publish_error coverage for positive but unreserved generations. Validation: git diff --check; bash scripts/check-source-safety.sh; make -j8 -f Makefile.cbm build/c/test-runner; CBM_ONLY_SUITE=pipeline ./build/c/test-runner. Signed-off-by: Andrew Hundt --- src/pipeline/pipeline_delta.c | 17 +++++++++++++++++ tests/test_pipeline.c | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/pipeline/pipeline_delta.c b/src/pipeline/pipeline_delta.c index 6e54f45bc..75969e993 100644 --- a/src/pipeline/pipeline_delta.c +++ b/src/pipeline/pipeline_delta.c @@ -1072,6 +1072,19 @@ static bool delta_plan_affected_paths_in_batch(const cbm_pipeline_file_delta_pla return true; } +static bool delta_batch_has_positive_generation(const cbm_pipeline_file_delta_t *const *deltas, + int delta_count) { + if (!deltas || delta_count <= 0) { + return false; + } + for (int i = 0; i < delta_count; i++) { + if (!deltas[i] || deltas[i]->delta.generation <= 0) { + return false; + } + } + return true; +} + int cbm_pipeline_apply_file_delta_batch(cbm_store_t *store, const cbm_pipeline_file_delta_t *const *deltas, int delta_count, int max_affected_paths, @@ -1085,6 +1098,10 @@ int cbm_pipeline_apply_file_delta_batch(cbm_store_t *store, delta_plan_set_fallback(out, cbm_delta_reason_frontier_requires_batch); return CBM_STORE_OK; } + if (!delta_batch_has_positive_generation(deltas, delta_count)) { + delta_plan_set_fallback(out, cbm_delta_reason_missing_generation); + return CBM_STORE_OK; + } if (delta_count == 1 && deltas[0] && deltas[0]->change_kind == CBM_PIPELINE_DELTA_CHANGE_DELETE) { rc = cbm_store_delete_file_delta_complete( diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 63d1fd294..92262f0c1 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -3942,6 +3942,38 @@ TEST(pipeline_file_delta_apply_falls_back_on_publish_error) { PASS(); } +TEST(pipeline_file_delta_apply_falls_back_without_generation) { + enum { PIPELINE_DELTA_APPLY_ONE = 1 }; + cbm_store_t *s = cbm_store_open_memory(); + ASSERT_NOT_NULL(s); + ASSERT_EQ(cbm_store_upsert_project(s, "test", "/tmp/test"), CBM_STORE_OK); + ASSERT_EQ(pipeline_delta_seed_existing_ownership(s, "test", "lib.go", "test.lib.Old"), + CBM_STORE_OK); + + cbm_store_symbol_export_t exports[1] = { + {.qualified_name = "test.lib.Value", .node_id = CBM_STORE_NO_NODE_ID}}; + cbm_pipeline_file_delta_t delta = { + .delta = {.project = "test", .rel_path = "lib.go", .exports = exports, .export_count = 1}}; + cbm_file_hash_t hash = {0}; + cbm_file_state_t state = {0}; + pipeline_delta_attach_test_metadata(&delta, &hash, &state); + delta.delta.generation = 0; + delta.file_state.generation = 0; + + const cbm_pipeline_file_delta_t *deltas[] = {&delta}; + cbm_pipeline_file_delta_plan_t plan = {0}; + ASSERT_EQ(cbm_pipeline_apply_file_delta_batch(s, deltas, PIPELINE_DELTA_APPLY_ONE, + CBM_SZ_4, &plan), + CBM_STORE_OK); + ASSERT_EQ(plan.route, CBM_PIPELINE_DELTA_ROUTE_FALLBACK); + ASSERT_STR_EQ(plan.reason, "missing_generation"); + ASSERT_EQ(pipeline_delta_store_qn_exists(s, "test", "test.lib.Value"), 0); + + cbm_pipeline_file_delta_plan_free(&plan); + cbm_store_close(s); + PASS(); +} + TEST(pipeline_file_delta_plan_falls_back_without_existing_ownership) { cbm_store_t *s = cbm_store_open_memory(); ASSERT_NOT_NULL(s); @@ -9402,6 +9434,7 @@ SUITE(pipeline) { RUN_TEST(pipeline_file_state_persist_helper_rolls_back_on_failure); RUN_TEST(pipeline_file_delta_plan_candidate_from_frontier); RUN_TEST(pipeline_file_delta_apply_falls_back_on_publish_error); + RUN_TEST(pipeline_file_delta_apply_falls_back_without_generation); RUN_TEST(pipeline_file_delta_plan_falls_back_without_existing_ownership); RUN_TEST(pipeline_file_delta_plan_falls_back_on_external_inbound_edge); RUN_TEST(pipeline_file_delta_plan_falls_back_on_unowned_structural_inbound_edge);