a3903caa0b
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1039 lines
40 KiB
C
1039 lines
40 KiB
C
/*
|
|
* test_config_json_like.c — Structure-preserving JSON/JSONC/JSON5 edits.
|
|
*
|
|
* This suite is intentionally standalone. The installation work that consumes
|
|
* the editor can register it with the main runner alongside its build wiring.
|
|
*/
|
|
#include "test_framework.h"
|
|
|
|
#define CBM_JSON_LIKE_ENABLE_TEST_API 1
|
|
#include "../src/cli/config_json_like.h"
|
|
#include "../src/foundation/compat.h"
|
|
#include "../src/foundation/compat_fs.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
|
|
|
|
typedef struct {
|
|
char directory[512];
|
|
char path[640];
|
|
} jl_fixture_t;
|
|
|
|
static int jl_fixture_open(jl_fixture_t *fixture) {
|
|
snprintf(fixture->directory, sizeof(fixture->directory), "%s/cbm-json-like-XXXXXX",
|
|
cbm_tmpdir());
|
|
if (!cbm_mkdtemp(fixture->directory)) {
|
|
return -1;
|
|
}
|
|
int written =
|
|
snprintf(fixture->path, sizeof(fixture->path), "%s/config.json", fixture->directory);
|
|
return written >= 0 && (size_t)written < sizeof(fixture->path) ? 0 : -1;
|
|
}
|
|
|
|
static void jl_fixture_close(jl_fixture_t *fixture) {
|
|
cbm_dir_t *directory = cbm_opendir(fixture->directory);
|
|
if (directory) {
|
|
cbm_dirent_t *entry = NULL;
|
|
while ((entry = cbm_readdir(directory)) != NULL) {
|
|
if (strncmp(entry->name, "config.json.cbm.tmp.", strlen("config.json.cbm.tmp.")) == 0) {
|
|
char temp_path[sizeof(fixture->path) + 96U];
|
|
int written = snprintf(temp_path, sizeof(temp_path), "%s/%s", fixture->directory,
|
|
entry->name);
|
|
if (written >= 0 && (size_t)written < sizeof(temp_path)) {
|
|
(void)cbm_unlink(temp_path);
|
|
}
|
|
}
|
|
}
|
|
cbm_closedir(directory);
|
|
}
|
|
(void)cbm_unlink(fixture->path);
|
|
(void)cbm_rmdir(fixture->directory);
|
|
}
|
|
|
|
static size_t jl_temp_file_count(const jl_fixture_t *fixture) {
|
|
cbm_dir_t *directory = cbm_opendir(fixture->directory);
|
|
if (!directory) {
|
|
return SIZE_MAX;
|
|
}
|
|
size_t count = 0U;
|
|
cbm_dirent_t *entry = NULL;
|
|
while ((entry = cbm_readdir(directory)) != NULL) {
|
|
if (strncmp(entry->name, "config.json.cbm.tmp.", strlen("config.json.cbm.tmp.")) == 0) {
|
|
count++;
|
|
}
|
|
}
|
|
cbm_closedir(directory);
|
|
return count;
|
|
}
|
|
|
|
static int jl_write(const char *path, const char *content) {
|
|
FILE *file = cbm_fopen(path, "wb");
|
|
if (!file) {
|
|
return -1;
|
|
}
|
|
size_t length = strlen(content);
|
|
size_t written = fwrite(content, 1U, length, file);
|
|
int close_result = fclose(file);
|
|
return written == length && close_result == 0 ? 0 : -1;
|
|
}
|
|
|
|
static char *jl_read(const char *path) {
|
|
FILE *file = cbm_fopen(path, "rb");
|
|
if (!file || fseek(file, 0, SEEK_END) != 0) {
|
|
if (file) {
|
|
fclose(file);
|
|
}
|
|
return NULL;
|
|
}
|
|
long size = ftell(file);
|
|
if (size < 0 || fseek(file, 0, SEEK_SET) != 0) {
|
|
fclose(file);
|
|
return NULL;
|
|
}
|
|
char *content = malloc((size_t)size + 1U);
|
|
if (!content) {
|
|
fclose(file);
|
|
return NULL;
|
|
}
|
|
size_t read_count = fread(content, 1U, (size_t)size, file);
|
|
int failed = ferror(file);
|
|
fclose(file);
|
|
if (read_count != (size_t)size || failed) {
|
|
free(content);
|
|
return NULL;
|
|
}
|
|
content[read_count] = '\0';
|
|
return content;
|
|
}
|
|
|
|
static int jl_failed_unchanged(const char *path, const char *original,
|
|
const char *const *object_path, size_t path_len, const char *key,
|
|
const char *value) {
|
|
if (jl_write(path, original) != 0) {
|
|
return 0;
|
|
}
|
|
if (cbm_json_like_upsert_entry(path, object_path, path_len, key, value) == 0) {
|
|
return 0;
|
|
}
|
|
char *after = jl_read(path);
|
|
int unchanged = after && strcmp(after, original) == 0;
|
|
free(after);
|
|
return unchanged;
|
|
}
|
|
|
|
static size_t jl_occurrences(const char *text, const char *needle) {
|
|
size_t count = 0;
|
|
size_t needle_length = strlen(needle);
|
|
while (needle_length > 0U && (text = strstr(text, needle)) != NULL) {
|
|
count++;
|
|
text += needle_length;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
typedef struct {
|
|
const char *content;
|
|
const char *backup_path;
|
|
bool replace_identity;
|
|
int result;
|
|
} jl_precommit_change_t;
|
|
|
|
static void jl_change_before_commit(const char *path, void *context) {
|
|
jl_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 = jl_write(path, change->content);
|
|
}
|
|
|
|
TEST(config_json_like_rejects_stale_content_and_cleans_temp) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
ASSERT_EQ(jl_write(fixture.path, "{\"keep\":1}\n"), 0);
|
|
const char *root[] = {NULL};
|
|
jl_precommit_change_t change = {
|
|
.content = "{\"concurrent\":true}\n",
|
|
.backup_path = NULL,
|
|
.replace_identity = false,
|
|
.result = -1,
|
|
};
|
|
cbm_json_like_set_precommit_hook_for_testing(jl_change_before_commit, &change);
|
|
int result = cbm_json_like_upsert_entry(fixture.path, root, 0U, "owned", "true");
|
|
cbm_json_like_set_precommit_hook_for_testing(NULL, NULL);
|
|
|
|
ASSERT_EQ(change.result, 0);
|
|
ASSERT_EQ(result, -1);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, "{\"concurrent\":true}\n");
|
|
free(content);
|
|
ASSERT_EQ(jl_temp_file_count(&fixture), 0U);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_rejects_stale_identity_with_same_content) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *original = "{\"keep\":1}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, original), 0);
|
|
char backup[sizeof(fixture.path) + 32U];
|
|
ASSERT(snprintf(backup, sizeof(backup), "%s/original.json", fixture.directory) > 0);
|
|
const char *root[] = {NULL};
|
|
jl_precommit_change_t change = {
|
|
.content = original,
|
|
.backup_path = backup,
|
|
.replace_identity = true,
|
|
.result = -1,
|
|
};
|
|
cbm_json_like_set_precommit_hook_for_testing(jl_change_before_commit, &change);
|
|
int result = cbm_json_like_upsert_entry(fixture.path, root, 0U, "owned", "true");
|
|
cbm_json_like_set_precommit_hook_for_testing(NULL, NULL);
|
|
|
|
ASSERT_EQ(change.result, 0);
|
|
ASSERT_EQ(result, -1);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, original);
|
|
free(content);
|
|
ASSERT_EQ(jl_temp_file_count(&fixture), 0U);
|
|
ASSERT_EQ(cbm_unlink(backup), 0);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_missing_target_race_does_not_replace_winner) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *root[] = {NULL};
|
|
jl_precommit_change_t race = {
|
|
.content = "{\"winner\":true}\n",
|
|
.backup_path = NULL,
|
|
.replace_identity = false,
|
|
.result = -1,
|
|
};
|
|
cbm_json_like_set_prepublish_hook_for_testing(jl_change_before_commit, &race);
|
|
int result = cbm_json_like_upsert_entry(fixture.path, root, 0U, "owned", "true");
|
|
cbm_json_like_set_prepublish_hook_for_testing(NULL, NULL);
|
|
ASSERT_EQ(race.result, 0);
|
|
ASSERT_EQ(result, -1);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, "{\"winner\":true}\n");
|
|
free(content);
|
|
ASSERT_EQ(jl_temp_file_count(&fixture), 0U);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_existing_target_swap_after_check_preserves_winner) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *original = "{\"keep\":1}\n";
|
|
const char *winner = "{\"winner\":true}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, original), 0);
|
|
char backup[sizeof(fixture.path) + 32U];
|
|
ASSERT(snprintf(backup, sizeof(backup), "%s/original.json", fixture.directory) > 0);
|
|
const char *root[] = {NULL};
|
|
jl_precommit_change_t race = {
|
|
.content = winner,
|
|
.backup_path = backup,
|
|
.replace_identity = true,
|
|
.result = -1,
|
|
};
|
|
|
|
cbm_json_like_set_prepublish_hook_for_testing(jl_change_before_commit, &race);
|
|
int result = cbm_json_like_upsert_entry(fixture.path, root, 0U, "owned", "true");
|
|
cbm_json_like_set_prepublish_hook_for_testing(NULL, NULL);
|
|
|
|
ASSERT_EQ(race.result, 0);
|
|
ASSERT_EQ(result, -1);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, winner);
|
|
free(content);
|
|
ASSERT_EQ(jl_temp_file_count(&fixture), 0U);
|
|
ASSERT_EQ(cbm_unlink(backup), 0);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_rejects_non_regular_path) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
ASSERT_EQ(cbm_mkdir(fixture.path), 0);
|
|
const char *root[] = {NULL};
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, root, 0U, "owned", "true"), -1);
|
|
ASSERT_EQ(jl_temp_file_count(&fixture), 0U);
|
|
ASSERT_EQ(cbm_rmdir(fixture.path), 0);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
TEST(config_json_like_rejects_symlink_without_touching_target) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
char target[sizeof(fixture.path) + 32U];
|
|
ASSERT(snprintf(target, sizeof(target), "%s/target.json", fixture.directory) > 0);
|
|
const char *original = "{\"target\":true}\n";
|
|
ASSERT_EQ(jl_write(target, original), 0);
|
|
ASSERT_EQ(symlink(target, fixture.path), 0);
|
|
|
|
const char *root[] = {NULL};
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, root, 0U, "owned", "true"), -1);
|
|
struct stat link_state;
|
|
ASSERT_EQ(lstat(fixture.path, &link_state), 0);
|
|
ASSERT(S_ISLNK(link_state.st_mode));
|
|
char *content = jl_read(target);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, original);
|
|
free(content);
|
|
ASSERT_EQ(jl_temp_file_count(&fixture), 0U);
|
|
|
|
ASSERT_EQ(cbm_unlink(fixture.path), 0);
|
|
ASSERT_EQ(cbm_unlink(target), 0);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_rejects_hard_link_without_splitting_identity) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
char alias[sizeof(fixture.path) + 32U];
|
|
ASSERT(snprintf(alias, sizeof(alias), "%s/alias.json", fixture.directory) > 0);
|
|
const char *original = "{\"shared\":true}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, original), 0);
|
|
ASSERT_EQ(link(fixture.path, alias), 0);
|
|
|
|
const char *root[] = {NULL};
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, root, 0U, "owned", "true"), -1);
|
|
char *content = jl_read(alias);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, original);
|
|
free(content);
|
|
|
|
ASSERT_EQ(cbm_unlink(alias), 0);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_preserves_owner_group_and_mode) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
ASSERT_EQ(jl_write(fixture.path, "{\"keep\":1}\n"), 0);
|
|
ASSERT_EQ(chmod(fixture.path, 0640), 0);
|
|
struct stat before;
|
|
ASSERT_EQ(stat(fixture.path, &before), 0);
|
|
|
|
const char *root[] = {NULL};
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, root, 0U, "owned", "true"), 0);
|
|
struct stat after;
|
|
ASSERT_EQ(stat(fixture.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);
|
|
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
#endif
|
|
|
|
TEST(config_json_like_supports_bom_and_common_json5_whitespace) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *original = "\xEF\xBB\xBF"
|
|
"{\x0C"
|
|
"theme:'dark',\x0B"
|
|
"// retained\xE2\x80\xA8"
|
|
"mcp:\xC2\xA0{servers:{}},\xE2\x80\xA9"
|
|
"}";
|
|
ASSERT_EQ(jl_write(fixture.path, original), 0);
|
|
const char *path[] = {"mcp", "servers"};
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, path, 2U, "owned", "true"), 0);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT(memcmp(content, "\xEF\xBB\xBF", 3U) == 0);
|
|
ASSERT(strstr(content, "// retained\xE2\x80\xA8") != NULL);
|
|
ASSERT(strstr(content, "theme:'dark'") != NULL);
|
|
ASSERT(strstr(content, "\"owned\": true") != NULL);
|
|
free(content);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_rejects_json5_decimal_escapes_byte_identical) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *root[] = {NULL};
|
|
ASSERT(jl_failed_unchanged(fixture.path, "{bad:'\\8'}\n", root, 0U, "owned", "true"));
|
|
ASSERT(jl_failed_unchanged(fixture.path, "{bad:'\\01'}\n", root, 0U, "owned", "true"));
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_fresh_strict_upsert_replace_remove) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *root[] = {NULL};
|
|
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, root, 0U, "owned", "{\"command\":\"cbm\"}"),
|
|
0);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, "{\n \"owned\": {\"command\":\"cbm\"}\n}\n");
|
|
free(content);
|
|
|
|
ASSERT_EQ(jl_write(fixture.path, "{\"keep\":1,\"owned\":false}\n"), 0);
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, root, 0U, "owned", "[1,2]"), 0);
|
|
content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, "{\"keep\":1,\"owned\":[1,2]}\n");
|
|
free(content);
|
|
|
|
ASSERT_EQ(cbm_json_like_remove_entry(fixture.path, root, 0U, "owned"), 0);
|
|
content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT(strstr(content, "\"keep\":1") != NULL);
|
|
ASSERT(strstr(content, "owned") == NULL);
|
|
free(content);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_preserves_jsonc_comments) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *original = "{\n"
|
|
" // user preference\n"
|
|
" \"theme\": \"dark\",\n"
|
|
" \"mcpServers\": {\n"
|
|
" /* retained server */\n"
|
|
" \"other\": {\"command\": \"other\"},\n"
|
|
" },\n"
|
|
"}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, original), 0);
|
|
const char *path[] = {"mcpServers"};
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, path, 1U, "codebase-memory",
|
|
"{\"command\":\"cbm\"}"),
|
|
0);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT(strstr(content, "// user preference") != NULL);
|
|
ASSERT(strstr(content, "/* retained server */") != NULL);
|
|
ASSERT(strstr(content, "\"theme\": \"dark\"") != NULL);
|
|
ASSERT(strstr(content, "\"other\": {\"command\": \"other\"}") != NULL);
|
|
ASSERT(strstr(content, "\"codebase-memory\": {\"command\":\"cbm\"}") != NULL);
|
|
free(content);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_openclaw_json5_nested_servers) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *original =
|
|
"{ theme: 'dark', mcp: { servers: { other: { command: 'other' }, }, }, }";
|
|
ASSERT_EQ(jl_write(fixture.path, original), 0);
|
|
const char *path[] = {"mcp", "servers"};
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, path, 2U, "codebase-memory",
|
|
"{\"command\":\"cbm\",\"args\":[]}"),
|
|
0);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT(strstr(content, "theme: 'dark'") != NULL);
|
|
ASSERT(strstr(content, "other: { command: 'other' }") != NULL);
|
|
ASSERT(strstr(content, "\"codebase-memory\"") != NULL);
|
|
free(content);
|
|
|
|
ASSERT_EQ(cbm_json_like_remove_entry(fixture.path, path, 2U, "codebase-memory"), 0);
|
|
content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT(strstr(content, "codebase-memory") == NULL);
|
|
ASSERT(strstr(content, "theme: 'dark'") != NULL);
|
|
ASSERT(strstr(content, "other: { command: 'other' }") != NULL);
|
|
free(content);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_creates_missing_nested_path) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
ASSERT_EQ(jl_write(fixture.path, "{\n \"theme\": \"dark\"\n}\n"), 0);
|
|
const char *path[] = {"mcp", "servers"};
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, path, 2U, "codebase-memory",
|
|
"{\"command\":\"cbm\"}"),
|
|
0);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT(strstr(content, "\"theme\": \"dark\"") != NULL);
|
|
ASSERT(strstr(content, "\"mcp\": {") != NULL);
|
|
ASSERT(strstr(content, "\"servers\": {") != NULL);
|
|
ASSERT(strstr(content, "\"codebase-memory\": {\"command\":\"cbm\"}") != NULL);
|
|
free(content);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_rejects_duplicate_path_byte_identical) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *path[] = {"mcp"};
|
|
const char *duplicate = "{\"mcp\":{}, 'mcp':{}}\n";
|
|
ASSERT(jl_failed_unchanged(fixture.path, duplicate, path, 1U, "owned", "true"));
|
|
|
|
const char *root[] = {NULL};
|
|
const char *duplicate_entry = "{\"owned\":1, owned:2}\n";
|
|
ASSERT(jl_failed_unchanged(fixture.path, duplicate_entry, root, 0U, "owned", "3"));
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_rejects_invalid_and_malformed_byte_identical) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *root[] = {NULL};
|
|
ASSERT(jl_failed_unchanged(fixture.path, "{\"keep\":1}\n", root, 0U, "owned", "{\"broken\":}"));
|
|
ASSERT(jl_failed_unchanged(fixture.path, "{\"keep\":1}\n", root, 0U, "owned", ".5"));
|
|
ASSERT(jl_failed_unchanged(fixture.path, "{\"keep\":1 /* never closes", root, 0U, "owned",
|
|
"true"));
|
|
ASSERT(jl_failed_unchanged(fixture.path, "{\"keep\":[1,2}\n", root, 0U, "owned", "true"));
|
|
|
|
const char *path[] = {"mcp"};
|
|
ASSERT(jl_failed_unchanged(fixture.path, "{\"mcp\":false}\n", path, 1U, "owned", "true"));
|
|
ASSERT_EQ(jl_write(fixture.path, "{\"keep\":1}\n"), 0);
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, root, 0U, "bad\nkey", "true"), -1);
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(NULL, root, 0U, "owned", "true"), -1);
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, NULL, 0U, "owned", "true"), -1);
|
|
const char *deep_path[64];
|
|
for (size_t i = 0; i < sizeof(deep_path) / sizeof(deep_path[0]); i++) {
|
|
deep_path[i] = "level";
|
|
}
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, deep_path, 64U, "owned", "true"), -1);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, "{\"keep\":1}\n");
|
|
free(content);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_upsert_is_byte_idempotent) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
ASSERT_EQ(jl_write(fixture.path, "{\n \"mcp\": {}\n}\n"), 0);
|
|
const char *path[] = {"mcp"};
|
|
const char *value = "{\"command\":\"cbm\",\"args\":[\"serve\"]}";
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, path, 1U, "owned", value), 0);
|
|
char *first = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(first);
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, path, 1U, "owned", value), 0);
|
|
char *second = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(second);
|
|
ASSERT_STR_EQ(second, first);
|
|
free(first);
|
|
free(second);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_ignores_braces_and_comments_inside_strings) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *original = "{\"note\":\"} // text /* text */ \\\" {\",\"mcp\":{\"servers\":{}}}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, original), 0);
|
|
const char *path[] = {"mcp", "servers"};
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, path, 2U, "owned", "{\"command\":\"cbm\"}"),
|
|
0);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT(strstr(content, "} // text /* text */ \\\" {") != NULL);
|
|
ASSERT(strstr(content, "\"owned\": {\"command\":\"cbm\"}") != NULL);
|
|
free(content);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_handles_trailing_commas) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *original = "{\n mcp: {\n servers: {\n other: [1, 2,],\n },\n },\n}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, original), 0);
|
|
const char *path[] = {"mcp", "servers"};
|
|
ASSERT_EQ(cbm_json_like_upsert_entry(fixture.path, path, 2U, "owned", "true"), 0);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT(strstr(content, "other: [1, 2,]") != NULL);
|
|
ASSERT(strstr(content, "\"owned\": true,") != NULL);
|
|
free(content);
|
|
ASSERT_EQ(cbm_json_like_remove_entry(fixture.path, path, 2U, "owned"), 0);
|
|
content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT(strstr(content, "owned") == NULL);
|
|
ASSERT(strstr(content, "other: [1, 2,]") != NULL);
|
|
free(content);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_removes_first_middle_last_and_only) {
|
|
static const char *inputs[] = {
|
|
"{\"owned\":1,\"b\":2,\"c\":3}",
|
|
"{\"a\":1,\"owned\":2,\"c\":3}",
|
|
"{\"a\":1,\"b\":2,\"owned\":3}",
|
|
"{\"owned\":1}",
|
|
};
|
|
static const char *required[] = {"\"b\":2", "\"a\":1", "\"b\":2", "{"};
|
|
const char *root[] = {NULL};
|
|
|
|
for (size_t i = 0; i < sizeof(inputs) / sizeof(inputs[0]); i++) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
ASSERT_EQ(jl_write(fixture.path, inputs[i]), 0);
|
|
ASSERT_EQ(cbm_json_like_remove_entry(fixture.path, root, 0U, "owned"), 0);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT(strstr(content, "owned") == NULL);
|
|
ASSERT(strstr(content, required[i]) != NULL);
|
|
ASSERT_EQ(cbm_json_like_remove_entry(fixture.path, root, 0U, "owned"), 0);
|
|
free(content);
|
|
jl_fixture_close(&fixture);
|
|
}
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_removal_preserves_comments_and_siblings) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *original =
|
|
"{\n \"first\": 1,\n /* keep this comment */\n \"owned\": 2, // keep this too\n"
|
|
" \"last\": 3\n}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, original), 0);
|
|
const char *root[] = {NULL};
|
|
ASSERT_EQ(cbm_json_like_remove_entry(fixture.path, root, 0U, "owned"), 0);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT(strstr(content, "owned") == NULL);
|
|
ASSERT(strstr(content, "/* keep this comment */") != NULL);
|
|
ASSERT(strstr(content, "// keep this too") != NULL);
|
|
ASSERT(strstr(content, "\"first\": 1") != NULL);
|
|
ASSERT(strstr(content, "\"last\": 3") != NULL);
|
|
free(content);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_top_level_array_unique_string) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *original = "{\n"
|
|
" theme: 'dark',\n"
|
|
" instructions: [\n"
|
|
" '~/rules/other.md',\n"
|
|
" // keep this user comment\n"
|
|
" ],\n"
|
|
"}\n";
|
|
const char *owned = "~/.config/kilo/rules/codebase-memory-mcp.md";
|
|
ASSERT_EQ(jl_write(fixture.path, original), 0);
|
|
ASSERT_EQ(cbm_json_like_add_unique_string(fixture.path, "instructions", owned), 0);
|
|
char *first = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(first);
|
|
ASSERT(strstr(first, "// keep this user comment") != NULL);
|
|
ASSERT(strstr(first, "'~/rules/other.md'") != NULL);
|
|
ASSERT_EQ(jl_occurrences(first, owned), 1U);
|
|
|
|
ASSERT_EQ(cbm_json_like_add_unique_string(fixture.path, "instructions", owned), 0);
|
|
char *second = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(second);
|
|
ASSERT_STR_EQ(second, first);
|
|
free(first);
|
|
free(second);
|
|
|
|
ASSERT_EQ(cbm_json_like_remove_string(fixture.path, "instructions", owned), 0);
|
|
char *removed = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(removed);
|
|
ASSERT(strstr(removed, owned) == NULL);
|
|
ASSERT(strstr(removed, "// keep this user comment") != NULL);
|
|
ASSERT(strstr(removed, "'~/rules/other.md'") != NULL);
|
|
free(removed);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_top_level_array_create_escape_and_fail_closed) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *owned = "C:\\rules\\\"main\".md";
|
|
ASSERT_EQ(cbm_json_like_add_unique_string(fixture.path, "instructions", owned), 0);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT(strstr(content, "\"instructions\": [") != NULL);
|
|
ASSERT(strstr(content, "C:") != NULL);
|
|
ASSERT(strstr(content, "rules") != NULL);
|
|
free(content);
|
|
ASSERT_EQ(cbm_json_like_remove_string(fixture.path, "instructions", owned), 0);
|
|
content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT(strstr(content, "rules") == NULL);
|
|
free(content);
|
|
|
|
const char *duplicate = "{instructions:['owned', \"owned\",], keep:true}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, duplicate), 0);
|
|
ASSERT_EQ(cbm_json_like_add_unique_string(fixture.path, "instructions", "owned"), -1);
|
|
content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, duplicate);
|
|
free(content);
|
|
ASSERT_EQ(cbm_json_like_remove_string(fixture.path, "instructions", "owned"), -1);
|
|
content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, duplicate);
|
|
free(content);
|
|
|
|
const char *wrong_type = "{instructions:false, keep:true}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, wrong_type), 0);
|
|
ASSERT_EQ(cbm_json_like_add_unique_string(fixture.path, "instructions", "owned"), -1);
|
|
content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, wrong_type);
|
|
free(content);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_nested_array_creates_missing_path_and_escapes) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *path[] = {"agents", "defaults", "compaction"};
|
|
const char *owned = "Session \"memory\"\\path\nsection";
|
|
|
|
ASSERT_EQ(cbm_json_like_add_unique_string_at_path(fixture.path, path, 3U,
|
|
"postCompactionSections", owned),
|
|
0);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT(strstr(content, "\"agents\": {") != NULL);
|
|
ASSERT(strstr(content, "\"defaults\": {") != NULL);
|
|
ASSERT(strstr(content, "\"compaction\": {") != NULL);
|
|
ASSERT(strstr(content, "\"postCompactionSections\": [") != NULL);
|
|
ASSERT(strstr(content, "Session \\\"memory\\\"\\\\path\\nsection") != NULL);
|
|
free(content);
|
|
|
|
ASSERT_EQ(cbm_json_like_remove_string_at_path(fixture.path, path, 3U, "postCompactionSections",
|
|
owned),
|
|
0);
|
|
content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT(strstr(content, "postCompactionSections") != NULL);
|
|
ASSERT(strstr(content, "Session") == NULL);
|
|
free(content);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_nested_array_preserves_jsonc_and_is_idempotent) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *original = "{\n"
|
|
" // retain root comment\n"
|
|
" theme: 'dark',\n"
|
|
" options: {\n"
|
|
" keep: true,\n"
|
|
" context_paths: [\n"
|
|
" 'memory.md.bak',\n"
|
|
" // retain array comment\n"
|
|
" ],\n"
|
|
" },\n"
|
|
"}\n";
|
|
const char *path[] = {"options"};
|
|
const char *owned = "memory.md";
|
|
ASSERT_EQ(jl_write(fixture.path, original), 0);
|
|
|
|
ASSERT_EQ(
|
|
cbm_json_like_add_unique_string_at_path(fixture.path, path, 1U, "context_paths", owned), 0);
|
|
char *first = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(first);
|
|
ASSERT(strstr(first, "// retain root comment") != NULL);
|
|
ASSERT(strstr(first, "// retain array comment") != NULL);
|
|
ASSERT(strstr(first, "theme: 'dark'") != NULL);
|
|
ASSERT(strstr(first, "keep: true") != NULL);
|
|
ASSERT(strstr(first, "'memory.md.bak',") != NULL);
|
|
ASSERT_EQ(jl_occurrences(first, "\"memory.md\""), 1U);
|
|
|
|
ASSERT_EQ(
|
|
cbm_json_like_add_unique_string_at_path(fixture.path, path, 1U, "context_paths", owned), 0);
|
|
char *second = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(second);
|
|
ASSERT_STR_EQ(second, first);
|
|
free(first);
|
|
free(second);
|
|
|
|
ASSERT_EQ(cbm_json_like_remove_string_at_path(fixture.path, path, 1U, "context_paths", owned),
|
|
0);
|
|
char *removed = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(removed);
|
|
ASSERT(strstr(removed, "\"memory.md\"") == NULL);
|
|
ASSERT(strstr(removed, "'memory.md.bak'") != NULL);
|
|
ASSERT(strstr(removed, "// retain array comment") != NULL);
|
|
ASSERT(strstr(removed, "keep: true") != NULL);
|
|
free(removed);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_nested_array_fails_closed_on_ambiguous_or_invalid_paths) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *path[] = {"options"};
|
|
const char *wrong_type = "{options:false, keep:true}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, wrong_type), 0);
|
|
ASSERT_EQ(
|
|
cbm_json_like_add_unique_string_at_path(fixture.path, path, 1U, "context_paths", "owned"),
|
|
-1);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, wrong_type);
|
|
free(content);
|
|
|
|
const char *duplicate_path = "{options:{context_paths:[]}, options:{keep:true}}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, duplicate_path), 0);
|
|
ASSERT_EQ(
|
|
cbm_json_like_add_unique_string_at_path(fixture.path, path, 1U, "context_paths", "owned"),
|
|
-1);
|
|
content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, duplicate_path);
|
|
free(content);
|
|
|
|
const char *duplicate_array =
|
|
"{options:{context_paths:['owned'], context_paths:[]}, keep:true}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, duplicate_array), 0);
|
|
ASSERT_EQ(cbm_json_like_remove_string_at_path(fixture.path, path, 1U, "context_paths", "owned"),
|
|
-1);
|
|
content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, duplicate_array);
|
|
free(content);
|
|
|
|
ASSERT_EQ(
|
|
cbm_json_like_add_unique_string_at_path(fixture.path, NULL, 1U, "context_paths", "owned"),
|
|
-1);
|
|
const char *null_path[] = {NULL};
|
|
ASSERT_EQ(cbm_json_like_add_unique_string_at_path(fixture.path, null_path, 1U, "context_paths",
|
|
"owned"),
|
|
-1);
|
|
const char *control_path[] = {"bad\npath"};
|
|
ASSERT_EQ(cbm_json_like_remove_string_at_path(fixture.path, control_path, 1U, "context_paths",
|
|
"owned"),
|
|
-1);
|
|
const char *deep_path[64];
|
|
for (size_t i = 0; i < sizeof(deep_path) / sizeof(deep_path[0]); i++) {
|
|
deep_path[i] = "level";
|
|
}
|
|
ASSERT_EQ(cbm_json_like_add_unique_string_at_path(fixture.path, deep_path, 64U, "context_paths",
|
|
"owned"),
|
|
-1);
|
|
content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, duplicate_array);
|
|
free(content);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
TEST(config_json_like_nested_array_rejects_symlink_and_hardlink) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *path[] = {"options"};
|
|
const char *original = "{options:{context_paths:[]}}\n";
|
|
char target[sizeof(fixture.path) + 32U];
|
|
ASSERT(snprintf(target, sizeof(target), "%s/target.json", fixture.directory) > 0);
|
|
ASSERT_EQ(jl_write(target, original), 0);
|
|
ASSERT_EQ(symlink(target, fixture.path), 0);
|
|
ASSERT_EQ(
|
|
cbm_json_like_add_unique_string_at_path(fixture.path, path, 1U, "context_paths", "owned"),
|
|
-1);
|
|
char *workspace = (char *)(uintptr_t)1U;
|
|
ASSERT_EQ(cbm_json_like_get_string_at_path(fixture.path, path, 1U, "context_paths", &workspace),
|
|
-1);
|
|
ASSERT_NULL(workspace);
|
|
char *content = jl_read(target);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, original);
|
|
free(content);
|
|
ASSERT_EQ(cbm_unlink(fixture.path), 0);
|
|
|
|
ASSERT_EQ(jl_write(fixture.path, original), 0);
|
|
char alias[sizeof(fixture.path) + 32U];
|
|
ASSERT(snprintf(alias, sizeof(alias), "%s/alias.json", fixture.directory) > 0);
|
|
ASSERT_EQ(link(fixture.path, alias), 0);
|
|
ASSERT_EQ(cbm_json_like_remove_string_at_path(fixture.path, path, 1U, "context_paths", "owned"),
|
|
-1);
|
|
workspace = (char *)(uintptr_t)1U;
|
|
ASSERT_EQ(cbm_json_like_get_string_at_path(fixture.path, path, 1U, "context_paths", &workspace),
|
|
-1);
|
|
ASSERT_NULL(workspace);
|
|
content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, original);
|
|
free(content);
|
|
ASSERT_EQ(cbm_unlink(alias), 0);
|
|
ASSERT_EQ(cbm_unlink(target), 0);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
#endif
|
|
|
|
TEST(config_json_like_nested_array_rejects_precommit_content_and_identity_races) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *path[] = {"options"};
|
|
const char *original = "{options:{context_paths:['owned']}, keep:true}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, original), 0);
|
|
jl_precommit_change_t content_change = {
|
|
.content = "{concurrent:true}\n",
|
|
.backup_path = NULL,
|
|
.replace_identity = false,
|
|
.result = -1,
|
|
};
|
|
cbm_json_like_set_precommit_hook_for_testing(jl_change_before_commit, &content_change);
|
|
int result =
|
|
cbm_json_like_add_unique_string_at_path(fixture.path, path, 1U, "context_paths", "new");
|
|
cbm_json_like_set_precommit_hook_for_testing(NULL, NULL);
|
|
ASSERT_EQ(content_change.result, 0);
|
|
ASSERT_EQ(result, -1);
|
|
char *content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, "{concurrent:true}\n");
|
|
free(content);
|
|
ASSERT_EQ(jl_temp_file_count(&fixture), 0U);
|
|
|
|
ASSERT_EQ(jl_write(fixture.path, original), 0);
|
|
char backup[sizeof(fixture.path) + 32U];
|
|
ASSERT(snprintf(backup, sizeof(backup), "%s/original.json", fixture.directory) > 0);
|
|
jl_precommit_change_t identity_change = {
|
|
.content = original,
|
|
.backup_path = backup,
|
|
.replace_identity = true,
|
|
.result = -1,
|
|
};
|
|
cbm_json_like_set_precommit_hook_for_testing(jl_change_before_commit, &identity_change);
|
|
result = cbm_json_like_remove_string_at_path(fixture.path, path, 1U, "context_paths", "owned");
|
|
cbm_json_like_set_precommit_hook_for_testing(NULL, NULL);
|
|
ASSERT_EQ(identity_change.result, 0);
|
|
ASSERT_EQ(result, -1);
|
|
content = jl_read(fixture.path);
|
|
ASSERT_NOT_NULL(content);
|
|
ASSERT_STR_EQ(content, original);
|
|
free(content);
|
|
ASSERT_EQ(jl_temp_file_count(&fixture), 0U);
|
|
ASSERT_EQ(cbm_unlink(backup), 0);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_nested_string_lookup_decodes_json5_workspace) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *document =
|
|
"{agents:{defaults:{workspace:'~\\/Open\\x43law\\u0020work\\\'space'}}}\n";
|
|
const char *path[] = {"agents", "defaults"};
|
|
ASSERT_EQ(jl_write(fixture.path, document), 0);
|
|
|
|
char *workspace = NULL;
|
|
ASSERT_EQ(cbm_json_like_get_string_at_path(fixture.path, path, 2U, "workspace", &workspace), 0);
|
|
ASSERT_NOT_NULL(workspace);
|
|
ASSERT_STR_EQ(workspace, "~/OpenClaw work'space");
|
|
free(workspace);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
TEST(config_json_like_nested_string_lookup_distinguishes_missing_and_fails_closed) {
|
|
jl_fixture_t fixture;
|
|
ASSERT_EQ(jl_fixture_open(&fixture), 0);
|
|
const char *path[] = {"agents", "defaults"};
|
|
char *workspace = (char *)(uintptr_t)1U;
|
|
ASSERT_EQ(cbm_json_like_get_string_at_path(fixture.path, path, 2U, "workspace", &workspace), 1);
|
|
ASSERT_NULL(workspace);
|
|
|
|
const char *missing = "{agents:{defaults:{keep:true}}}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, missing), 0);
|
|
workspace = (char *)(uintptr_t)1U;
|
|
ASSERT_EQ(cbm_json_like_get_string_at_path(fixture.path, path, 2U, "workspace", &workspace), 1);
|
|
ASSERT_NULL(workspace);
|
|
|
|
const char *non_string = "{agents:{defaults:{workspace:false}}}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, non_string), 0);
|
|
workspace = (char *)(uintptr_t)1U;
|
|
ASSERT_EQ(cbm_json_like_get_string_at_path(fixture.path, path, 2U, "workspace", &workspace),
|
|
-1);
|
|
ASSERT_NULL(workspace);
|
|
|
|
const char *duplicate_key = "{agents:{defaults:{workspace:'one', workspace:'two'}}}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, duplicate_key), 0);
|
|
workspace = (char *)(uintptr_t)1U;
|
|
ASSERT_EQ(cbm_json_like_get_string_at_path(fixture.path, path, 2U, "workspace", &workspace),
|
|
-1);
|
|
ASSERT_NULL(workspace);
|
|
|
|
const char *duplicate_path =
|
|
"{agents:{defaults:{workspace:'one'}}, agents:{defaults:{workspace:'two'}}}\n";
|
|
ASSERT_EQ(jl_write(fixture.path, duplicate_path), 0);
|
|
workspace = (char *)(uintptr_t)1U;
|
|
ASSERT_EQ(cbm_json_like_get_string_at_path(fixture.path, path, 2U, "workspace", &workspace),
|
|
-1);
|
|
ASSERT_NULL(workspace);
|
|
jl_fixture_close(&fixture);
|
|
PASS();
|
|
}
|
|
|
|
SUITE(config_json_like) {
|
|
RUN_TEST(config_json_like_rejects_stale_content_and_cleans_temp);
|
|
RUN_TEST(config_json_like_rejects_stale_identity_with_same_content);
|
|
RUN_TEST(config_json_like_missing_target_race_does_not_replace_winner);
|
|
RUN_TEST(config_json_like_existing_target_swap_after_check_preserves_winner);
|
|
RUN_TEST(config_json_like_rejects_non_regular_path);
|
|
#ifndef _WIN32
|
|
RUN_TEST(config_json_like_rejects_symlink_without_touching_target);
|
|
RUN_TEST(config_json_like_rejects_hard_link_without_splitting_identity);
|
|
RUN_TEST(config_json_like_preserves_owner_group_and_mode);
|
|
#endif
|
|
RUN_TEST(config_json_like_supports_bom_and_common_json5_whitespace);
|
|
RUN_TEST(config_json_like_rejects_json5_decimal_escapes_byte_identical);
|
|
RUN_TEST(config_json_like_fresh_strict_upsert_replace_remove);
|
|
RUN_TEST(config_json_like_preserves_jsonc_comments);
|
|
RUN_TEST(config_json_like_openclaw_json5_nested_servers);
|
|
RUN_TEST(config_json_like_creates_missing_nested_path);
|
|
RUN_TEST(config_json_like_rejects_duplicate_path_byte_identical);
|
|
RUN_TEST(config_json_like_rejects_invalid_and_malformed_byte_identical);
|
|
RUN_TEST(config_json_like_upsert_is_byte_idempotent);
|
|
RUN_TEST(config_json_like_ignores_braces_and_comments_inside_strings);
|
|
RUN_TEST(config_json_like_handles_trailing_commas);
|
|
RUN_TEST(config_json_like_removes_first_middle_last_and_only);
|
|
RUN_TEST(config_json_like_removal_preserves_comments_and_siblings);
|
|
RUN_TEST(config_json_like_top_level_array_unique_string);
|
|
RUN_TEST(config_json_like_top_level_array_create_escape_and_fail_closed);
|
|
RUN_TEST(config_json_like_nested_array_creates_missing_path_and_escapes);
|
|
RUN_TEST(config_json_like_nested_array_preserves_jsonc_and_is_idempotent);
|
|
RUN_TEST(config_json_like_nested_array_fails_closed_on_ambiguous_or_invalid_paths);
|
|
#ifndef _WIN32
|
|
RUN_TEST(config_json_like_nested_array_rejects_symlink_and_hardlink);
|
|
#endif
|
|
RUN_TEST(config_json_like_nested_array_rejects_precommit_content_and_identity_races);
|
|
RUN_TEST(config_json_like_nested_string_lookup_decodes_json5_workspace);
|
|
RUN_TEST(config_json_like_nested_string_lookup_distinguishes_missing_and_fails_closed);
|
|
}
|