Files
paseo/packages/app/src/git/use-forge-search-query.test.ts
T
nllptrxandMohamed Boudra a8ebd390fa feat(forge): pluggable forge abstraction + GitLab and Gitea/Forgejo/Codeberg (#1913)
* refactor(forge): forge-neutral foundation (GitHub-only)

Decouple git-hosting from GitHub behind a neutral abstraction (issue #1616), GitHub-only for now; existing GitHub behaviour is unchanged.

- Forge manifest, neutral ForgeService contract, forge registry + resolver, and a client forge-module registry.
- GitHub code renamed to the neutral shape; PR/Issue attachment wording preserved.
- forge.search.response enums parse tolerantly (unknown kind/auth state degrade instead of breaking the client).
- createPullRequest reports typed CLI/auth errors instead of a generic message.
- forge-resolver host/remote caches are LRU-bounded.
- Forge host trust is explicit: only a known cloud host or a CLI-authenticated host is ever talked to; an unauthenticated GitHub Enterprise host fails resolution instead of routing to github.com.
- Docs: forge-providers guide, glossary and i18n forge-copy conventions, architecture and rpc-namespacing terminology.
- Vitest React Native mocks (unistyles, svg, linking, lucide) consolidated into shared aliased test-stubs.

* feat(forge): GitLab adapter, forge-aware UI, pipelines and approvals

GitLab adapter over the glab CLI on the neutral contracts: MR status, forge-aware UI, pipeline tree, and N-of-M approvals.

- threadIsResolved is part of the neutral timeline item.
- Pipeline load failures show an error instead of an empty section.
- Manual pipeline jobs render as pending.
- Fork/detached MR head pipelines are fetched by MR iid (glab ci get --merge-request).

* feat(forge): Gitea family adapter (Gitea, Forgejo, Codeberg)

One adapter over the tea CLI serving Gitea, Forgejo, and Codeberg on the neutral contracts.

- CI status aggregates commit statuses and Actions runs together.
- Gitea's terminal "warning" state maps to failure on server and client.
- Gitea Actions check details are reachable from the PR pane by workflowRunId.

* refactor(forge): localize compatibility handling

* test(forge): expect normalized GitLab facts

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-17 15:03:26 +08:00

283 lines
7.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { buildForgeSearchQueryOptions, forgeSearchQueryKey } from "./use-forge-search-query";
describe("forgeSearchQueryKey", () => {
it("keeps the shared cache key shape for no-kinds searches", () => {
expect(forgeSearchQueryKey("server-1", "/repo", " 123 ")).toEqual([
"forge-search",
"server-1",
"/repo",
"forge",
"123",
]);
});
it("adds a deterministic kinds key when kinds are specified", () => {
expect(forgeSearchQueryKey("server-1", "/repo", "123", ["change_request", "issue"])).toEqual([
"forge-search",
"server-1",
"/repo",
"forge",
"123",
"change_request,issue",
]);
});
it("separates legacy GitHub fallback results from forge search results", () => {
expect(forgeSearchQueryKey("server-1", "/repo", "123", undefined, "github")).toEqual([
"forge-search",
"server-1",
"/repo",
"github",
"123",
]);
});
});
describe("buildForgeSearchQueryOptions", () => {
it("forwards kinds to the forge search request when specified", async () => {
const requests: unknown[] = [];
const query = buildForgeSearchQueryOptions({
client: {
async searchForge(options) {
requests.push(options);
return {
items: [],
authState: "authenticated",
error: null,
requestId: "request-1",
};
},
},
serverId: "server-1",
cwd: "/repo",
query: " 123 ",
kinds: ["change_request"],
enabled: true,
});
await query.queryFn();
expect(requests).toEqual([
{ cwd: "/repo", query: "123", limit: 20, kinds: ["change_request"] },
]);
});
it("uses the legacy GitHub search request when forge search is unsupported", async () => {
const forgeRequests: unknown[] = [];
const githubRequests: unknown[] = [];
const query = buildForgeSearchQueryOptions({
client: {
async searchForge(options) {
forgeRequests.push(options);
return {
items: [],
authState: "authenticated",
error: null,
requestId: "forge-request",
};
},
async searchGitHub(options) {
githubRequests.push(options);
return {
items: [],
featuresEnabled: true,
authState: "authenticated",
githubFeaturesEnabled: true,
error: null,
requestId: "github-request",
};
},
},
serverId: "server-1",
cwd: "/repo",
query: " 456 ",
kinds: ["issue"],
enabled: true,
supportsForgeSearch: false,
});
await query.queryFn();
expect(forgeRequests).toEqual([]);
expect(githubRequests).toEqual([
{ cwd: "/repo", query: "456", limit: 20, kinds: ["github-issue"] },
]);
});
it("normalizes legacy GitHub PR items into neutral change-request items", async () => {
const query = buildForgeSearchQueryOptions({
client: {
async searchForge() {
throw new Error("unexpected forge search");
},
async searchGitHub() {
return {
items: [
{
kind: "pr" as const,
number: 17,
title: "Fix search",
url: "https://github.com/acme/repo/pull/17",
state: "open",
body: null,
labels: ["bug"],
baseRefName: "main",
headRefName: "fix-search",
},
],
featuresEnabled: true,
authState: "authenticated" as const,
githubFeaturesEnabled: true,
error: null,
requestId: "github-request",
};
},
},
serverId: "server-1",
cwd: "/repo",
query: " 456 ",
enabled: true,
supportsForgeSearch: false,
});
const result = await query.queryFn();
expect(result.items).toEqual([
{
kind: "change_request",
number: 17,
title: "Fix search",
url: "https://github.com/acme/repo/pull/17",
state: "open",
body: null,
labels: ["bug"],
baseRefName: "main",
headRefName: "fix-search",
},
]);
});
it("interprets modern search payloads at the query boundary", async () => {
const query = buildForgeSearchQueryOptions({
client: {
async searchForge() {
return {
items: [
{
kind: "issue",
number: 23,
title: "Keep this",
url: "https://gitlab.com/acme/repo/-/issues/23",
state: "open",
body: null,
labels: [],
},
{ kind: "future_kind", futureField: true },
],
authState: "future_auth_state",
error: null,
requestId: "forge-request",
};
},
},
serverId: "server-1",
cwd: "/repo",
query: "23",
enabled: true,
supportsForgeSearch: true,
});
const result = await query.queryFn();
expect(result.items).toHaveLength(1);
expect(result.items[0]?.kind).toBe("issue");
expect(result.authState).toBe("unauthenticated");
});
it("derives legacy search auth from legacy feature flags", async () => {
const query = buildForgeSearchQueryOptions({
client: {
async searchForge() {
throw new Error("unexpected forge search");
},
async searchGitHub() {
return {
items: [],
githubFeaturesEnabled: false,
error: null,
requestId: "github-request",
};
},
},
serverId: "server-1",
cwd: "/repo",
query: "23",
enabled: true,
supportsForgeSearch: false,
});
expect((await query.queryFn()).authState).toBe("unauthenticated");
});
it("invokes forge search bound to the client so this-dependent methods work", async () => {
const client = new ThisDependentSearchClient();
const query = buildForgeSearchQueryOptions({
client,
serverId: "server-1",
cwd: "/repo",
query: " 789 ",
enabled: true,
supportsForgeSearch: true,
});
const result = await query.queryFn();
expect(result.requestId).toBe("forge.search.request");
expect(client.requests).toEqual([{ cwd: "/repo", query: "789", limit: 20 }]);
});
it("invokes the legacy GitHub search bound to the client", async () => {
const client = new ThisDependentSearchClient();
const query = buildForgeSearchQueryOptions({
client,
serverId: "server-1",
cwd: "/repo",
query: " 789 ",
enabled: true,
supportsForgeSearch: false,
});
const result = await query.queryFn();
expect(result.requestId).toBe("github_search_request");
expect(client.requests).toEqual([{ cwd: "/repo", query: "789", limit: 20 }]);
});
});
class ThisDependentSearchClient {
readonly requests: unknown[] = [];
private send(requestId: string, options: { cwd: string; query: string; limit?: number }) {
this.requests.push(options);
return Promise.resolve({
items: [],
featuresEnabled: true,
authState: "authenticated" as const,
githubFeaturesEnabled: true,
error: null,
requestId,
});
}
async searchForge(options: { cwd: string; query: string; limit?: number }) {
return this.send("forge.search.request", options);
}
async searchGitHub(options: { cwd: string; query: string; limit?: number }) {
return this.send("github_search_request", options);
}
}