|
| 1 | +/* |
| 2 | + * repro_issue221.c -- Regression guard for bug #221. |
| 3 | + * |
| 4 | + * Bug #221: "'install' command does not work for opencode in windows 11" |
| 5 | + * |
| 6 | + * ROOT CAUSE: |
| 7 | + * find_in_path (src/cli/cli.c) probed only the bare executable name |
| 8 | + * "opencode" for each PATH entry. On Windows, CLI tools installed via |
| 9 | + * mise/npm/scoop ship as extension-bearing shims (.cmd, .ps1, .exe), so |
| 10 | + * the bare-name probe never matched and cbm_find_cli("opencode", ...) always |
| 11 | + * returned an empty string. The installer therefore concluded opencode was |
| 12 | + * absent and skipped wiring it even when it was present on PATH. |
| 13 | + * |
| 14 | + * FIX (commit 0485d3f, "fix(cli): probe Windows PATHEXT variants in |
| 15 | + * find_in_path (#221)"): |
| 16 | + * On _WIN32, find_in_path now iterates the common PATHEXT variants |
| 17 | + * (.exe, .cmd, .bat, .ps1) for each PATH directory after the bare-name |
| 18 | + * probe fails, matching whichever extension-qualified file is present. |
| 19 | + * |
| 20 | + * REGRESSION GUARD -- expected GREEN on current main (fix is in): |
| 21 | + * The fix was committed as 0485d3f and CI (build-windows + test-windows) |
| 22 | + * was green before merge. This test is therefore expected to PASS on the |
| 23 | + * current codebase. It will turn RED if find_in_path is accidentally |
| 24 | + * regressed to bare-name-only lookup. |
| 25 | + * |
| 26 | + * CROSS-PLATFORM STRATEGY: |
| 27 | + * On POSIX: create a plain executable named "opencode" (no extension). |
| 28 | + * Bare-name lookup has always worked here, so the test confirms |
| 29 | + * cbm_find_cli("opencode", ...) resolves correctly -- the baseline. |
| 30 | + * On Windows: create "opencode.cmd" (the most common shim format). |
| 31 | + * Before the fix, find_in_path returned "" for this case; after |
| 32 | + * the fix it returns the .cmd path -- the regression guard proper. |
| 33 | + * Both branches exercise the same public function and assertion; only the |
| 34 | + * fixture filename differs. |
| 35 | + * |
| 36 | + * NOTE: no slash-star inside this block comment to avoid nested-comment UB. |
| 37 | + */ |
| 38 | + |
| 39 | +#include <foundation/compat.h> |
| 40 | +#include "test_framework.h" |
| 41 | +#include "test_helpers.h" |
| 42 | +#include <cli/cli.h> |
| 43 | + |
| 44 | +#include <string.h> |
| 45 | +#include <stdlib.h> |
| 46 | +#include <stdio.h> |
| 47 | + |
| 48 | +/* ── Minimal local helpers (mirror test_cli.c pattern) ──────────────────── */ |
| 49 | + |
| 50 | +static int repro221_write_file(const char *path, const char *content) { |
| 51 | + FILE *f = fopen(path, "w"); |
| 52 | + if (!f) |
| 53 | + return -1; |
| 54 | + fprintf(f, "%s", content); |
| 55 | + fclose(f); |
| 56 | + return 0; |
| 57 | +} |
| 58 | + |
| 59 | +/* ── Test ───────────────────────────────────────────────────────────────── */ |
| 60 | + |
| 61 | +/* |
| 62 | + * repro_issue221_opencode_pathext_lookup |
| 63 | + * |
| 64 | + * Verify that cbm_find_cli("opencode", ...) resolves the opencode executable |
| 65 | + * (or its Windows .cmd shim) when the containing directory is on PATH. |
| 66 | + * |
| 67 | + * CORRECT BEHAVIOUR (post-fix): |
| 68 | + * cbm_find_cli returns a non-empty string whose basename starts with |
| 69 | + * "opencode" -- meaning find_in_path found the file. |
| 70 | + * |
| 71 | + * BUGGY BEHAVIOUR (pre-fix, Windows only): |
| 72 | + * cbm_find_cli returns "" because find_in_path only probed the bare name |
| 73 | + * "opencode" and never tried "opencode.cmd" / "opencode.exe" / etc. |
| 74 | + * |
| 75 | + * GREEN on current main (fix present): ASSERT fires with a non-empty result. |
| 76 | + * RED if regressed: ASSERT fires because result is empty. |
| 77 | + */ |
| 78 | +TEST(repro_issue221_opencode_pathext_lookup) { |
| 79 | + /* Create an isolated temp directory to act as a fake PATH entry. */ |
| 80 | + char tmpdir[256]; |
| 81 | + snprintf(tmpdir, sizeof(tmpdir), "/tmp/repro221-XXXXXX"); |
| 82 | + if (!cbm_mkdtemp(tmpdir)) |
| 83 | + FAIL("cbm_mkdtemp failed"); |
| 84 | + |
| 85 | + /* |
| 86 | + * Choose the fixture filename to match the platform convention: |
| 87 | + * POSIX -- "opencode" (plain executable; bare-name lookup) |
| 88 | + * Windows -- "opencode.cmd" (most common shim installed by mise/npm) |
| 89 | + * |
| 90 | + * On Windows (pre-fix) find_in_path returned "" for "opencode.cmd" |
| 91 | + * because only the bare name was probed. The fix tries .cmd before |
| 92 | + * moving to the next PATH entry, so the shim is found. |
| 93 | + */ |
| 94 | +#ifdef _WIN32 |
| 95 | + const char *fixture_name = "opencode.cmd"; |
| 96 | + const char *fixture_content = "@echo off\r\nrem fake opencode shim\r\n"; |
| 97 | +#else |
| 98 | + const char *fixture_name = "opencode"; |
| 99 | + const char *fixture_content = "#!/bin/sh\n# fake opencode\n"; |
| 100 | +#endif |
| 101 | + |
| 102 | + char fixture_path[512]; |
| 103 | + snprintf(fixture_path, sizeof(fixture_path), "%s/%s", tmpdir, fixture_name); |
| 104 | + |
| 105 | + if (repro221_write_file(fixture_path, fixture_content) != 0) |
| 106 | + FAIL("failed to write opencode fixture"); |
| 107 | + |
| 108 | + /* Make executable (no-op on Windows -- extension decides executability). */ |
| 109 | + th_make_executable(fixture_path); |
| 110 | + |
| 111 | + /* Swap PATH so only tmpdir is searched, isolating the lookup. */ |
| 112 | + const char *raw_path = getenv("PATH"); |
| 113 | + char *old_path = raw_path ? strdup(raw_path) : NULL; |
| 114 | + cbm_setenv("PATH", tmpdir, 1); |
| 115 | + |
| 116 | + /* |
| 117 | + * The function under test: cbm_find_cli is the public API that calls |
| 118 | + * find_in_path internally. We pass a non-existent home_dir so fallback |
| 119 | + * paths (~/.local/bin etc.) are never tried -- the only possible match |
| 120 | + * is the fixture file created above. |
| 121 | + * |
| 122 | + * Pre-fix (Windows): find_in_path probed "<tmpdir>/opencode" (absent) |
| 123 | + * and returned false. cbm_find_cli returned "". |
| 124 | + * Post-fix (Windows): find_in_path also probes "<tmpdir>/opencode.cmd" |
| 125 | + * (present), finds it, and cbm_find_cli returns the full path. |
| 126 | + * POSIX (before and after): bare-name probe succeeds immediately. |
| 127 | + */ |
| 128 | + const char *result = cbm_find_cli("opencode", "/nonexistent-home-dir"); |
| 129 | + |
| 130 | + /* Restore PATH before any assertion so cleanup is always reached. */ |
| 131 | + if (old_path) { |
| 132 | + cbm_setenv("PATH", old_path, 1); |
| 133 | + free(old_path); |
| 134 | + } |
| 135 | + |
| 136 | + /* |
| 137 | + * PRIMARY ASSERTION -- regression guard for #221. |
| 138 | + * |
| 139 | + * cbm_find_cli MUST return a non-empty path that contains "opencode". |
| 140 | + * |
| 141 | + * GREEN (current main, fix present): result points to the fixture file. |
| 142 | + * RED (if regressed to bare-name-only on Windows): result is "". |
| 143 | + */ |
| 144 | + ASSERT_FALSE(result == NULL); |
| 145 | + ASSERT(result[0] != '\0'); |
| 146 | + ASSERT(strstr(result, "opencode") != NULL); |
| 147 | + |
| 148 | + /* Cleanup fixture and temp dir. */ |
| 149 | + (void)remove(fixture_path); |
| 150 | + (void)rmdir(tmpdir); |
| 151 | + |
| 152 | + PASS(); |
| 153 | +} |
| 154 | + |
| 155 | +/* ── Suite ──────────────────────────────────────────────────────────────── */ |
| 156 | +SUITE(repro_issue221) { |
| 157 | + RUN_TEST(repro_issue221_opencode_pathext_lookup); |
| 158 | +} |
0 commit comments