/* The Qt app embeds the engine in-process: it creates an agent from a * config, registers its own platform tools with owner "app:*" and dispatches * them exactly like the model would. No HTTP server, no port, no token and * no second process -- this is the embedding path app/ (Qt) relies on, kept * runnable without Qt so it guards the C side on every `make check`. */ #define _POSIX_C_SOURCE 200809L #include "cJSON.h" #include "cst_agent.h" #include "cst_test_util.h" #include "cst_tools.h" #include #include #include static int nfail; #define EXPECT(cond, msg) \ do { \ if (!(cond)) { \ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \ nfail++; \ } \ } while (0) /* What a platform tool implemented by the front end looks like on the C * side: a callback plus whatever the front end needs to keep. */ struct app_ctx { int calls; char last[128]; }; static int app_notify_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, cst_tool_result_t *r, void *ud) { (void)ctx; struct app_ctx *a = ud; const char *msg = NULL; for (int i = 0; i < c->nargs; i++) if (strcmp(c->args[i].key, "message") == 0) msg = c->args[i].value; a->calls++; snprintf(a->last, sizeof(a->last), "%s", msg ? msg : ""); char buf[256]; snprintf(buf, sizeof(buf), "notified: %s", msg ? msg : ""); r->out = strdup(buf); r->out_len = r->out ? strlen(r->out) : 0; r->exit_code = 0; return 0; } static void dispatch_notify(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *call, cst_tool_result_t *r, struct app_ctx *actx) { EXPECT(cst_tool_run(ctx, call, r) == 0, "the app tool runs"); EXPECT(r->out && strstr(r->out, "notified: hi") != NULL, "its output is the tool result"); EXPECT(actx->calls == 1 && strcmp(actx->last, "hi") == 0, "the C callback saw the argument"); cst_tool_result_free(r); } int main(void) { char root[1024]; if (test_tmpdir(root, sizeof(root), "cst-embed") != 0) { fprintf(stderr, "cannot create a scratch dir\n"); return 1; } cst_agent_config_t cfg; memset(&cfg, 0, sizeof(cfg)); snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", root); snprintf(cfg.session_path, sizeof(cfg.session_path), "%s/s.jsonl", root); snprintf(cfg.provider, sizeof(cfg.provider), "deepseek"); snprintf(cfg.model, sizeof(cfg.model), "deepseek-flash"); snprintf(cfg.api_base, sizeof(cfg.api_base), "https://api.deepseek.com/v1"); snprintf(cfg.api_key, sizeof(cfg.api_key), "test-key"); snprintf(cfg.mode, sizeof(cfg.mode), "general"); cfg.max_steps = 1; cfg.headless = 1; cst_agent_t *a = cst_agent_create(&cfg); EXPECT(a != NULL, "the app can create an engine in-process"); if (!a) { test_rmrf(root); return 1; } EXPECT(cst_agent_session_path(a) != NULL, "the agent owns its session"); /* the front end registers its own tool: android-only, policy "notify" */ static const char *const ANDROID_ONLY[] = {"android", NULL}; struct app_ctx actx; memset(&actx, 0, sizeof(actx)); cst_tool_def_t def; memset(&def, 0, sizeof(def)); def.name = "app_notify"; def.desc = "Send a system notification (provided by the app)."; def.perm = CST_TP_ASK; def.perm_name = "notify"; def.platforms = ANDROID_ONLY; def.run_ex = app_notify_run; def.ud = &actx; def.owner = "app:test"; EXPECT(cst_tool_register(&def) == 0, "the app registers a tool in-process"); const cst_tool_def_t *t = cst_tool_find("app_notify"); EXPECT(t != NULL, "the registry knows it"); if (!t) { cst_agent_destroy(a); test_rmrf(root); return 1; } EXPECT(strcmp(cst_tool_perm_name(t), "notify") == 0, "its policy name survives registration"); EXPECT(t->run_ex == app_notify_run && t->ud == &actx, "so does the callback and its userdata"); /* on a desktop host it must not be offered (the probe is the only gate) */ setenv("CST_PLATFORM", "linux", 1); cst_tool_meta_reload(); char why[160]; EXPECT(cst_tool_available(t, why, sizeof(why)) != 0, "an android-only tool is not available on this host"); EXPECT(!cst_tool_active(cst_agent_config(a), t), "the agent's config does not offer it"); /* ... and once the host is the phone it is, through the same config */ setenv("CST_PLATFORM", "android", 1); cst_tool_meta_reload(); EXPECT(cst_tool_available(t, why, sizeof(why)) == 0, "available on android"); EXPECT(cst_tool_active(cst_agent_config(a), t), "and offered there"); char *names = cst_tools_names(cst_agent_config(a)); EXPECT(names && strstr(names, "app_notify") != NULL, "it is part of the active tool set"); free(names); char *prompt = cst_tools_prompt(cst_agent_config(a)); EXPECT(prompt && strstr(prompt, "app_notify") != NULL, "and the prompt describes it like any built-in"); free(prompt); void *schema = cst_tools_schema_cfg(cst_agent_config(a)); char *sj = schema ? cJSON_PrintUnformatted(schema) : NULL; EXPECT(sj && strstr(sj, "app_notify") != NULL, "tools[] carries it too"); free(sj); if (schema) cJSON_Delete(schema); /* dispatch: the front end's callback is what runs */ ctl_tool_call_t call; memset(&call, 0, sizeof(call)); snprintf(call.name, sizeof(call.name), "app_notify"); snprintf(call.args[0].key, sizeof(call.args[0].key), "message"); call.args[0].value = strdup("hi"); call.args[0].value_len = strlen("hi"); call.nargs = 1; cst_tool_result_t r; cst_tool_ctx_t ctx; memset(&ctx, 0, sizeof(ctx)); ctx.workspace_root = root; ctx.agent = a; dispatch_notify(&ctx, &call, &r, &actx); free(call.args[0].value); /* a name that is already taken by a tool that *can* run here is refused */ cst_tool_def_t dup; memset(&dup, 0, sizeof(dup)); dup.name = "read_file"; dup.desc = "not allowed to shadow a working built-in"; dup.perm = CST_TP_READ; dup.run_ex = app_notify_run; dup.owner = "app:test"; EXPECT(cst_tool_find("read_file")->owner == NULL, "read_file is a built-in"); EXPECT(cst_tool_register(&dup) != 0, "a working built-in cannot be shadowed"); /* dropping the app's tools leaves the built-in registry untouched */ EXPECT(cst_tool_unregister_owner("app:test") == 1, "the app unregisters"); EXPECT(cst_tool_find("app_notify") == NULL, "its tool is gone"); EXPECT(cst_tool_find("read_file") != NULL, "the built-ins are still there"); cst_agent_destroy(a); test_rmrf(root); if (nfail) { fprintf(stderr, "test_embed: %d failed\n", nfail); return 1; } printf("test_embed: OK\n"); return 0; }