Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/cli/config_yaml_edit.c
Original file line number Diff line number Diff line change
Expand Up @@ -1266,8 +1266,8 @@ static int yaml_find_comment(const char *data, size_t start, size_t end, size_t
* The test is positional and deliberately conservative: an indicator is only
* honoured as one when nothing has appeared before it in the value. `key: *a`
* is still an alias and still refused; `key: 2 * 3` and `key: a*b` are text.
* `{`/`}` keep their existing treatment — empty flow mappings are a separate
* change with its own semantics. */
* `{`/`}` keep their existing treatment here; mapping-body validation admits
* exact empty flow mappings through a context-specific exception. */
static int yaml_range_has_unsupported(const char *data, size_t start, size_t end) {
char quote = '\0';
/* Last significant character seen, so an indicator can be judged by what
Expand Down Expand Up @@ -1387,6 +1387,19 @@ static int yaml_tail_is_explicit_empty_mapping(const char *data, size_t colon, s
return 0;
}

static int yaml_mapping_body_line_has_unsupported(const yaml_doc_t *doc, const yaml_line_t *line) {
size_t start = line->start + line->indent;
size_t colon = 0U;
bool empty_mapping = false;
if (yaml_find_mapping_colon(doc->data, start, line->text_end, &colon) == 0 &&
yaml_tail_is_explicit_empty_mapping(doc->data, colon, line->text_end, &empty_mapping) ==
0 &&
empty_mapping) {
return yaml_range_has_unsupported(doc->data, start, colon + YAML_UNIT);
}
return yaml_range_has_unsupported(doc->data, start, line->text_end);
}

static int yaml_value_starts_multiline(const char *data, size_t colon, size_t end) {
size_t pos = yaml_skip_spaces(data, colon + YAML_UNIT, end);
return pos < end && (data[pos] == '|' || data[pos] == '>');
Expand All @@ -1404,7 +1417,7 @@ static int yaml_validate_mapping_body(const yaml_doc_t *doc, size_t first_line,
continue;
}
if (line->indent < YAML_ENTRY_INDENT || (line->indent & YAML_UNIT) != 0U ||
yaml_range_has_unsupported(doc->data, line->start + line->indent, line->text_end)) {
yaml_mapping_body_line_has_unsupported(doc, line)) {
return YAML_ERROR;
}
size_t colon = 0U;
Expand Down
104 changes: 104 additions & 0 deletions tests/test_config_yaml_edit.c
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,107 @@ TEST(config_yaml_edit_goose_extensions_preserve_siblings) {
PASS();
}

TEST(config_yaml_edit_goose_accepts_empty_flow_mapping_in_sibling_issue1673) {
const char *initial = "extensions:\n"
" demo:\n"
" enabled: true\n"
" type: stdio\n"
" cmd: /bin/true\n"
" args:\n"
" - --serve\n"
" envs: {}\n"
" env_keys: []\n"
" timeout: 300\n"
"GOOSE_THINKING_EFFORT: max\n";
const char *block = " type: stdio\n"
" cmd: \"/opt/codebase-memory-mcp\"\n"
" args: []\n"
" enabled: true\n";
const char *expected = "extensions:\n"
" demo:\n"
" enabled: true\n"
" type: stdio\n"
" cmd: /bin/true\n"
" args:\n"
" - --serve\n"
" envs: {}\n"
" env_keys: []\n"
" timeout: 300\n"
" codebase-memory-mcp:\n"
" type: stdio\n"
" cmd: \"/opt/codebase-memory-mcp\"\n"
" args: []\n"
" enabled: true\n"
"GOOSE_THINKING_EFFORT: max\n";
yaml_fixture_t fixture;
ASSERT_EQ(yaml_fixture_init(&fixture, initial), 0);

ASSERT_EQ(cbm_yaml_upsert_owned_mapping_entry(fixture.path, "extensions", "codebase-memory-mcp",
block),
CBM_YAML_IDENTITY_EDIT_OK);
char *installed = yaml_read_alloc(fixture.path);
ASSERT_NOT_NULL(installed);
ASSERT_STR_EQ(installed, expected);

free(installed);
th_cleanup(fixture.dir);
PASS();
}

TEST(config_yaml_edit_goose_still_rejects_nonempty_flow_mapping_issue1673) {
const char *initial = "extensions:\n"
" demo:\n"
" enabled: true\n"
" type: stdio\n"
" cmd: /bin/true\n"
" args: []\n"
" envs: {FOO: bar}\n"
" env_keys: []\n"
" timeout: 300\n";
const char *block = " type: stdio\n"
" cmd: \"/opt/codebase-memory-mcp\"\n"
" args: []\n"
" enabled: true\n";
yaml_fixture_t fixture;
ASSERT_EQ(yaml_fixture_init(&fixture, initial), 0);

ASSERT_EQ(cbm_yaml_upsert_owned_mapping_entry(fixture.path, "extensions", "codebase-memory-mcp",
block),
CBM_YAML_IDENTITY_EDIT_ERROR);
char *unchanged = yaml_read_alloc(fixture.path);
ASSERT_NOT_NULL(unchanged);
ASSERT_STR_EQ(unchanged, initial);

free(unchanged);
th_cleanup(fixture.dir);
PASS();
}

TEST(config_yaml_edit_goose_still_rejects_merge_key_with_empty_mapping_issue1673) {
const char *initial = "extensions:\n"
" demo:\n"
" type: stdio\n"
" cmd: /bin/true\n"
" <<: {}\n";
const char *block = " type: stdio\n"
" cmd: \"/opt/codebase-memory-mcp\"\n"
" args: []\n"
" enabled: true\n";
yaml_fixture_t fixture;
ASSERT_EQ(yaml_fixture_init(&fixture, initial), 0);

ASSERT_EQ(cbm_yaml_upsert_owned_mapping_entry(fixture.path, "extensions", "codebase-memory-mcp",
block),
CBM_YAML_IDENTITY_EDIT_ERROR);
char *unchanged = yaml_read_alloc(fixture.path);
ASSERT_NOT_NULL(unchanged);
ASSERT_STR_EQ(unchanged, initial);

free(unchanged);
th_cleanup(fixture.dir);
PASS();
}

TEST(config_yaml_edit_owned_agent_mapping_installs_idempotently_and_removes_exact_state) {
struct owned_mapping_case {
const char *section;
Expand Down Expand Up @@ -1592,6 +1693,9 @@ SUITE(config_yaml_edit) {
RUN_TEST(config_yaml_edit_hermes_mapping_lifecycle);
RUN_TEST(config_yaml_edit_hermes_creates_missing_section);
RUN_TEST(config_yaml_edit_goose_extensions_preserve_siblings);
RUN_TEST(config_yaml_edit_goose_accepts_empty_flow_mapping_in_sibling_issue1673);
RUN_TEST(config_yaml_edit_goose_still_rejects_nonempty_flow_mapping_issue1673);
RUN_TEST(config_yaml_edit_goose_still_rejects_merge_key_with_empty_mapping_issue1673);
RUN_TEST(config_yaml_edit_owned_agent_mapping_installs_idempotently_and_removes_exact_state);
RUN_TEST(config_yaml_edit_owned_agent_mapping_preserves_foreign_same_name_state);
RUN_TEST(config_yaml_edit_mapping_remove_first_middle_last);
Expand Down
Loading