@@ -159,10 +159,10 @@ static char *agent_deep_same_name_json(size_t depth) {
159159
160160TEST (agent_clients_registry_is_stable_and_callback_driven ) {
161161 static const char * expected [] = {
162- "qoder" , "kimi" , "gitlab-duo" , "rovo-dev" , "amp" , "devin " ,
163- "tabnine " , "continue " , "visual-studio " , "trae" , "roo-code " , "amazon-q " ,
164- "codebuddy" , "ibm-bob-ide " , "ibm-bob-shell " , "pochi " , "pi" , "sourcegraph-cody " ,
165- "omp" ,
162+ "qoder" , "kimi" , "gitlab-duo" , "rovo-dev" , "amp " ,
163+ "devin " , "tabnine " , "continue " , "visual-studio " , "trae " ,
164+ "roo-code " , "amazon-q " , "codebuddy " , "ibm-bob-ide" , "ibm-bob-shell " ,
165+ "pochi" , "pi" , "sourcegraph-cody" , " omp" ,
166166 };
167167 static const uint32_t expected_capabilities [] = {
168168 CBM_AGENT_CAP_MCP | CBM_AGENT_CAP_SKILL | CBM_AGENT_CAP_AGENT | CBM_AGENT_CAP_HOOK ,
@@ -866,11 +866,8 @@ TEST(agent_clients_json_schemas_are_exact_and_policy_neutral) {
866866
867867TEST (agent_clients_new_standard_json_profiles_preserve_foreign_entries ) {
868868 static const cbm_agent_client_id_t clients [] = {
869- CBM_AGENT_CLIENT_CODEBUDDY ,
870- CBM_AGENT_CLIENT_IBM_BOB_IDE ,
871- CBM_AGENT_CLIENT_IBM_BOB_SHELL ,
872- CBM_AGENT_CLIENT_POCHI ,
873- CBM_AGENT_CLIENT_OMP ,
869+ CBM_AGENT_CLIENT_CODEBUDDY , CBM_AGENT_CLIENT_IBM_BOB_IDE , CBM_AGENT_CLIENT_IBM_BOB_SHELL ,
870+ CBM_AGENT_CLIENT_POCHI , CBM_AGENT_CLIENT_OMP ,
874871 };
875872 for (size_t i = 0U ; i < sizeof (clients ) / sizeof (clients [0 ]); i ++ ) {
876873 const char * foreign =
@@ -941,30 +938,29 @@ TEST(agent_clients_omp_resolves_to_injected_agent_dir_when_provided) {
941938 char resolved [512 ];
942939
943940 /* Default (no env) keeps the documented ~/.omp/agent path. */
944- ASSERT_EQ (cbm_agent_client_resolve_path ( CBM_AGENT_CLIENT_OMP , & options , resolved ,
945- sizeof (resolved )),
946- 0 );
941+ ASSERT_EQ (
942+ cbm_agent_client_resolve_path ( CBM_AGENT_CLIENT_OMP , & options , resolved , sizeof (resolved )),
943+ 0 );
947944 ASSERT_STR_EQ (resolved , "/home/tester/.omp/agent/mcp.json" );
948945
949946 /* Named profile directory takes precedence over the home-relative default. */
950947 options .omp_agent_dir = "/home/tester/.omp/profiles/work/agent" ;
951- ASSERT_EQ (cbm_agent_client_resolve_path ( CBM_AGENT_CLIENT_OMP , & options , resolved ,
952- sizeof (resolved )),
953- 0 );
948+ ASSERT_EQ (
949+ cbm_agent_client_resolve_path ( CBM_AGENT_CLIENT_OMP , & options , resolved , sizeof (resolved )),
950+ 0 );
954951 ASSERT_STR_EQ (resolved , "/home/tester/.omp/profiles/work/agent/mcp.json" );
955952
956953 /* PI_CODING_AGENT_DIR relocations also flow through the resolved option. */
957954 options .omp_agent_dir = "/srv/omp-shared/agent" ;
958- ASSERT_EQ (cbm_agent_client_resolve_path ( CBM_AGENT_CLIENT_OMP , & options , resolved ,
959- sizeof (resolved )),
960- 0 );
955+ ASSERT_EQ (
956+ cbm_agent_client_resolve_path ( CBM_AGENT_CLIENT_OMP , & options , resolved , sizeof (resolved )),
957+ 0 );
961958 ASSERT_STR_EQ (resolved , "/srv/omp-shared/agent/mcp.json" );
962959 PASS ();
963960}
964961
965962TEST (agent_clients_omp_profile_does_not_register_global_instructions_capability ) {
966- const cbm_agent_client_profile_t * profile =
967- cbm_agent_client_by_id (CBM_AGENT_CLIENT_OMP );
963+ const cbm_agent_client_profile_t * profile = cbm_agent_client_by_id (CBM_AGENT_CLIENT_OMP );
968964 ASSERT_NOT_NULL (profile );
969965 ASSERT_EQ (profile -> capabilities & CBM_AGENT_CAP_INSTRUCTIONS , 0U );
970966 ASSERT_NEQ (profile -> capabilities & CBM_AGENT_CAP_MCP , 0U );
@@ -973,7 +969,6 @@ TEST(agent_clients_omp_profile_does_not_register_global_instructions_capability)
973969 PASS ();
974970}
975971
976-
977972TEST (agent_clients_remove_only_canonical_and_missing_is_noop ) {
978973 char * dir = NULL ;
979974 char * path = agent_fixture ("{\"keep\":true}\n" , & dir );
@@ -1321,18 +1316,44 @@ TEST(client_adapter_opencode_covers_lifecycle_read_and_compaction) {
13211316 PASS ();
13221317}
13231318
1324- /* #2077: OpenCode's V2 loader only reads the default export and needs an
1325- * id plus a setup()/effect() function; the old named export had neither. */
1319+ /* #2077/#2089/#2204: OpenCode's loaders changed shape across releases. The
1320+ * 1.18.x server runtime dispatches the hooks returned by default.server; the
1321+ * 2.x runtime ignores server and calls setup(ctx), whose context provides the
1322+ * tool and session hook domains, so the same augmentation is registered
1323+ * there. Older V2 preview loaders expose no domains; setup returns early for
1324+ * them instead of throwing. */
13261325TEST (client_adapter_opencode_exports_the_v2_default_definition_issue2077 ) {
13271326 char * js = cbm_client_adapter_opencode ("/usr/local/bin/codebase-memory-mcp" );
13281327 ASSERT_NOT_NULL (js );
13291328 ASSERT_NOT_NULL (strstr (js , "export default {" ));
13301329 ASSERT_NOT_NULL (strstr (js , "id: 'codebase-memory-augment'" ));
1331- /* Hooks live under server(), which the server runtime reads; setup()
1332- * stays empty since the V2 config loader has no tool domain yet. */
1333- ASSERT_NOT_NULL (strstr (js , "setup() {}" ));
1330+ /* OpenCode 1.18.x server runtime. */
13341331 ASSERT_NOT_NULL (strstr (js , "server: async (ctx) => {" ));
1335- ASSERT_NULL (strstr (js , "async setup(ctx) {" ));
1332+ /* OpenCode 2 registers through the setup context; those domains exist on
1333+ * 2.x, so setup is no longer the empty placeholder #2089 pinned. */
1334+ ASSERT_NOT_NULL (strstr (js , "async setup(ctx) {" ));
1335+ ASSERT_NOT_NULL (strstr (js , "ctx.tool.hook('execute.after'" ));
1336+ ASSERT_NOT_NULL (strstr (js , "ctx.session.hook('compaction'" ));
1337+ ASSERT_NOT_NULL (strstr (js , "ctx.session.hook('context'" ));
1338+ ASSERT_NOT_NULL (strstr (js , "if (!ctx?.tool?.hook || !ctx?.session?.hook) return;" ));
1339+ /* content is the surface the model reads; prefer it over output. */
1340+ const char * content_first = strstr (js , "if (typeof result.content === 'string') {" );
1341+ const char * output_fallback = strstr (js , "if (typeof result.output === 'string') {" );
1342+ ASSERT_NOT_NULL (content_first );
1343+ ASSERT_NOT_NULL (output_fallback );
1344+ ASSERT_TRUE (content_first < output_fallback );
1345+ ASSERT_NOT_NULL (strstr (js , "if (result.content === undefined) {" ));
1346+ /* Reinjection is tracked per session; server() yields once setup() has
1347+ * registered, so a host dispatching both entries cannot append twice. */
1348+ ASSERT_NOT_NULL (strstr (js , "const pendingReinject = new Set();" ));
1349+ ASSERT_NOT_NULL (strstr (js , "pendingReinject.add(event.sessionID);" ));
1350+ ASSERT_NOT_NULL (strstr (js , "if (!pendingReinject.delete(event.sessionID)) return;" ));
1351+ ASSERT_NOT_NULL (strstr (js , "let setupRegistered = false;" ));
1352+ ASSERT_NOT_NULL (strstr (js , "if (setupRegistered) return;" ));
1353+ ASSERT_NOT_NULL (strstr (js , "setupRegistered = true;" ));
1354+ ASSERT_NOT_NULL (strstr (js , "event.result = appendResult(event.result, extra);" ));
1355+ /* The empty setup placeholder and the old named export must both be gone. */
1356+ ASSERT_NULL (strstr (js , "setup() {}" ));
13361357 ASSERT_NULL (strstr (js , "export const CodebaseMemory" ));
13371358 free (js );
13381359 PASS ();
0 commit comments