mirror of
https://github.com/Comfy-Org/comfy-typescript-sdk.git
synced 2026-11-04 23:47:27 +00:00
[PR #83] [MERGED] test: check cross-SDK surface parity against the Python SDK #85
Labels
No labels
audit:ai-readiness
audit:community
audit:github
audit:health-scoring
audit:policy
audit:release
audit:supply-chain
bug
cursor-review-pin
effort:medium
effort:small
effort:trivial
priority:nice-to-have
priority:recommended
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/comfy-typescript-sdk#85
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/comfy-typescript-sdk/pull/83
Author: @mattmillerai
Created: 8/25/2026
Status: ✅ Merged
Merged: 8/26/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-8506-cross-sdk-surface-parity📝 Commits (1)
fc7ac22test: check cross-SDK surface parity against the Python SDK📊 Changes
9 files changed (+856 additions, -2 deletions)
View changed files
📝
.github/workflows/ci.yml(+23 -0)📝
.prettierignore(+6 -0)📝
AGENTS.md(+30 -1)📝
package.json(+3 -1)➕
parity/README.md(+35 -0)➕
parity/python-surface.json(+76 -0)➕
scripts/python-surface.mjs(+251 -0)➕
scripts/sync-python-surface.mjs(+125 -0)➕
src/sdk/surface-parity.test.ts(+307 -0)📄 Description
ELI-5
We ship two SDKs — Python and TypeScript — that are supposed to expose the same things under the same names. They are maintained by separate pull requests in separate repositories, so they drift, and nobody finds out until a user copies a Python example into TypeScript and the method is not there. This adds a test that compares the two surfaces and fails saying exactly which name went missing and in which direction.
What is compared
models—comfy.modelshere against the PythonModelsnamespace class.routerErrors.*againstcomfy_sdk.router_exceptions.error_typeeach of those classes maps to, per class, not just as a set — a class wired to the wrong bucket is caught, not only a missing one.error_typeset as a whole (ROUTER_ERROR_TYPESon both sides).ComfyErrorand friends), which is where the credential asymmetry below actually shows up as a name.Where it lives, and how the other repository's surface is obtained
A cross-repo check needs a stated mechanism rather than an implicit one, so here it is. The work is split in two, mirroring how this repo already handles the vendored OpenAPI spec:
pnpm test→src/sdk/surface-parity.test.tsparity/python-surface.json. This is the parity assertion.sdk-parityCI job →pnpm check:sdk-parityscripts/sync-python-surface.mjsreads four files from the publicComfy-Org/comfy-python-sdkrepository over plain HTTPS (raw.githubusercontent.com— no token, no clone) and extracts the surface withscripts/python-surface.mjs, which parses those files rather than importing the package: importing would need a Python toolchain, a virtualenv and the Python SDK's runtime dependencies inside a Node repo's CI, which is a lot of moving parts for a check whose only job is to be trustworthy.The snapshot is committed rather than fetched at test time for the same reason
spec/openapi.yamlis vendored: the assertion has to run offline and deterministically on every PR, and a test that reaches the network fails for reasons that have nothing to do with the code under review. Thesdk-parityjob is what stops the snapshot going quietly stale — without it the offline half would keep passing over a frozen copy.Documented in-repo at
parity/README.mdandAGENTS.mdsection 6.The intentional asymmetries
Declared in a commented
INTENTIONAL_ASYMMETRIESallowlist at the top of the test. Anything not in it fails.result-envelope— Python returns the provider payload directly; TypeScript returns{ data, requestId }. Return types only, no name differs, so the entry suppresses nothing. It is declared anyway so a reviewer who notices the difference finds it listed as a decision rather than wondering whether the check can see it.credential-resolution— Python resolves credentials per client instance, TypeScript configures them once at module level. The one name this changes isMissingApiKey(Python) versusMissingCredentials(TypeScript), so the entry carries that rename.no-sync-variant— Python shipsModels/AsyncModels, TypeScript is promise-native with onecomfy.models.AsyncModelsis therefore not expected to have a counterpart here — but it is still checked, against the sync class: the async twin may introduce no method name of its own, which is what stops a suffixedrun_asyncfrom becoming a Python-only method nobody notices is absent.The allowlist also fails on an entry that no longer applies (a rename whose Python name is gone), so it cannot rot into a blanket exemption.
Not passing vacuously
A broken introspection step must not read as parity, so this fails loudly in three places instead of returning an empty set: every extractor in
scripts/python-surface.mjsthrows on an empty or unrecognized result; the snapshot loader rejects any empty section; and the TypeScript side asserts each introspected group is non-empty before diffing.models.pygrowing a third public class is a hard failure too, rather than a surface the check silently does not look at.Verification of the failure modes
Each assertion was exercised against a deliberately mutated snapshot, confirming it fails and names the symbol:
Models.submitcomfy.models: `submit` exists in the Python SDK but not in the TypeScript SDKAsyncModels.run_asyncAsyncModels declares methods the synchronous class does not: [ 'run_async' ]ProviderErrorrenamedrouterErrors: `ProviderFailure` exists in the Python SDK but not in the TypeScript SDK+ the converse lineProviderTimeoutrebucketedrouterErrors.ProviderTimeout: error_type is "provider_timeout" in the TypeScript SDK but "upstream_timeout" in the Python SDKforbiddendropped,rate_limitedaddederror_type: `rate_limited` exists in the Python SDK but not in the TypeScript SDK+ the converse lineMissingApiKeyto matchthe allowlist renames `MissingApiKey`, which the Python SDK no longer exportsparity/python-surface.json has an empty `routerErrorClasses` … an empty section is a broken extraction, not parityTwo of these (the unrecognized-class guard and the empty router set) are pinned as tests in the file; the rest are assertions over the snapshot, exercised by mutating it locally.
Coverage, including the part this does not cover
Compared today: 38 symbols — 1
modelsmethod, 12 router error classes, 11error_typevalues, 14 root-exported error classes.Not compared: 24 of the package root's 39 exported symbols (
Comfy,Workflow,Job,Asset,Output, their factories, and the base-URL / timeout / retry constants) and the 6 non-class members ofrouterErrors(the two header-name constants,ROUTER_ERROR_TYPESand its two halves,toRouterError). Those are outside the three criteria this check was written to, and several of them are constants whose values would matter more than their names. Widening it is a deliberate next step, not an oversight — see Residual.Provenance
pnpm lintclean;pnpm format:checkclean;pnpm typecheckclean;pnpm test365 passed / 0 failed across 21 files;node scripts/check-spec-drift.mjsin sync;node scripts/check-public-repo-hygiene.mjsclean;node scripts/sync-python-surface.mjsmatches the Python SDK's default branch;pnpm build+pnpm packsucceed and the tarball contains noparity/orsurface-parityentries; and the seven failure modes tabled above each reproduced against a mutated snapshot.Residual
Not fixed by this PR, and each worth its own follow-up:
X-Comfy-Error-Typeat all, TypeScript'sERROR_TYPE_BY_STATUSmaps400and422toinvalid_input; the Python SDK deliberately omits both, its comment reasoning that the contract pins no single bucket to either and that guessing betweeninvalid_inputandcontent_policy_violationwould tell a caller to retry a deterministic refusal. Both sides also handle a499the same way, so this is specifically the400/422pair. One of the two is wrong and a caller'sexcept/catchbranch differs by language today. This is behaviour, not surface, so the check added here does not and will not catch it — it was found by reading both sources while building the check.RouterError's own defaulterror_typediffers —"internal_error"in TypeScript,""in Python. The base class is excluded from the per-classerror_typecomparison for exactly this reason, and the exclusion is commented in the test. Also behavioural rather than a name, and it interacts with item 1: an unknown bucket carries a different default value depending on which SDK you are in.pnpm test, while a change landing in the Python repo is caught only the next time this repo's CI runs — so drift originating on the Python side has a detection lag equal to the gap between PRs here. A scheduled run of thesdk-parityjob, or a mirror job in the Python repo, would close that.sdk-parityjob is not a required check. That is deliberate per the scope ("get it running first, then decide"), and it is worth deciding: as configured, a Python-side surface change reddens this job on unrelated PRs in this repo until somebody refreshes the snapshot. That is the intended loudness, but it is the cost that gets a parity check disabled, so it should be a conscious call.modelsclass introduces no method name the sync one lacks.routerErrorsmembers): the workflow-API classesComfy,Workflow,Job,Asset,Outputand their factories have no name-parity assertion, so a method added toWorkflowin one SDK and not the other is still invisible.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.