mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
## Summary Settings > Integrations now reflects the server's actual integration readiness instead of only static `settings.toml` booleans. This adds `/api/v1/system/integrations` as the runtime source of truth, covering server config, vault credential presence, and Slack Socket Mode connection state. ## What Changed - Added shared `fabro-types` integration status models and reused them from `fabro-api` to avoid duplicate API/domain types. - Added `GET /api/v1/system/integrations` to the OpenAPI spec, Rust server routes, demo routes, and generated TypeScript client. - Reports GitHub and Slack status as `disabled`, `missing_credentials`, `configured`, `connecting`, `connected`, or `error`, with non-secret metadata and missing credential names. - Tracks Slack Socket Mode runtime state from the Slack connection loop and respects explicit `server.integrations.slack.enabled = false` even when vault tokens exist. - Updated the Integrations settings page to read the new runtime endpoint, so a vault-configured Slack setup no longer appears simply as disabled. ## Verification - `cargo build -p fabro-api` - `cargo nextest run -p fabro-api system_integrations` - `cargo nextest run -p fabro-config resolved_server_integrations_are_slack_only_for_chat` - `cargo nextest run -p fabro-slack run_event_loop_notifies_connected_status` - `cargo nextest run -p fabro-server --features test-support --test it get_system_integrations` - `cargo nextest run -p fabro-server` - `cargo +nightly-2026-04-14 fmt --check --all` - `cd apps/fabro-web && bun test app/routes/settings-integrations.test.tsx app/lib/query-keys.test.ts` - `cd apps/fabro-web && bun run typecheck` - `cd apps/fabro-web && bun run build` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
99 lines
3.2 KiB
TypeScript
99 lines
3.2 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
|
|
import { queryKeys } from "./query-keys";
|
|
import { queryKeysForRunEvent } from "./run-events";
|
|
|
|
describe("queryKeys", () => {
|
|
test("uses semantic tuples as stable SWR keys and keeps SSE URLs explicit", () => {
|
|
expect(queryKeys.auth.me()).toEqual(["auth", "me"]);
|
|
expect(queryKeys.runs.files("run 1")).toEqual([
|
|
"runs",
|
|
"files",
|
|
"run 1",
|
|
"scope",
|
|
"committed",
|
|
]);
|
|
expect(queryKeys.runs.files("run 1", { kind: "scope", scope: "all" })).toEqual([
|
|
"runs",
|
|
"files",
|
|
"run 1",
|
|
"scope",
|
|
"all",
|
|
]);
|
|
expect(
|
|
queryKeys.runs.files("run 1", {
|
|
kind: "commit",
|
|
fromSha: "abc1234",
|
|
toSha: "def5678",
|
|
}),
|
|
).toEqual(["runs", "files", "run 1", "commit", "abc1234", "def5678"]);
|
|
expect(queryKeys.runs.commits("run 1")).toEqual(["runs", "commits", "run 1"]);
|
|
expect(queryKeys.runs.graph("run-1", "TB")).toEqual(["runs", "graph", "run-1", "TB"]);
|
|
expect(queryKeys.runs.stageLog("run 1", "build step@2", 12, 34)).toEqual([
|
|
"runs",
|
|
"stage-log",
|
|
"run 1",
|
|
"build step@2",
|
|
12,
|
|
34,
|
|
]);
|
|
expect(queryKeys.runs.stageEvents("run 1", "build step")).toEqual([
|
|
"runs",
|
|
"stage-events",
|
|
"run 1",
|
|
"build step",
|
|
]);
|
|
expect(queryKeys.runs.stageContextWindow("run 1", "build step@2")).toEqual([
|
|
"runs",
|
|
"stage-context-window",
|
|
"run 1",
|
|
"build step@2",
|
|
]);
|
|
expect(queryKeys.runs.sandbox("run 1")).toEqual(["runs", "sandbox", "run 1"]);
|
|
expect(queryKeys.system.integrations()).toEqual(["system", "integrations"]);
|
|
expect(queryKeys.system.attachUrl()).toBe("/api/v1/attach");
|
|
expect(queryKeys.runs.attachUrl("run 1")).toBe("/api/v1/runs/run%201/attach");
|
|
});
|
|
|
|
test("event-mapped keys match query hook resources", () => {
|
|
expect(queryKeysForRunEvent("run-1", "checkpoint.completed")).toEqual(
|
|
[
|
|
...queryKeys.runs.filesAllScopes("run-1"),
|
|
queryKeys.runs.commits("run-1"),
|
|
],
|
|
);
|
|
expect(queryKeysForRunEvent("run-1", "stage.completed", "stage-1")).toEqual([
|
|
queryKeys.runs.stages("run-1"),
|
|
queryKeys.runs.billing("run-1"),
|
|
queryKeys.runs.events("run-1", 1000),
|
|
queryKeys.runs.graph("run-1", "LR"),
|
|
queryKeys.runs.graph("run-1", "TB"),
|
|
queryKeys.runs.detail("run-1"),
|
|
queryKeys.runs.stageEvents("run-1", "stage-1"),
|
|
queryKeys.runs.stageContextWindow("run-1", "stage-1"),
|
|
]);
|
|
expect(queryKeysForRunEvent("run-1", "run.title.updated")).toEqual([
|
|
queryKeys.runs.detail("run-1"),
|
|
]);
|
|
});
|
|
|
|
test("agent activity events invalidate per-stage resources", () => {
|
|
for (const event of [
|
|
"stage.prompt",
|
|
"agent.message",
|
|
"agent.tool.started",
|
|
"agent.tool.completed",
|
|
"command.started",
|
|
"command.completed",
|
|
]) {
|
|
expect(queryKeysForRunEvent("run-1", event, "stage-1")).toEqual([
|
|
queryKeys.runs.stageEvents("run-1", "stage-1"),
|
|
queryKeys.runs.stageContextWindow("run-1", "stage-1"),
|
|
]);
|
|
}
|
|
});
|
|
|
|
test("agent activity events without a node_id invalidate nothing", () => {
|
|
expect(queryKeysForRunEvent("run-1", "agent.message")).toEqual([]);
|
|
});
|
|
});
|