feat: scaffold CLI and minimal issue list (task 0001)

Tracer bullet for gitea-axi: runnable npm package on axi-sdk-js with
gitea-js as the sole HTTP layer, ESM on Node 20+.

- issue list with --state/--limit, default fields, count line from
  X-Total-Count, type=issues guard, explicit empty state, and next-step
  suggestions
- repo context detection from the git origin remote (SSH/scp/HTTPS),
  tea credential discovery with the three-way login-matching split, and
  -R/--repo and --login overrides (flag > env > auto)
- token retrieval via tea login helper get: tea's login list JSON
  carries no token (ADR 0001 amended)
- full AxiError classification table with path-based 404 split, TOON
  errors on stdout, exit codes 0/1/2
- test mode (GITEA_AXI_API_URL/TOKEN/REPO) suppressing subprocesses,
  fixture server, and vitest suites driving the CLI seam (50 tests)
This commit is contained in:
2026-07-11 07:11:07 -04:00
parent 21a075f8cd
commit 38026f963d
34 changed files with 3892 additions and 0 deletions

119
test/context.test.ts Normal file
View File

@@ -0,0 +1,119 @@
import { afterEach, describe, expect, it } from "vitest";
import { startFixtureServer, type FixtureServer } from "./fixture-server.js";
import { runCliTest, testModeEnv } from "./harness.js";
let server: FixtureServer | undefined;
afterEach(async () => {
await server?.close();
server = undefined;
});
describe("context overrides", () => {
it("prefers the -R flag over GITEA_AXI_REPO", async () => {
server = await startFixtureServer([
{ method: "GET", path: "/api/v1/repos/flagowner/flagrepo/issues", body: [] },
]);
const { exitCode } = await runCliTest(
["issue", "list", "-R", "flagowner/flagrepo"],
{ env: testModeEnv(server.url) },
);
expect(exitCode).toBe(0);
expect(server.requests[0]!.path).toBe("/api/v1/repos/flagowner/flagrepo/issues");
});
it("accepts -R before the command", async () => {
server = await startFixtureServer([
{ method: "GET", path: "/api/v1/repos/flagowner/flagrepo/issues", body: [] },
]);
const { exitCode } = await runCliTest(
["-R", "flagowner/flagrepo", "issue", "list"],
{ env: testModeEnv(server.url) },
);
expect(exitCode).toBe(0);
expect(server.requests[0]!.path).toBe("/api/v1/repos/flagowner/flagrepo/issues");
});
it("accepts the --repo=OWNER/NAME form", async () => {
server = await startFixtureServer([
{ method: "GET", path: "/api/v1/repos/flagowner/flagrepo/issues", body: [] },
]);
const { exitCode } = await runCliTest(
["issue", "list", "--repo=flagowner/flagrepo"],
{ env: testModeEnv(server.url) },
);
expect(exitCode).toBe(0);
expect(server.requests[0]!.path).toBe("/api/v1/repos/flagowner/flagrepo/issues");
});
it("includes the -R override in next-step suggestions when context came from a flag", async () => {
server = await startFixtureServer([
{ method: "GET", path: "/api/v1/repos/flagowner/flagrepo/issues", body: [] },
]);
const { stdout } = await runCliTest(
["issue", "list", "-R", "flagowner/flagrepo"],
{ env: testModeEnv(server.url) },
);
expect(stdout).toContain("-R flagowner/flagrepo");
});
it("includes the -R override in suggestions when context came from the environment", async () => {
server = await startFixtureServer([
{ method: "GET", path: "/api/v1/repos/testowner/testrepo/issues", body: [] },
]);
const { stdout } = await runCliTest(["issue", "list"], {
env: testModeEnv(server.url),
});
expect(stdout).toContain("-R testowner/testrepo");
});
it("fails with REPO_NOT_FOUND when test mode has no repository context", async () => {
server = await startFixtureServer([]);
const { stdout, exitCode } = await runCliTest(["issue", "list"], {
env: { GITEA_AXI_API_URL: server.url, GITEA_AXI_TOKEN: "test-token" },
});
expect(exitCode).toBe(1);
expect(stdout).toContain("code: REPO_NOT_FOUND");
expect(stdout).toContain("GITEA_AXI_REPO");
});
it("rejects a malformed -R value with exit code 2", async () => {
server = await startFixtureServer([]);
const { stdout, exitCode } = await runCliTest(
["issue", "list", "-R", "not-a-repo"],
{ env: testModeEnv(server.url) },
);
expect(exitCode).toBe(2);
expect(stdout).toContain("code: VALIDATION_ERROR");
expect(stdout).toContain("OWNER/NAME");
});
it("rejects -R without a value with exit code 2", async () => {
const { stdout, exitCode } = await runCliTest(["issue", "list", "-R"], {
env: { GITEA_AXI_API_URL: "http://127.0.0.1:1" },
});
expect(exitCode).toBe(2);
expect(stdout).toContain("code: VALIDATION_ERROR");
});
it("test mode never spawns git or tea", async () => {
// An empty PATH makes any subprocess spawn fail loudly; success here
// proves the git and tea subprocesses were suppressed.
server = await startFixtureServer([
{ method: "GET", path: "/api/v1/repos/testowner/testrepo/issues", body: [] },
]);
const { exitCode } = await runCliTest(["issue", "list"], {
env: { ...testModeEnv(server.url), PATH: "" },
});
expect(exitCode).toBe(0);
});
});