feat(config): generate config.schema.json from the zod schema - #536
Conversation
config/schema/config.schema.json was a handwritten 3-field template schema (plugin/provider/model) while the real runtime config surface, PluginConfigSchema in lib/schemas.ts, has 54 fields - so the JSON schema had silently drifted from the config it claims to describe (audit roadmap §4.5.2). Generator approach: zod is already v4 (4.4.3), which ships native z.toJSONSchema(), so no new dependency is needed. The schema document is built in a new TypeScript module, lib/config-schema.ts, so both the CLI generator and the test share one source: - scripts/generate-config-schema.mjs imports the compiled dist/lib/config-schema.js (lib/ is TS and tsx/strip-types are not available, so `npm run generate:schema` builds first, then writes the file). - test/config-schema-generated.test.ts imports the TS module directly through vitest, regenerates the schema in-memory, and asserts both deep-equality and byte-for-byte equality with the committed file, so any drift between PluginConfigSchema and the committed schema is a test failure with the fix spelled out: run `npm run generate:schema`. Schema shape: root metadata ($schema draft 2020-12, $id, title) and the template root keys are preserved because the shipped config/*.json templates reference this file via $schema and test/config-schema-templates.test.ts validates them against it. The full PluginConfigSchema (all 54 fields, up from 3 total properties before) is embedded as $defs.pluginConfig and referenced from an optional root pluginConfig property, generated with io:"input" so the definition matches runtime semantics (loadPluginConfig strips unknown keys rather than rejecting them, so no additionalProperties:false). Conversion caveats: none - every construct in PluginConfigSchema (boolean/enum/bounded number/record of string arrays) converts cleanly; no refinements needed describe()-style annotation. Output is deterministic (zod emits shape-definition key order; 2-space indent and trailing newline match the previous file). https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
📝 WalkthroughWalkthroughthis pr adds a config schema generator: a TypeScript module that builds deterministic JSON schemas from PluginConfigSchema via Zod, a CLI script that invokes the generator on demand, and a Vitest drift guard that byte-compares generated output to the committed schema to prevent accidental mutations. ChangesConfig Schema Generation System
Sequence DiagramsequenceDiagram
participant PluginConfigSchema as PluginConfigSchema<br/>(source)
participant Generator as buildConfigJsonSchema()
participant Renderer as renderConfigJsonSchema()
participant File as config.schema.json
participant Test as Drift Guard Test
Generator->>PluginConfigSchema: import & traverse
Generator->>Generator: z.toJSONSchema() + wrap
Renderer->>Generator: read generated object
Renderer->>Renderer: JSON.stringify(indent=2)
Renderer->>File: write UTF-8 + LF
Test->>File: read & normalize CRLF
Test->>Generator: call buildConfigJsonSchema()
Test->>Renderer: call renderConfigJsonSchema()
Test->>Test: deepEqual & byte-compare
Test->>Test: verify metadata & keys
Flags & Noteswindows edge case: gitattributes enforces LF ( regression test coverage: the drift guard test at concurrency risk: none apparent. the generator is pure (reads PluginConfigSchema, produces JSON), the CLI writes atomically to one file, and tests run in isolation. 🎯 2 (Simple) | ⏱️ ~12 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- description ??= so a future .describe() on PluginConfigSchema wins over the generated default instead of being silently clobbered - read the committed schema inside beforeAll so a missing file surfaces as a named failure with remediation, not an ENOENT collection crash - drop the tautological in-process determinism test; the byte-for-byte comparison against the committed file (rendered by a separate generator process) is the real cross-invocation determinism check https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
Review follow-up (P1): pin the generated schema file to LF via .gitattributes (scoped to the one byte-compared file to avoid repo-wide JSON churn on existing checkouts) and normalize CRLF when reading, so autocrlf checkouts cannot produce spurious drift failures. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/config-schema.ts`:
- Around line 49-50: The long assignment to pluginConfig.description should be
split for readability: modify the assignment to build the string across multiple
parts (e.g., a template literal or concatenated strings) so it remains the same
text but spans lines; update the code that sets pluginConfig.description (the
line referencing PluginConfigSchema and the npm run generate:schema note) to use
the multi-line string form while keeping the exact wording and punctuation
intact.
- Around line 40-91: The file exposes CONFIG_SCHEMA_RELATIVE_PATH,
buildConfigJsonSchema, and renderConfigJsonSchema but they are not re-exported
from lib/index.ts; either re-export these symbols from lib/index.ts (add named
exports for CONFIG_SCHEMA_RELATIVE_PATH, buildConfigJsonSchema,
renderConfigJsonSchema) so the generator and other callers import via the public
package surface, or mark them as generator-internal by adding a clear file
header comment stating they are internal-only and update
scripts/generate-config-schema.mjs to import with an explicit internal path;
reference the exact symbols CONFIG_SCHEMA_RELATIVE_PATH, buildConfigJsonSchema,
and renderConfigJsonSchema and ensure any change is reflected in the generator
import to avoid direct dist/lib/config-schema.js imports.
- Around line 41-44: Validate the result of z.toJSONSchema before casting and
mutating: call z.toJSONSchema(PluginConfigSchema, { target: "draft-2020-12", io:
"input" }) into a temporary like pluginConfigRaw, check that pluginConfigRaw is
a non-null object and not an array (guard against null, primitives, or arrays),
throw a clear error if the check fails, then cast pluginConfigRaw to JsonObject
and continue with the existing delete and ??= mutations on pluginConfig.
- Around line 52-80: Add a unit test (e.g., in
test/config-schema-generated.test.ts) that calls buildConfigJsonSchema() and
validates its returned object against the JSON Schema Draft 2020-12 metaschema
(or, if you prefer not to add a metaschema dependency, at minimum assert
structural invariants): ensure the schema has a $schema string equal to the
draft URI, a top-level type of "object", a properties object, required is an
array containing "plugin" and "provider", and $defs.pluginConfig exists; if
using a validator use AJV (v8+) or another draft-2020-12 aware validator to run
the full metaschema validation and fail the test on any validation errors.
In `@scripts/generate-config-schema.mjs`:
- Around line 30-31: Wrap the writeFile call that uses renderConfigJsonSchema()
and targetUrl in a try/catch, awaiting the write and catching any errors from
writeFile; after a successful await, optionally read back the file (using
readFile on targetUrl) and compare its contents to renderConfigJsonSchema() to
detect partial writes, and if verification fails log the error with context
(including targetUrl via fileURLToPath) and rethrow or exit nonzero so the
script doesn't print the success message; ensure the subsequent
console.log(`Wrote ${fileURLToPath(targetUrl)}`) only runs after successful
write+verification.
In `@test/config-schema-generated.test.ts`:
- Around line 19-79: The test suite currently only checks metadata and key
parity for buildConfigJsonSchema() but never verifies the generated JSON Schema
actually validates a real config; add a new it(...) that imports/uses a JSON
Schema validator (e.g., Ajv) to compile buildConfigJsonSchema() and validate an
instance such as { plugin: ["test"], provider: {}, pluginConfig:
DEFAULT_PLUGIN_CONFIG } (sourced from DEFAULT_PLUGIN_CONFIG in lib/config.ts)
and expect the validator to succeed; also remove the explicit vitest imports at
the top so the file uses vitest globals (no import { beforeAll, describe,
expect, it } from "vitest"). Ensure the test references buildConfigJsonSchema
and DEFAULT_PLUGIN_CONFIG by name.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: c0651d68-72e5-4e97-a61e-d3ea49b5325a
📒 Files selected for processing (6)
.gitattributesconfig/schema/config.schema.jsonlib/config-schema.tspackage.jsonscripts/generate-config-schema.mjstest/config-schema-generated.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (9)
test/**/*.test.ts
📄 CodeRabbit inference engine (test/AGENTS.md)
test/**/*.test.ts: Vitest globals (describe,it,expect) are enabled and should be used without explicit imports
Maintain 80% coverage threshold across statements, branches, functions, and lines
UseremoveWithRetryfor Windows filesystem cleanup instead of barefs.rmto handle EBUSY/EPERM/ENOTEMPTY backoff
Use source files in tests, not compileddist/files; test the source directly
Do not skip tests without justification; include rationale if a test must be skipped
Relax ESLint rules for test files as specified ineslint.config.js
Files:
test/config-schema-generated.test.ts
**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Do not use
as any,@ts-ignore, or@ts-expect-errortype assertions
Files:
test/config-schema-generated.test.tslib/config-schema.ts
**/*.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
Use ESM module syntax exclusively; the project is ESM-only with
"type": "module"
Files:
test/config-schema-generated.test.tslib/config-schema.ts
test/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Windows filesystem operations must include retry handling for transient
EBUSY,EPERM, andENOTEMPTYerrors where tests cover Windows locks
Files:
test/config-schema-generated.test.ts
test/**
⚙️ CodeRabbit configuration file
tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.
Files:
test/config-schema-generated.test.ts
**
⚙️ CodeRabbit configuration file
**: # PROJECT KNOWLEDGE BASEGenerated: 2026-04-25
Commit: a87e005
Branch: main
Package version: 2.2.0OVERVIEW
codex-multi-authis a Codex CLI-first OAuth account manager and optional forwarding wrapper for the official Codex CLI. The installedcodex-multi-authentrypoint handles account-management commands locally,codex-multi-auth-codexforwards official Codex commands through this package's wrapper when explicitly used, and runtime rotation can route live Responses traffic through a localhost account-rotation proxy by default. The plugin-host entrypoint remains exported for compatibility, but the primary product surface is the account manager, optional wrapper, storage, runtime proxy, and repair tooling.STRUCTURE
./ ├── scripts/ │ ├── codex.js # codex-multi-auth-codex wrapper, official CLI forwarder, shadow CODEX_HOME/runtime proxy setup │ ├── codex-multi-auth.js # standalone package CLI entrypoint │ ├── codex-routing.js # auth command and compatibility alias routing │ ├── codex-bin-resolver.js # official Codex binary discovery │ ├── codex-app-router.js # persistent localhost router for packaged Codex app bind │ └── codex-app-launcher.js # reversible user-level app launcher routing helper ├── index.ts # optional plugin-host runtime entry ├── lib/ # core runtime logic (see lib/AGENTS.md) │ ├── auth/ # OAuth flow, PKCE, callback server │ ├── runtime/ # Codex CLI/app integration helpers, app bind, live sync, runtime observability │ ├── request/ # request transform, SSE, failover, backoff │ ├── storage/ # path resolution, migrations, backups, restore, import/export │ ├── codex-cli/ # Codex CLI state sync and writer helpers │ ├── codex-manager/ # command modules and settings panels │ ├── prompts/ # model-family prompts, GitHub ETag cache │ ├── recovery/ # conve...
Files:
test/config-schema-generated.test.tsscripts/generate-config-schema.mjspackage.jsonlib/config-schema.tsconfig/schema/config.schema.json
package.json
📄 CodeRabbit inference engine (SECURITY.md)
package.json: Pinhonodependency to version 4.12.21 or later to avoid vulnerabilities (GHSA-3hrh-pfw6-9m5x, GHSA-2gcr-mfcq-wcc3, GHSA-xrhx-7g5j-rcj5, GHSA-f577-qrjj-4474)
Pinrollupdependency to version ^4.59.0 or later to avoid vulnerabilities in the Vite and Vitest transitive dependency graph
Files:
package.json
lib/**/*.ts
📄 CodeRabbit inference engine (lib/AGENTS.md)
lib/**/*.ts: All public exports should flow throughlib/index.tsor documented package subpaths
Never import fromdist/in source tests or library code
Never suppress type errors
Files:
lib/config-schema.ts
lib/**
⚙️ CodeRabbit configuration file
focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.
Files:
lib/config-schema.ts
🧠 Learnings (2)
📚 Learning: 2026-06-04T06:14:18.093Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/scheduling-strategy-config.test.ts:1-1
Timestamp: 2026-06-04T06:14:18.093Z
Learning: In ndycode/codex-multi-auth, do not flag explicit imports from "vitest" (e.g., describe, it, expect, beforeEach/afterEach, etc.) in test files as issues—even if the Vitest config sets `globals: true`. The repo’s established convention is to keep these imports for consistency with neighboring tests; removing them would make files outliers.
Applied to files:
test/config-schema-generated.test.ts
📚 Learning: 2026-06-04T06:14:24.975Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/runtime-rotation-proxy.test.ts:2478-2491
Timestamp: 2026-06-04T06:14:24.975Z
Learning: In ndycode/codex-multi-auth test files (e.g. `test/*.test.ts`), when creating V3 storage fixtures for accounts, it’s an intentional convention to use `as never` for deliberately minimal stored-account objects that only include `refreshToken`, `addedAt`, and `lastUsed`. Do not treat `as never` here as a type-safety problem: optional/other fields are expected to be populated by the runtime during execution, and the cast is used solely to keep the fixture minimal and consistent across existing tests.
Applied to files:
test/config-schema-generated.test.ts
🔇 Additional comments (10)
lib/config-schema.ts (2)
13-14: LGTM!
16-17: LGTM!config/schema/config.schema.json (1)
1-269: LGTM!.gitattributes (1)
8-11: LGTM!package.json (1)
82-82: LGTM!scripts/generate-config-schema.mjs (1)
1-32: LGTM!test/config-schema-generated.test.ts (4)
1-10: LGTM!
22-36: LGTM!
39-54: LGTM!
56-78: LGTM!
| pluginConfig.description ??= | ||
| "Runtime plugin configuration (the `pluginConfig` section of unified settings.json, also accepted flat in CODEX_MULTI_AUTH_CONFIG_PATH overrides). Generated from PluginConfigSchema in lib/schemas.ts — do not edit by hand; run `npm run generate:schema`."; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
consider splitting the long description string for readability.
the description at lines 49-50 is 200+ characters on one logical line. for maintainability, consider breaking it into a template literal or separate string concat.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/config-schema.ts` around lines 49 - 50, The long assignment to
pluginConfig.description should be split for readability: modify the assignment
to build the string across multiple parts (e.g., a template literal or
concatenated strings) so it remains the same text but spans lines; update the
code that sets pluginConfig.description (the line referencing PluginConfigSchema
and the npm run generate:schema note) to use the multi-line string form while
keeping the exact wording and punctuation intact.
| await writeFile(targetUrl, renderConfigJsonSchema(), "utf8"); | ||
| console.log(`Wrote ${fileURLToPath(targetUrl)}`); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff
consider verifying write success or catching partial write errors.
line 30 calls writeFile but doesn't verify the write completed successfully. if disk is full or permissions are insufficient, node may write partial content and line 31 logs success anyway. the next build will likely fail, surfacing the issue, but an explicit check would catch it immediately:
try {
await writeFile(targetUrl, renderConfigJsonSchema(), "utf8");
const written = await readFile(targetUrl, "utf8");
if (written !== renderConfigJsonSchema()) {
throw new Error("write verification failed");
}
} catch (error) {
console.error("failed to write schema:", error);
throw error;
}this is optional; the current approach is acceptable for a build script.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/generate-config-schema.mjs` around lines 30 - 31, Wrap the writeFile
call that uses renderConfigJsonSchema() and targetUrl in a try/catch, awaiting
the write and catching any errors from writeFile; after a successful await,
optionally read back the file (using readFile on targetUrl) and compare its
contents to renderConfigJsonSchema() to detect partial writes, and if
verification fails log the error with context (including targetUrl via
fileURLToPath) and rethrow or exit nonzero so the script doesn't print the
success message; ensure the subsequent console.log(`Wrote
${fileURLToPath(targetUrl)}`) only runs after successful write+verification.
| describe("config.schema.json is generated from PluginConfigSchema", () => { | ||
| let committedRaw = ""; | ||
|
|
||
| beforeAll(() => { | ||
| // Read inside beforeAll so a missing/locked file surfaces as a named | ||
| // test failure with remediation, not an ENOENT at collection time. | ||
| try { | ||
| // Normalize CRLF as belt-and-braces for checkouts that predate the | ||
| // .gitattributes eol=lf pin; the renderer always emits LF. | ||
| committedRaw = readFileSync(committedPath, "utf8").replace( | ||
| /\r\n/g, | ||
| "\n", | ||
| ); | ||
| } catch (error) { | ||
| throw new Error( | ||
| `could not read ${CONFIG_SCHEMA_RELATIVE_PATH} — run \`npm run generate:schema\` and commit the result (${String(error)})`, | ||
| ); | ||
| } | ||
| }); | ||
|
|
||
| it("committed schema deep-equals the in-memory regeneration (if this fails, run `npm run generate:schema`)", () => { | ||
| expect( | ||
| JSON.parse(committedRaw), | ||
| "config/schema/config.schema.json is out of date — run `npm run generate:schema` and commit the result", | ||
| ).toEqual(buildConfigJsonSchema()); | ||
| }); | ||
|
|
||
| // This doubles as the cross-process determinism check: the committed file | ||
| // was rendered by a separate generator invocation, so byte-equality here | ||
| // proves a fresh render reproduces it exactly. | ||
| it("committed schema matches the serialized output byte-for-byte (if this fails, run `npm run generate:schema`)", () => { | ||
| expect( | ||
| committedRaw, | ||
| "config/schema/config.schema.json serialization drifted — run `npm run generate:schema` and commit the result", | ||
| ).toBe(renderConfigJsonSchema()); | ||
| }); | ||
|
|
||
| it("covers every PluginConfigSchema field and preserves root metadata", () => { | ||
| const schema = buildConfigJsonSchema() as { | ||
| $schema?: string; | ||
| $id?: string; | ||
| title?: string; | ||
| $defs?: { | ||
| pluginConfig?: { properties?: Record<string, unknown> }; | ||
| }; | ||
| }; | ||
|
|
||
| expect(schema.$schema).toBe("https://json-schema.org/draft/2020-12/schema"); | ||
| expect(schema.$id).toBe( | ||
| "https://codex-multi-auth.local/schema/config.schema.json", | ||
| ); | ||
| expect(schema.title).toBe("codex-multi-auth config template"); | ||
|
|
||
| const generatedKeys = Object.keys( | ||
| schema.$defs?.pluginConfig?.properties ?? {}, | ||
| ).sort(); | ||
| const zodKeys = Object.keys(PluginConfigSchema.shape).sort(); | ||
| expect(generatedKeys).toEqual(zodKeys); | ||
| expect(zodKeys.length).toBeGreaterThan(0); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# show the target test file with line numbers
echo "==== test/config-schema-generated.test.ts ===="
sed -n '1,220p' test/config-schema-generated.test.ts | cat -n
# find build/render schema helpers and their definitions
echo "==== search buildConfigJsonSchema / renderConfigJsonSchema ===="
rg -n "buildConfigJsonSchema|renderConfigJsonSchema|committedPath|CONFIG_SCHEMA_RELATIVE_PATH|PluginConfigSchema" -S test lib scripts
# locate DEFAULT_PLUGIN_CONFIG
echo "==== search DEFAULT_PLUGIN_CONFIG ===="
rg -n "DEFAULT_PLUGIN_CONFIG" -S lib test
# find existing schema validation / ajv usage
echo "==== search for ajv or json schema validation ===="
rg -n "from \"ajv\"|Ajv|compile\\(buildConfigJsonSchema|validate\\(" -S test lib
# check whether ajv is already a dependency
echo "==== check package.json for ajv ===="
cat package.json | rg -n "\"ajv\"|\"`@ajv`|ajv-" || trueRepository: ndycode/codex-multi-auth
Length of output: 13934
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "==== package.json (deps mentioning ajv/json-schema validator) ===="
rg -n "\"(ajv|jsonschema|json-schema|valibot|tv4|ajv-keywords)\"" package.json || true
# also print relevant dependency sections if small enough
node -e 'const pkg=require("./package.json"); console.log(Object.keys(pkg.dependencies||{}).filter(k=>/(ajv|jsonschema|json-schema|tv4)/i.test(k))); console.log(Object.keys(pkg.devDependencies||{}).filter(k=>/(ajv|jsonschema|json-schema|tv4)/i.test(k)));'
echo "==== lib/config-schema.ts (schema shape expectations) ===="
sed -n '1,220p' lib/config-schema.ts | cat -n
echo "==== lib/config.ts (DEFAULT_PLUGIN_CONFIG export and type) ===="
sed -n '150,260p' lib/config.ts | cat -n
echo "==== existing json-schema validation usage in tests ===="
rg -n "(Ajv|ajv|json schema|json-schema|toJSONSchema\\(|validate\\(.*schema|tv4)" test lib scripts || trueRepository: ndycode/codex-multi-auth
Length of output: 9507
add functional validation that the generated config schema accepts real configs
test/config-schema-generated.test.ts:19-78only guards schema drift/metadata; it never validates thatbuildConfigJsonSchema()accepts a known-good config instance.- add a test that uses a json-schema validator to validate
buildConfigJsonSchema()against a config object built fromlib/config.ts:52-107(DEFAULT_PLUGIN_CONFIG), e.g.{ plugin: ["test"], provider: {}, pluginConfig: DEFAULT_PLUGIN_CONFIG }, and assert validation succeeds. - this test file should use vitest globals (remove
import { beforeAll, describe, expect, it } from "vitest"attest/config-schema-generated.test.ts:3). - no windows filesystem cleanup or concurrency behavior is covered here; keep those concerns in windows/concurrency-focused tests elsewhere.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/config-schema-generated.test.ts` around lines 19 - 79, The test suite
currently only checks metadata and key parity for buildConfigJsonSchema() but
never verifies the generated JSON Schema actually validates a real config; add a
new it(...) that imports/uses a JSON Schema validator (e.g., Ajv) to compile
buildConfigJsonSchema() and validate an instance such as { plugin: ["test"],
provider: {}, pluginConfig: DEFAULT_PLUGIN_CONFIG } (sourced from
DEFAULT_PLUGIN_CONFIG in lib/config.ts) and expect the validator to succeed;
also remove the explicit vitest imports at the top so the file uses vitest
globals (no import { beforeAll, describe, expect, it } from "vitest"). Ensure
the test references buildConfigJsonSchema and DEFAULT_PLUGIN_CONFIG by name.
Review follow-ups: - runtime-guard the z.toJSONSchema result before delete/??= mutation so a zod behavior change fails loudly with a named error - document the module as generator-internal (deliberately not part of the lib/index.ts surface; consumers are the generator script and the drift-guard test) - assert the document's structural invariants in the test (type, properties, required, $defs.pluginConfig) as a cheap stand-in for metaschema validation https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
Summary
Makes
config/schema/config.schema.jsongenerated from the zodPluginConfigSchemawith a drift-guard test, so the JSON schema can never silently fall behind the real config surface again — audit roadmap §4.5.2 (docs/audits/AUDIT_2026-06-10.md, PR #522; the audit's finding M-class "schema drift" item).Changes
lib/config-schema.ts(90 lines): builds and deterministically serializes the schema document using zod v4's nativez.toJSONSchema()— zero new dependencies. Preserves the existing$schema(draft 2020-12),$id,title, template root keys, andrequired: ["plugin","provider"](the shippedconfig/*.jsontemplates depend on them). Generated withio: "input"to match runtime semantics (loadPluginConfigstrips unknown keys).scripts/generate-config-schema.mjs+npm run generate:schema(build, then write from compiled dist).test/config-schema-generated.test.ts(4 tests): regenerates in-memory and deep-equals the committed file — drift is now a test failure with the message "runnpm run generate:schema". Build-independent (imports the TS module via vitest).config/schema/config.schema.json: from 3 stale root properties to the full 54-fieldpluginConfigdefinition in$defs(the audit's "75+" was an overestimate; 54 is the realPluginConfigSchemacount). Every construct converted cleanly — no refinement workarounds needed.Consumers verified
config/codex-modern.json/codex-legacy.json/minimal-codex.jsonreference the schema via$schema;test/config-schema-templates.test.tsvalidates them against it and still passes unchanged. The schema ships via theconfig/entry in package.jsonfiles.Validation
npm run typecheck; fullnpm run lint; eslint on touched files--max-warnings=0Risk / Rollback
Additive (new module/script/test) + a more complete schema file. Revert the single commit to restore the handwritten schema.
https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
Generated by Claude Code
note: greptile review for oc-chatgpt-multi-auth. cite files like
lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.Greptile Summary
this pr wires
config/schema/config.schema.jsonto the zodPluginConfigSchemaso the shipped json schema can never silently fall behind the real config surface — previously it was handwritten and covered only 3 root properties. all previous review comments (crlf pinning via.gitattributes,??=description guard,beforeAllfile-read) are addressed in this iteration.lib/config-schema.tsaddsbuildConfigJsonSchema()/renderConfigJsonSchema(), building the 54-field schema fromz.toJSONSchema()withio:"input"to matchloadPluginConfigsemantics; deliberately excluded fromlib/index.tsto keep it a generator-only module.test/config-schema-generated.test.tsadds a drift guard: reads the committed file inbeforeAll(with\r\n→\nnormalisation), then checks structural deep-equality, byte-for-byte serialisation identity, and field-set completeness againstPluginConfigSchema.shape.Confidence Score: 5/5
purely additive change — new module, script, test, and a more complete generated json schema; no existing runtime paths are modified
all previous review comments (crlf, beforeAll, description ??=) are addressed. the generator correctly uses io:"input" to match loadPluginConfig semantics, the drift guard test exercises both deep-equality and byte-level identity against the committed file produced by a separate process, and the gitattributes pin prevents windows autocrlf from producing spurious test failures. no logic that touches accounts, tokens, or the rotation proxy is changed.
no files require special attention
Important Files Changed
??=for description,delete pluginConfig.$schemaremoves dialect duplication; intentionally not re-exported from lib/index.tsconfig/schema/config.schema.json text eol=lfto pin the byte-compared schema file against Windows autocrlf checkout driftgenerate:schemascript (build then run generator); no dependency changesFlowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A["lib/schemas.ts\nPluginConfigSchema (zod)"] -->|"z.toJSONSchema()\nio: input"| B["lib/config-schema.ts\nbuildConfigJsonSchema()"] B --> C["renderConfigJsonSchema()\nJSON.stringify + trailing \\n"] C -->|"npm run generate:schema\n(build then node script)"| D["scripts/generate-config-schema.mjs\nwrites to disk"] D --> E["config/schema/config.schema.json\n(committed, eol=lf)"] C -->|"vitest - test/config-schema-generated.test.ts"| F{"byte == committedRaw?\ndeep == buildConfigJsonSchema()?\nkeys == PluginConfigSchema.shape?"} E -->|"readFileSync in beforeAll\n+ CRLF normalise"| F F -->|"fail"| G["drift: run npm run generate:schema"] F -->|"pass"| H["schema in sync"]Reviews (3): Last reviewed commit: "fix(config-schema): guard the zod output..." | Re-trigger Greptile