d0351fd99a
Signed-off-by: astandrik <astandrik@yandex-team.ru>
1450 lines
63 KiB
C
1450 lines
63 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"
|
|
#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);
|
|
ASSERT_EQ(cbm_toml_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned = true\n"), -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
|
|
|
|
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);
|
|
#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);
|
|
}
|