mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-12 09:47:20 +08:00
Every config editor (JSON-like, TOML, YAML, text) opens with O_NOFOLLOW and refuses symlinks, so a link planted at ~/.cursor/mcp.json cannot point a privileged writer at another file. Users keep exactly those files as symlinks into a dotfiles checkout; the blanket refusal made install and uninstall fail on them and, before the previous commit, turned uninstall into data loss (#1954). Decision C: a symlink the INVOKING user owns, resolving to a regular file the same user owns, is followed — as an explicit opt-in of the agent-configuration writers only. cbm_install_agent_configs and the uninstall activation register the user's configuration roots (HOME, XDG_CONFIG_HOME) for the duration of their work; a link whose own directory is outside every registered root, or any link seen by another editor caller, stays refused, so repository paths never follow. One shared helper (src/cli/config_edit_path.c) runs the race-safe sequence: lstat(link) → owner == invoking uid (a root process follows root-owned links only) → realpath → the target's parent directory opened as a dirfd (O_DIRECTORY|O_NOFOLLOW; owned by the invoking user or root, not writable by others unless sticky) → openat(dirfd, base, O_NOFOLLOW) → fstat: regular file, same owner, link count 1, and the same (dev, ino) as stat(realpath). Each editor reads through that validated descriptor, stages its temp file with openat(dirfd, …, O_CREAT|O_EXCL|O_NOFOLLOW), runs its pre-publish snapshot comparison against the resolved path, and commits with renameat on the pinned dirfd, so the link itself is never replaced. Every other link is refused with an observable reason (surfaced in the uninstall diagnostics): another owner, root over a non-root link, dangling, a directory or special target, a hard-linked or swapped target, a foreign or world-writable parent. Whether the target must also stay under a root is a pending user decision behind CBM_CONFIG_EDIT_TARGET_UNDER_ROOT (default: anywhere the user owns). Windows never follows reparse points. Tests: each editor suite gains a same-owner link edited in place under the opt-in (link preserved and still pointing at the same target, temp file staged beside the target, bytes identical to the same edit on a plain file, no stray temp files) and keeps its refusal cases; the JSON-like suite adds no-opt-in, outside-root, dangling, directory, identity-skew and world-writable-parent refusals. The foreign-owner and root-run refusals move the observer through a test seam (a test cannot create a foreign-owned link without root; a real root run demotes the link first). test_cli.c drives `uninstall -y` end to end through a symlinked ~/.cursor/mcp.json. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1512 lines
65 KiB
C
1512 lines
65 KiB
C
/*
|
|
* test_config_toml_edit.c — Standalone tests for conservative TOML edits.
|
|
*
|
|
* This suite is intentionally not registered in test_main.c.
|
|
*/
|
|
#define CBM_TOML_EDIT_ENABLE_TEST_API 1
|
|
#include "cli/config_toml_edit.h"
|
|
#define CBM_CONFIG_EDIT_PATH_ENABLE_TEST_API 1
|
|
#include "cli/config_edit_path.h"
|
|
#include "foundation/compat.h"
|
|
#include "foundation/compat_fs.h"
|
|
#include "test_framework.h"
|
|
#include "test_helpers.h"
|
|
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#ifndef _WIN32
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#define CTE_PATH_CAP 1024
|
|
#define CTE_FILE_CAP 16384
|
|
|
|
static const char *CTE_BEGIN = "# BEGIN codebase-memory-mcp";
|
|
static const char *CTE_END = "# END codebase-memory-mcp";
|
|
static const char *CTE_TABLE = "mcp_servers";
|
|
static const char *CTE_KEY = "name";
|
|
static const char *CTE_IDENTITY = "codebase-memory-mcp";
|
|
static const char *CTE_BODY = "name = \"codebase-memory-mcp\"\n"
|
|
"command = \"codebase-memory-mcp\"\n";
|
|
static const char *CTE_CODEX_BEGIN = "# >>> codebase-memory-mcp SessionStart >>>";
|
|
static const char *CTE_CODEX_END = "# <<< codebase-memory-mcp SessionStart <<<";
|
|
static const char *CTE_CODEX_COMMAND = "codebase-memory-mcp hook-augment";
|
|
static const char *CTE_CODEX_BLOCK =
|
|
"[[hooks.SessionStart]]\n"
|
|
"matcher = \"startup|resume|clear|compact\"\n\n"
|
|
"[[hooks.SessionStart.hooks]]\n"
|
|
"type = \"command\"\ncommand = \"/opt/codebase-memory-mcp hook-augment\"\n"
|
|
"command_windows = \"& C:\\\\bin\\\\codebase-memory-mcp.exe hook-augment\"\ntimeout = 5\n\n"
|
|
"[[hooks.SubagentStart]]\nmatcher = \"*\"\n\n"
|
|
"[[hooks.SubagentStart.hooks]]\n"
|
|
"type = \"command\"\ncommand = \"/opt/codebase-memory-mcp hook-augment\"\n"
|
|
"command_windows = \"& C:\\\\bin\\\\codebase-memory-mcp.exe hook-augment\"\ntimeout = 5\n";
|
|
|
|
static int cte_codex_edit(const char *path, cbm_toml_codex_hook_action_t action, int check_only) {
|
|
return cbm_toml_reconcile_codex_hooks(path, CTE_CODEX_BEGIN, CTE_CODEX_END, CTE_CODEX_COMMAND,
|
|
CTE_CODEX_COMMAND, action, check_only);
|
|
}
|
|
|
|
static int cte_codex_edit_commands_detailed(const char *path, const char *command,
|
|
const char *command_windows,
|
|
cbm_toml_codex_hook_action_t action, int check_only,
|
|
cbm_toml_codex_hook_failure_t *failure) {
|
|
return cbm_toml_reconcile_codex_hooks_detailed(path, CTE_CODEX_BEGIN, CTE_CODEX_END, command,
|
|
command_windows, action, check_only, failure);
|
|
}
|
|
|
|
static int cte_codex_edit_detailed(const char *path, cbm_toml_codex_hook_action_t action,
|
|
int check_only, cbm_toml_codex_hook_failure_t *failure) {
|
|
return cte_codex_edit_commands_detailed(path, CTE_CODEX_COMMAND, CTE_CODEX_COMMAND, action,
|
|
check_only, failure);
|
|
}
|
|
|
|
static int cte_fixture(char *dir, size_t dir_size, char *path, size_t path_size) {
|
|
char *created = th_mktempdir("cbm_toml_edit");
|
|
if (!created) {
|
|
return -1;
|
|
}
|
|
int dir_len = snprintf(dir, dir_size, "%s", created);
|
|
int path_len = snprintf(path, path_size, "%s/config.toml", dir);
|
|
if (dir_len < 0 || (size_t)dir_len >= dir_size || path_len < 0 ||
|
|
(size_t)path_len >= path_size) {
|
|
th_cleanup(created);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int cte_read(const char *path, char *output, size_t output_size) {
|
|
if (!path || !output || output_size == 0) {
|
|
return -1;
|
|
}
|
|
FILE *file = cbm_fopen(path, "rb");
|
|
if (!file) {
|
|
return -1;
|
|
}
|
|
if (fseek(file, 0, SEEK_END) != 0) {
|
|
fclose(file);
|
|
return -1;
|
|
}
|
|
long raw_size = ftell(file);
|
|
if (raw_size < 0 || (size_t)raw_size >= output_size || fseek(file, 0, SEEK_SET) != 0) {
|
|
fclose(file);
|
|
return -1;
|
|
}
|
|
size_t size = (size_t)raw_size;
|
|
size_t read_count = size ? fread(output, 1, size, file) : 0;
|
|
int close_error = fclose(file);
|
|
if (read_count != size || close_error != 0) {
|
|
return -1;
|
|
}
|
|
output[size] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
static int cte_occurrences(const char *text, const char *needle) {
|
|
int count = 0;
|
|
size_t needle_len = strlen(needle);
|
|
const char *cursor = text;
|
|
while ((cursor = strstr(cursor, needle)) != NULL) {
|
|
++count;
|
|
cursor += needle_len;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static int cte_replace_once(const char *source, const char *needle, const char *replacement,
|
|
char *output, size_t output_size) {
|
|
const char *match = strstr(source, needle);
|
|
if (!match) {
|
|
return -1;
|
|
}
|
|
size_t prefix_len = (size_t)(match - source);
|
|
size_t needle_len = strlen(needle);
|
|
size_t replacement_len = strlen(replacement);
|
|
size_t suffix_len = strlen(match + needle_len);
|
|
if (prefix_len + replacement_len + suffix_len >= output_size) {
|
|
return -1;
|
|
}
|
|
memcpy(output, source, prefix_len);
|
|
memcpy(output + prefix_len, replacement, replacement_len);
|
|
memcpy(output + prefix_len + replacement_len, match + needle_len, suffix_len + 1U);
|
|
return 0;
|
|
}
|
|
|
|
static size_t cte_temp_count(const char *dir) {
|
|
cbm_dir_t *directory = cbm_opendir(dir);
|
|
if (!directory) {
|
|
return SIZE_MAX;
|
|
}
|
|
size_t count = 0;
|
|
cbm_dirent_t *entry = NULL;
|
|
while ((entry = cbm_readdir(directory)) != NULL) {
|
|
if (strncmp(entry->name, "config.toml.", strlen("config.toml.")) == 0) {
|
|
count++;
|
|
}
|
|
}
|
|
cbm_closedir(directory);
|
|
return count;
|
|
}
|
|
|
|
typedef struct {
|
|
const char *content;
|
|
const char *backup_path;
|
|
bool replace_identity;
|
|
int result;
|
|
} cte_precommit_change_t;
|
|
|
|
static void cte_change_before_commit(const char *path, void *context) {
|
|
cte_precommit_change_t *change = context;
|
|
if (change->replace_identity &&
|
|
(!change->backup_path || cbm_rename_replace(path, change->backup_path) != 0)) {
|
|
change->result = -1;
|
|
return;
|
|
}
|
|
change->result = th_write_file(path, change->content);
|
|
}
|
|
|
|
static int cte_assert_unchanged_after_managed_upsert(const char *path, const char *original,
|
|
const char *block) {
|
|
char actual[CTE_FILE_CAP];
|
|
return th_write_file(path, original) == 0 &&
|
|
cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, block) == -1 &&
|
|
cte_read(path, actual, sizeof(actual)) == 0 && strcmp(actual, original) == 0
|
|
? 1
|
|
: 0;
|
|
}
|
|
|
|
static int cte_assert_unchanged_after_vibe_upsert(const char *path, const char *original,
|
|
const char *body) {
|
|
char actual[CTE_FILE_CAP];
|
|
return th_write_file(path, original) == 0 &&
|
|
cbm_toml_upsert_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY,
|
|
body) == -1 &&
|
|
cte_read(path, actual, sizeof(actual)) == 0 && strcmp(actual, original) == 0
|
|
? 1
|
|
: 0;
|
|
}
|
|
|
|
TEST(config_toml_rejects_stale_content_and_identity) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char backup[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT(snprintf(backup, sizeof(backup), "%s/original.toml", dir) > 0);
|
|
ASSERT_EQ(th_write_file(path, "keep = true\n"), 0);
|
|
|
|
cte_precommit_change_t content_change = {
|
|
.content = "concurrent = true\n",
|
|
.backup_path = NULL,
|
|
.replace_identity = false,
|
|
.result = -1,
|
|
};
|
|
cbm_toml_set_precommit_hook_for_testing(cte_change_before_commit, &content_change);
|
|
int result = cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n");
|
|
cbm_toml_set_precommit_hook_for_testing(NULL, NULL);
|
|
ASSERT_EQ(content_change.result, 0);
|
|
ASSERT_EQ(result, -1);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "concurrent = true\n");
|
|
ASSERT_EQ(cte_temp_count(dir), 0U);
|
|
|
|
ASSERT_EQ(th_write_file(path, "keep = true\n"), 0);
|
|
cte_precommit_change_t identity_change = {
|
|
.content = "keep = true\n",
|
|
.backup_path = backup,
|
|
.replace_identity = true,
|
|
.result = -1,
|
|
};
|
|
cbm_toml_set_precommit_hook_for_testing(cte_change_before_commit, &identity_change);
|
|
result = cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n");
|
|
cbm_toml_set_precommit_hook_for_testing(NULL, NULL);
|
|
ASSERT_EQ(identity_change.result, 0);
|
|
ASSERT_EQ(result, -1);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "keep = true\n");
|
|
ASSERT_EQ(cte_temp_count(dir), 0U);
|
|
ASSERT_EQ(cbm_unlink(backup), 0);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_missing_target_race_does_not_replace_winner) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
cte_precommit_change_t race = {
|
|
.content = "winner = true\n",
|
|
.backup_path = NULL,
|
|
.replace_identity = false,
|
|
.result = -1,
|
|
};
|
|
cbm_toml_set_prepublish_hook_for_testing(cte_change_before_commit, &race);
|
|
int result = cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n");
|
|
cbm_toml_set_prepublish_hook_for_testing(NULL, NULL);
|
|
ASSERT_EQ(race.result, 0);
|
|
ASSERT_EQ(result, -1);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "winner = true\n");
|
|
ASSERT_EQ(cte_temp_count(dir), 0U);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_existing_target_swap_after_check_preserves_winner) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char backup[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
const char *original = "keep = true\n";
|
|
const char *winner = "winner = true\n";
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT(snprintf(backup, sizeof(backup), "%s/original.toml", dir) > 0);
|
|
ASSERT_EQ(th_write_file(path, original), 0);
|
|
cte_precommit_change_t race = {
|
|
.content = winner,
|
|
.backup_path = backup,
|
|
.replace_identity = true,
|
|
.result = -1,
|
|
};
|
|
|
|
cbm_toml_set_prepublish_hook_for_testing(cte_change_before_commit, &race);
|
|
int result = cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n");
|
|
cbm_toml_set_prepublish_hook_for_testing(NULL, NULL);
|
|
|
|
ASSERT_EQ(race.result, 0);
|
|
ASSERT_EQ(result, -1);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, winner);
|
|
ASSERT_EQ(cte_temp_count(dir), 0U);
|
|
ASSERT_EQ(cbm_unlink(backup), 0);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_rejects_non_regular_path) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(cbm_mkdir(path), 0);
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n"), -1);
|
|
ASSERT_EQ(cte_temp_count(dir), 0U);
|
|
ASSERT_EQ(cbm_rmdir(path), 0);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
TEST(config_toml_rejects_symlink_hardlink_and_preserves_metadata) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char target[CTE_PATH_CAP];
|
|
char alias[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT(snprintf(target, sizeof(target), "%s/target.toml", dir) > 0);
|
|
ASSERT(snprintf(alias, sizeof(alias), "%s/alias.toml", dir) > 0);
|
|
ASSERT_EQ(th_write_file(target, "target = true\n"), 0);
|
|
ASSERT_EQ(symlink(target, path), 0);
|
|
/* Foreign-owned link (observer moved by the test seam): still refused. */
|
|
ASSERT_EQ(cbm_config_edit_path_follow_add_root(dir), 0);
|
|
cbm_config_edit_path_set_invoking_uid_for_test((unsigned)geteuid() + 1U, 1);
|
|
int foreign_rc = cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n");
|
|
cbm_config_edit_path_set_invoking_uid_for_test(0U, 0);
|
|
cbm_config_edit_path_follow_clear();
|
|
ASSERT_EQ(foreign_rc, -1);
|
|
struct stat link_state;
|
|
ASSERT_EQ(lstat(path, &link_state), 0);
|
|
ASSERT(S_ISLNK(link_state.st_mode));
|
|
ASSERT_EQ(cte_read(target, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "target = true\n");
|
|
ASSERT_EQ(cbm_unlink(path), 0);
|
|
|
|
ASSERT_EQ(th_write_file(path, "shared = true\n"), 0);
|
|
ASSERT_EQ(link(path, alias), 0);
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n"), -1);
|
|
ASSERT_EQ(cte_read(alias, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "shared = true\n");
|
|
ASSERT_EQ(cbm_unlink(alias), 0);
|
|
|
|
ASSERT_EQ(chmod(path, 04755), 0);
|
|
struct stat privileged;
|
|
ASSERT_EQ(stat(path, &privileged), 0);
|
|
/* Some sandboxed filesystems silently clear set-id bits even when chmod
|
|
* reports success. Exercise the rejection contract only when the fixture
|
|
* can actually retain the privileged bit. */
|
|
if ((privileged.st_mode & S_ISUID) != 0) {
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n"), -1);
|
|
}
|
|
ASSERT_EQ(chmod(path, 0640), 0);
|
|
struct stat before;
|
|
ASSERT_EQ(stat(path, &before), 0);
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n"), 0);
|
|
struct stat after;
|
|
ASSERT_EQ(stat(path, &after), 0);
|
|
ASSERT_EQ(after.st_uid, before.st_uid);
|
|
ASSERT_EQ(after.st_gid, before.st_gid);
|
|
ASSERT_EQ(after.st_mode & 07777, before.st_mode & 07777);
|
|
ASSERT_EQ(cbm_unlink(target), 0);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
#endif
|
|
|
|
#ifndef _WIN32
|
|
/* Decision C (#1954): a managed block behind a user-owned symlink is edited
|
|
* through the link — the link survives and the target carries the same bytes
|
|
* the same edit produces on a plain file. */
|
|
TEST(config_toml_follows_user_owned_symlink_in_place) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char target[CTE_PATH_CAP];
|
|
char control[CTE_PATH_CAP];
|
|
char through[CTE_FILE_CAP];
|
|
char plain[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT(snprintf(target, sizeof(target), "%s/target.toml", dir) > 0);
|
|
ASSERT(snprintf(control, sizeof(control), "%s/control.toml", dir) > 0);
|
|
ASSERT_EQ(th_write_file(target, "target = true\n"), 0);
|
|
ASSERT_EQ(th_write_file(control, "target = true\n"), 0);
|
|
ASSERT_EQ(symlink("target.toml", path), 0);
|
|
ASSERT_EQ(cbm_config_edit_path_follow_add_root(dir), 0);
|
|
|
|
int through_rc = cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n");
|
|
int plain_rc = cbm_toml_upsert_managed_block(control, CTE_BEGIN, CTE_END, "owned = true\n");
|
|
if (through_rc != 0 || plain_rc != 0) {
|
|
cbm_config_edit_path_follow_clear();
|
|
}
|
|
ASSERT_EQ(through_rc, 0);
|
|
ASSERT_EQ(plain_rc, 0);
|
|
struct stat link_state;
|
|
ASSERT_EQ(lstat(path, &link_state), 0);
|
|
ASSERT(S_ISLNK(link_state.st_mode));
|
|
ASSERT_EQ(cte_read(target, through, sizeof(through)), 0);
|
|
ASSERT_EQ(cte_read(control, plain, sizeof(plain)), 0);
|
|
ASSERT_STR_EQ(through, plain);
|
|
ASSERT_NOT_NULL(strstr(through, "owned = true"));
|
|
|
|
int through_remove_rc = cbm_toml_remove_managed_block(path, CTE_BEGIN, CTE_END);
|
|
int plain_remove_rc = cbm_toml_remove_managed_block(control, CTE_BEGIN, CTE_END);
|
|
cbm_config_edit_path_follow_clear();
|
|
ASSERT_EQ(through_remove_rc, 0);
|
|
ASSERT_EQ(plain_remove_rc, 0);
|
|
ASSERT_EQ(lstat(path, &link_state), 0);
|
|
ASSERT(S_ISLNK(link_state.st_mode));
|
|
ASSERT_EQ(cte_read(target, through, sizeof(through)), 0);
|
|
ASSERT_EQ(cte_read(control, plain, sizeof(plain)), 0);
|
|
ASSERT_STR_EQ(through, plain);
|
|
ASSERT_NULL(strstr(through, "owned = true"));
|
|
ASSERT_EQ(cte_temp_count(dir), 0U);
|
|
ASSERT_EQ(cbm_unlink(target), 0);
|
|
ASSERT_EQ(cbm_unlink(control), 0);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
#endif
|
|
|
|
TEST(config_toml_managed_markers_ignore_multiline_strings) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
const char *original = "basic = \"\"\"\n"
|
|
"# BEGIN codebase-memory-mcp\n"
|
|
"# END codebase-memory-mcp\n"
|
|
"\"\"\"\n"
|
|
"literal = '''\n"
|
|
"# BEGIN codebase-memory-mcp\n"
|
|
"# END codebase-memory-mcp\n"
|
|
"'''\n";
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, original), 0);
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n"), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_EQ(cte_occurrences(actual, CTE_BEGIN), 3);
|
|
ASSERT_EQ(cte_occurrences(actual, CTE_END), 3);
|
|
ASSERT_NOT_NULL(strstr(actual, "owned = true"));
|
|
ASSERT_EQ(cbm_toml_remove_managed_block(path, CTE_BEGIN, CTE_END), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, original);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_managed_rejects_marker_in_block_and_unclosed_multiline) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, "keep = true\n"), 0);
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END,
|
|
"value = true\n# END codebase-memory-mcp\n"),
|
|
-1);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "keep = true\n");
|
|
|
|
const char *unclosed = "description = \"\"\"\n# BEGIN codebase-memory-mcp\n";
|
|
ASSERT_EQ(th_write_file(path, unclosed), 0);
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n"), -1);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, unclosed);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_codex_semantic_conflicts_fail_closed) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
const char *codex_block = "[mcp_servers.codebase-memory-mcp]\ncommand = \"new\"\n";
|
|
static const char *conflicts[] = {
|
|
"[mcp_servers.\"codebase-memory-mcp\"]\ncommand = \"old\"\n",
|
|
"[\"mcp_servers\".'codebase-memory-mcp']\ncommand = \"old\"\n",
|
|
"mcp_servers.\"codebase-memory-mcp\".command = \"old\"\n",
|
|
"[mcp_servers]\n\"codebase-memory-mcp\".command = \"old\"\n",
|
|
("[mcp_servers.codebase-memory-mcp]\ncommand = \"one\"\n"
|
|
"[mcp_servers.\"codebase-memory-mcp\"]\ncommand = \"two\"\n"),
|
|
};
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
for (size_t i = 0; i < sizeof(conflicts) / sizeof(conflicts[0]); ++i) {
|
|
ASSERT(cte_assert_unchanged_after_managed_upsert(path, conflicts[i], codex_block));
|
|
}
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_vibe_body_validation_fail_closed) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
const char *original = "keep = true\n";
|
|
static const char *invalid_bodies[] = {
|
|
"command = \"missing identity\"\n",
|
|
"name = \"wrong\"\ncommand = \"x\"\n",
|
|
"name = \"codebase-memory-mcp\"\nname = \"codebase-memory-mcp\"\n",
|
|
"name = \"codebase-memory-mcp\"\n[evil]\npwned = true\n",
|
|
"name = \"codebase-memory-mcp\"\n[[evil]]\npwned = true\n",
|
|
"name = \"codebase-memory-mcp\"\ncommand.value = \"x\"\n",
|
|
"name = \"codebase-memory-mcp\"\ndescription = \"\"\"ambiguous\"\"\"\n",
|
|
"name = \"codebase-memory-mcp\"\nthis is not an assignment\n",
|
|
};
|
|
for (size_t i = 0; i < sizeof(invalid_bodies) / sizeof(invalid_bodies[0]); ++i) {
|
|
ASSERT(cte_assert_unchanged_after_vibe_upsert(path, original, invalid_bodies[i]));
|
|
}
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_target_table_rejects_significant_nonassignments_byte_identically) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
static const char *malformed[] = {
|
|
"[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\"\n"
|
|
"this is not an assignment\n"
|
|
"command = \"winner\"\n",
|
|
"[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\"\n"
|
|
"command \"missing equals\"\n",
|
|
"[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\"\n"
|
|
"@invalid\n",
|
|
};
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
for (size_t i = 0U; i < sizeof(malformed) / sizeof(malformed[0]); ++i) {
|
|
ASSERT_EQ(th_write_file(path, malformed[i]), 0);
|
|
ASSERT_EQ(
|
|
cbm_toml_upsert_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY, CTE_BODY),
|
|
-1);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, malformed[i]);
|
|
|
|
ASSERT_EQ(cbm_toml_remove_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY), -1);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, malformed[i]);
|
|
}
|
|
|
|
const char *malformed_regular = "[mcp_servers.codebase-memory-mcp]\n"
|
|
"command = \"winner\"\n"
|
|
"this is not an assignment\n"
|
|
"[unrelated]\n"
|
|
"keep = true\n";
|
|
ASSERT_EQ(th_write_file(path, malformed_regular), 0);
|
|
ASSERT_EQ(
|
|
cbm_toml_remove_legacy_table(path, "mcp_servers.codebase-memory-mcp", CTE_BEGIN, CTE_END),
|
|
-1);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, malformed_regular);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_legacy_remove_reports_foreign_table_without_mutation) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
const char *foreign = "theme = \"dark\"\n"
|
|
"[mcp_servers.codebase-memory-mcp]\n"
|
|
"command = \"/opt/user-tool\"\n"
|
|
"args = []\n"
|
|
"user_field = true\n";
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, foreign), 0);
|
|
ASSERT_EQ(
|
|
cbm_toml_remove_legacy_table(path, "mcp_servers.codebase-memory-mcp", CTE_BEGIN, CTE_END),
|
|
1);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, foreign);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_vibe_non_array_and_dotted_conflicts_fail_closed) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
static const char *conflicts[] = {
|
|
"[mcp_servers]\nenabled = true\n",
|
|
"[\"mcp_servers\"]\nenabled = true\n",
|
|
"mcp_servers = []\n",
|
|
"mcp_servers.child = {}\n",
|
|
"[mcp_servers.child]\nenabled = true\n",
|
|
"[[mcp_servers.child]]\nname = \"nested\"\n",
|
|
};
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
for (size_t i = 0; i < sizeof(conflicts) / sizeof(conflicts[0]); ++i) {
|
|
ASSERT(cte_assert_unchanged_after_vibe_upsert(path, conflicts[i], CTE_BODY));
|
|
}
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_vibe_remove_includes_descendant_tables) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, "[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\"\n"
|
|
"command = \"owned\"\n"
|
|
"[mcp_servers.environment]\n"
|
|
"TOKEN = \"owned\"\n"
|
|
"[[mcp_servers.children]]\n"
|
|
"value = \"owned-child\"\n\n"
|
|
"[[mcp_servers]]\n"
|
|
"name = \"other\"\n"
|
|
"command = \"keep\"\n"),
|
|
0);
|
|
ASSERT_EQ(cbm_toml_remove_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_NULL(strstr(actual, "TOKEN"));
|
|
ASSERT_NULL(strstr(actual, "owned-child"));
|
|
ASSERT_NOT_NULL(strstr(actual, "name = \"other\""));
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_vibe_reinstall_preserves_user_fields_and_descendants) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
const char *desired = "name = \"codebase-memory-mcp\"\n"
|
|
"transport = \"stdio\"\n"
|
|
"command = \"new\"\n";
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, "[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\" # identity comment\n"
|
|
"command = \"old\"\n"
|
|
"timeout = 45 # user field\n"
|
|
"args = [\"--user\"]\n"
|
|
"[mcp_servers.environment]\n"
|
|
"KEEP = \"yes\"\n"),
|
|
0);
|
|
ASSERT_EQ(cbm_toml_upsert_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY, desired),
|
|
0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_NOT_NULL(strstr(actual, "command = \"new\""));
|
|
ASSERT_NULL(strstr(actual, "command = \"old\""));
|
|
ASSERT_NOT_NULL(strstr(actual, "transport = \"stdio\""));
|
|
ASSERT_NOT_NULL(strstr(actual, "timeout = 45 # user field"));
|
|
ASSERT_NOT_NULL(strstr(actual, "args = [\"--user\"]"));
|
|
ASSERT_NOT_NULL(strstr(actual, "[mcp_servers.environment]"));
|
|
ASSERT_NOT_NULL(strstr(actual, "KEEP = \"yes\""));
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_preserves_bom_crlf_and_handles_no_final_newline) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, "\xEF\xBB\xBFkeep = true\r\n"), 0);
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n"), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT(memcmp(actual, "\xEF\xBB\xBF", 3U) == 0);
|
|
ASSERT_NOT_NULL(strstr(actual, "keep = true\r\n"));
|
|
ASSERT_NOT_NULL(strstr(actual, "# BEGIN codebase-memory-mcp\r\nowned = true\r\n"));
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n"), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_EQ(cte_occurrences(actual, CTE_BEGIN), 1);
|
|
ASSERT_EQ(cbm_toml_remove_managed_block(path, CTE_BEGIN, CTE_END), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "\xEF\xBB\xBFkeep = true\r\n");
|
|
|
|
ASSERT_EQ(th_write_file(path, "\xEF\xBB\xBF# BEGIN codebase-memory-mcp\r\n"
|
|
"old = true\r\n"
|
|
"# END codebase-memory-mcp\r\n"),
|
|
0);
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "new = true\n"), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT(memcmp(actual, "\xEF\xBB\xBF# BEGIN", 10U) == 0);
|
|
ASSERT_EQ(cte_occurrences(actual, CTE_BEGIN), 1);
|
|
ASSERT_NOT_NULL(strstr(actual, "new = true\r\n"));
|
|
ASSERT_EQ(cbm_toml_remove_managed_block(path, CTE_BEGIN, CTE_END), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "\xEF\xBB\xBF");
|
|
|
|
ASSERT_EQ(th_write_file(path, "\xEF\xBB\xBF[[\"mcp_servers\"]]\r\n"
|
|
"name = \"codebase-memory-mcp\"\r\n"
|
|
"command = \"old\""),
|
|
0);
|
|
ASSERT_EQ(cbm_toml_upsert_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY, CTE_BODY),
|
|
0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT(memcmp(actual, "\xEF\xBB\xBF", 3U) == 0);
|
|
ASSERT_NOT_NULL(strstr(actual, "command = \"codebase-memory-mcp\"\r\n"));
|
|
|
|
ASSERT_EQ(th_write_file(path, "keep = true"), 0);
|
|
ASSERT_EQ(cbm_toml_upsert_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY, CTE_BODY),
|
|
0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_NOT_NULL(strstr(actual, "keep = true\n\n[[mcp_servers]]\n"));
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_rejects_oversized_input_file) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
FILE *file = cbm_fopen(path, "wb");
|
|
ASSERT_NOT_NULL(file);
|
|
char chunk[4096];
|
|
memset(chunk, '#', sizeof(chunk));
|
|
for (size_t i = 0; i < (16U * 1024U * 1024U) / sizeof(chunk) + 1U; ++i) {
|
|
ASSERT_EQ(fwrite(chunk, 1U, sizeof(chunk), file), sizeof(chunk));
|
|
}
|
|
ASSERT_EQ(fclose(file), 0);
|
|
ASSERT_EQ(cbm_toml_remove_managed_block(path, CTE_BEGIN, CTE_END), -1);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_escape_windows_path_quotes) {
|
|
char escaped[256];
|
|
ASSERT_EQ(
|
|
cbm_toml_escape_basic_string("C:\\Users\\Jane\\tool \"quoted\"", escaped, sizeof(escaped)),
|
|
0);
|
|
ASSERT_STR_EQ(escaped, "C:\\\\Users\\\\Jane\\\\tool \\\"quoted\\\"");
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_escape_newlines_controls) {
|
|
char escaped[256];
|
|
ASSERT_EQ(cbm_toml_escape_basic_string("line1\nline2\t\r\b\f\x01", escaped, sizeof(escaped)),
|
|
0);
|
|
ASSERT_STR_EQ(escaped, "line1\\nline2\\t\\r\\b\\f\\u0001");
|
|
|
|
char too_small[4] = "bad";
|
|
ASSERT_EQ(cbm_toml_escape_basic_string("overflow", too_small, sizeof(too_small)), -1);
|
|
ASSERT_STR_EQ(too_small, "");
|
|
ASSERT_EQ(cbm_toml_escape_basic_string(NULL, escaped, sizeof(escaped)), -1);
|
|
ASSERT_EQ(cbm_toml_escape_basic_string("value", NULL, 0), -1);
|
|
char invalid_utf8[] = {(char)0xff, '\0'};
|
|
ASSERT_EQ(cbm_toml_escape_basic_string(invalid_utf8, escaped, sizeof(escaped)), -1);
|
|
ASSERT_STR_EQ(escaped, "");
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_managed_fresh_missing_file) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "enabled = true\n"), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "# BEGIN codebase-memory-mcp\n"
|
|
"enabled = true\n"
|
|
"# END codebase-memory-mcp\n");
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_managed_replace_preserves_unrelated) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, "title = \"keep\"\n"
|
|
"# BEGIN codebase-memory-mcp\n"
|
|
"old = true\n"
|
|
"# END codebase-memory-mcp\n"
|
|
"tail = \"keep\"\n"),
|
|
0);
|
|
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "new = \"value\""), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "title = \"keep\"\n"
|
|
"# BEGIN codebase-memory-mcp\n"
|
|
"new = \"value\"\n"
|
|
"# END codebase-memory-mcp\n"
|
|
"tail = \"keep\"\n");
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_managed_remove) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, "before = true\n"
|
|
"# BEGIN codebase-memory-mcp\n"
|
|
"owned = true\n"
|
|
"# END codebase-memory-mcp\n"
|
|
"after = true\n"),
|
|
0);
|
|
|
|
ASSERT_EQ(cbm_toml_remove_managed_block(path, CTE_BEGIN, CTE_END), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "before = true\nafter = true\n");
|
|
ASSERT_EQ(cbm_toml_remove_managed_block(path, CTE_BEGIN, CTE_END), 0);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_managed_idempotent) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char first[CTE_FILE_CAP];
|
|
char second[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, "keep = true\n"), 0);
|
|
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n"), 0);
|
|
ASSERT_EQ(cte_read(path, first, sizeof(first)), 0);
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n"), 0);
|
|
ASSERT_EQ(cte_read(path, second, sizeof(second)), 0);
|
|
ASSERT_STR_EQ(first, second);
|
|
ASSERT_EQ(cte_occurrences(second, CTE_BEGIN), 1);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_managed_unbalanced_duplicate_fail_closed) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
const char *unbalanced = "keep = true\n"
|
|
"# BEGIN codebase-memory-mcp\n"
|
|
"owned = true\n";
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, unbalanced), 0);
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "new = true\n"), -1);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, unbalanced);
|
|
|
|
const char *duplicate = "# BEGIN codebase-memory-mcp\n"
|
|
"one = true\n"
|
|
"# END codebase-memory-mcp\n"
|
|
"# BEGIN codebase-memory-mcp\n"
|
|
"two = true\n"
|
|
"# END codebase-memory-mcp\n";
|
|
ASSERT_EQ(th_write_file(path, duplicate), 0);
|
|
ASSERT_EQ(cbm_toml_remove_managed_block(path, CTE_BEGIN, CTE_END), -1);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, duplicate);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_vibe_insert_among_other_tables) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
const char *original = "# global comment\n"
|
|
"[settings]\n"
|
|
"theme = \"dark\"\n\n"
|
|
"[[mcp_servers]]\n"
|
|
"name = \"other-server\"\n"
|
|
"command = \"other\"\n";
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, original), 0);
|
|
|
|
ASSERT_EQ(cbm_toml_upsert_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY, CTE_BODY),
|
|
0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "# global comment\n"
|
|
"[settings]\n"
|
|
"theme = \"dark\"\n\n"
|
|
"[[mcp_servers]]\n"
|
|
"name = \"other-server\"\n"
|
|
"command = \"other\"\n\n"
|
|
"[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\"\n"
|
|
"command = \"codebase-memory-mcp\"\n");
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_vibe_replace_target_preserves_comments_tables) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
const char *replacement = "name = \"codebase-memory-mcp\"\n"
|
|
"command = \"/new/path\"\n";
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, "# keep top\n"
|
|
"[[mcp_servers]]\n"
|
|
"name = \"other-server\"\n"
|
|
"command = \"other\"\n\n"
|
|
"# keep target preface\n"
|
|
"[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\" # owned\n"
|
|
"command = \"old\"\n"
|
|
"args = [\"--old\"]\n\n"
|
|
"# keep after target\n"
|
|
"[ui]\n"
|
|
"enabled = true\n"),
|
|
0);
|
|
|
|
ASSERT_EQ(
|
|
cbm_toml_upsert_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY, replacement), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_NOT_NULL(strstr(actual, "# keep top"));
|
|
ASSERT_NOT_NULL(strstr(actual, "# keep target preface"));
|
|
ASSERT_NOT_NULL(strstr(actual, "# keep after target"));
|
|
ASSERT_NOT_NULL(strstr(actual, "name = \"other-server\""));
|
|
ASSERT_NOT_NULL(strstr(actual, "command = \"/new/path\""));
|
|
ASSERT_NULL(strstr(actual, "command = \"old\""));
|
|
ASSERT_NOT_NULL(strstr(actual, "--old"));
|
|
ASSERT_EQ(cte_occurrences(actual, "[[mcp_servers]]"), 2);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_vibe_owned_table_installs_idempotently_and_removes_exact_state) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
const char *canonical = "name = \"codebase-memory-mcp\"\n"
|
|
"transport = \"stdio\"\n"
|
|
"command = \"/opt/codebase-memory-mcp\"\n"
|
|
"args = []\n";
|
|
const char *installed = "[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\"\n"
|
|
"transport = \"stdio\"\n"
|
|
"command = \"/opt/codebase-memory-mcp\"\n"
|
|
"args = []\n";
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
|
|
ASSERT_EQ(
|
|
cbm_toml_upsert_owned_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY, canonical),
|
|
0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, installed);
|
|
|
|
ASSERT_EQ(
|
|
cbm_toml_upsert_owned_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY, canonical),
|
|
0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, installed);
|
|
|
|
ASSERT_EQ(
|
|
cbm_toml_remove_owned_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY, canonical),
|
|
0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "");
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_vibe_owned_table_preserves_foreign_same_name_state) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
const char *canonical = "name = \"codebase-memory-mcp\"\n"
|
|
"transport = \"stdio\"\n"
|
|
"command = \"/opt/codebase-memory-mcp\"\n"
|
|
"args = []\n";
|
|
const char *foreign_cases[] = {
|
|
"# user-owned Vibe server\n"
|
|
"[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\"\n"
|
|
"transport = \"stdio\"\n"
|
|
"command = \"/opt/user-owned-mcp\"\n"
|
|
"args = [\"--custom\"]\n",
|
|
"[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\"\n"
|
|
"transport = \"stdio\"\n"
|
|
"command = \"/opt/codebase-memory-mcp\"\n"
|
|
"args = []\n"
|
|
"startup_timeout_sec = 45\n",
|
|
};
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
|
|
for (size_t i = 0U; i < sizeof(foreign_cases) / sizeof(foreign_cases[0]); i++) {
|
|
ASSERT_EQ(th_write_file(path, foreign_cases[i]), 0);
|
|
ASSERT_EQ(cbm_toml_upsert_owned_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY,
|
|
canonical),
|
|
CBM_TOML_OWNED_EDIT_FOREIGN);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, foreign_cases[i]);
|
|
|
|
ASSERT_EQ(cbm_toml_remove_owned_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY,
|
|
canonical),
|
|
CBM_TOML_OWNED_EDIT_FOREIGN);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, foreign_cases[i]);
|
|
}
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_vibe_remove_first_target) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, "[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\"\n"
|
|
"command = \"owned\"\n\n"
|
|
"[[mcp_servers]]\n"
|
|
"name = \"other\"\n"
|
|
"command = \"keep\"\n"),
|
|
0);
|
|
ASSERT_EQ(cbm_toml_remove_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_NULL(strstr(actual, "command = \"owned\""));
|
|
ASSERT_NOT_NULL(strstr(actual, "name = \"other\""));
|
|
ASSERT_EQ(cte_occurrences(actual, "[[mcp_servers]]"), 1);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_vibe_remove_middle_target) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, "[[mcp_servers]]\n"
|
|
"name = \"first\"\n\n"
|
|
"[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\"\n"
|
|
"command = \"owned\"\n\n"
|
|
"[[mcp_servers]]\n"
|
|
"name = \"last\"\n"),
|
|
0);
|
|
ASSERT_EQ(cbm_toml_remove_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_NOT_NULL(strstr(actual, "name = \"first\""));
|
|
ASSERT_NOT_NULL(strstr(actual, "name = \"last\""));
|
|
ASSERT_NULL(strstr(actual, CTE_IDENTITY));
|
|
ASSERT_EQ(cte_occurrences(actual, "[[mcp_servers]]"), 2);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_vibe_remove_last_target) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, "[[mcp_servers]]\n"
|
|
"name = \"other\"\n\n"
|
|
"[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\"\n"),
|
|
0);
|
|
ASSERT_EQ(cbm_toml_remove_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_NOT_NULL(strstr(actual, "name = \"other\""));
|
|
ASSERT_NULL(strstr(actual, CTE_IDENTITY));
|
|
ASSERT_EQ(cte_occurrences(actual, "[[mcp_servers]]"), 1);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_vibe_remove_only_target) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, "[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\"\n"
|
|
"command = \"owned\"\n"),
|
|
0);
|
|
ASSERT_EQ(cbm_toml_remove_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "");
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_vibe_literal_and_basic_identity) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, "[[mcp_servers]]\n"
|
|
"name = 'codebase-memory-mcp' # literal\n"
|
|
"command = 'owned'\n"),
|
|
0);
|
|
ASSERT_EQ(cbm_toml_remove_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "");
|
|
|
|
ASSERT_EQ(th_write_file(path, "[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-\\u006dcp\" # basic\n"
|
|
"command = \"old\"\n"),
|
|
0);
|
|
ASSERT_EQ(cbm_toml_upsert_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY, CTE_BODY),
|
|
0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\"\n"
|
|
"command = \"codebase-memory-mcp\"\n");
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_vibe_duplicate_target_fail_closed) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
const char *duplicate = "# preserve exactly\n"
|
|
"[[mcp_servers]]\n"
|
|
"name = \"codebase-memory-mcp\"\n"
|
|
"command = \"first\"\n\n"
|
|
"[[mcp_servers]]\n"
|
|
"name = 'codebase-memory-mcp'\n"
|
|
"command = 'second'\n";
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, duplicate), 0);
|
|
ASSERT_EQ(cbm_toml_upsert_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY, CTE_BODY),
|
|
-1);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, duplicate);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_vibe_ambiguous_target_fail_closed) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
const char *ambiguous = "[[mcp_servers]]\n"
|
|
"command = \"missing-name\"\n";
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, ambiguous), 0);
|
|
ASSERT_EQ(cbm_toml_remove_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY), -1);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, ambiguous);
|
|
|
|
const char *multiline = "[[mcp_servers]]\n"
|
|
"description = \"\"\"\n"
|
|
"name = \"codebase-memory-mcp\"\n"
|
|
"\"\"\"\n"
|
|
"name = \"other\"\n";
|
|
ASSERT_EQ(th_write_file(path, multiline), 0);
|
|
ASSERT_EQ(cbm_toml_upsert_named_array_table(path, CTE_TABLE, CTE_KEY, CTE_IDENTITY, CTE_BODY),
|
|
0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_NOT_NULL(strstr(actual, "description = \"\"\""));
|
|
ASSERT_NOT_NULL(strstr(actual, "name = \"other\""));
|
|
ASSERT_NOT_NULL(strstr(actual, "name = \"codebase-memory-mcp\""));
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_codex_reconciles_minimal_owned_forms) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
char before[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(cte_codex_edit(path, CBM_TOML_CODEX_HOOK_UPSERT, 1), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), -1);
|
|
ASSERT_EQ(cte_codex_edit(path, CBM_TOML_CODEX_HOOK_UPSERT, 0), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_NOT_NULL(strstr(actual, CTE_CODEX_BEGIN));
|
|
ASSERT_EQ(cte_codex_edit(path, CBM_TOML_CODEX_HOOK_REMOVE, 0), 0);
|
|
|
|
static const char *fixtures[] = {
|
|
"[hooks]\nSessionStart = [{ matcher = \"startup|resume|clear|compact\", hooks = "
|
|
"[{ type = \"command\", command = 'echo \"Code discovery: prefer "
|
|
"codebase-memory-mcp\"' }] }]\n",
|
|
"\"hooks\" . 'SessionStart' = [ {matcher='startup|resume|clear|compact', "
|
|
"hooks=[{type='command', command='/opt/codebase-memory-mcp hook-augment', "
|
|
"command_windows='& C:\\\\bin\\\\codebase-memory-mcp.exe hook-augment',timeout=5}]} ]\n",
|
|
};
|
|
for (size_t i = 0U; i < sizeof(fixtures) / sizeof(fixtures[0]); ++i) {
|
|
ASSERT_EQ(th_write_file(path, fixtures[i]), 0);
|
|
ASSERT_EQ(cte_read(path, before, sizeof(before)), 0);
|
|
ASSERT_EQ(cte_codex_edit(path, CBM_TOML_CODEX_HOOK_UPSERT, 1), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, before);
|
|
ASSERT_EQ(cte_codex_edit(path, CBM_TOML_CODEX_HOOK_UPSERT, 0), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_NULL(strstr(actual, "SessionStart = ["));
|
|
ASSERT_EQ(cte_occurrences(actual, CTE_CODEX_BEGIN), 1);
|
|
ASSERT_EQ(cte_read(path, before, sizeof(before)), 0);
|
|
ASSERT_EQ(cte_codex_edit(path, CBM_TOML_CODEX_HOOK_UPSERT, 0), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, before);
|
|
ASSERT_EQ(cte_codex_edit(path, CBM_TOML_CODEX_HOOK_REMOVE, 0), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_NULL(strstr(actual, "hook-augment"));
|
|
}
|
|
|
|
ASSERT_EQ(th_write_file(path, CTE_CODEX_BLOCK), 0);
|
|
ASSERT_EQ(cte_codex_edit(path, CBM_TOML_CODEX_HOOK_UPSERT, 0), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_EQ(cte_occurrences(actual, CTE_CODEX_BEGIN), 1);
|
|
ASSERT_EQ(cte_occurrences(actual, "[[hooks.SessionStart]]"), 1);
|
|
char damaged[CTE_FILE_CAP];
|
|
ASSERT_GT(snprintf(damaged, sizeof(damaged), "%s%s\n%s%s\n%s\n", fixtures[0], CTE_CODEX_BEGIN,
|
|
CTE_CODEX_BLOCK, CTE_CODEX_END, "keep = true"),
|
|
0);
|
|
ASSERT_EQ(th_write_file(path, damaged), 0);
|
|
ASSERT_EQ(cte_codex_edit(path, CBM_TOML_CODEX_HOOK_UPSERT, 0), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_NULL(strstr(actual, "SessionStart = ["));
|
|
ASSERT_EQ(cte_occurrences(actual, CTE_CODEX_BEGIN), 1);
|
|
ASSERT_NOT_NULL(strstr(actual, "keep = true"));
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
/* #1633: keep this fixture literal. It is the managed AOT block written by the
|
|
* v0.10.2 release, with the release's POSIX and PowerShell command builders. */
|
|
TEST(config_toml_codex_accepts_v0102_managed_windows_crlf) {
|
|
static const char *command =
|
|
"'C:\\Users\\Example\\AppData\\Local\\Programs\\codebase-memory-mcp\\"
|
|
"codebase-memory-mcp.exe' hook-augment";
|
|
static const char *command_windows =
|
|
"& 'C:\\Users\\Example\\AppData\\Local\\Programs\\codebase-memory-mcp\\"
|
|
"codebase-memory-mcp.exe' hook-augment";
|
|
static const char *original =
|
|
"\xEF\xBB\xBF[mcp_servers.other]\r\n"
|
|
"command = \"other\"\r\n"
|
|
"keep = true\r\n"
|
|
"# >>> codebase-memory-mcp SessionStart >>>\r\n"
|
|
"[[hooks.SessionStart]]\r\n"
|
|
"matcher = \"startup|resume|clear|compact\"\r\n"
|
|
"\r\n"
|
|
"[[hooks.SessionStart.hooks]]\r\n"
|
|
"type = \"command\"\r\n"
|
|
"command = \"'C:\\\\Users\\\\Example\\\\AppData\\\\Local\\\\Programs\\\\"
|
|
"codebase-memory-mcp\\\\codebase-memory-mcp.exe' hook-augment\"\r\n"
|
|
"command_windows = \"& 'C:\\\\Users\\\\Example\\\\AppData\\\\Local\\\\Programs\\\\"
|
|
"codebase-memory-mcp\\\\codebase-memory-mcp.exe' hook-augment\"\r\n"
|
|
"timeout = 5\r\n"
|
|
"\r\n"
|
|
"[[hooks.SubagentStart]]\r\n"
|
|
"matcher = \"*\"\r\n"
|
|
"\r\n"
|
|
"[[hooks.SubagentStart.hooks]]\r\n"
|
|
"type = \"command\"\r\n"
|
|
"command = \"'C:\\\\Users\\\\Example\\\\AppData\\\\Local\\\\Programs\\\\"
|
|
"codebase-memory-mcp\\\\codebase-memory-mcp.exe' hook-augment\"\r\n"
|
|
"command_windows = \"& 'C:\\\\Users\\\\Example\\\\AppData\\\\Local\\\\Programs\\\\"
|
|
"codebase-memory-mcp\\\\codebase-memory-mcp.exe' hook-augment\"\r\n"
|
|
"timeout = 5\r\n"
|
|
"# <<< codebase-memory-mcp SessionStart <<<\r\n";
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char before[CTE_FILE_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
char invalid[2][CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, original), 0);
|
|
ASSERT_EQ(cte_read(path, before, sizeof(before)), 0);
|
|
|
|
cbm_toml_codex_hook_failure_t failure = CBM_TOML_CODEX_HOOK_FAILURE_INVALID_ARGUMENT;
|
|
int reconcile_rc = cte_codex_edit_commands_detailed(path, command, command_windows,
|
|
CBM_TOML_CODEX_HOOK_UPSERT, 1, &failure);
|
|
ASSERT_EQ(failure, CBM_TOML_CODEX_HOOK_FAILURE_NONE);
|
|
ASSERT_EQ(reconcile_rc, 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, before);
|
|
|
|
ASSERT_EQ(cte_codex_edit_commands_detailed(path, command, command_windows,
|
|
CBM_TOML_CODEX_HOOK_UPSERT, 0, &failure),
|
|
0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, before);
|
|
ASSERT_EQ(cte_occurrences(actual, CTE_CODEX_BEGIN), 1);
|
|
ASSERT_EQ(cte_occurrences(actual, "[[hooks.SessionStart]]"), 1);
|
|
ASSERT_EQ(cte_occurrences(actual, "[[hooks.SubagentStart]]"), 1);
|
|
ASSERT_NOT_NULL(strstr(actual, "[mcp_servers.other]"));
|
|
ASSERT_NOT_NULL(strstr(actual, "keep = true"));
|
|
for (const char *cursor = actual; *cursor; ++cursor) {
|
|
if (*cursor == '\n') {
|
|
ASSERT(cursor > actual && cursor[-1] == '\r');
|
|
}
|
|
}
|
|
|
|
ASSERT_EQ(cte_codex_edit_commands_detailed(path, command, command_windows,
|
|
CBM_TOML_CODEX_HOOK_UPSERT, 0, &failure),
|
|
0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, before);
|
|
|
|
ASSERT_EQ(cte_codex_edit_commands_detailed(path, command, command_windows,
|
|
CBM_TOML_CODEX_HOOK_REMOVE, 0, &failure),
|
|
0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_NOT_NULL(strstr(actual, "[mcp_servers.other]"));
|
|
ASSERT_NOT_NULL(strstr(actual, "keep = true"));
|
|
ASSERT_NULL(strstr(actual, CTE_CODEX_BEGIN));
|
|
ASSERT_NULL(strstr(actual, "hook-augment"));
|
|
|
|
static const char *owned_line =
|
|
"command = \"'C:\\\\Users\\\\Example\\\\AppData\\\\Local\\\\Programs\\\\"
|
|
"codebase-memory-mcp\\\\codebase-memory-mcp.exe' hook-augment\"\r\n";
|
|
ASSERT_EQ(cte_replace_once(original, owned_line, "command = \"foreign\"\r\n", invalid[0],
|
|
sizeof(invalid[0])),
|
|
0);
|
|
ASSERT_EQ(cte_replace_once(original, "type = \"command\"\r\n",
|
|
"type = \"command\"\r\nenabled = true\r\n", invalid[1],
|
|
sizeof(invalid[1])),
|
|
0);
|
|
for (size_t i = 0U; i < sizeof(invalid) / sizeof(invalid[0]); ++i) {
|
|
ASSERT_EQ(th_write_file(path, invalid[i]), 0);
|
|
failure = CBM_TOML_CODEX_HOOK_FAILURE_NONE;
|
|
ASSERT_EQ(cte_codex_edit_commands_detailed(path, command, command_windows,
|
|
CBM_TOML_CODEX_HOOK_UPSERT, 1, &failure),
|
|
-1);
|
|
ASSERT_EQ(failure, CBM_TOML_CODEX_HOOK_FAILURE_AMBIGUOUS_OWNERSHIP);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, invalid[i]);
|
|
ASSERT_EQ(cte_codex_edit_commands_detailed(path, command, command_windows,
|
|
CBM_TOML_CODEX_HOOK_UPSERT, 0, &failure),
|
|
-1);
|
|
ASSERT_EQ(failure, CBM_TOML_CODEX_HOOK_FAILURE_AMBIGUOUS_OWNERSHIP);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, invalid[i]);
|
|
}
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_codex_rejects_ambiguous_inline_byte_identically) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
static const char *invalid[] = {
|
|
("[hooks]\nSessionStart = [{matcher='startup|resume|clear|compact',hooks=[{type="
|
|
"'command',command='codebase-memory-mcp hook-augment',command_windows="
|
|
"'codebase-memory-mcp hook-augment',timeout=5},{type='command',command='foreign'}]}]\n"),
|
|
("[hooks]\nSessionStart = [{matcher='startup|resume|clear|compact',hooks=[{type="
|
|
"'command',command='codebase-memory-mcp hook-augment',command_windows="
|
|
"'codebase-memory-mcp hook-augment',timeout=5,owner='user'}]}]\n"),
|
|
"[hooks]\nSessionStart=[]\nSessionStart=[]\n",
|
|
"[hooks]\nSessionStart = [\n { matcher = 'startup|resume|clear|compact' }\n]\n",
|
|
("[hooks]\nSessionStart = [{matcher='startup|resume|clear|compact',hooks=[{type="
|
|
"'command',command='echo \"Code discovery: prefer codebase-memory-mcp\"'}]}] # keep\n"),
|
|
"[hooks]\nSessionStart = [{ matcher = 'startup|resume|clear|compact'\n",
|
|
};
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
for (size_t i = 0U; i < sizeof(invalid) / sizeof(invalid[0]); ++i) {
|
|
ASSERT_EQ(th_write_file(path, invalid[i]), 0);
|
|
ASSERT_EQ(cte_codex_edit(path, CBM_TOML_CODEX_HOOK_UPSERT, 1), -1);
|
|
ASSERT_EQ(cte_codex_edit(path, CBM_TOML_CODEX_HOOK_UPSERT, 0), -1);
|
|
ASSERT_EQ(cte_codex_edit(path, CBM_TOML_CODEX_HOOK_REMOVE, 0), -1);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, invalid[i]);
|
|
}
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_codex_reports_stable_failure_reasons) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
static const struct {
|
|
const char *content;
|
|
cbm_toml_codex_hook_failure_t expected;
|
|
const char *name;
|
|
} cases[] = {
|
|
{
|
|
.content =
|
|
"[hooks]\nSessionStart = [{ matcher = 'startup|resume|clear|compact', hooks = ["
|
|
"{ type = 'command', command = 'codebase-memory-mcp hook-augment' }, "
|
|
"{ type = 'command', command = 'foreign' }] }]\n",
|
|
.expected = CBM_TOML_CODEX_HOOK_FAILURE_AMBIGUOUS_OWNERSHIP,
|
|
.name = "ambiguous_hook_ownership",
|
|
},
|
|
{
|
|
.content =
|
|
"[hooks]\n"
|
|
"SessionStart = [{ matcher = 'startup|resume|clear|compact', hooks = [{ type = "
|
|
"'command', command = 'echo \"Code discovery: prefer codebase-memory-mcp\"' }] "
|
|
"}]\n"
|
|
"SessionStart = [{ matcher = 'startup|resume|clear|compact', hooks = [{ type = "
|
|
"'command', command = 'echo \"Code discovery: prefer codebase-memory-mcp\"' }] "
|
|
"}]\n",
|
|
.expected = CBM_TOML_CODEX_HOOK_FAILURE_CONFLICTING_HOOKS,
|
|
.name = "conflicting_hook_representations",
|
|
},
|
|
{
|
|
.content = "[hooks\n",
|
|
.expected = CBM_TOML_CODEX_HOOK_FAILURE_MALFORMED_CONFIG,
|
|
.name = "malformed_config",
|
|
},
|
|
};
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
for (size_t i = 0U; i < sizeof(cases) / sizeof(cases[0]); ++i) {
|
|
cbm_toml_codex_hook_failure_t failure = CBM_TOML_CODEX_HOOK_FAILURE_NONE;
|
|
ASSERT_EQ(th_write_file(path, cases[i].content), 0);
|
|
ASSERT_EQ(cte_codex_edit_detailed(path, CBM_TOML_CODEX_HOOK_UPSERT, 1, &failure), -1);
|
|
ASSERT_EQ(failure, cases[i].expected);
|
|
ASSERT_STR_EQ(cbm_toml_codex_hook_failure_name(failure), cases[i].name);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, cases[i].content);
|
|
}
|
|
|
|
cbm_toml_codex_hook_failure_t failure = CBM_TOML_CODEX_HOOK_FAILURE_INVALID_ARGUMENT;
|
|
ASSERT_EQ(th_write_file(path, "keep = true\n"), 0);
|
|
ASSERT_EQ(cte_codex_edit_detailed(path, CBM_TOML_CODEX_HOOK_UPSERT, 1, &failure), 0);
|
|
ASSERT_EQ(failure, CBM_TOML_CODEX_HOOK_FAILURE_NONE);
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_toml_codex_preserves_bom_crlf_and_foreign_aot) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
const char *foreign = "\xEF\xBB\xBF[[hooks.SessionStart]]\r\nmatcher = \"custom\"\r\n"
|
|
"[[hooks.SessionStart.hooks]]\r\ntype = \"command\"\r\n"
|
|
"command = \"foreign\"\r\ntimeout = 9\r\n";
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, foreign), 0);
|
|
ASSERT_EQ(cte_codex_edit(path, CBM_TOML_CODEX_HOOK_UPSERT, 0), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_EQ((unsigned char)actual[0], 0xEFU);
|
|
ASSERT_NOT_NULL(strstr(actual, "command = \"foreign\""));
|
|
ASSERT_NOT_NULL(strstr(actual, "\r\n# >>> codebase-memory-mcp SessionStart >>>\r\n"));
|
|
for (const char *cursor = actual; *cursor; ++cursor) {
|
|
if (*cursor == '\n') {
|
|
ASSERT(cursor > actual && cursor[-1] == '\r');
|
|
}
|
|
}
|
|
ASSERT_EQ(cte_codex_edit(path, CBM_TOML_CODEX_HOOK_REMOVE, 0), 0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_NOT_NULL(strstr(actual, "command = \"foreign\""));
|
|
ASSERT_NULL(strstr(actual, CTE_CODEX_COMMAND));
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
/* #1558: a duplicated install left a Codex config.toml carrying a CLOSING
|
|
* managed marker with no opener. Every later install then failed that client
|
|
* with op=legacy_hook_cleanup and aborted the whole activation.
|
|
*
|
|
* We are the only writer of these markers, so the imbalance was our own
|
|
* residue. Refusing to touch a file we cannot parse is the right default in
|
|
* general; it is the wrong default for a mess we made. Removal now strips the
|
|
* stray line — its bounds are known exactly, so nothing is guessed. */
|
|
TEST(config_toml_remove_self_heals_orphan_closing_marker_issue1558) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, "user_key = true\n"
|
|
"# <<< codebase-memory-mcp SessionStart <<<\n"
|
|
"other_key = 1\n"),
|
|
0);
|
|
ASSERT_EQ(cbm_toml_remove_managed_block(path, "# >>> codebase-memory-mcp SessionStart >>>",
|
|
"# <<< codebase-memory-mcp SessionStart <<<"),
|
|
0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
/* The stray marker is gone; the user's own content is untouched. */
|
|
ASSERT_STR_EQ(actual, "user_key = true\nother_key = 1\n");
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
/* The mirror case: an opening marker with no closer heals the same way. */
|
|
TEST(config_toml_remove_self_heals_orphan_opening_marker_issue1558) {
|
|
char dir[CTE_PATH_CAP];
|
|
char path[CTE_PATH_CAP];
|
|
char actual[CTE_FILE_CAP];
|
|
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
|
|
ASSERT_EQ(th_write_file(path, "# >>> codebase-memory-mcp SessionStart >>>\n"
|
|
"keep = true\n"),
|
|
0);
|
|
ASSERT_EQ(cbm_toml_remove_managed_block(path, "# >>> codebase-memory-mcp SessionStart >>>",
|
|
"# <<< codebase-memory-mcp SessionStart <<<"),
|
|
0);
|
|
ASSERT_EQ(cte_read(path, actual, sizeof(actual)), 0);
|
|
ASSERT_STR_EQ(actual, "keep = true\n");
|
|
th_cleanup(dir);
|
|
PASS();
|
|
}
|
|
|
|
SUITE(config_toml_edit) {
|
|
RUN_TEST(config_toml_remove_self_heals_orphan_closing_marker_issue1558);
|
|
RUN_TEST(config_toml_remove_self_heals_orphan_opening_marker_issue1558);
|
|
RUN_TEST(config_toml_rejects_stale_content_and_identity);
|
|
RUN_TEST(config_toml_missing_target_race_does_not_replace_winner);
|
|
RUN_TEST(config_toml_existing_target_swap_after_check_preserves_winner);
|
|
RUN_TEST(config_toml_rejects_non_regular_path);
|
|
#ifndef _WIN32
|
|
RUN_TEST(config_toml_rejects_symlink_hardlink_and_preserves_metadata);
|
|
RUN_TEST(config_toml_follows_user_owned_symlink_in_place);
|
|
#endif
|
|
RUN_TEST(config_toml_managed_markers_ignore_multiline_strings);
|
|
RUN_TEST(config_toml_managed_rejects_marker_in_block_and_unclosed_multiline);
|
|
RUN_TEST(config_toml_codex_semantic_conflicts_fail_closed);
|
|
RUN_TEST(config_toml_vibe_body_validation_fail_closed);
|
|
RUN_TEST(config_toml_vibe_non_array_and_dotted_conflicts_fail_closed);
|
|
RUN_TEST(config_toml_vibe_remove_includes_descendant_tables);
|
|
RUN_TEST(config_toml_vibe_reinstall_preserves_user_fields_and_descendants);
|
|
RUN_TEST(config_toml_preserves_bom_crlf_and_handles_no_final_newline);
|
|
RUN_TEST(config_toml_rejects_oversized_input_file);
|
|
RUN_TEST(config_toml_escape_windows_path_quotes);
|
|
RUN_TEST(config_toml_escape_newlines_controls);
|
|
RUN_TEST(config_toml_managed_fresh_missing_file);
|
|
RUN_TEST(config_toml_managed_replace_preserves_unrelated);
|
|
RUN_TEST(config_toml_managed_remove);
|
|
RUN_TEST(config_toml_managed_idempotent);
|
|
RUN_TEST(config_toml_managed_unbalanced_duplicate_fail_closed);
|
|
RUN_TEST(config_toml_vibe_insert_among_other_tables);
|
|
RUN_TEST(config_toml_vibe_replace_target_preserves_comments_tables);
|
|
RUN_TEST(config_toml_vibe_owned_table_installs_idempotently_and_removes_exact_state);
|
|
RUN_TEST(config_toml_vibe_owned_table_preserves_foreign_same_name_state);
|
|
RUN_TEST(config_toml_vibe_remove_first_target);
|
|
RUN_TEST(config_toml_vibe_remove_middle_target);
|
|
RUN_TEST(config_toml_vibe_remove_last_target);
|
|
RUN_TEST(config_toml_vibe_remove_only_target);
|
|
RUN_TEST(config_toml_vibe_literal_and_basic_identity);
|
|
RUN_TEST(config_toml_vibe_duplicate_target_fail_closed);
|
|
RUN_TEST(config_toml_vibe_ambiguous_target_fail_closed);
|
|
RUN_TEST(config_toml_target_table_rejects_significant_nonassignments_byte_identically);
|
|
RUN_TEST(config_toml_codex_reconciles_minimal_owned_forms);
|
|
RUN_TEST(config_toml_codex_accepts_v0102_managed_windows_crlf);
|
|
RUN_TEST(config_toml_codex_rejects_ambiguous_inline_byte_identically);
|
|
RUN_TEST(config_toml_codex_reports_stable_failure_reasons);
|
|
RUN_TEST(config_toml_codex_preserves_bom_crlf_and_foreign_aot);
|
|
RUN_TEST(config_toml_legacy_remove_reports_foreign_table_without_mutation);
|
|
}
|