Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions src/commands/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ describe("searchAction", () => {
}

it("calls unified search service with parsed targets and filters", async () => {
const search = mock(() => Promise.resolve(defaultUnifiedSearchOutcome));
const search = mock((_: UnifiedSearchParams) =>
Promise.resolve(defaultUnifiedSearchOutcome),
);
const deps = createDeps({
codeNavigationService: createMockCodeNavigationService({ search }),
});
Expand Down Expand Up @@ -86,7 +88,9 @@ describe("searchAction", () => {
});

it("passes repeatable --source values through as source filters", async () => {
const search = mock(() => Promise.resolve(defaultUnifiedSearchOutcome));
const search = mock((_: UnifiedSearchParams) =>
Promise.resolve(defaultUnifiedSearchOutcome),
);
const deps = createDeps({
codeNavigationService: createMockCodeNavigationService({ search }),
});
Expand All @@ -109,6 +113,28 @@ describe("searchAction", () => {
consoleSpy.mockRestore();
});

it("preserves omitted repo refs for CLI discovery search targets", async () => {
const search = mock((_: UnifiedSearchParams) =>
Promise.resolve(defaultUnifiedSearchOutcome),
);
const deps = createDeps({
codeNavigationService: createMockCodeNavigationService({ search }),
});
const consoleSpy = spyOn(console, "log").mockImplementation(() => {});

await searchAction(
"router middleware",
{ in: ["https://github.com/expressjs/express"] },
deps,
);

const call = search.mock.calls[0]?.[0];
expect(call?.targets[0]).toEqual({
repoUrl: "https://github.com/expressjs/express",
});
consoleSpy.mockRestore();
});

it("uses the shared search default limit and preserves explicit limits", async () => {
const search = mock((_: UnifiedSearchParams) =>
Promise.resolve(defaultUnifiedSearchOutcome),
Expand Down
13 changes: 4 additions & 9 deletions src/commands/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,9 @@ function formatUnifiedSearchMetadata(

const lines: string[] = [];
if (entry.locator.pageId) {
lines.push(` ${dim("pageId:", useColors)} ${entry.locator.pageId}`);
if (entry.type === "documentation_page") {
lines.push(` ${dim("pageId:", useColors)} ${entry.locator.pageId}`);
}
}

const sourceBadge =
Expand All @@ -822,18 +824,11 @@ function formatUnifiedSearchMetadata(
: entry.locator.sourceKind?.toLowerCase() === "crawled"
? "[crawled]"
: undefined;
if (entry.locator.sourceUrl) {
if (entry.locator.sourceUrl && entry.type === "documentation_page") {
lines.push(
` ${dim("source:", useColors)} ${sourceBadge ? `${sourceBadge} ` : ""}${entry.locator.sourceUrl}`,
);
}

if (entry.type === "repository_doc" && entry.locator.filePath) {
const ref = entry.locator.requestedRef ?? entry.locator.gitRef;
lines.push(
` ${dim("file:", useColors)} ${entry.locator.filePath}${ref ? ` @ ${ref}` : ""}`,
);
}

return lines;
}
17 changes: 8 additions & 9 deletions src/services/client-update-required-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@ export class ClientUpdateRequiredError extends Error {
}

export function isClientUpdateRequiredGraphQLError(input: {
message: string;
message?: string;
code?: string;
}): boolean {
if (input.code === "CLIENT_UPDATE_REQUIRED") {
return true;
}

const message = input.message;
if (!isGraphQLSchemaMismatchMessage(message)) {
return false;
}
return input.code === "CLIENT_UPDATE_REQUIRED";
}

export function isGraphQLSchemaMismatchError(input: {
message: string;
code?: string;
}): boolean {
if (!isGraphQLSchemaMismatchMessage(input.message)) return false;
return (
!input.code ||
input.code === "GRAPHQL_VALIDATION_FAILED" ||
Expand Down
83 changes: 80 additions & 3 deletions src/services/code-navigation-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
mock,
spyOn,
} from "bun:test";
import { ClientUpdateRequiredError } from "./client-update-required-error.js";
import {
CodeNavigationBackendError,
CodeNavigationFileNotFoundError,
Expand Down Expand Up @@ -668,6 +667,15 @@ describe("CodeNavigationServiceImpl", () => {
expect(parsed.operation).toBe("search");
expect(parsed.graphqlQuery).toContain("query UnifiedSearch(");
expect(parsed.graphqlQuery).toContain("filters: $filters");
expect(parsed.graphqlQuery).toContain("requestedTargetLabel");
expect(parsed.graphqlQuery).toContain("freshTargetLabel");
expect(parsed.graphqlQuery).toContain("servedTargetLabel");
expect(parsed.graphqlQuery).toContain("freshness");
expect(parsed.graphqlQuery).toContain("requestedSources");
expect(parsed.graphqlQuery).toContain("targetMode");
expect(parsed.graphqlQuery).toContain("requestedTargets");
expect(parsed.graphqlQuery).toContain("resolvedRequested");
expect(parsed.graphqlQuery).toContain("requestedRefKind");
expect(parsed.variables).toEqual({
targets: [{ registry: "NPM", name: "express" }],
query: "router middleware secret text",
Expand Down Expand Up @@ -761,7 +769,7 @@ describe("CodeNavigationServiceImpl", () => {
}
});

it("classifies GraphQL schema mismatch as ClientUpdateRequiredError", async () => {
it("classifies GraphQL schema mismatch as backend protocol error", async () => {
mockFetch(() =>
Promise.resolve(
new Response(
Expand All @@ -787,7 +795,76 @@ describe("CodeNavigationServiceImpl", () => {
targets: [{ registry: "NPM", packageName: "express" }],
query: "middleware",
}),
).rejects.toBeInstanceOf(ClientUpdateRequiredError);
).rejects.toMatchObject({
name: "CodeNavigationBackendError",
message: expect.stringContaining("Backend protocol mismatch"),
});
});

it("exposes GraphQL schema mismatch details when code-nav-wire debug is enabled", async () => {
process.env.GITHITS_DEBUG = "code-nav-wire";
const stderrSpy = spyOn(process.stderr, "write").mockImplementation(
() => true as never,
);
mockFetch(() =>
Promise.resolve(
new Response(
JSON.stringify({
errors: [
{
message: 'Cannot query field "search" on type "Query".',
extensions: { code: "GRAPHQL_VALIDATION_FAILED" },
},
],
}),
{ status: 200 },
),
),
);
const service = new CodeNavigationServiceImpl(
BASE_URL,
createMockTokenProvider(),
);

await expect(
service.search({
targets: [{ registry: "NPM", packageName: "express" }],
query: "middleware",
}),
).rejects.toMatchObject({
name: "CodeNavigationBackendError",
message: 'Cannot query field "search" on type "Query".',
});
stderrSpy.mockRestore();
});

it("honors explicit backend CLIENT_UPDATE_REQUIRED errors", async () => {
mockFetch(() =>
Promise.resolve(
new Response(
JSON.stringify({
errors: [
{
message: "Client version is no longer supported.",
extensions: { code: "CLIENT_UPDATE_REQUIRED" },
},
],
}),
{ status: 200 },
),
),
);
const service = new CodeNavigationServiceImpl(
BASE_URL,
createMockTokenProvider(),
);

await expect(
service.search({
targets: [{ registry: "NPM", packageName: "express" }],
query: "middleware",
}),
).rejects.toMatchObject({ name: "ClientUpdateRequiredError" });
});

it("sends grepRepo variables with the correct shape", async () => {
Expand Down
Loading
Loading