Merge pull request #811 from DeusData/distill/793-walk-exclusions

fix(pipeline): honor discovery exclusions in pkgmap/path-alias/envscan walks
This commit is contained in:
Martin Vogel
2026-07-04 04:03:53 +02:00
committed by GitHub
10 changed files with 285 additions and 27 deletions
+15 -8
View File
@@ -363,16 +363,22 @@ static int scan_env_file(const char *full_path, const char *rel, file_type_t ft,
/* Process a single directory entry for env scanning. Returns bindings added. */
static int process_env_entry(cbm_dirent_t *ent, const char *dir_path, const char *root_path,
cbm_env_binding_t *out, int max_out, char path_stack[][CBM_SZ_512],
int *stack_top) {
int *stack_top, char **excluded_dirs, int excluded_count) {
char full_path[CBM_SZ_512];
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, ent->name);
const char *rel = full_path + strlen(root_path);
while (*rel == '/') {
rel++;
}
struct stat st;
if (stat(full_path, &st) != 0) {
return 0;
}
if (S_ISDIR(st.st_mode)) {
if (!is_ignored_dir(ent->name) && *stack_top < CBM_SZ_256) {
if (!is_ignored_dir(ent->name) &&
!cbm_pipeline_relpath_is_excluded(rel, excluded_dirs, excluded_count) &&
*stack_top < CBM_SZ_256) {
strncpy(path_stack[*stack_top], full_path, sizeof(path_stack[0]) - 1);
path_stack[*stack_top][sizeof(path_stack[0]) - SKIP_ONE] = '\0';
(*stack_top)++;
@@ -386,14 +392,11 @@ static int process_env_entry(cbm_dirent_t *ent, const char *dir_path, const char
if (ft == FT_UNKNOWN) {
return 0;
}
const char *rel = full_path + strlen(root_path);
while (*rel == '/') {
rel++;
}
return scan_env_file(full_path, rel, ft, out, max_out);
}
int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int max_out) {
int cbm_scan_project_env_urls_excluded(const char *root_path, cbm_env_binding_t *out, int max_out,
char **excluded_dirs, int excluded_count) {
if (!root_path || !out || max_out <= 0) {
return 0;
}
@@ -418,9 +421,13 @@ int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int
cbm_dirent_t *ent;
while ((ent = cbm_readdir(d)) && count < max_out) {
count += process_env_entry(ent, dir_path, root_path, out + count, max_out - count,
path_stack, &stack_top);
path_stack, &stack_top, excluded_dirs, excluded_count);
}
cbm_closedir(d);
}
return count;
}
int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int max_out) {
return cbm_scan_project_env_urls_excluded(root_path, out, max_out, NULL, 0);
}
+1 -1
View File
@@ -818,7 +818,7 @@ static void merge_pkg_entries(cbm_pipeline_ctx_t *ctx, cbm_pkg_entries_t *pkg_en
* by the main discoverer (package.json, composer.json — in
* IGNORED_JSON_FILES) still feed pkgmap. Append into worker 0's
* array so the existing merge below sees them. */
cbm_pkgmap_scan_repo(ctx->repo_path, &pkg_entries[0]);
cbm_pkgmap_scan_repo(ctx->repo_path, &pkg_entries[0], ctx->excluded_dirs, ctx->excluded_count);
cbm_pipeline_set_pkgmap(cbm_pkgmap_build(pkg_entries, worker_count, ctx->project_name));
for (int i = 0; i < worker_count; i++) {
cbm_pkg_entries_free(&pkg_entries[i]);
+11 -7
View File
@@ -845,7 +845,7 @@ static bool pkgmap_is_reparse_point(const char *abs_path) {
* it hang. On Windows we additionally skip reparse points before
* descending as a best-effort early-out. */
static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_entries_t *entries,
int depth) {
int depth, char **excluded_dirs, int excluded_count) {
if (depth >= PKGMAP_WALK_MAX_DEPTH) {
cbm_log_info("pkgmap.walk", "depth_cap", rel_dir && rel_dir[0] ? rel_dir : ".");
return 0;
@@ -874,7 +874,8 @@ static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_ent
continue;
}
if (S_ISDIR(st.st_mode)) {
if (cbm_should_skip_dir(name, CBM_MODE_FULL)) {
if (cbm_should_skip_dir(name, CBM_MODE_FULL) ||
cbm_pipeline_relpath_is_excluded(rel_path, excluded_dirs, excluded_count)) {
continue;
}
#ifdef _WIN32
@@ -886,7 +887,8 @@ static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_ent
continue;
}
#endif
parsed += pkgmap_walk_dir(abs_path, rel_path, entries, depth + 1);
parsed += pkgmap_walk_dir(abs_path, rel_path, entries, depth + 1, excluded_dirs,
excluded_count);
continue;
}
if (!S_ISREG(st.st_mode)) {
@@ -920,11 +922,12 @@ static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_ent
* Windows reparse points, so it cannot hang on directory junctions.
* This is what lets bare workspace imports (e.g. "@org/pkg" declared in
* an ignored package.json) resolve on Windows as well as POSIX. */
int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries) {
int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries, char **excluded_dirs,
int excluded_count) {
if (!repo_path || !entries) {
return 0;
}
int parsed = pkgmap_walk_dir(repo_path, "", entries, 0);
int parsed = pkgmap_walk_dir(repo_path, "", entries, 0, excluded_dirs, excluded_count);
cbm_log_info("pkgmap.scan_repo", "manifests", pkgmap_itoa(parsed));
return parsed;
}
@@ -961,7 +964,8 @@ CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file
* (the canonical case: package.json, which is in IGNORED_JSON_FILES).
* Falls back to the files[]-only behaviour if repo_path is NULL. */
CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_info_t *files,
int file_count, const char *project_name) {
int file_count, const char *project_name,
char **excluded_dirs, int excluded_count) {
cbm_pkg_entries_t entries;
cbm_pkg_entries_init(&entries);
@@ -985,7 +989,7 @@ CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_i
free(source);
}
int from_walk = cbm_pkgmap_scan_repo(repo_path, &entries);
int from_walk = cbm_pkgmap_scan_repo(repo_path, &entries, excluded_dirs, excluded_count);
cbm_log_info("pkgmap.scan", "manifests_from_files", pkgmap_itoa(from_files),
"manifests_from_walk", pkgmap_itoa(from_walk), "entries",
pkgmap_itoa(entries.count));
+18 -4
View File
@@ -20,6 +20,8 @@
#include "pipeline/path_alias.h"
#include "pipeline/pipeline_internal.h"
#include "foundation/compat.h"
#include "foundation/compat_fs.h"
#include "foundation/constants.h"
@@ -380,7 +382,8 @@ static const char *const TS_CONFIG_NAMES[] = {"tsconfig.json", "jsconfig.json"};
enum { TS_CONFIG_NAMES_COUNT = 2 };
static void find_alias_files(const char *abs_dir, const char *rel_dir, alias_config_hit_t *out,
int *count, int max_count, int depth) {
int *count, int max_count, int depth, char **excluded_dirs,
int excluded_count) {
if (*count >= max_count || depth > CBM_PATH_ALIAS_MAX_DEPTH) {
return;
}
@@ -422,12 +425,18 @@ static void find_alias_files(const char *abs_dir, const char *rel_dir, alias_con
} else {
snprintf(child_rel, sizeof(child_rel), "%s/%s", rel_dir, name);
}
find_alias_files(child_abs, child_rel, out, count, max_count, depth + 1);
if (cbm_pipeline_relpath_is_excluded(child_rel, excluded_dirs, excluded_count)) {
continue;
}
find_alias_files(child_abs, child_rel, out, count, max_count, depth + 1, excluded_dirs,
excluded_count);
}
cbm_closedir(d);
}
cbm_path_alias_collection_t *cbm_load_path_aliases(const char *repo_path) {
cbm_path_alias_collection_t *cbm_load_path_aliases_excluded(const char *repo_path,
char **excluded_dirs,
int excluded_count) {
if (!repo_path) {
return NULL;
}
@@ -436,7 +445,8 @@ cbm_path_alias_collection_t *cbm_load_path_aliases(const char *repo_path) {
return NULL;
}
int count = 0;
find_alias_files(repo_path, "", hits, &count, CBM_PATH_ALIAS_MAX_FILES, 0);
find_alias_files(repo_path, "", hits, &count, CBM_PATH_ALIAS_MAX_FILES, 0, excluded_dirs,
excluded_count);
if (count >= CBM_PATH_ALIAS_MAX_FILES) {
cbm_log_warn("path_alias.files.cap_hit", "repo", repo_path, "kept", "256_of_more");
}
@@ -479,6 +489,10 @@ cbm_path_alias_collection_t *cbm_load_path_aliases(const char *repo_path) {
return coll;
}
cbm_path_alias_collection_t *cbm_load_path_aliases(const char *repo_path) {
return cbm_load_path_aliases_excluded(repo_path, NULL, 0);
}
const cbm_path_alias_map_t *cbm_path_alias_find_for_file(const cbm_path_alias_collection_t *coll,
const char *rel_path) {
if (!coll || !rel_path) {
+3
View File
@@ -72,6 +72,9 @@ typedef struct {
* are found (also NULL on out-of-memory). Caller frees with
* cbm_path_alias_collection_free. */
cbm_path_alias_collection_t *cbm_load_path_aliases(const char *repo_path);
cbm_path_alias_collection_t *cbm_load_path_aliases_excluded(const char *repo_path,
char **excluded_dirs,
int excluded_count);
/* Free a collection produced by cbm_load_path_aliases. NULL-safe. */
void cbm_path_alias_collection_free(cbm_path_alias_collection_t *coll);
+7 -3
View File
@@ -737,8 +737,9 @@ static int run_sequential_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
* Use the repo-walking variant so manifests filtered out by the main
* discoverer (package.json, composer.json) still feed pkgmap and let
* workspace imports like `@my/pkg` resolve to their target Module. */
cbm_pipeline_set_pkgmap(
cbm_pkgmap_build_from_repo(ctx->repo_path, files, file_count, ctx->project_name));
cbm_pipeline_set_pkgmap(cbm_pkgmap_build_from_repo(ctx->repo_path, files, file_count,
ctx->project_name, ctx->excluded_dirs,
ctx->excluded_count));
CBMFileResult **seq_cache = (CBMFileResult **)calloc(file_count, sizeof(CBMFileResult *));
if (seq_cache) {
@@ -1261,7 +1262,8 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
/* Phase 2b: Load build-tool path aliases (tsconfig/jsconfig today). NULL
* when no usable configs are found — non-TS projects pay nothing. */
path_aliases = cbm_load_path_aliases(p->repo_path);
path_aliases =
cbm_load_path_aliases_excluded(p->repo_path, p->excluded_dirs, p->excluded_count);
/* Build shared context for pass functions */
cbm_pipeline_ctx_t ctx = {
@@ -1273,6 +1275,8 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
.pipeline = p, /* so passes can record per-file skips (Track B) */
.mode = (int)p->mode,
.path_aliases = path_aliases,
.excluded_dirs = p->excluded_dirs,
.excluded_count = p->excluded_count,
};
rc = run_extraction_phase(p, &ctx, files, file_count);
+12 -1
View File
@@ -806,7 +806,16 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
cbm_log_info("incremental.registry_seed", "symbols", itoa_buf(cbm_registry_size(registry)),
"elapsed_ms", itoa_buf((int)elapsed_ms(t)));
cbm_path_alias_collection_t *path_aliases = cbm_load_path_aliases(cbm_pipeline_repo_path(p));
/* Discovery exclusions (gitignore + skip dirs) captured by the run that
* routed here. Borrowed from the pipeline so the auxiliary repo walks
* (pkgmap via merge_pkg_entries, path aliases) skip excluded subtrees on
* incremental runs too — same borrow as the full path (#792/#804). */
char **excluded_dirs = NULL;
int excluded_count = 0;
cbm_pipeline_get_excluded(p, &excluded_dirs, &excluded_count);
cbm_path_alias_collection_t *path_aliases =
cbm_load_path_aliases_excluded(cbm_pipeline_repo_path(p), excluded_dirs, excluded_count);
cbm_pipeline_ctx_t ctx = {
.project_name = project,
@@ -817,6 +826,8 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
.pipeline = p, /* so passes can record per-file skips (Track B) */
.mode = cbm_pipeline_get_mode(p),
.path_aliases = path_aliases,
.excluded_dirs = excluded_dirs,
.excluded_count = excluded_count,
};
for (int i = 0; i < ci; i++) {
+34 -3
View File
@@ -16,6 +16,7 @@
#include "cbm.h"
#include "lsp/go_lsp.h" /* CBMLSPDef for cbm_parallel_resolve cross-LSP inputs */
#include <stdatomic.h>
#include <string.h>
/* ── Shared pipeline constants ─────────────────────────────────── */
@@ -81,8 +82,30 @@ typedef struct {
* configs are an easy follow-on). NULL when no usable configs were found.
* Owned by pipeline.c / pipeline_incremental.c. */
const cbm_path_alias_collection_t *path_aliases;
/* Directory subtrees excluded during discovery. Borrowed from pipeline.c. */
char **excluded_dirs;
int excluded_count;
} cbm_pipeline_ctx_t;
static inline int cbm_pipeline_relpath_is_excluded(const char *rel_path, char *const *excluded_dirs,
int excluded_count) {
if (!rel_path || rel_path[0] == '\0' || !excluded_dirs || excluded_count <= 0) {
return 0;
}
for (int i = 0; i < excluded_count; i++) {
const char *excluded = excluded_dirs[i];
if (!excluded || excluded[0] == '\0') {
continue;
}
size_t n = strlen(excluded);
if (strncmp(rel_path, excluded, n) == 0 && (rel_path[n] == '\0' || rel_path[n] == '/')) {
return SKIP_ONE;
}
}
return 0;
}
/* Get the current pipeline's package map (NULL if none). */
CBMHashTable *cbm_pipeline_get_pkgmap(void);
void cbm_pipeline_set_pkgmap(CBMHashTable *map);
@@ -131,9 +154,11 @@ CBMHashTable *cbm_pkgmap_build(cbm_pkg_entries_t *worker_entries, int worker_cou
const char *project_name);
/* Build pkgmap by reading manifest files from the files array (sequential path). */
int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries);
int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries, char **excluded_dirs,
int excluded_count);
CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_info_t *files,
int file_count, const char *project_name);
int file_count, const char *project_name,
char **excluded_dirs, int excluded_count);
CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file_count,
const char *project_name);
@@ -530,8 +555,14 @@ typedef struct {
/* Scan a project directory for environment variable assignments with URL values.
* Walks the filesystem, scans Dockerfiles, shell scripts, .env, YAML, TOML,
* Terraform, and .properties files. Filters out secrets.
* Returns number of bindings written to out (up to max_out). */
* Returns number of bindings written to out (up to max_out).
* NOTE: this walker currently has no production callers — it is exercised
* only by tests. The _excluded variant honors discovery exclusions for
* consistency with the pkgmap/path-alias walks (#792); the plain variant
* scans unexcluded (NULL exclusion list). */
int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int max_out);
int cbm_scan_project_env_urls_excluded(const char *root_path, cbm_env_binding_t *out, int max_out,
char **excluded_dirs, int excluded_count);
/* ── Incremental pipeline (pipeline_incremental.c) ───────────────── */
+57
View File
@@ -336,6 +336,62 @@ TEST(path_alias_loader_monorepo_dotdot_climb) {
PASS();
}
/* ── Loader honors discovery exclusions (#792) ─────────────────── */
/* find_alias_files must not descend into discovery-excluded subtrees.
* Control run first (no exclusions → both configs collected) so the
* exclusion assertion below cannot pass vacuously. */
TEST(path_alias_loader_honors_discovery_exclusions) {
char tmpl[256];
snprintf(tmpl, sizeof(tmpl), "/tmp/cbm_palias_excl_XXXXXX");
char *root = cbm_mkdtemp(tmpl);
ASSERT_NOT_NULL(root);
char sub[512];
snprintf(sub, sizeof(sub), "%s/big_generated", root);
cbm_mkdir(sub);
char path[512];
snprintf(path, sizeof(path), "%s/tsconfig.json", root);
ASSERT_EQ(write_file(path,
"{\n \"compilerOptions\": {\n \"paths\": {\n"
" \"@root/*\": [\"shared/*\"]\n }\n }\n}\n"),
0);
snprintf(path, sizeof(path), "%s/big_generated/tsconfig.json", root);
ASSERT_EQ(write_file(path,
"{\n \"compilerOptions\": {\n \"paths\": {\n"
" \"@gen/*\": [\"./src/*\"]\n }\n }\n}\n"),
0);
/* Control: the unexcluded loader collects BOTH configs. */
cbm_path_alias_collection_t *coll = cbm_load_path_aliases(root);
ASSERT_NOT_NULL(coll);
ASSERT_EQ(coll->count, 2);
cbm_path_alias_collection_free(coll);
/* Excluding big_generated drops its config; the root one survives. */
char *excluded[] = {(char *)"big_generated"};
coll = cbm_load_path_aliases_excluded(root, excluded, 1);
ASSERT_NOT_NULL(coll);
ASSERT_EQ(coll->count, 1);
const cbm_path_alias_map_t *m = cbm_path_alias_find_for_file(coll, "src/x.ts");
ASSERT_NOT_NULL(m);
char *r = cbm_path_alias_resolve(m, "@root/utils");
ASSERT_NOT_NULL(r);
ASSERT_STR_EQ(r, "shared/utils");
free(r);
cbm_path_alias_collection_free(coll);
snprintf(path, sizeof(path), "%s/big_generated/tsconfig.json", root);
unlink(path);
snprintf(path, sizeof(path), "%s/tsconfig.json", root);
unlink(path);
snprintf(path, sizeof(path), "%s/big_generated", root);
rmdir(path);
rmdir(root);
PASS();
}
/* ── Loader returns NULL when no configs found ─────────────────── */
TEST(path_alias_loader_no_configs) {
@@ -363,5 +419,6 @@ void suite_path_alias(void) {
RUN_TEST(path_alias_find_for_file_nearest_ancestor);
RUN_TEST(path_alias_loader_monorepo);
RUN_TEST(path_alias_loader_monorepo_dotdot_climb);
RUN_TEST(path_alias_loader_honors_discovery_exclusions);
RUN_TEST(path_alias_loader_no_configs);
}
+127
View File
@@ -4845,6 +4845,129 @@ TEST(envscan_non_url_values_skipped) {
PASS();
}
/* ── Discovery-exclusion plumbing in auxiliary repo walks (#792) ── */
/* Boundary semantics of the shared exclusion predicate: anchored at the
* repo root, matches the excluded dir itself and its subtree, but never
* sibling names sharing a prefix. Regression guard for issue #792. */
TEST(pipeline_relpath_excluded_boundary) {
char *excluded[] = {(char *)"vendor_big", (char *)"packages/big"};
/* Exact match and subtree paths are excluded. */
ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("vendor_big", excluded, 2));
ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("vendor_big/lib/package.json", excluded, 2));
ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("packages/big", excluded, 2));
ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("packages/big/src/x.ts", excluded, 2));
/* Sibling names sharing the prefix are NOT excluded ('/'-boundary). */
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_bigger", excluded, 2));
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor", excluded, 2));
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("packages/bigger/x.ts", excluded, 2));
/* Exclusions are root-anchored prefixes, not substring matches. */
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("src/vendor_big/x.c", excluded, 2));
/* NULL / empty safety. */
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded(NULL, excluded, 2));
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("", excluded, 2));
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_big", NULL, 0));
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_big", excluded, 0));
char *with_empty[] = {(char *)""};
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_big", with_empty, 1));
PASS();
}
/* Helper: does the entries array contain a package with this name? */
static int pkg_entries_has_name(const cbm_pkg_entries_t *e, const char *name) {
for (int i = 0; i < e->count; i++) {
if (e->items[i].pkg_name && strcmp(e->items[i].pkg_name, name) == 0)
return 1;
}
return 0;
}
/* The pkgmap repo walk must honor discovery exclusions (issue #792: a
* gitignored huge subtree kept the pkgmap walk busy for 15 minutes).
* Control run first (no exclusions → BOTH manifests parsed) so the
* exclusion assertion below cannot pass vacuously. */
TEST(pkgmap_scan_repo_honors_discovery_exclusions) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_pkgmap_excl_XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("tmpdir");
char dir[512];
snprintf(dir, sizeof(dir), "%s/packages", tmpdir);
cbm_mkdir(dir);
snprintf(dir, sizeof(dir), "%s/packages/app", tmpdir);
cbm_mkdir(dir);
snprintf(dir, sizeof(dir), "%s/vendor_big", tmpdir);
cbm_mkdir(dir);
snprintf(dir, sizeof(dir), "%s/vendor_big/lib", tmpdir);
cbm_mkdir(dir);
write_temp_file(tmpdir, "packages/app/package.json",
"{\"name\":\"@org/app\",\"main\":\"index.js\"}\n");
write_temp_file(tmpdir, "vendor_big/lib/package.json",
"{\"name\":\"@org/vendored\",\"main\":\"index.js\"}\n");
/* Control: NULL exclusion list — the walk reaches and parses BOTH
* manifests (proves the excluded one is reachable + parseable). */
cbm_pkg_entries_t control;
cbm_pkg_entries_init(&control);
cbm_pkgmap_scan_repo(tmpdir, &control, NULL, 0);
ASSERT_TRUE(pkg_entries_has_name(&control, "@org/app"));
ASSERT_TRUE(pkg_entries_has_name(&control, "@org/vendored"));
cbm_pkg_entries_free(&control);
/* With vendor_big excluded (as discovery reports for a gitignored
* subtree): the walk must not descend into it. */
char *excluded[] = {(char *)"vendor_big"};
cbm_pkg_entries_t entries;
cbm_pkg_entries_init(&entries);
cbm_pkgmap_scan_repo(tmpdir, &entries, excluded, 1);
ASSERT_TRUE(pkg_entries_has_name(&entries, "@org/app"));
ASSERT_FALSE(pkg_entries_has_name(&entries, "@org/vendored"));
cbm_pkg_entries_free(&entries);
th_rmtree(tmpdir);
PASS();
}
/* The env-URL walk must honor discovery exclusions the same way (#792).
* Control run first via the NULL-exclusion wrapper so the exclusion
* assertion cannot pass vacuously. */
TEST(envscan_walk_honors_discovery_exclusions) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_envscan_excl_XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("tmpdir");
char dir[512];
snprintf(dir, sizeof(dir), "%s/big_generated", tmpdir);
cbm_mkdir(dir);
write_temp_file(tmpdir, "deploy.sh",
"#!/bin/bash\nexport CONTROL_URL=\"https://api.example.com/v1\"\n");
write_temp_file(tmpdir, "big_generated/env.sh",
"#!/bin/bash\nexport EXCLUDED_URL=\"https://excluded.example.com/v1\"\n");
/* Control: the NULL-exclusion wrapper sees both bindings. */
cbm_env_binding_t bindings[32];
int count = cbm_scan_project_env_urls(tmpdir, bindings, 32);
ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "CONTROL_URL"));
ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "EXCLUDED_URL"));
/* With big_generated excluded, its binding must disappear. */
char *excluded[] = {(char *)"big_generated"};
count = cbm_scan_project_env_urls_excluded(tmpdir, bindings, 32, excluded, 1);
ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "CONTROL_URL"));
ASSERT_TRUE(find_binding_by_key(bindings, count, "EXCLUDED_URL") == NULL);
th_rmtree(tmpdir);
PASS();
}
/* ── Git history tests (port of githistory_test.go) ────────────── */
/* Port of Go TestIsTrackableFile from githistory_test.go */
@@ -6364,6 +6487,10 @@ SUITE(pipeline) {
RUN_TEST(envscan_secret_file_exclusion);
RUN_TEST(envscan_skips_ignored_dirs);
RUN_TEST(envscan_non_url_values_skipped);
/* Discovery-exclusion plumbing in auxiliary repo walks (#792) */
RUN_TEST(pipeline_relpath_excluded_boundary);
RUN_TEST(pkgmap_scan_repo_honors_discovery_exclusions);
RUN_TEST(envscan_walk_honors_discovery_exclusions);
/* Function registry / resolver */
RUN_TEST(registry_resolve_single_candidate);
RUN_TEST(registry_fuzzy_nonexistent);