Files
Martin Vogel aa2366811b
DCO / dco (push) Has been cancelled
fix: eliminate installer filesystem races
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-14 21:13:52 +02:00

684 lines
28 KiB
C

/*
* test_config_text_edit.c — Hardened managed-text editor contracts.
*/
#include "../src/cli/config_text_edit.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);
ASSERT_EQ(cbm_text_write_owned_document(path, "owned\n"), -1);
ASSERT_EQ(cbm_text_migrate_owned_document_mode(path, "owned\n", NULL, 0U, 0755U), -1);
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();
}
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();
}
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_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_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);
}