diff --git a/src/pipeline/pipeline_delta.c b/src/pipeline/pipeline_delta.c index f19ee16c3..e67f592c5 100644 --- a/src/pipeline/pipeline_delta.c +++ b/src/pipeline/pipeline_delta.c @@ -10,9 +10,12 @@ static const char cbm_delta_edge_imports[] = "IMPORTS"; static const char cbm_delta_prop_is_exported[] = "is_exported"; static const char cbm_delta_reason_candidate[] = "candidate"; +static const char cbm_delta_reason_delete_requires_full[] = "delete_requires_full"; static const char cbm_delta_reason_frontier_error[] = "frontier_error"; static const char cbm_delta_reason_frontier_too_large[] = "frontier_too_large"; static const char cbm_delta_reason_invalid_input[] = "invalid_input"; +static const char cbm_delta_reason_missing_file_metadata[] = "missing_file_metadata"; +static const char cbm_delta_reason_rename_requires_full[] = "rename_requires_full"; static const char cbm_delta_reason_unsupported_edges[] = "unsupported_edges"; enum { CBM_DELTA_GROWTH = 2 }; @@ -248,6 +251,20 @@ static void delta_plan_set_fallback(cbm_pipeline_file_delta_plan_t *plan, const plan->reason = reason; } +static bool delta_field_matches(const char *actual, const char *expected) { + return actual && expected && strcmp(actual, expected) == 0; +} + +static bool delta_file_metadata_complete(const cbm_store_file_delta_t *delta) { + return delta && delta->file_hash && delta->file_state && + delta_field_matches(delta->file_hash->project, delta->project) && + delta_field_matches(delta->file_hash->rel_path, delta->rel_path) && + delta->file_hash->sha256 && + delta_field_matches(delta->file_state->project, delta->project) && + delta_field_matches(delta->file_state->rel_path, delta->rel_path) && + delta->file_state->content_hash && delta->file_state->indexed_at; +} + int cbm_pipeline_plan_file_delta(cbm_store_t *store, const cbm_pipeline_file_delta_t *delta, int max_affected_paths, cbm_pipeline_file_delta_plan_t *out) { if (!out) { @@ -259,10 +276,22 @@ int cbm_pipeline_plan_file_delta(cbm_store_t *store, const cbm_pipeline_file_del max_affected_paths <= 0) { return CBM_STORE_OK; } + if (delta->change_kind == CBM_PIPELINE_DELTA_CHANGE_DELETE) { + delta_plan_set_fallback(out, cbm_delta_reason_delete_requires_full); + return CBM_STORE_OK; + } + if (delta->change_kind == CBM_PIPELINE_DELTA_CHANGE_RENAME) { + delta_plan_set_fallback(out, cbm_delta_reason_rename_requires_full); + return CBM_STORE_OK; + } if (delta->unsupported_edge_count > 0) { delta_plan_set_fallback(out, cbm_delta_reason_unsupported_edges); return CBM_STORE_OK; } + if (!delta_file_metadata_complete(&delta->delta)) { + delta_plan_set_fallback(out, cbm_delta_reason_missing_file_metadata); + return CBM_STORE_OK; + } const char **new_export_qns = NULL; if (delta->delta.export_count > 0) { diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index 9bc633120..59cebcfae 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -118,6 +118,12 @@ typedef struct { cbm_store_symbol_export_t *exports; cbm_store_import_ref_t *imports; int unsupported_edge_count; + enum { + CBM_PIPELINE_DELTA_CHANGE_UPSERT = 0, + CBM_PIPELINE_DELTA_CHANGE_DELETE = 1, + CBM_PIPELINE_DELTA_CHANGE_RENAME = 2, + } change_kind; + const char *old_rel_path; /* borrowed; set for rename preflight only */ } cbm_pipeline_file_delta_t; typedef enum { diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 55dd23a00..ddab4fdcf 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -3170,6 +3170,30 @@ static int pipeline_delta_plan_contains_path(const cbm_pipeline_file_delta_plan_ return 0; } +static void pipeline_delta_attach_test_metadata(cbm_pipeline_file_delta_t *delta, + cbm_file_hash_t *hash, + cbm_file_state_t *state) { + enum { PIPELINE_DELTA_TEST_GENERATION = 1 }; + *hash = (cbm_file_hash_t){.project = delta->delta.project, + .rel_path = delta->delta.rel_path, + .sha256 = "test-hash", + .mtime_ns = 1, + .size = 10}; + *state = (cbm_file_state_t){.project = delta->delta.project, + .rel_path = delta->delta.rel_path, + .content_hash = "test-content", + .git_oid = "", + .mtime_ns = 1, + .size = 10, + .language = "go", + .pass_fingerprint = "test-pass", + .generation = PIPELINE_DELTA_TEST_GENERATION, + .indexed_at = "2026-06-30T00:00:00Z"}; + delta->delta.generation = PIPELINE_DELTA_TEST_GENERATION; + delta->delta.file_hash = hash; + delta->delta.file_state = state; +} + TEST(pipeline_file_delta_descriptor_from_gbuf) { cbm_gbuf_t *gb = cbm_gbuf_new("proj", "/tmp/proj"); ASSERT_NOT_NULL(gb); @@ -3250,6 +3274,9 @@ TEST(pipeline_file_delta_plan_candidate_from_frontier) { {.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); cbm_pipeline_file_delta_plan_t plan = {0}; ASSERT_EQ(cbm_pipeline_plan_file_delta(s, &delta, CBM_SZ_4, &plan), CBM_STORE_OK); @@ -3263,6 +3290,23 @@ TEST(pipeline_file_delta_plan_candidate_from_frontier) { PASS(); } +TEST(pipeline_file_delta_plan_falls_back_without_file_metadata) { + 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); + cbm_pipeline_file_delta_t delta = {.delta = {.project = "test", .rel_path = "main.go"}}; + + cbm_pipeline_file_delta_plan_t plan = {0}; + ASSERT_EQ(cbm_pipeline_plan_file_delta(s, &delta, CBM_SZ_4, &plan), CBM_STORE_OK); + ASSERT_EQ(plan.route, CBM_PIPELINE_DELTA_ROUTE_FALLBACK); + ASSERT_STR_EQ(plan.reason, "missing_file_metadata"); + ASSERT_EQ(plan.affected_count, 0); + + cbm_pipeline_file_delta_plan_free(&plan); + cbm_store_close(s); + PASS(); +} + TEST(pipeline_file_delta_plan_falls_back_on_unsupported_edges) { cbm_store_t *s = cbm_store_open_memory(); ASSERT_NOT_NULL(s); @@ -3280,6 +3324,41 @@ TEST(pipeline_file_delta_plan_falls_back_on_unsupported_edges) { PASS(); } +TEST(pipeline_file_delta_plan_falls_back_on_delete) { + cbm_store_t *s = cbm_store_open_memory(); + ASSERT_NOT_NULL(s); + cbm_pipeline_file_delta_t delta = {.delta = {.project = "test", .rel_path = "gone.go"}, + .change_kind = CBM_PIPELINE_DELTA_CHANGE_DELETE}; + + cbm_pipeline_file_delta_plan_t plan = {0}; + ASSERT_EQ(cbm_pipeline_plan_file_delta(s, &delta, CBM_SZ_4, &plan), CBM_STORE_OK); + ASSERT_EQ(plan.route, CBM_PIPELINE_DELTA_ROUTE_FALLBACK); + ASSERT_STR_EQ(plan.reason, "delete_requires_full"); + ASSERT_EQ(plan.affected_count, 0); + + cbm_pipeline_file_delta_plan_free(&plan); + cbm_store_close(s); + PASS(); +} + +TEST(pipeline_file_delta_plan_falls_back_on_rename) { + cbm_store_t *s = cbm_store_open_memory(); + ASSERT_NOT_NULL(s); + cbm_pipeline_file_delta_t delta = {.delta = {.project = "test", .rel_path = "new.go"}, + .change_kind = CBM_PIPELINE_DELTA_CHANGE_RENAME, + .old_rel_path = "old.go"}; + + cbm_pipeline_file_delta_plan_t plan = {0}; + ASSERT_EQ(cbm_pipeline_plan_file_delta(s, &delta, CBM_SZ_4, &plan), CBM_STORE_OK); + ASSERT_EQ(plan.route, CBM_PIPELINE_DELTA_ROUTE_FALLBACK); + ASSERT_STR_EQ(plan.reason, "rename_requires_full"); + ASSERT_EQ(plan.affected_count, 0); + + cbm_pipeline_file_delta_plan_free(&plan); + cbm_store_close(s); + PASS(); +} + TEST(pipeline_file_delta_plan_falls_back_on_large_frontier) { cbm_store_t *s = cbm_store_open_memory(); ASSERT_NOT_NULL(s); @@ -3298,6 +3377,9 @@ TEST(pipeline_file_delta_plan_falls_back_on_large_frontier) { {.qualified_name = "test.lib.Hot", .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); cbm_pipeline_file_delta_plan_t plan = {0}; ASSERT_EQ(cbm_pipeline_plan_file_delta(s, &delta, CBM_SZ_2, &plan), CBM_STORE_OK); @@ -7647,7 +7729,10 @@ SUITE(pipeline) { RUN_TEST(pipeline_file_delta_descriptor_from_gbuf); RUN_TEST(pipeline_file_delta_descriptor_marks_unsupported_edges); RUN_TEST(pipeline_file_delta_plan_candidate_from_frontier); + RUN_TEST(pipeline_file_delta_plan_falls_back_without_file_metadata); RUN_TEST(pipeline_file_delta_plan_falls_back_on_unsupported_edges); + RUN_TEST(pipeline_file_delta_plan_falls_back_on_delete); + RUN_TEST(pipeline_file_delta_plan_falls_back_on_rename); RUN_TEST(pipeline_file_delta_plan_falls_back_on_large_frontier); /* File persistence */ RUN_TEST(store_file_persistence);