Files
codebase-memory-mcp/tests/test_config_text_edit.c
T
Cristian Castillo 0fd8180699 fix(cli): leave a user-owned OpenCode module untouched instead of appending the block
The generated OpenCode adapter is an auto-loaded module, so it is installed as a
managed block rather than a whole-file write. When the file on disk carried
content but none of our markers, the upsert appended the block anyway, even
though the block's own header tells readers to "remove the markers to take
ownership".

The appended prelude redeclares module-level names the user's own module also
declares -- `import { spawn }`, `const BIN`, `function augment` -- and both
sides declare a default export once the block itself carries one, so the result
cannot be parsed at all:

    SyntaxError: Identifier 'spawn' has already been declared

The client then drops the whole extension with only a load warning to show for
it. A hand-port around #2204 is therefore not a durable end state.

Probe block ownership before writing. A marker-less file that carries content is
left alone and reported in both the real run and the dry run, instead of being
turned into an unparseable hybrid. An absent, empty, whitespace-only, or
already-ours file still installs or refreshes exactly as before.

Refs #2204

Signed-off-by: Cristian Castillo <c.a.c.m997@gmail.com>
2026-10-14 11:16:00 -05:00

793 lines
33 KiB
C

/*
* test_config_text_edit.c — Hardened managed-text editor contracts.
*/
#include "../src/cli/config_text_edit.h"
#define CBM_CONFIG_EDIT_PATH_ENABLE_TEST_API 1
#include "../src/cli/config_edit_path.h"
#include "test_framework.h"
#include "test_helpers.h"
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#ifndef _WIN32
#include <fcntl.h>
#include <unistd.h>
#endif
#define CTE_BEGIN "<!-- codebase-memory-mcp:start -->"
#define CTE_END "<!-- codebase-memory-mcp:end -->"
#define CTE_LIMIT (16U * 1024U * 1024U)
#define CTE_PATH_CAP 1024U
/* Expected test/public contracts for the post-verification race fix. Keeping
* these declarations here makes this RED test file independent of production
* header edits while the implementation is developed. */
void cbm_text_set_prepublish_hook_for_testing(cbm_text_precommit_test_hook_t hook, void *context);
void cbm_text_set_temp_closed_hook_for_testing(cbm_text_precommit_test_hook_t hook, void *context);
int cbm_text_write_owned_document_if_unchanged(const char *file_path, const char *owned_content,
const char *expected_content,
size_t expected_length);
static int cte_fixture(char *dir, size_t dir_size, char *path, size_t path_size) {
char *created = th_mktempdir("cbm_text_edit");
if (!created) {
return -1;
}
int dir_count = snprintf(dir, dir_size, "%s", created);
int path_count = snprintf(path, path_size, "%s/AGENTS.md", created);
if (dir_count < 0 || (size_t)dir_count >= dir_size || path_count < 0 ||
(size_t)path_count >= path_size) {
th_cleanup(created);
return -1;
}
return 0;
}
static int cte_write_bytes(const char *path, const char *data, size_t len) {
FILE *file = cbm_fopen(path, "wb");
if (!file) {
return -1;
}
int result = len == 0U || fwrite(data, 1U, len, file) == len ? 0 : -1;
if (fclose(file) != 0) {
result = -1;
}
return result;
}
static char *cte_read_bytes(const char *path, size_t *len_out) {
FILE *file = cbm_fopen(path, "rb");
if (!file || fseek(file, 0L, SEEK_END) != 0) {
if (file) {
fclose(file);
}
return NULL;
}
long raw_len = ftell(file);
if (raw_len < 0 || fseek(file, 0L, SEEK_SET) != 0) {
fclose(file);
return NULL;
}
size_t len = (size_t)raw_len;
char *data = (char *)malloc(len + 1U);
if (!data) {
fclose(file);
return NULL;
}
size_t count = len == 0U ? 0U : fread(data, 1U, len, file);
int failed = ferror(file) || fclose(file) != 0 || count != len;
if (failed) {
free(data);
return NULL;
}
data[len] = '\0';
*len_out = len;
return data;
}
static int cte_assert_bytes(const char *path, const char *expected, size_t expected_len) {
size_t actual_len = 0U;
char *actual = cte_read_bytes(path, &actual_len);
int matches = actual && actual_len == expected_len &&
(actual_len == 0U || memcmp(actual, expected, actual_len) == 0);
free(actual);
return matches;
}
static int cte_path_exists(const char *path) {
struct stat state;
return stat(path, &state) == 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 = 0U;
cbm_dirent_t *entry = NULL;
while ((entry = cbm_readdir(directory)) != NULL) {
if (strstr(entry->name, ".cbm-text-") != NULL) {
count++;
}
}
cbm_closedir(directory);
return count;
}
typedef struct {
const char *content;
const char *backup_path;
int replace_identity;
int result;
} cte_precommit_change_t;
static void cte_change_before_commit(const char *path, void *context) {
cte_precommit_change_t *change = (cte_precommit_change_t *)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);
}
#ifndef _WIN32
static void cte_replace_closed_temp(const char *path, void *context) {
cte_precommit_change_t *change = (cte_precommit_change_t *)context;
if (!change->backup_path || cbm_rename_replace(path, change->backup_path) != 0) {
change->result = -1;
return;
}
change->result = th_write_file(path, change->content);
if (change->result == 0 && chmod(path, 0644) != 0) {
change->result = -1;
}
}
#endif
TEST(config_text_managed_insert_preserves_bom_comments_and_is_idempotent) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
const char original[] = "\xEF\xBB\xBF# User heading\n<!-- arbitrary comment -->\n";
const char expected[] = "\xEF\xBB\xBF# User heading\n<!-- arbitrary comment -->\n" CTE_BEGIN
"\nUse search_graph first.\n" CTE_END "\n";
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT_EQ(cte_write_bytes(path, original, sizeof(original) - 1U), 0);
ASSERT_EQ(cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "Use search_graph first.\n"),
0);
ASSERT(cte_assert_bytes(path, expected, sizeof(expected) - 1U));
ASSERT_EQ(cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "Use search_graph first.\n"),
0);
ASSERT(cte_assert_bytes(path, expected, sizeof(expected) - 1U));
ASSERT_EQ(cte_temp_count(dir), 0U);
th_cleanup(dir);
PASS();
}
TEST(config_text_managed_replace_preserves_crlf_surroundings) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
const char original[] = "before\r\n" CTE_BEGIN "\r\nold\r\n" CTE_END "\r\nafter\r\n";
const char expected[] =
"before\r\n" CTE_BEGIN "\r\nnew one\r\nnew two\r\n" CTE_END "\r\nafter\r\n";
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT_EQ(cte_write_bytes(path, original, sizeof(original) - 1U), 0);
ASSERT_EQ(cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "new one\nnew two"), 0);
ASSERT(cte_assert_bytes(path, expected, sizeof(expected) - 1U));
th_cleanup(dir);
PASS();
}
TEST(config_text_managed_no_final_newline_round_trip) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
const char original[] = "user text";
const char installed[] = "user text\n" CTE_BEGIN "\nowned\n" CTE_END;
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT_EQ(cte_write_bytes(path, original, sizeof(original) - 1U), 0);
ASSERT_EQ(cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned"), 0);
ASSERT(cte_assert_bytes(path, installed, sizeof(installed) - 1U));
ASSERT_EQ(cbm_text_remove_managed_block(path, CTE_BEGIN, CTE_END), 0);
ASSERT(cte_assert_bytes(path, original, sizeof(original) - 1U));
th_cleanup(dir);
PASS();
}
TEST(config_text_managed_remove_preserves_user_bytes) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
const char original[] = "alpha\n" CTE_BEGIN "\nowned\n" CTE_END "\nomega\n";
const char expected[] = "alpha\nomega\n";
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT_EQ(cte_write_bytes(path, original, sizeof(original) - 1U), 0);
ASSERT_EQ(cbm_text_remove_managed_block(path, CTE_BEGIN, CTE_END), 0);
ASSERT(cte_assert_bytes(path, expected, sizeof(expected) - 1U));
ASSERT_EQ(cbm_text_remove_managed_block(path, CTE_BEGIN, CTE_END), 0);
ASSERT(cte_assert_bytes(path, expected, sizeof(expected) - 1U));
th_cleanup(dir);
PASS();
}
TEST(config_text_managed_malformed_markers_fail_closed) {
static const char *cases[] = {
CTE_BEGIN "\nowned\n",
CTE_END "\n",
CTE_BEGIN "\n" CTE_BEGIN "\n" CTE_END "\n" CTE_END "\n",
CTE_BEGIN "\n" CTE_END "\n" CTE_BEGIN "\n" CTE_END "\n",
"prefix " CTE_BEGIN " suffix\n",
CTE_BEGIN "\ninside " CTE_END " text\n" CTE_END "\n",
};
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
for (size_t i = 0U; i < sizeof(cases) / sizeof(cases[0]); i++) {
size_t len = strlen(cases[i]);
ASSERT_EQ(cte_write_bytes(path, cases[i], len), 0);
ASSERT_EQ(cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "new"), -1);
ASSERT(cte_assert_bytes(path, cases[i], len));
ASSERT_EQ(cbm_text_remove_managed_block(path, CTE_BEGIN, CTE_END), -1);
ASSERT(cte_assert_bytes(path, cases[i], len));
}
th_cleanup(dir);
PASS();
}
TEST(config_text_managed_rejects_unsafe_markers_and_owned_content) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT_EQ(th_write_file(path, "keep\n"), 0);
ASSERT_EQ(cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "nested " CTE_BEGIN), -1);
ASSERT_EQ(cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "nested " CTE_END), -1);
ASSERT_EQ(cbm_text_upsert_managed_block(path, "same", "same", "owned"), -1);
ASSERT_EQ(cbm_text_upsert_managed_block(path, "bad\nmarker", CTE_END, "owned"), -1);
ASSERT_EQ(cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "bad\x01text"), -1);
ASSERT(cte_assert_bytes(path, "keep\n", strlen("keep\n")));
th_cleanup(dir);
PASS();
}
TEST(config_text_owned_document_write_remove_exact_only) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
const char owned[] = "# Managed\n\nUse the graph.\n";
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT_EQ(cbm_text_write_owned_document(path, owned), 0);
ASSERT_EQ(cbm_text_create_owned_document(path, owned), -1);
ASSERT_EQ(cbm_text_ensure_owned_document(path, owned), 0);
ASSERT(cte_assert_bytes(path, owned, sizeof(owned) - 1U));
ASSERT_EQ(cbm_text_write_owned_document(path, owned), 0);
ASSERT_EQ(th_write_file(path, "# User changed this\n"), 0);
ASSERT_EQ(cbm_text_ensure_owned_document(path, owned), -1);
ASSERT_EQ(cbm_text_remove_owned_document(path, owned), 1);
ASSERT(cte_assert_bytes(path, "# User changed this\n", strlen("# User changed this\n")));
ASSERT_EQ(cbm_text_write_owned_document(path, owned), 0);
ASSERT_EQ(cbm_text_remove_owned_document(path, owned), 0);
ASSERT(!cte_path_exists(path));
ASSERT_EQ(cbm_text_ensure_owned_document(path, owned), 0);
ASSERT(cte_assert_bytes(path, owned, sizeof(owned) - 1U));
ASSERT_EQ(cbm_text_remove_owned_document(path, owned), 0);
ASSERT_EQ(cbm_text_create_owned_document(path, owned), 0);
ASSERT(cte_assert_bytes(path, owned, sizeof(owned) - 1U));
ASSERT_EQ(cbm_text_remove_owned_document(path, owned), 0);
ASSERT_EQ(cbm_text_remove_owned_document(path, owned), 0);
th_cleanup(dir);
PASS();
}
TEST(config_text_owned_document_migrates_exact_releases_only) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
const char *current = "name: codebase-memory\ntier: verify\n";
const char *released[] = {"name: codebase-memory\n", "name: codebase-memory\nlegacy: 2\n"};
const char *modified = "name: codebase-memory\nuser-note: keep\n";
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT_EQ(cbm_text_migrate_owned_document(path, current, released, 2U), 0);
ASSERT(cte_assert_bytes(path, current, strlen(current)));
ASSERT_EQ(th_write_file(path, released[1]), 0);
ASSERT_EQ(cbm_text_migrate_owned_document(path, current, released, 2U), 0);
ASSERT(cte_assert_bytes(path, current, strlen(current)));
ASSERT_EQ(th_write_file(path, modified), 0);
ASSERT_EQ(cbm_text_migrate_owned_document(path, current, released, 2U), 1);
ASSERT(cte_assert_bytes(path, modified, strlen(modified)));
ASSERT_EQ(cbm_text_migrate_owned_document_mode(path, current, released, 2U, 04755U), -1);
ASSERT(cte_assert_bytes(path, modified, strlen(modified)));
ASSERT_EQ(cbm_text_migrate_owned_document_mode(path, current, released, 2U, 0200U), -1);
ASSERT(cte_assert_bytes(path, modified, strlen(modified)));
ASSERT_EQ(cbm_text_remove_owned_document_any(path, current, released, 2U), 1);
ASSERT(cte_assert_bytes(path, modified, strlen(modified)));
ASSERT_EQ(th_write_file(path, released[0]), 0);
ASSERT_EQ(cbm_text_remove_owned_document_any(path, current, released, 2U), 0);
ASSERT_FALSE(cte_path_exists(path));
th_cleanup(dir);
PASS();
}
TEST(config_text_rejects_invalid_utf8_and_controls) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
const char invalid_utf8[] = {'k', 'e', 'e', 'p', (char)0xC0, (char)0xAF, '\n'};
const char c1_control[] = {'k', 'e', 'e', 'p', (char)0xC2, (char)0x85, '\n'};
const char nul_byte[] = {'k', 'e', 'e', 'p', '\0', 'x', '\n'};
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT_EQ(cte_write_bytes(path, invalid_utf8, sizeof(invalid_utf8)), 0);
ASSERT_EQ(cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned"), -1);
ASSERT(cte_assert_bytes(path, invalid_utf8, sizeof(invalid_utf8)));
ASSERT_EQ(cte_write_bytes(path, c1_control, sizeof(c1_control)), 0);
ASSERT_EQ(cbm_text_write_owned_document(path, "replacement\n"), -1);
ASSERT(cte_assert_bytes(path, c1_control, sizeof(c1_control)));
ASSERT_EQ(cte_write_bytes(path, nul_byte, sizeof(nul_byte)), 0);
ASSERT_EQ(cbm_text_remove_managed_block(path, CTE_BEGIN, CTE_END), -1);
ASSERT(cte_assert_bytes(path, nul_byte, sizeof(nul_byte)));
ASSERT_EQ(cbm_text_write_owned_document(path, "bad\x7ftext"), -1);
th_cleanup(dir);
PASS();
}
TEST(config_text_rejects_oversized_existing_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, 'a', sizeof(chunk));
size_t remaining = CTE_LIMIT + 1U;
while (remaining != 0U) {
size_t amount = remaining < sizeof(chunk) ? remaining : sizeof(chunk);
ASSERT_EQ(fwrite(chunk, 1U, amount, file), amount);
remaining -= amount;
}
ASSERT_EQ(fclose(file), 0);
ASSERT_EQ(cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned"), -1);
struct stat state;
ASSERT_EQ(stat(path, &state), 0);
ASSERT_EQ((uint64_t)state.st_size, (uint64_t)CTE_LIMIT + 1U);
th_cleanup(dir);
PASS();
}
TEST(config_text_rejects_non_regular_paths) {
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_text_write_owned_document(path, "owned\n"), -1);
ASSERT_EQ(cbm_rmdir(path), 0);
#ifndef _WIN32
ASSERT_EQ(mkfifo(path, 0600), 0);
ASSERT_EQ(cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned"), -1);
ASSERT_EQ(cbm_unlink(path), 0);
#endif
ASSERT_EQ(cte_temp_count(dir), 0U);
th_cleanup(dir);
PASS();
}
#ifndef _WIN32
TEST(config_text_rejects_links_privileged_mode_and_preserves_metadata) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
char target[CTE_PATH_CAP];
char alias[CTE_PATH_CAP];
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT(snprintf(target, sizeof(target), "%s/target.md", dir) > 0);
ASSERT(snprintf(alias, sizeof(alias), "%s/alias.md", dir) > 0);
ASSERT_EQ(th_write_file(target, "target\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_write_rc = cbm_text_write_owned_document(path, "owned\n");
int foreign_migrate_rc = cbm_text_migrate_owned_document_mode(path, "owned\n", NULL, 0U, 0755U);
cbm_config_edit_path_set_invoking_uid_for_test(0U, 0);
cbm_config_edit_path_follow_clear();
ASSERT_EQ(foreign_write_rc, -1);
ASSERT_EQ(foreign_migrate_rc, -1);
ASSERT(cte_assert_bytes(target, "target\n", strlen("target\n")));
ASSERT_EQ(cbm_unlink(path), 0);
ASSERT_EQ(th_write_file(path, "shared\n"), 0);
ASSERT_EQ(link(path, alias), 0);
ASSERT_EQ(cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned"), -1);
ASSERT_EQ(cbm_text_migrate_owned_document_mode(path, "owned\n", NULL, 0U, 0755U), -1);
ASSERT_EQ(cbm_unlink(alias), 0);
ASSERT_EQ(chmod(path, 04755), 0);
struct stat privileged;
ASSERT_EQ(stat(path, &privileged), 0);
if ((privileged.st_mode & S_ISUID) != 0) {
ASSERT_EQ(cbm_text_write_owned_document(path, "owned\n"), -1);
}
ASSERT_EQ(chmod(path, 0640), 0);
struct stat before;
struct stat after;
ASSERT_EQ(stat(path, &before), 0);
ASSERT_EQ(cbm_text_write_owned_document(path, "owned\n"), 0);
ASSERT_EQ(stat(path, &after), 0);
ASSERT_EQ(after.st_mode & 0777U, before.st_mode & 0777U);
ASSERT_EQ(after.st_uid, before.st_uid);
ASSERT_EQ(after.st_gid, before.st_gid);
ASSERT_EQ(cte_temp_count(dir), 0U);
th_cleanup(dir);
PASS();
}
/* 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_text_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];
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT(snprintf(target, sizeof(target), "%s/target.md", dir) > 0);
ASSERT(snprintf(control, sizeof(control), "%s/control.md", dir) > 0);
ASSERT_EQ(th_write_file(target, "# user notes\n"), 0);
ASSERT_EQ(th_write_file(control, "# user notes\n"), 0);
ASSERT_EQ(symlink("target.md", path), 0);
ASSERT_EQ(cbm_config_edit_path_follow_add_root(dir), 0);
int through_rc = cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned");
int plain_rc = cbm_text_upsert_managed_block(control, CTE_BEGIN, CTE_END, "owned");
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));
size_t through_len = 0U;
size_t plain_len = 0U;
char *through = cte_read_bytes(target, &through_len);
char *plain = cte_read_bytes(control, &plain_len);
ASSERT_NOT_NULL(through);
ASSERT_NOT_NULL(plain);
ASSERT_EQ(through_len, plain_len);
ASSERT_EQ(memcmp(through, plain, plain_len), 0);
ASSERT_NOT_NULL(strstr(through, "owned"));
free(through);
free(plain);
int through_remove_rc = cbm_text_remove_managed_block(path, CTE_BEGIN, CTE_END);
int plain_remove_rc = cbm_text_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));
through = cte_read_bytes(target, &through_len);
plain = cte_read_bytes(control, &plain_len);
ASSERT_NOT_NULL(through);
ASSERT_NOT_NULL(plain);
ASSERT_EQ(through_len, plain_len);
ASSERT_EQ(memcmp(through, plain, plain_len), 0);
ASSERT_NULL(strstr(through, "owned"));
free(through);
free(plain);
ASSERT_EQ(cte_temp_count(dir), 0U);
th_cleanup(dir);
PASS();
}
TEST(config_text_owned_document_mode_publishes_exact_bytes_atomically) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
const char *current = "#!/bin/sh\necho current\n";
const char *released[] = {"#!/bin/sh\necho released\n"};
const char *foreign = "#!/bin/sh\necho foreign\n";
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT_EQ(cbm_text_migrate_owned_document_mode(path, current, released, 1U, 0755U), 0);
struct stat state;
ASSERT_EQ(stat(path, &state), 0);
ASSERT_EQ(state.st_mode & 0777U, 0755U);
ASSERT_EQ(state.st_uid, geteuid());
ASSERT(cte_assert_bytes(path, current, strlen(current)));
struct stat stable;
ASSERT_EQ(stat(path, &stable), 0);
ASSERT_EQ(cbm_text_migrate_owned_document_mode(path, current, released, 1U, 0755U), 0);
ASSERT_EQ(stat(path, &state), 0);
ASSERT_EQ(state.st_ino, stable.st_ino);
ASSERT_EQ(chmod(path, 0600), 0);
struct stat before;
ASSERT_EQ(stat(path, &before), 0);
ASSERT_EQ(cbm_text_migrate_owned_document_mode(path, current, released, 1U, 0755U), 0);
ASSERT_EQ(stat(path, &state), 0);
ASSERT_EQ(state.st_mode & 0777U, 0755U);
ASSERT(state.st_ino != before.st_ino);
ASSERT(cte_assert_bytes(path, current, strlen(current)));
ASSERT_EQ(th_write_file(path, released[0]), 0);
ASSERT_EQ(chmod(path, 0640), 0);
ASSERT_EQ(cbm_text_migrate_owned_document_mode(path, current, released, 1U, 0755U), 0);
ASSERT_EQ(stat(path, &state), 0);
ASSERT_EQ(state.st_mode & 0777U, 0755U);
ASSERT(cte_assert_bytes(path, current, strlen(current)));
ASSERT_EQ(th_write_file(path, foreign), 0);
ASSERT_EQ(chmod(path, 0640), 0);
ASSERT_EQ(cbm_text_migrate_owned_document_mode(path, current, released, 1U, 0755U), 1);
ASSERT_EQ(stat(path, &state), 0);
ASSERT_EQ(state.st_mode & 0777U, 0640U);
ASSERT(cte_assert_bytes(path, foreign, strlen(foreign)));
ASSERT_EQ(cte_temp_count(dir), 0U);
th_cleanup(dir);
PASS();
}
TEST(config_text_owned_document_mode_rejects_prepublish_replacement) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
char backup[CTE_PATH_CAP];
const char *current = "#!/bin/sh\necho current\n";
const char *winner = "#!/bin/sh\necho winner\n";
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT(snprintf(backup, sizeof(backup), "%s/original.sh", dir) > 0);
ASSERT_EQ(th_write_file(path, current), 0);
ASSERT_EQ(chmod(path, 0600), 0);
cte_precommit_change_t race = {
.content = winner, .backup_path = backup, .replace_identity = 1, .result = -1};
cbm_text_set_prepublish_hook_for_testing(cte_change_before_commit, &race);
int result = cbm_text_migrate_owned_document_mode(path, current, NULL, 0U, 0755U);
cbm_text_set_prepublish_hook_for_testing(NULL, NULL);
ASSERT_EQ(race.result, 0);
ASSERT_EQ(result, -1);
ASSERT(cte_assert_bytes(path, winner, strlen(winner)));
struct stat state;
ASSERT_EQ(stat(path, &state), 0);
ASSERT_EQ(state.st_mode & 0111U, 0U);
ASSERT_EQ(cte_temp_count(dir), 0U);
ASSERT_EQ(cbm_unlink(backup), 0);
th_cleanup(dir);
PASS();
}
TEST(config_text_owned_document_mode_rejects_closed_temp_replacement) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
char backup[CTE_PATH_CAP];
const char *current = "#!/bin/sh\necho current\n";
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT(snprintf(backup, sizeof(backup), "%s/original-temp", dir) > 0);
cte_precommit_change_t race = {
.content = current, .backup_path = backup, .replace_identity = 1, .result = -1};
cbm_text_set_temp_closed_hook_for_testing(cte_replace_closed_temp, &race);
int result = cbm_text_migrate_owned_document_mode(path, current, NULL, 0U, 0755U);
cbm_text_set_temp_closed_hook_for_testing(NULL, NULL);
ASSERT_EQ(race.result, 0);
ASSERT_EQ(result, -1);
ASSERT_FALSE(cte_path_exists(path));
ASSERT(cte_assert_bytes(backup, current, strlen(current)));
struct stat trusted;
ASSERT_EQ(stat(backup, &trusted), 0);
ASSERT_EQ(trusted.st_mode & 0777U, 0755U);
th_cleanup(dir);
PASS();
}
#endif
TEST(config_text_rejects_stale_content_and_identity) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
char backup[CTE_PATH_CAP];
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT(snprintf(backup, sizeof(backup), "%s/original.md", dir) > 0);
ASSERT_EQ(th_write_file(path, "keep\n"), 0);
cte_precommit_change_t content_change = {
.content = "concurrent\n", .backup_path = NULL, .replace_identity = 0, .result = -1};
cbm_text_set_precommit_hook_for_testing(cte_change_before_commit, &content_change);
int result = cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned");
cbm_text_set_precommit_hook_for_testing(NULL, NULL);
ASSERT_EQ(content_change.result, 0);
ASSERT_EQ(result, -1);
ASSERT(cte_assert_bytes(path, "concurrent\n", strlen("concurrent\n")));
ASSERT_EQ(cte_temp_count(dir), 0U);
ASSERT_EQ(th_write_file(path, "keep\n"), 0);
cte_precommit_change_t identity_change = {
.content = "keep\n", .backup_path = backup, .replace_identity = 1, .result = -1};
cbm_text_set_precommit_hook_for_testing(cte_change_before_commit, &identity_change);
result = cbm_text_write_owned_document(path, "owned\n");
cbm_text_set_precommit_hook_for_testing(NULL, NULL);
ASSERT_EQ(identity_change.result, 0);
ASSERT_EQ(result, -1);
ASSERT(cte_assert_bytes(path, "keep\n", strlen("keep\n")));
ASSERT_EQ(cbm_unlink(backup), 0);
ASSERT_EQ(cte_temp_count(dir), 0U);
th_cleanup(dir);
PASS();
}
TEST(config_text_missing_target_race_does_not_replace_winner) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
cte_precommit_change_t race = {
.content = "winner\n", .backup_path = NULL, .replace_identity = 0, .result = -1};
cbm_text_set_precommit_hook_for_testing(cte_change_before_commit, &race);
int result = cbm_text_write_owned_document(path, "owned\n");
cbm_text_set_precommit_hook_for_testing(NULL, NULL);
ASSERT_EQ(race.result, 0);
ASSERT_EQ(result, -1);
ASSERT(cte_assert_bytes(path, "winner\n", strlen("winner\n")));
ASSERT_EQ(cte_temp_count(dir), 0U);
th_cleanup(dir);
PASS();
}
TEST(config_text_exact_remove_rechecks_stale_snapshot) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
const char owned[] = "owned\n";
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT_EQ(th_write_file(path, owned), 0);
cte_precommit_change_t change = {
.content = "winner\n", .backup_path = NULL, .replace_identity = 0, .result = -1};
cbm_text_set_precommit_hook_for_testing(cte_change_before_commit, &change);
int result = cbm_text_remove_owned_document(path, owned);
cbm_text_set_precommit_hook_for_testing(NULL, NULL);
ASSERT_EQ(change.result, 0);
ASSERT_EQ(result, -1);
ASSERT(cte_assert_bytes(path, "winner\n", strlen("winner\n")));
th_cleanup(dir);
PASS();
}
TEST(config_text_existing_target_swap_after_check_preserves_winner) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
char backup[CTE_PATH_CAP];
const char *original = "keep\n";
const char *winner = "winner\n";
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT(snprintf(backup, sizeof(backup), "%s/original.md", dir) > 0);
ASSERT_EQ(th_write_file(path, original), 0);
cte_precommit_change_t race = {
.content = winner, .backup_path = backup, .replace_identity = 1, .result = -1};
cbm_text_set_prepublish_hook_for_testing(cte_change_before_commit, &race);
int result = cbm_text_write_owned_document(path, "owned\n");
cbm_text_set_prepublish_hook_for_testing(NULL, NULL);
ASSERT_EQ(race.result, 0);
ASSERT_EQ(result, -1);
ASSERT(cte_assert_bytes(path, winner, strlen(winner)));
ASSERT_EQ(cte_temp_count(dir), 0U);
ASSERT_EQ(cbm_unlink(backup), 0);
th_cleanup(dir);
PASS();
}
TEST(config_text_exact_remove_swap_after_check_preserves_winner) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
char backup[CTE_PATH_CAP];
const char *owned = "owned\n";
const char *winner = "winner\n";
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT(snprintf(backup, sizeof(backup), "%s/original.md", dir) > 0);
ASSERT_EQ(th_write_file(path, owned), 0);
cte_precommit_change_t race = {
.content = winner, .backup_path = backup, .replace_identity = 1, .result = -1};
cbm_text_set_prepublish_hook_for_testing(cte_change_before_commit, &race);
int result = cbm_text_remove_owned_document(path, owned);
cbm_text_set_prepublish_hook_for_testing(NULL, NULL);
ASSERT_EQ(race.result, 0);
ASSERT_EQ(result, -1);
ASSERT(cte_assert_bytes(path, winner, strlen(winner)));
ASSERT_EQ(cbm_unlink(backup), 0);
th_cleanup(dir);
PASS();
}
TEST(config_text_write_owned_document_if_unchanged_rejects_stale_snapshot) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
const char *original = "name: Continue\n";
const char *updated = "name: Continue\nmcpServers:\n - name: codebase-memory-mcp\n";
const char *winner = "name: Continue\nuser-setting: preserved\n";
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
ASSERT_EQ(th_write_file(path, original), 0);
ASSERT_EQ(cbm_text_write_owned_document_if_unchanged(path, updated, original, strlen(original)),
0);
ASSERT(cte_assert_bytes(path, updated, strlen(updated)));
ASSERT_EQ(th_write_file(path, winner), 0);
ASSERT_EQ(cbm_text_write_owned_document_if_unchanged(path, updated, original, strlen(original)),
-1);
ASSERT(cte_assert_bytes(path, winner, strlen(winner)));
ASSERT_EQ(cbm_text_write_owned_document_if_unchanged(path, updated, NULL, 0U), -1);
ASSERT(cte_assert_bytes(path, winner, strlen(winner)));
ASSERT_EQ(cte_temp_count(dir), 0U);
th_cleanup(dir);
PASS();
}
/* #2204: the generated OpenCode module must not be appended to a file the user
* owns. The probe separates "may be written" (absent, whitespace-only, or
* already carrying our markers) from "the user's file" (content, no markers) so
* the installer can leave it alone instead of producing an unparseable hybrid. */
TEST(config_text_managed_block_status_reports_foreign_module) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
const char whitespace[] = " \n\t\n";
const char foreign[] = "import { spawn } from 'node:child_process';\n"
"export default { id: 'codebase-memory-augment', setup() {} };\n";
const char unbalanced[] = CTE_BEGIN "\nowned\n";
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
/* No file yet, an empty file, and a whitespace-only file are all writable. */
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), 0);
ASSERT_EQ(cte_write_bytes(path, "", 0U), 0);
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), 0);
ASSERT_EQ(cte_write_bytes(path, whitespace, sizeof(whitespace) - 1U), 0);
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), 0);
/* Our own block refreshes in place, so it is writable too. */
ASSERT_EQ(cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned"), 0);
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), 0);
/* A hand-ported module with no markers is the user's: refuse, never append. */
ASSERT_EQ(cte_write_bytes(path, foreign, sizeof(foreign) - 1U), 0);
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), 1);
/* Unbalanced markers and malformed marker arguments are unsafe states. */
ASSERT_EQ(cte_write_bytes(path, unbalanced, sizeof(unbalanced) - 1U), 0);
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), -1);
ASSERT_EQ(cbm_text_managed_block_status(path, "same", "same"), -1);
ASSERT_EQ(cbm_text_managed_block_status(NULL, CTE_BEGIN, CTE_END), -1);
th_cleanup(dir);
PASS();
}
SUITE(config_text_edit) {
RUN_TEST(config_text_managed_insert_preserves_bom_comments_and_is_idempotent);
RUN_TEST(config_text_managed_replace_preserves_crlf_surroundings);
RUN_TEST(config_text_managed_no_final_newline_round_trip);
RUN_TEST(config_text_managed_remove_preserves_user_bytes);
RUN_TEST(config_text_managed_malformed_markers_fail_closed);
RUN_TEST(config_text_managed_rejects_unsafe_markers_and_owned_content);
RUN_TEST(config_text_managed_block_status_reports_foreign_module);
RUN_TEST(config_text_owned_document_write_remove_exact_only);
RUN_TEST(config_text_owned_document_migrates_exact_releases_only);
RUN_TEST(config_text_rejects_invalid_utf8_and_controls);
RUN_TEST(config_text_rejects_oversized_existing_file);
RUN_TEST(config_text_rejects_non_regular_paths);
#ifndef _WIN32
RUN_TEST(config_text_rejects_links_privileged_mode_and_preserves_metadata);
RUN_TEST(config_text_follows_user_owned_symlink_in_place);
RUN_TEST(config_text_owned_document_mode_publishes_exact_bytes_atomically);
RUN_TEST(config_text_owned_document_mode_rejects_prepublish_replacement);
RUN_TEST(config_text_owned_document_mode_rejects_closed_temp_replacement);
#endif
RUN_TEST(config_text_rejects_stale_content_and_identity);
RUN_TEST(config_text_missing_target_race_does_not_replace_winner);
RUN_TEST(config_text_exact_remove_rechecks_stale_snapshot);
RUN_TEST(config_text_existing_target_swap_after_check_preserves_winner);
RUN_TEST(config_text_exact_remove_swap_after_check_preserves_winner);
RUN_TEST(config_text_write_owned_document_if_unchanged_rejects_stale_snapshot);
}