feat: add search commands (task 0016)
Some checks failed
CI / test (pull_request) Failing after 45s

Add `search issues <query>` and `search prs <query>`, the full-text escape
hatch the forbidden `--search` flag on the list commands redirects to.

Both hit Gitea's cross-repo issue-search endpoint with the query, a `type`
of issues or pulls, and the owner param, then filter results to the current
repository client-side via each result's `repository` field — the endpoint
has no repo-name filter. The count line reports `count: N of T total` with
`T` from the filtered set (ADR 0005), never the endpoint's cross-repo
`X-Total-Count`.

The positional query is required (VALIDATION_ERROR if missing). Flags:
`--state` (default open), `--label` (comma-separated names passed straight
through as the API `labels` param), `--limit` (default 30), and `--fields`.
Default output is the locator schema (`number`, `title`, `state`, `author`,
`created`) under `issues:` / `pull_requests:` blocks matching the list
commands — search finds the number, `issue view` / `pr view` load the detail.

Covered by fixture-server tests for both types, cross-repo filtering, the
count rule, each flag, the empty state, and missing-query validation, plus
end-to-end tests against a live Gitea instance.
This commit is contained in:
2026-07-14 09:55:20 -04:00
parent 6661239afe
commit 4c3dde26b4
5 changed files with 695 additions and 7 deletions

72
test/e2e/search.test.ts Normal file
View File

@@ -0,0 +1,72 @@
import { beforeAll, describe, expect, it } from "vitest";
import { runCliTest } from "../harness.js";
import { provisionInstance, seedBranch, type E2EInstance } from "./provision.js";
/**
* The end-to-end tier for the search commands. The `/repos/issues/search`
* endpoint spans repositories and reports an unfiltered cross-repo total, so the
* live response shape and the `type`/`owner`/`q` query behavior are precisely
* what the fixture server can only simulate. Here both variants run against a
* live, disposable Gitea instance and are asserted to return real matches under
* the locator schema.
*/
const E2E_URL = process.env.GITEA_AXI_E2E_URL;
const RELATIVE_TIME = /(just now|\d+(m|h|d|mo|y) ago)/;
describe.skipIf(!E2E_URL)("end-to-end: search commands", () => {
let instance: E2EInstance;
const branch = "e2e-search-branch";
function env(overrides: Record<string, string> = {}): Record<string, string> {
return {
GITEA_AXI_API_URL: instance.baseUrl,
GITEA_AXI_TOKEN: instance.token,
GITEA_AXI_REPO: `${instance.owner}/${instance.repo}`,
...overrides,
};
}
beforeAll(async () => {
instance = await provisionInstance(E2E_URL!);
// A fresh repo has no pull requests; seed a branch with a diff and open one
// through the CLI dogfood path so `search prs` has a real match to find.
await seedBranch(instance, branch);
const created = await runCliTest(
[
"pr", "create",
"--title", "E2E search pull request",
"--head", branch,
"--base", "main",
],
{ env: env() },
);
expect(created.exitCode).toBe(0);
}, 150_000);
it("returns live issue matches under the locator schema", async () => {
const { stdout, exitCode } = await runCliTest(["search", "issues", "issue"], {
env: env(),
});
expect(exitCode).toBe(0);
// The block header is the default locator schema: the live search response
// is Issue-shaped and rendered by the same field set as `issue list`.
expect(stdout).toMatch(/^issues\[\d+\]\{number,title,state,author,created\}:$/m);
// Real matches from the current repo: a seeded open issue title, the author
// column (the instance owner), and a rendered relative-time `created`.
expect(stdout).toContain(instance.openTitles[0]!);
expect(stdout).toContain(instance.owner);
expect(stdout).toMatch(RELATIVE_TIME);
});
it("returns live pull-request matches under the locator schema", async () => {
const { stdout, exitCode } = await runCliTest(["search", "prs", "search"], {
env: env(),
});
expect(exitCode).toBe(0);
expect(stdout).toMatch(/^pull_requests\[\d+\]\{number,title,state,author,created\}:$/m);
expect(stdout).toContain("E2E search pull request");
});
});