mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-28 07:03:20 +08:00
- cbm_mkdtemp: _mktemp + _mkdir on Windows - cbm_setenv/cbm_unsetenv: _putenv_s on Windows - cbm_pipe: _pipe on Windows - Replace bare POSIX calls in 12 test files with compat wrappers - All test files now include compat.h
1256 lines
43 KiB
C
1256 lines
43 KiB
C
/*
|
|
* test_cli.c — Tests for CLI subcommands: install, uninstall, update, version.
|
|
*
|
|
* Port of Go test files:
|
|
* - cmd/codebase-memory-mcp/cli_test.go (11 tests)
|
|
* - cmd/codebase-memory-mcp/install_test.go (24 tests)
|
|
* - cmd/codebase-memory-mcp/update_test.go (5 tests)
|
|
* - internal/selfupdate/selfupdate_test.go (7 tests)
|
|
*
|
|
* Total: 47 Go tests → 47 C tests
|
|
*/
|
|
#include "../src/foundation/compat.h"
|
|
#include "test_framework.h"
|
|
#include <cli/cli.h>
|
|
#include <foundation/yaml.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <zlib.h>
|
|
|
|
/* Helper: create a file with content */
|
|
static int write_test_file(const char *path, const char *content) {
|
|
FILE *f = fopen(path, "w");
|
|
if (!f)
|
|
return -1;
|
|
fprintf(f, "%s", content);
|
|
fclose(f);
|
|
return 0;
|
|
}
|
|
|
|
/* Helper: read a file into static buffer */
|
|
static const char *read_test_file(const char *path) {
|
|
static char buf[8192];
|
|
FILE *f = fopen(path, "r");
|
|
if (!f)
|
|
return NULL;
|
|
size_t n = fread(buf, 1, sizeof(buf) - 1, f);
|
|
fclose(f);
|
|
buf[n] = '\0';
|
|
return buf;
|
|
}
|
|
|
|
/* Helper: mkdirp */
|
|
static int test_mkdirp(const char *path) {
|
|
char tmp[1024];
|
|
snprintf(tmp, sizeof(tmp), "%s", path);
|
|
for (char *p = tmp + 1; *p; p++) {
|
|
if (*p == '/') {
|
|
*p = '\0';
|
|
cbm_mkdir(tmp);
|
|
*p = '/';
|
|
}
|
|
}
|
|
return cbm_mkdir(tmp) == 0 || errno == EEXIST ? 0 : -1;
|
|
}
|
|
|
|
/* Helper: recursive remove */
|
|
static void test_rmdir_r(const char *path) {
|
|
char cmd[1024];
|
|
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", path);
|
|
system(cmd);
|
|
}
|
|
|
|
/* Helper: create tar.gz with a single file */
|
|
static unsigned char *create_test_targz(const char *filename, const unsigned char *content,
|
|
int content_len, int *out_len) {
|
|
/* Build tar data: 512-byte header + content padded to 512-byte boundary + 2x512 zero blocks */
|
|
int data_blocks = (content_len + 511) / 512;
|
|
int tar_size = 512 + data_blocks * 512 + 1024; /* header + data + end-of-archive */
|
|
unsigned char *tar = calloc(1, (size_t)tar_size);
|
|
if (!tar)
|
|
return NULL;
|
|
|
|
/* Filename (bytes 0-99) */
|
|
strncpy((char *)tar, filename, 99);
|
|
|
|
/* Mode (bytes 100-107): octal 0700 */
|
|
memcpy(tar + 100, "0000700\0", 8);
|
|
|
|
/* UID/GID (bytes 108-123): 0 */
|
|
memcpy(tar + 108, "0000000\0", 8);
|
|
memcpy(tar + 116, "0000000\0", 8);
|
|
|
|
/* Size (bytes 124-135): octal */
|
|
char size_str[12];
|
|
snprintf(size_str, sizeof(size_str), "%011o", content_len);
|
|
memcpy(tar + 124, size_str, 11);
|
|
|
|
/* Mtime (bytes 136-147): 0 */
|
|
memcpy(tar + 136, "00000000000\0", 12);
|
|
|
|
/* Type flag (byte 156): '0' = regular file */
|
|
tar[156] = '0';
|
|
|
|
/* Checksum (bytes 148-155): compute over header with checksum field as spaces */
|
|
memset(tar + 148, ' ', 8);
|
|
unsigned int checksum = 0;
|
|
for (int i = 0; i < 512; i++)
|
|
checksum += tar[i];
|
|
snprintf((char *)tar + 148, 7, "%06o", checksum);
|
|
tar[154] = '\0';
|
|
|
|
/* File content */
|
|
memcpy(tar + 512, content, (size_t)content_len);
|
|
|
|
/* Compress with gzip */
|
|
z_stream strm = {0};
|
|
if (deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 16 + MAX_WBITS, 8,
|
|
Z_DEFAULT_STRATEGY) != Z_OK) {
|
|
free(tar);
|
|
return NULL;
|
|
}
|
|
|
|
size_t gz_cap = (size_t)tar_size + 256;
|
|
unsigned char *gz = malloc(gz_cap);
|
|
if (!gz) {
|
|
deflateEnd(&strm);
|
|
free(tar);
|
|
return NULL;
|
|
}
|
|
|
|
strm.next_in = tar;
|
|
strm.avail_in = (unsigned int)tar_size;
|
|
strm.next_out = gz;
|
|
strm.avail_out = (unsigned int)gz_cap;
|
|
|
|
deflate(&strm, Z_FINISH);
|
|
*out_len = (int)(gz_cap - strm.avail_out);
|
|
|
|
deflateEnd(&strm);
|
|
free(tar);
|
|
return gz;
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Version comparison tests (port of selfupdate_test.go)
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(cli_compare_versions) {
|
|
/* Port of TestCompareVersions — 13 cases */
|
|
ASSERT(cbm_compare_versions("0.2.1", "0.2.0") > 0);
|
|
ASSERT_EQ(cbm_compare_versions("0.2.0", "0.2.0"), 0);
|
|
ASSERT(cbm_compare_versions("0.1.9", "0.2.0") < 0);
|
|
ASSERT(cbm_compare_versions("0.10.0", "0.2.0") > 0);
|
|
ASSERT(cbm_compare_versions("1.0.0", "0.99.99") > 0);
|
|
ASSERT(cbm_compare_versions("0.0.1", "0.0.2") < 0);
|
|
ASSERT_EQ(cbm_compare_versions("v0.2.1", "0.2.1"), 0);
|
|
ASSERT_EQ(cbm_compare_versions("0.2.1", "v0.2.1"), 0);
|
|
ASSERT(cbm_compare_versions("0.2.1-dev", "0.2.1") < 0);
|
|
ASSERT(cbm_compare_versions("0.2.1", "0.2.1-dev") > 0);
|
|
ASSERT_EQ(cbm_compare_versions("0.2.1-dev", "0.2.1-dev"), 0);
|
|
ASSERT(cbm_compare_versions("0.3.0", "0.2.1-dev") > 0);
|
|
ASSERT(cbm_compare_versions("0.2.0", "0.2.1-dev") < 0);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Version get/set (port of TestCLI_Version)
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(cli_version_get_set) {
|
|
cbm_cli_set_version("1.2.3");
|
|
ASSERT_STR_EQ(cbm_cli_get_version(), "1.2.3");
|
|
cbm_cli_set_version("dev");
|
|
ASSERT_STR_EQ(cbm_cli_get_version(), "dev");
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Shell RC detection (port of TestDetectShellRC + BashWithBashrc)
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(cli_detect_shell_rc_zsh) {
|
|
char tmpdir[] = "/tmp/cli-rc-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
/* Save and override SHELL — must strdup because setenv may realloc env block */
|
|
const char *raw = getenv("SHELL");
|
|
char *old_shell = raw ? strdup(raw) : NULL;
|
|
cbm_setenv("SHELL", "/bin/zsh", 1);
|
|
|
|
const char *rc = cbm_detect_shell_rc(tmpdir);
|
|
ASSERT_NOT_NULL(rc);
|
|
ASSERT(strstr(rc, ".zshrc") != NULL);
|
|
|
|
if (old_shell) {
|
|
cbm_setenv("SHELL", old_shell, 1);
|
|
free(old_shell);
|
|
} else
|
|
cbm_unsetenv("SHELL");
|
|
rmdir(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_detect_shell_rc_bash) {
|
|
char tmpdir[] = "/tmp/cli-rc-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
const char *raw = getenv("SHELL");
|
|
char *old_shell = raw ? strdup(raw) : NULL;
|
|
cbm_setenv("SHELL", "/bin/bash", 1);
|
|
|
|
/* No .bashrc → falls back to .bash_profile */
|
|
const char *rc = cbm_detect_shell_rc(tmpdir);
|
|
ASSERT_NOT_NULL(rc);
|
|
ASSERT(strstr(rc, ".bash_profile") != NULL);
|
|
|
|
if (old_shell) {
|
|
cbm_setenv("SHELL", old_shell, 1);
|
|
free(old_shell);
|
|
} else
|
|
cbm_unsetenv("SHELL");
|
|
rmdir(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_detect_shell_rc_bash_with_bashrc) {
|
|
/* Port of TestDetectShellRC_BashWithBashrc */
|
|
char tmpdir[] = "/tmp/cli-rc-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
const char *raw = getenv("SHELL");
|
|
char *old_shell = raw ? strdup(raw) : NULL;
|
|
cbm_setenv("SHELL", "/bin/bash", 1);
|
|
|
|
/* Create .bashrc */
|
|
char bashrc[512];
|
|
snprintf(bashrc, sizeof(bashrc), "%s/.bashrc", tmpdir);
|
|
write_test_file(bashrc, "# test\n");
|
|
|
|
const char *rc = cbm_detect_shell_rc(tmpdir);
|
|
ASSERT_STR_EQ(rc, bashrc);
|
|
|
|
unlink(bashrc);
|
|
if (old_shell) {
|
|
cbm_setenv("SHELL", old_shell, 1);
|
|
free(old_shell);
|
|
} else
|
|
cbm_unsetenv("SHELL");
|
|
rmdir(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_detect_shell_rc_fish) {
|
|
char tmpdir[] = "/tmp/cli-rc-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
const char *raw = getenv("SHELL");
|
|
char *old_shell = raw ? strdup(raw) : NULL;
|
|
cbm_setenv("SHELL", "/usr/bin/fish", 1);
|
|
|
|
const char *rc = cbm_detect_shell_rc(tmpdir);
|
|
ASSERT(strstr(rc, ".config/fish/config.fish") != NULL);
|
|
|
|
if (old_shell) {
|
|
cbm_setenv("SHELL", old_shell, 1);
|
|
free(old_shell);
|
|
} else
|
|
cbm_unsetenv("SHELL");
|
|
rmdir(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_detect_shell_rc_default) {
|
|
char tmpdir[] = "/tmp/cli-rc-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
const char *raw = getenv("SHELL");
|
|
char *old_shell = raw ? strdup(raw) : NULL;
|
|
cbm_setenv("SHELL", "/bin/sh", 1);
|
|
|
|
const char *rc = cbm_detect_shell_rc(tmpdir);
|
|
ASSERT(strstr(rc, ".profile") != NULL);
|
|
|
|
if (old_shell) {
|
|
cbm_setenv("SHELL", old_shell, 1);
|
|
free(old_shell);
|
|
} else
|
|
cbm_unsetenv("SHELL");
|
|
rmdir(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* CLI binary detection (port of TestFindCLI_*)
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(cli_find_cli_not_found) {
|
|
/* Port of TestFindCLI_NotFound */
|
|
char tmpdir[] = "/tmp/cli-find-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
const char *raw = getenv("PATH");
|
|
char *old_path = raw ? strdup(raw) : NULL;
|
|
cbm_setenv("PATH", tmpdir, 1);
|
|
|
|
const char *result = cbm_find_cli("nonexistent-binary-xyz", tmpdir);
|
|
ASSERT_STR_EQ(result, "");
|
|
|
|
if (old_path) {
|
|
cbm_setenv("PATH", old_path, 1);
|
|
free(old_path);
|
|
}
|
|
rmdir(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_find_cli_on_path) {
|
|
/* Port of TestFindCLI_FoundOnPATH */
|
|
char tmpdir[] = "/tmp/cli-find-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char fakecli[512];
|
|
snprintf(fakecli, sizeof(fakecli), "%s/fakecli", tmpdir);
|
|
write_test_file(fakecli, "#!/bin/sh\n");
|
|
chmod(fakecli, 0500);
|
|
|
|
const char *raw = getenv("PATH");
|
|
char *old_path = raw ? strdup(raw) : NULL;
|
|
cbm_setenv("PATH", tmpdir, 1);
|
|
|
|
const char *result = cbm_find_cli("fakecli", tmpdir);
|
|
ASSERT(result[0] != '\0');
|
|
ASSERT(strstr(result, "fakecli") != NULL);
|
|
|
|
if (old_path) {
|
|
cbm_setenv("PATH", old_path, 1);
|
|
free(old_path);
|
|
}
|
|
unlink(fakecli);
|
|
rmdir(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_find_cli_fallback_paths) {
|
|
/* Port of TestFindCLI_FallbackPaths */
|
|
char tmpdir[] = "/tmp/cli-find-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char localbin[512];
|
|
snprintf(localbin, sizeof(localbin), "%s/.local/bin", tmpdir);
|
|
test_mkdirp(localbin);
|
|
|
|
char fakecli[512];
|
|
snprintf(fakecli, sizeof(fakecli), "%s/testcli", localbin);
|
|
write_test_file(fakecli, "#!/bin/sh\n");
|
|
chmod(fakecli, 0500);
|
|
|
|
const char *raw = getenv("PATH");
|
|
char *old_path = raw ? strdup(raw) : NULL;
|
|
cbm_setenv("PATH", "/nonexistent", 1);
|
|
|
|
const char *result = cbm_find_cli("testcli", tmpdir);
|
|
ASSERT_STR_EQ(result, fakecli);
|
|
|
|
if (old_path) {
|
|
cbm_setenv("PATH", old_path, 1);
|
|
free(old_path);
|
|
}
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Dry-run flag parsing (port of TestDryRun)
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(cli_dry_run_flags) {
|
|
/* Port of TestDryRun — just verifies the pattern */
|
|
bool dry_run = false, force = false;
|
|
const char *args[] = {"--dry-run", "--force"};
|
|
for (int i = 0; i < 2; i++) {
|
|
if (strcmp(args[i], "--dry-run") == 0)
|
|
dry_run = true;
|
|
if (strcmp(args[i], "--force") == 0)
|
|
force = true;
|
|
}
|
|
ASSERT_TRUE(dry_run);
|
|
ASSERT_TRUE(force);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Skill file management tests (port of install_test.go skill tests)
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(cli_skill_creation) {
|
|
/* Port of TestInstallSkillCreation */
|
|
char tmpdir[] = "/tmp/cli-skill-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char skills_dir[512];
|
|
snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", tmpdir);
|
|
|
|
int written = cbm_install_skills(skills_dir, false, false);
|
|
ASSERT_EQ(written, CBM_SKILL_COUNT);
|
|
|
|
/* Verify all 4 skills exist and have content */
|
|
const cbm_skill_t *sk = cbm_get_skills();
|
|
for (int i = 0; i < CBM_SKILL_COUNT; i++) {
|
|
char path[1024];
|
|
snprintf(path, sizeof(path), "%s/%s/SKILL.md", skills_dir, sk[i].name);
|
|
const char *data = read_test_file(path);
|
|
ASSERT_NOT_NULL(data);
|
|
ASSERT(strlen(data) > 0);
|
|
/* Check YAML frontmatter */
|
|
ASSERT(strncmp(data, "---\n", 4) == 0);
|
|
/* Check name field */
|
|
ASSERT(strstr(data, sk[i].name) != NULL);
|
|
}
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_skill_idempotent) {
|
|
/* Port of TestInstallIdempotent */
|
|
char tmpdir[] = "/tmp/cli-skill-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char skills_dir[512];
|
|
snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", tmpdir);
|
|
|
|
/* Install twice */
|
|
cbm_install_skills(skills_dir, false, false);
|
|
int second = cbm_install_skills(skills_dir, false, false);
|
|
|
|
/* Second install should write 0 (skills exist, no force) */
|
|
ASSERT_EQ(second, 0);
|
|
|
|
/* All skills should still exist */
|
|
const cbm_skill_t *sk = cbm_get_skills();
|
|
for (int i = 0; i < CBM_SKILL_COUNT; i++) {
|
|
char path[1024];
|
|
snprintf(path, sizeof(path), "%s/%s/SKILL.md", skills_dir, sk[i].name);
|
|
struct stat st;
|
|
ASSERT_EQ(stat(path, &st), 0);
|
|
}
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_skill_force_overwrite) {
|
|
/* Port of TestCLI_InstallForceOverwrites */
|
|
char tmpdir[] = "/tmp/cli-skill-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char skills_dir[512];
|
|
snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", tmpdir);
|
|
|
|
cbm_install_skills(skills_dir, false, false);
|
|
int force_count = cbm_install_skills(skills_dir, true, false);
|
|
|
|
/* Force should overwrite all */
|
|
ASSERT_EQ(force_count, CBM_SKILL_COUNT);
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_uninstall_removes_skills) {
|
|
/* Port of TestUninstallRemovesSkills */
|
|
char tmpdir[] = "/tmp/cli-skill-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char skills_dir[512];
|
|
snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", tmpdir);
|
|
|
|
cbm_install_skills(skills_dir, false, false);
|
|
int removed = cbm_remove_skills(skills_dir, false);
|
|
ASSERT_EQ(removed, CBM_SKILL_COUNT);
|
|
|
|
/* Verify all removed */
|
|
const cbm_skill_t *sk = cbm_get_skills();
|
|
for (int i = 0; i < CBM_SKILL_COUNT; i++) {
|
|
char path[1024];
|
|
snprintf(path, sizeof(path), "%s/%s", skills_dir, sk[i].name);
|
|
struct stat st;
|
|
ASSERT(stat(path, &st) != 0);
|
|
}
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_remove_old_monolithic_skill) {
|
|
/* Port of TestRemoveOldMonolithicSkill */
|
|
char tmpdir[] = "/tmp/cli-skill-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char skills_dir[512];
|
|
snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", tmpdir);
|
|
|
|
/* Create old monolithic skill */
|
|
char old_dir[1024];
|
|
snprintf(old_dir, sizeof(old_dir), "%s/codebase-memory-mcp", skills_dir);
|
|
test_mkdirp(old_dir);
|
|
char old_file[1024];
|
|
snprintf(old_file, sizeof(old_file), "%s/SKILL.md", old_dir);
|
|
write_test_file(old_file, "old skill");
|
|
|
|
bool removed = cbm_remove_old_monolithic_skill(skills_dir, false);
|
|
ASSERT_TRUE(removed);
|
|
|
|
struct stat st;
|
|
ASSERT(stat(old_dir, &st) != 0);
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_skill_files_content) {
|
|
/* Port of TestSkillFilesContent */
|
|
const cbm_skill_t *sk = cbm_get_skills();
|
|
ASSERT_EQ(CBM_SKILL_COUNT, 4);
|
|
|
|
/* Check exploring skill */
|
|
bool found_exploring = false, found_tracing = false;
|
|
bool found_quality = false, found_reference = false;
|
|
for (int i = 0; i < CBM_SKILL_COUNT; i++) {
|
|
if (strcmp(sk[i].name, "codebase-memory-exploring") == 0) {
|
|
found_exploring = true;
|
|
ASSERT(strstr(sk[i].content, "search_graph") != NULL);
|
|
ASSERT(strstr(sk[i].content, "get_graph_schema") != NULL);
|
|
}
|
|
if (strcmp(sk[i].name, "codebase-memory-tracing") == 0) {
|
|
found_tracing = true;
|
|
ASSERT(strstr(sk[i].content, "trace_call_path") != NULL);
|
|
ASSERT(strstr(sk[i].content, "direction") != NULL);
|
|
ASSERT(strstr(sk[i].content, "detect_changes") != NULL);
|
|
}
|
|
if (strcmp(sk[i].name, "codebase-memory-quality") == 0) {
|
|
found_quality = true;
|
|
ASSERT(strstr(sk[i].content, "max_degree=0") != NULL);
|
|
ASSERT(strstr(sk[i].content, "exclude_entry_points") != NULL);
|
|
}
|
|
if (strcmp(sk[i].name, "codebase-memory-reference") == 0) {
|
|
found_reference = true;
|
|
ASSERT(strstr(sk[i].content, "query_graph") != NULL);
|
|
ASSERT(strstr(sk[i].content, "Cypher") != NULL);
|
|
ASSERT(strstr(sk[i].content, "14 total") != NULL);
|
|
}
|
|
}
|
|
ASSERT_TRUE(found_exploring);
|
|
ASSERT_TRUE(found_tracing);
|
|
ASSERT_TRUE(found_quality);
|
|
ASSERT_TRUE(found_reference);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_codex_instructions) {
|
|
/* Port of TestCodexInstructionsCreation */
|
|
const char *instr = cbm_get_codex_instructions();
|
|
ASSERT_NOT_NULL(instr);
|
|
ASSERT(strstr(instr, "Codebase Knowledge Graph") != NULL);
|
|
ASSERT(strstr(instr, "trace_call_path") != NULL);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Editor MCP config tests (Cursor/Windsurf/Gemini)
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(cli_editor_mcp_install) {
|
|
/* Port of TestEditorMCPInstall */
|
|
char tmpdir[] = "/tmp/cli-mcp-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char configpath[512];
|
|
snprintf(configpath, sizeof(configpath), "%s/.cursor/mcp.json", tmpdir);
|
|
|
|
int rc = cbm_install_editor_mcp("/usr/local/bin/codebase-memory-mcp", configpath);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
const char *data = read_test_file(configpath);
|
|
ASSERT_NOT_NULL(data);
|
|
ASSERT(strstr(data, "mcpServers") != NULL);
|
|
ASSERT(strstr(data, "codebase-memory-mcp") != NULL);
|
|
ASSERT(strstr(data, "/usr/local/bin/codebase-memory-mcp") != NULL);
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_editor_mcp_idempotent) {
|
|
/* Port of TestEditorMCPInstallIdempotent */
|
|
char tmpdir[] = "/tmp/cli-mcp-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char configpath[512];
|
|
snprintf(configpath, sizeof(configpath), "%s/.cursor/mcp.json", tmpdir);
|
|
|
|
cbm_install_editor_mcp("/usr/local/bin/codebase-memory-mcp", configpath);
|
|
int rc = cbm_install_editor_mcp("/usr/local/bin/codebase-memory-mcp", configpath);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
/* Should still parse as valid JSON with only 1 server */
|
|
const char *data = read_test_file(configpath);
|
|
ASSERT_NOT_NULL(data);
|
|
/* Count occurrences of "codebase-memory-mcp" (should be exactly 1 in mcpServers) */
|
|
int count = 0;
|
|
const char *p = data;
|
|
while ((p = strstr(p, "\"codebase-memory-mcp\"")) != NULL) {
|
|
count++;
|
|
p += 20;
|
|
}
|
|
/* The key appears once as key name */
|
|
ASSERT_EQ(count, 1);
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_editor_mcp_preserves_others) {
|
|
/* Port of TestEditorMCPPreservesOtherServers */
|
|
char tmpdir[] = "/tmp/cli-mcp-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char configpath[512];
|
|
snprintf(configpath, sizeof(configpath), "%s/.cursor/mcp.json", tmpdir);
|
|
test_mkdirp(tmpdir);
|
|
char dir[512];
|
|
snprintf(dir, sizeof(dir), "%s/.cursor", tmpdir);
|
|
test_mkdirp(dir);
|
|
|
|
/* Write config with existing server */
|
|
write_test_file(configpath,
|
|
"{\"mcpServers\": {\"other-server\": {\"command\": \"/usr/bin/other\"}}}");
|
|
|
|
cbm_install_editor_mcp("/usr/local/bin/codebase-memory-mcp", configpath);
|
|
|
|
const char *data = read_test_file(configpath);
|
|
ASSERT_NOT_NULL(data);
|
|
ASSERT(strstr(data, "other-server") != NULL);
|
|
ASSERT(strstr(data, "codebase-memory-mcp") != NULL);
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_editor_mcp_uninstall) {
|
|
/* Port of TestEditorMCPUninstall */
|
|
char tmpdir[] = "/tmp/cli-mcp-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char configpath[512];
|
|
snprintf(configpath, sizeof(configpath), "%s/.cursor/mcp.json", tmpdir);
|
|
|
|
cbm_install_editor_mcp("/usr/local/bin/codebase-memory-mcp", configpath);
|
|
int rc = cbm_remove_editor_mcp(configpath);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
const char *data = read_test_file(configpath);
|
|
ASSERT_NOT_NULL(data);
|
|
/* codebase-memory-mcp should be removed */
|
|
ASSERT(strstr(data, "\"codebase-memory-mcp\"") == NULL);
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_gemini_mcp_install) {
|
|
/* Port of TestGeminiMCPInstall */
|
|
char tmpdir[] = "/tmp/cli-mcp-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char configpath[512];
|
|
snprintf(configpath, sizeof(configpath), "%s/.gemini/settings.json", tmpdir);
|
|
|
|
/* Gemini uses same mcpServers format as Cursor */
|
|
int rc = cbm_install_editor_mcp("/usr/local/bin/codebase-memory-mcp", configpath);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
const char *data = read_test_file(configpath);
|
|
ASSERT_NOT_NULL(data);
|
|
ASSERT(strstr(data, "mcpServers") != NULL);
|
|
ASSERT(strstr(data, "codebase-memory-mcp") != NULL);
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* VS Code MCP config tests
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(cli_vscode_mcp_install) {
|
|
/* Port of TestVSCodeMCPInstall */
|
|
char tmpdir[] = "/tmp/cli-mcp-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char configpath[512];
|
|
snprintf(configpath, sizeof(configpath), "%s/Code/User/mcp.json", tmpdir);
|
|
|
|
int rc = cbm_install_vscode_mcp("/usr/local/bin/codebase-memory-mcp", configpath);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
const char *data = read_test_file(configpath);
|
|
ASSERT_NOT_NULL(data);
|
|
ASSERT(strstr(data, "\"servers\"") != NULL);
|
|
ASSERT(strstr(data, "\"type\"") != NULL);
|
|
ASSERT(strstr(data, "\"stdio\"") != NULL);
|
|
ASSERT(strstr(data, "codebase-memory-mcp") != NULL);
|
|
ASSERT(strstr(data, "/usr/local/bin/codebase-memory-mcp") != NULL);
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_vscode_mcp_uninstall) {
|
|
/* Port of TestVSCodeMCPUninstall */
|
|
char tmpdir[] = "/tmp/cli-mcp-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char configpath[512];
|
|
snprintf(configpath, sizeof(configpath), "%s/Code/User/mcp.json", tmpdir);
|
|
|
|
cbm_install_vscode_mcp("/usr/local/bin/codebase-memory-mcp", configpath);
|
|
int rc = cbm_remove_vscode_mcp(configpath);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
const char *data = read_test_file(configpath);
|
|
ASSERT_NOT_NULL(data);
|
|
ASSERT(strstr(data, "\"codebase-memory-mcp\"") == NULL);
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Zed MCP config tests
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(cli_zed_mcp_install) {
|
|
/* Port of TestZedMCPInstall */
|
|
char tmpdir[] = "/tmp/cli-mcp-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char configpath[512];
|
|
snprintf(configpath, sizeof(configpath), "%s/.config/zed/settings.json", tmpdir);
|
|
|
|
int rc = cbm_install_zed_mcp("/usr/local/bin/codebase-memory-mcp", configpath);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
const char *data = read_test_file(configpath);
|
|
ASSERT_NOT_NULL(data);
|
|
ASSERT(strstr(data, "\"context_servers\"") != NULL);
|
|
ASSERT(strstr(data, "\"source\"") != NULL);
|
|
ASSERT(strstr(data, "\"custom\"") != NULL);
|
|
ASSERT(strstr(data, "codebase-memory-mcp") != NULL);
|
|
ASSERT(strstr(data, "/usr/local/bin/codebase-memory-mcp") != NULL);
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_zed_mcp_preserves_settings) {
|
|
/* Port of TestZedMCPPreservesSettings */
|
|
char tmpdir[] = "/tmp/cli-mcp-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char configpath[512];
|
|
snprintf(configpath, sizeof(configpath), "%s/.config/zed/settings.json", tmpdir);
|
|
char dir[512];
|
|
snprintf(dir, sizeof(dir), "%s/.config/zed", tmpdir);
|
|
test_mkdirp(dir);
|
|
|
|
/* Pre-existing Zed settings */
|
|
write_test_file(configpath, "{\"theme\": \"One Dark\", \"vim_mode\": true}");
|
|
|
|
cbm_install_zed_mcp("/usr/local/bin/codebase-memory-mcp", configpath);
|
|
|
|
const char *data = read_test_file(configpath);
|
|
ASSERT_NOT_NULL(data);
|
|
/* Original settings preserved */
|
|
ASSERT(strstr(data, "One Dark") != NULL);
|
|
ASSERT(strstr(data, "vim_mode") != NULL);
|
|
/* MCP server added */
|
|
ASSERT(strstr(data, "context_servers") != NULL);
|
|
ASSERT(strstr(data, "codebase-memory-mcp") != NULL);
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_zed_mcp_uninstall) {
|
|
/* Port of TestZedMCPUninstall */
|
|
char tmpdir[] = "/tmp/cli-mcp-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char configpath[512];
|
|
snprintf(configpath, sizeof(configpath), "%s/.config/zed/settings.json", tmpdir);
|
|
|
|
cbm_install_zed_mcp("/usr/local/bin/codebase-memory-mcp", configpath);
|
|
int rc = cbm_remove_zed_mcp(configpath);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
const char *data = read_test_file(configpath);
|
|
ASSERT_NOT_NULL(data);
|
|
ASSERT(strstr(data, "\"codebase-memory-mcp\"") == NULL);
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* PATH management tests (port of TestCLI_InstallPATHAppend)
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(cli_ensure_path_append) {
|
|
/* Port of TestCLI_InstallPATHAppend */
|
|
char tmpdir[] = "/tmp/cli-path-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char rcfile[512];
|
|
snprintf(rcfile, sizeof(rcfile), "%s/.zshrc", tmpdir);
|
|
write_test_file(rcfile, "# existing content\n");
|
|
|
|
int rc = cbm_ensure_path("/usr/local/bin", rcfile, false);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
const char *data = read_test_file(rcfile);
|
|
ASSERT(strstr(data, "export PATH=\"/usr/local/bin:$PATH\"") != NULL);
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_ensure_path_already_present) {
|
|
char tmpdir[] = "/tmp/cli-path-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char rcfile[512];
|
|
snprintf(rcfile, sizeof(rcfile), "%s/.zshrc", tmpdir);
|
|
write_test_file(rcfile, "export PATH=\"/usr/local/bin:$PATH\"\n");
|
|
|
|
int rc = cbm_ensure_path("/usr/local/bin", rcfile, false);
|
|
ASSERT_EQ(rc, 1); /* 1 = already present */
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_ensure_path_dry_run) {
|
|
char tmpdir[] = "/tmp/cli-path-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char rcfile[512];
|
|
snprintf(rcfile, sizeof(rcfile), "%s/.zshrc", tmpdir);
|
|
write_test_file(rcfile, "# clean\n");
|
|
|
|
int rc = cbm_ensure_path("/usr/local/bin", rcfile, true);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
/* File should NOT be modified */
|
|
const char *data = read_test_file(rcfile);
|
|
ASSERT(strstr(data, "export PATH") == NULL);
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* File copy tests (port of update_test.go)
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(cli_copy_file) {
|
|
/* Port of TestCopyFile */
|
|
char tmpdir[] = "/tmp/cli-copy-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char src[512], dst[512];
|
|
snprintf(src, sizeof(src), "%s/source", tmpdir);
|
|
snprintf(dst, sizeof(dst), "%s/dest", tmpdir);
|
|
|
|
write_test_file(src, "test content for copy");
|
|
|
|
int rc = cbm_copy_file(src, dst);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
const char *data = read_test_file(dst);
|
|
ASSERT_STR_EQ(data, "test content for copy");
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_copy_file_source_not_found) {
|
|
/* Port of TestCopyFile_SourceNotFound */
|
|
char tmpdir[] = "/tmp/cli-copy-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char src[512], dst[512];
|
|
snprintf(src, sizeof(src), "%s/nonexistent", tmpdir);
|
|
snprintf(dst, sizeof(dst), "%s/dest", tmpdir);
|
|
|
|
int rc = cbm_copy_file(src, dst);
|
|
ASSERT(rc != 0);
|
|
|
|
rmdir(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Tar.gz extraction tests (port of update_test.go)
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(cli_extract_binary_from_targz) {
|
|
/* Port of TestExtractBinaryFromTarGz */
|
|
const char *content = "fake binary content";
|
|
int gz_len;
|
|
unsigned char *gz =
|
|
create_test_targz("codebase-memory-mcp-linux-amd64", (const unsigned char *)content,
|
|
(int)strlen(content), &gz_len);
|
|
ASSERT_NOT_NULL(gz);
|
|
|
|
int out_len;
|
|
unsigned char *extracted = cbm_extract_binary_from_targz(gz, gz_len, &out_len);
|
|
ASSERT_NOT_NULL(extracted);
|
|
ASSERT_EQ(out_len, (int)strlen(content));
|
|
ASSERT_MEM_EQ(extracted, content, out_len);
|
|
|
|
free(extracted);
|
|
free(gz);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_extract_binary_from_targz_not_found) {
|
|
/* Port of TestExtractBinaryFromTarGz_NotFound */
|
|
const char *content = "hello";
|
|
int gz_len;
|
|
unsigned char *gz = create_test_targz("some-other-file", (const unsigned char *)content,
|
|
(int)strlen(content), &gz_len);
|
|
ASSERT_NOT_NULL(gz);
|
|
|
|
int out_len;
|
|
unsigned char *extracted = cbm_extract_binary_from_targz(gz, gz_len, &out_len);
|
|
ASSERT_NULL(extracted);
|
|
|
|
free(gz);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_extract_binary_from_targz_invalid_data) {
|
|
/* Port of TestExtractBinaryFromTarGz_InvalidData */
|
|
const unsigned char bad_data[] = "not a valid tar.gz";
|
|
int out_len;
|
|
unsigned char *extracted = cbm_extract_binary_from_targz(bad_data, sizeof(bad_data), &out_len);
|
|
ASSERT_NULL(extracted);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Skill dry-run tests
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(cli_install_dry_run) {
|
|
/* Port of TestCLI_InstallDryRun */
|
|
char tmpdir[] = "/tmp/cli-dry-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char skills_dir[512];
|
|
snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", tmpdir);
|
|
|
|
int count = cbm_install_skills(skills_dir, false, true);
|
|
ASSERT_EQ(count, CBM_SKILL_COUNT);
|
|
|
|
/* Skills should NOT be created */
|
|
const cbm_skill_t *sk = cbm_get_skills();
|
|
for (int i = 0; i < CBM_SKILL_COUNT; i++) {
|
|
char path[1024];
|
|
snprintf(path, sizeof(path), "%s/%s/SKILL.md", skills_dir, sk[i].name);
|
|
struct stat st;
|
|
ASSERT(stat(path, &st) != 0);
|
|
}
|
|
|
|
rmdir(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_uninstall_dry_run) {
|
|
/* Port of TestCLI_UninstallDryRun */
|
|
char tmpdir[] = "/tmp/cli-dry-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char skills_dir[512];
|
|
snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", tmpdir);
|
|
|
|
cbm_install_skills(skills_dir, false, false);
|
|
int removed = cbm_remove_skills(skills_dir, true);
|
|
ASSERT_EQ(removed, CBM_SKILL_COUNT);
|
|
|
|
/* Skills should still exist */
|
|
const cbm_skill_t *sk = cbm_get_skills();
|
|
for (int i = 0; i < CBM_SKILL_COUNT; i++) {
|
|
char path[1024];
|
|
snprintf(path, sizeof(path), "%s/%s/SKILL.md", skills_dir, sk[i].name);
|
|
struct stat st;
|
|
ASSERT_EQ(stat(path, &st), 0);
|
|
}
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Full install + uninstall lifecycle
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(cli_install_and_uninstall) {
|
|
/* Port of TestCLI_InstallAndUninstall */
|
|
char tmpdir[] = "/tmp/cli-full-XXXXXX";
|
|
if (!cbm_mkdtemp(tmpdir))
|
|
SKIP("cbm_mkdtemp failed");
|
|
|
|
char skills_dir[512];
|
|
snprintf(skills_dir, sizeof(skills_dir), "%s/.claude/skills", tmpdir);
|
|
|
|
/* Install */
|
|
int written = cbm_install_skills(skills_dir, false, false);
|
|
ASSERT_EQ(written, CBM_SKILL_COUNT);
|
|
|
|
/* Verify */
|
|
const cbm_skill_t *sk = cbm_get_skills();
|
|
for (int i = 0; i < CBM_SKILL_COUNT; i++) {
|
|
char path[1024];
|
|
snprintf(path, sizeof(path), "%s/%s/SKILL.md", skills_dir, sk[i].name);
|
|
struct stat st;
|
|
ASSERT_EQ(stat(path, &st), 0);
|
|
}
|
|
|
|
/* Uninstall */
|
|
int removed = cbm_remove_skills(skills_dir, false);
|
|
ASSERT_EQ(removed, CBM_SKILL_COUNT);
|
|
|
|
/* Verify removed */
|
|
for (int i = 0; i < CBM_SKILL_COUNT; i++) {
|
|
char path[1024];
|
|
snprintf(path, sizeof(path), "%s/%s", skills_dir, sk[i].name);
|
|
struct stat st;
|
|
ASSERT(stat(path, &st) != 0);
|
|
}
|
|
|
|
test_rmdir_r(tmpdir);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* YAML parser unit tests
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(cli_yaml_parse_simple) {
|
|
/* Basic key-value parsing */
|
|
const char *yaml = "name: test\nversion: 1.0\n";
|
|
cbm_yaml_node_t *root = cbm_yaml_parse(yaml, (int)strlen(yaml));
|
|
ASSERT_NOT_NULL(root);
|
|
ASSERT_STR_EQ(cbm_yaml_get_str(root, "name"), "test");
|
|
ASSERT_STR_EQ(cbm_yaml_get_str(root, "version"), "1.0");
|
|
cbm_yaml_free(root);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_yaml_parse_nested) {
|
|
/* Nested map */
|
|
const char *yaml = "parent:\n"
|
|
" child: value\n"
|
|
" number: 42\n";
|
|
cbm_yaml_node_t *root = cbm_yaml_parse(yaml, (int)strlen(yaml));
|
|
ASSERT_NOT_NULL(root);
|
|
ASSERT_STR_EQ(cbm_yaml_get_str(root, "parent.child"), "value");
|
|
ASSERT_FLOAT_EQ(cbm_yaml_get_float(root, "parent.number", 0), 42.0, 0.001);
|
|
cbm_yaml_free(root);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_yaml_parse_list) {
|
|
/* String list */
|
|
const char *yaml = "items:\n"
|
|
" - alpha\n"
|
|
" - beta\n"
|
|
" - gamma\n";
|
|
cbm_yaml_node_t *root = cbm_yaml_parse(yaml, (int)strlen(yaml));
|
|
ASSERT_NOT_NULL(root);
|
|
const char *items[8];
|
|
int count = cbm_yaml_get_str_list(root, "items", items, 8);
|
|
ASSERT_EQ(count, 3);
|
|
ASSERT_STR_EQ(items[0], "alpha");
|
|
ASSERT_STR_EQ(items[1], "beta");
|
|
ASSERT_STR_EQ(items[2], "gamma");
|
|
cbm_yaml_free(root);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_yaml_parse_bool) {
|
|
const char *yaml = "enabled: true\n"
|
|
"disabled: false\n"
|
|
"on_flag: yes\n"
|
|
"off_flag: no\n";
|
|
cbm_yaml_node_t *root = cbm_yaml_parse(yaml, (int)strlen(yaml));
|
|
ASSERT_NOT_NULL(root);
|
|
ASSERT_TRUE(cbm_yaml_get_bool(root, "enabled", false));
|
|
ASSERT_FALSE(cbm_yaml_get_bool(root, "disabled", true));
|
|
ASSERT_TRUE(cbm_yaml_get_bool(root, "on_flag", false));
|
|
ASSERT_FALSE(cbm_yaml_get_bool(root, "off_flag", true));
|
|
cbm_yaml_free(root);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_yaml_parse_comments) {
|
|
const char *yaml = "# This is a comment\n"
|
|
"key: value # inline comment\n"
|
|
"\n"
|
|
"# Another comment\n"
|
|
"other: data\n";
|
|
cbm_yaml_node_t *root = cbm_yaml_parse(yaml, (int)strlen(yaml));
|
|
ASSERT_NOT_NULL(root);
|
|
ASSERT_STR_EQ(cbm_yaml_get_str(root, "key"), "value");
|
|
ASSERT_STR_EQ(cbm_yaml_get_str(root, "other"), "data");
|
|
cbm_yaml_free(root);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_yaml_parse_empty) {
|
|
cbm_yaml_node_t *root = cbm_yaml_parse("", 0);
|
|
ASSERT_NOT_NULL(root);
|
|
ASSERT_NULL(cbm_yaml_get_str(root, "anything"));
|
|
cbm_yaml_free(root);
|
|
PASS();
|
|
}
|
|
|
|
TEST(cli_yaml_has) {
|
|
const char *yaml = "a:\n b: c\n";
|
|
cbm_yaml_node_t *root = cbm_yaml_parse(yaml, (int)strlen(yaml));
|
|
ASSERT_NOT_NULL(root);
|
|
ASSERT_TRUE(cbm_yaml_has(root, "a"));
|
|
ASSERT_TRUE(cbm_yaml_has(root, "a.b"));
|
|
ASSERT_FALSE(cbm_yaml_has(root, "a.c"));
|
|
ASSERT_FALSE(cbm_yaml_has(root, "x"));
|
|
cbm_yaml_free(root);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Suite definition
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
SUITE(cli) {
|
|
/* Version (2 tests — selfupdate_test.go) */
|
|
RUN_TEST(cli_compare_versions);
|
|
RUN_TEST(cli_version_get_set);
|
|
|
|
/* Shell RC detection (5 tests — install_test.go) */
|
|
RUN_TEST(cli_detect_shell_rc_zsh);
|
|
RUN_TEST(cli_detect_shell_rc_bash);
|
|
RUN_TEST(cli_detect_shell_rc_bash_with_bashrc);
|
|
RUN_TEST(cli_detect_shell_rc_fish);
|
|
RUN_TEST(cli_detect_shell_rc_default);
|
|
|
|
/* CLI binary detection (3 tests — install_test.go) */
|
|
RUN_TEST(cli_find_cli_not_found);
|
|
RUN_TEST(cli_find_cli_on_path);
|
|
RUN_TEST(cli_find_cli_fallback_paths);
|
|
|
|
/* Dry-run flag parsing (1 test — install_test.go) */
|
|
RUN_TEST(cli_dry_run_flags);
|
|
|
|
/* Skill management (7 tests — install_test.go) */
|
|
RUN_TEST(cli_skill_creation);
|
|
RUN_TEST(cli_skill_idempotent);
|
|
RUN_TEST(cli_skill_force_overwrite);
|
|
RUN_TEST(cli_uninstall_removes_skills);
|
|
RUN_TEST(cli_remove_old_monolithic_skill);
|
|
RUN_TEST(cli_skill_files_content);
|
|
RUN_TEST(cli_codex_instructions);
|
|
|
|
/* Editor MCP: Cursor/Windsurf/Gemini (5 tests — install_test.go) */
|
|
RUN_TEST(cli_editor_mcp_install);
|
|
RUN_TEST(cli_editor_mcp_idempotent);
|
|
RUN_TEST(cli_editor_mcp_preserves_others);
|
|
RUN_TEST(cli_editor_mcp_uninstall);
|
|
RUN_TEST(cli_gemini_mcp_install);
|
|
|
|
/* VS Code MCP (2 tests — install_test.go) */
|
|
RUN_TEST(cli_vscode_mcp_install);
|
|
RUN_TEST(cli_vscode_mcp_uninstall);
|
|
|
|
/* Zed MCP (3 tests — install_test.go) */
|
|
RUN_TEST(cli_zed_mcp_install);
|
|
RUN_TEST(cli_zed_mcp_preserves_settings);
|
|
RUN_TEST(cli_zed_mcp_uninstall);
|
|
|
|
/* PATH management (3 tests) */
|
|
RUN_TEST(cli_ensure_path_append);
|
|
RUN_TEST(cli_ensure_path_already_present);
|
|
RUN_TEST(cli_ensure_path_dry_run);
|
|
|
|
/* File copy (2 tests — update_test.go) */
|
|
RUN_TEST(cli_copy_file);
|
|
RUN_TEST(cli_copy_file_source_not_found);
|
|
|
|
/* Tar.gz extraction (3 tests — update_test.go) */
|
|
RUN_TEST(cli_extract_binary_from_targz);
|
|
RUN_TEST(cli_extract_binary_from_targz_not_found);
|
|
RUN_TEST(cli_extract_binary_from_targz_invalid_data);
|
|
|
|
/* Dry-run lifecycle (2 tests) */
|
|
RUN_TEST(cli_install_dry_run);
|
|
RUN_TEST(cli_uninstall_dry_run);
|
|
|
|
/* Full lifecycle (1 test — cli_test.go) */
|
|
RUN_TEST(cli_install_and_uninstall);
|
|
|
|
/* YAML parser (7 unit tests) */
|
|
RUN_TEST(cli_yaml_parse_simple);
|
|
RUN_TEST(cli_yaml_parse_nested);
|
|
RUN_TEST(cli_yaml_parse_list);
|
|
RUN_TEST(cli_yaml_parse_bool);
|
|
RUN_TEST(cli_yaml_parse_comments);
|
|
RUN_TEST(cli_yaml_parse_empty);
|
|
RUN_TEST(cli_yaml_has);
|
|
}
|