mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-10 22:43:37 +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)
117 lines
3.4 KiB
TypeScript
117 lines
3.4 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
import type {
|
|
SystemIntegrationsResponse,
|
|
SystemIntegrationStatus,
|
|
} from "@qltysh/fabro-api-client";
|
|
import TestRenderer, { act } from "react-test-renderer";
|
|
import { setupReactTestEnv } from "../lib/test-utils";
|
|
|
|
let systemIntegrations: SystemIntegrationsResponse | undefined;
|
|
let teardownReactTestEnv: (() => void) | undefined;
|
|
|
|
mock.module("../lib/queries", () => ({
|
|
useSystemIntegrations: () => ({ data: systemIntegrations }),
|
|
}));
|
|
|
|
const { default: SettingsIntegrations } = await import("./settings-integrations");
|
|
|
|
const mountedRenderers: TestRenderer.ReactTestRenderer[] = [];
|
|
|
|
function renderSettingsIntegrations() {
|
|
let renderer: TestRenderer.ReactTestRenderer | undefined;
|
|
act(() => {
|
|
renderer = TestRenderer.create(<SettingsIntegrations />);
|
|
});
|
|
mountedRenderers.push(renderer!);
|
|
return renderer!;
|
|
}
|
|
|
|
function textContent(node: ReturnType<TestRenderer.ReactTestRenderer["toJSON"]>): string {
|
|
if (node == null || typeof node === "boolean") return "";
|
|
if (typeof node === "string" || typeof node === "number") return String(node);
|
|
if (Array.isArray(node)) return node.map(textContent).join("");
|
|
return node.children?.map(textContent).join("") ?? "";
|
|
}
|
|
|
|
function sampleStatus(
|
|
overrides: Partial<SystemIntegrationStatus> = {},
|
|
): SystemIntegrationStatus {
|
|
return {
|
|
provider: "slack",
|
|
enabled: true,
|
|
configured: true,
|
|
status: "connected",
|
|
missing_credentials: [],
|
|
connection: {
|
|
kind: "socket_mode",
|
|
status: "connected",
|
|
last_connected_at: "2026-05-26T04:00:00Z",
|
|
last_error: null,
|
|
},
|
|
metadata: {},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function sampleIntegrations(
|
|
slack: Partial<SystemIntegrationStatus> = {},
|
|
): SystemIntegrationsResponse {
|
|
return {
|
|
data: [
|
|
sampleStatus({
|
|
provider: "github",
|
|
status: "configured",
|
|
connection: null,
|
|
metadata: { strategy: "app", slug: "fabro-sh" },
|
|
}),
|
|
sampleStatus({
|
|
metadata: { default_channel: "#fabro" },
|
|
...slack,
|
|
}),
|
|
],
|
|
};
|
|
}
|
|
|
|
describe("SettingsIntegrations route", () => {
|
|
beforeEach(() => {
|
|
teardownReactTestEnv = setupReactTestEnv();
|
|
});
|
|
|
|
afterEach(() => {
|
|
act(() => {
|
|
for (const renderer of mountedRenderers.splice(0)) {
|
|
renderer.unmount();
|
|
}
|
|
});
|
|
systemIntegrations = undefined;
|
|
teardownReactTestEnv?.();
|
|
teardownReactTestEnv = undefined;
|
|
});
|
|
|
|
test("renders Slack runtime connection status", () => {
|
|
systemIntegrations = sampleIntegrations();
|
|
|
|
const renderer = renderSettingsIntegrations();
|
|
const text = textContent(renderer.toJSON());
|
|
|
|
expect(text).toContain("Slack");
|
|
expect(text).toContain("Connected");
|
|
expect(text).toContain("channel: #fabro");
|
|
expect(text).not.toContain("Disabled");
|
|
});
|
|
|
|
test("renders missing Slack credential names", () => {
|
|
systemIntegrations = sampleIntegrations({
|
|
configured: false,
|
|
status: "missing_credentials",
|
|
missing_credentials: ["SLACK_APP_TOKEN", "SLACK_BOT_TOKEN"],
|
|
connection: null,
|
|
});
|
|
|
|
const renderer = renderSettingsIntegrations();
|
|
const text = textContent(renderer.toJSON());
|
|
|
|
expect(text).toContain("Missing credentials");
|
|
expect(text).toContain("missing: SLACK_APP_TOKEN, SLACK_BOT_TOKEN");
|
|
});
|
|
});
|