From ef58e046f8f43bd536d1d07d8520c5cd8bbd5e47 Mon Sep 17 00:00:00 2001 From: Andriy Polanski Date: Sun, 12 Jul 2026 19:44:54 +0000 Subject: [PATCH] docs(miner): add coding-agent credential troubleshooting table (#5175) --- packages/gittensory-miner/README.md | 10 +++ ...-credential-troubleshooting-readme.test.ts | 81 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 test/unit/miner-credential-troubleshooting-readme.test.ts diff --git a/packages/gittensory-miner/README.md b/packages/gittensory-miner/README.md index c4aa8f4df1..206bdf70c3 100644 --- a/packages/gittensory-miner/README.md +++ b/packages/gittensory-miner/README.md @@ -128,6 +128,16 @@ contract and provider behavior. | `MINER_CODING_AGENT_CODEX_MODEL` | Any Codex model string accepted by the local `codex` CLI | Unset means `codex-cli` uses the CLI's own default model. Ignored by `noop`, `claude-cli`, and `agent-sdk`. | | `MINER_CODING_AGENT_TIMEOUT_MS` | Positive integer milliseconds | Unset or invalid falls back to the CLI driver's default wall-clock timeout of `120000` ms. Ignored by `noop` and `agent-sdk`. | +### Recognizing a stale or missing coding-agent credential + +When an attempt fails on a `claude-cli` / `codex-cli` provider, the CLI-subprocess driver folds the CLI's own output into a machine-readable `error` string on the attempt result. The credential/auth failure modes below map that exact string to a symptom and a concrete remediation — mirroring ORB's hosted-side [Recognizing a stale or missing credential](../../apps/gittensory-ui/src/routes/docs.self-hosting-ai-providers.tsx) table. Every string is emitted by [`cli-subprocess-driver.ts`](../gittensory-engine/src/miner/cli-subprocess-driver.ts); nothing here is speculative. + +| Error string / pattern | Symptom | Remediation | +| --- | --- | --- | +| `claude_code_error_` | The `claude` CLI ran but its `--output-format json` envelope reported `is_error: true` (e.g. `claude_code_error_invalid_api_key`) — the OAuth token is missing, rejected, or expired. `gittensory-miner doctor` reports the same condition up front as `not authenticated: set CLAUDE_CODE_OAUTH_TOKEN`. | Regenerate a long-lived token with `claude setup-token` and set `CLAUDE_CODE_OAUTH_TOKEN`, then retry. | +| `codex_no_auth` | `codex exec` exited non-zero with no structured error in its JSONL stdout and only the `Reading prompt from stdin...` banner on stderr — its `auth.json` credential is missing or expired. The driver appends its own remediation hint (`... auth.json missing or expired -- run codex auth to authenticate`). | Run `codex auth` to re-authenticate; the next attempt reads the refreshed `auth.json` with no further restart. | +| `_exit_` | Generic non-zero-exit fallback when neither structured parser matched (e.g. `codex_exit_1: ...`, `claude_exit_1: ...`). The driver appends the redacted stderr slice — an auth failure that neither parser recognized still surfaces here. | Read the appended detail; if it points at authentication, re-run `claude setup-token` or `codex auth` for the failing provider, otherwise address the reported error directly. | + ## Commands ```sh diff --git a/test/unit/miner-credential-troubleshooting-readme.test.ts b/test/unit/miner-credential-troubleshooting-readme.test.ts new file mode 100644 index 0000000000..38811873a4 --- /dev/null +++ b/test/unit/miner-credential-troubleshooting-readme.test.ts @@ -0,0 +1,81 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +// Drift guard for the miner README's "Recognizing a stale or missing coding-agent credential" table +// (#5175). The table documents credential/auth failure modes surfaced by the CLI-subprocess driver, so +// every error string it lists MUST correspond to a literal constant actually emitted by the driver source +// -- otherwise the docs silently drift from the code. This is a documentation-only change, so there is no +// production behavior to regression-test; the driver's own error-string vocabulary is covered by +// test/unit/cli-subprocess-driver.test.ts (#5168/#5169). These tests only pin the docs↔code link. + +const readmePath = join(process.cwd(), "packages/gittensory-miner/README.md"); +const driverPath = join(process.cwd(), "packages/gittensory-engine/src/miner/cli-subprocess-driver.ts"); + +/** Each row of the README table: the error token as it appears in the first column, and the literal stem + * the CLI-subprocess driver source must actually contain to back it. Placeholders (``, ``, + * ``) are dynamic; the stem is the stable substring the driver emits verbatim. */ +const CREDENTIAL_ERROR_ROWS = [ + { readmeToken: "claude_code_error_", sourceStem: "claude_code_error_" }, + { readmeToken: "codex_no_auth", sourceStem: "codex_no_auth" }, + { readmeToken: "_exit_", sourceStem: "_exit_" }, +] as const; + +/** Slice the README down to the credential-troubleshooting section (heading → next heading). */ +function readCredentialSection(): string { + const readme = readFileSync(readmePath, "utf8"); + const heading = "### Recognizing a stale or missing coding-agent credential"; + const start = readme.indexOf(heading); + expect(start).toBeGreaterThanOrEqual(0); + const rest = readme.slice(start + heading.length); + const nextHeading = rest.search(/\n#{1,3} /); + return nextHeading === -1 ? rest : rest.slice(0, nextHeading); +} + +/** Pull every inline-code token out of the first column of each markdown table row in the section. */ +function firstColumnErrorTokens(section: string): string[] { + const tokens: string[] = []; + for (const line of section.split(/\r?\n/)) { + const trimmed = line.trim(); + // A data row starts with "|", excludes the header ("Error string") and the "| --- |" separator. + if (!trimmed.startsWith("|") || trimmed.includes("---") || trimmed.includes("Error string")) continue; + const firstCell = trimmed.split("|")[1]?.trim() ?? ""; + const match = firstCell.match(/`([^`]+)`/); + if (match?.[1]) tokens.push(match[1]); + } + return tokens; +} + +describe("gittensory-miner credential-troubleshooting README (#5175)", () => { + it("adds the credential-troubleshooting section covering the three required failure modes", () => { + const section = readCredentialSection(); + expect(section).toContain("Error string / pattern"); + expect(section).toContain("Symptom"); + expect(section).toContain("Remediation"); + // The three required cases: Claude Code envelope error, Codex auth failure, and the generic fallback. + for (const { readmeToken } of CREDENTIAL_ERROR_ROWS) { + expect(section).toContain(readmeToken); + } + }); + + it("backs every documented error string with a literal constant in the CLI-subprocess driver source", () => { + const driverSrc = readFileSync(driverPath, "utf8"); + for (const { readmeToken, sourceStem } of CREDENTIAL_ERROR_ROWS) { + expect(driverSrc, `${readmeToken} must be backed by "${sourceStem}" in the driver`).toContain(sourceStem); + } + }); + + it("invariant: the table never contains a first-column error string that is not backed by a driver constant", () => { + const section = readCredentialSection(); + const driverSrc = readFileSync(driverPath, "utf8"); + const tokens = firstColumnErrorTokens(section); + // The parse actually found the rows (guards against a silently-empty extraction masking drift). + expect(tokens).toEqual(CREDENTIAL_ERROR_ROWS.map((r) => r.readmeToken)); + for (const token of tokens) { + // Strip the dynamic `<...>` placeholders; whatever literal stem remains must exist in the driver. + const stem = token.replace(/<[^>]*>/g, ""); + expect(stem.length).toBeGreaterThan(0); + expect(driverSrc, `no driver constant backs README token "${token}" (stem "${stem}")`).toContain(stem); + } + }); +});