mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
test(rpc): add a compile-time params catalog parity gate (#20281)
* Add compile-time RPC params catalog parity gate Check each registered handler against its catalog params type in both directions, with explicit exceptions for the three uncatalogued schemas. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(rpc): keep the params generator off its own output The parity gate imports the generated catalog for types, and it lives under RPC_DIR, which indexableModules() scans for shared imports. That re-added OUTPUT_PATH after line 46 removed it, so the generator bundled and require()d the committed catalog. A catalog referencing a renamed or deleted shared export then crashed regeneration — in exactly the state that requires regenerating. Reproduced before and after: with a dangling reference injected into the catalog, `generate:rpc-params-catalog` threw; it now rewrites the file. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -51,7 +51,14 @@ function indexableModules() {
|
||||
const source = readFileSync(path.join(RPC_DIR, file), 'utf8')
|
||||
for (const [, specifier] of source.matchAll(/from\s+'(\.[^']+)'/g)) {
|
||||
const resolved = `${path.resolve(path.dirname(path.join(RPC_DIR, file)), specifier)}.ts`
|
||||
if (resolved.startsWith(`${SHARED_DIR}${path.sep}`) && existsSync(resolved)) {
|
||||
// Never re-add the generator's own output: a module under RPC_DIR may import the
|
||||
// catalog for a type-only contract, and bundling a stale catalog makes regeneration
|
||||
// crash in exactly the state that requires regenerating.
|
||||
if (
|
||||
resolved !== OUTPUT_PATH &&
|
||||
resolved.startsWith(`${SHARED_DIR}${path.sep}`) &&
|
||||
existsSync(resolved)
|
||||
) {
|
||||
modules.add(resolved)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import type {
|
||||
RpcMethodName,
|
||||
RpcParams
|
||||
} from '../../../shared/rpc-contract/rpc-params-catalog.generated'
|
||||
import type { RpcAnyMethodDeclaration } from './core'
|
||||
import type { ALL_RPC_METHODS } from './methods'
|
||||
|
||||
type RegisteredMethod = (typeof ALL_RPC_METHODS)[number]
|
||||
|
||||
// These schemas reach into src/main and have no shared catalog entry.
|
||||
type UncataloguedMethod = 'emulator.install' | 'orchestration.send' | 'orchestration.taskUpdate'
|
||||
|
||||
type IsAny<T> = 0 extends 1 & T ? true : false
|
||||
|
||||
type ParamsMatch<Host, Catalog> =
|
||||
IsAny<Host> extends true
|
||||
? false
|
||||
: IsAny<Catalog> extends true
|
||||
? false
|
||||
: [Host] extends [Catalog]
|
||||
? [Catalog] extends [Host]
|
||||
? true
|
||||
: false
|
||||
: false
|
||||
|
||||
// Distribute over declarations so each handler is checked, including streaming handlers.
|
||||
type MismatchedMethod<Method extends RpcAnyMethodDeclaration> =
|
||||
Method extends RpcAnyMethodDeclaration
|
||||
? Method['name'] extends RpcMethodName
|
||||
? ParamsMatch<Parameters<Method['handler']>[0], RpcParams<Method['name']>> extends true
|
||||
? never
|
||||
: Method['name']
|
||||
: Exclude<Method['name'], UncataloguedMethod>
|
||||
: never
|
||||
|
||||
type AssertNever<T extends never> = T
|
||||
|
||||
// Type-only gates belong in the node typecheck; runtime parsing is a separate contract.
|
||||
export type RpcParamsTypeParity = AssertNever<MismatchedMethod<RegisteredMethod>>
|
||||
export type RpcParamsUncataloguedMethods = AssertNever<
|
||||
Exclude<UncataloguedMethod, Exclude<RegisteredMethod['name'], RpcMethodName>>
|
||||
>
|
||||
Reference in New Issue
Block a user