Skip to content

fix: real-toolchain-validated SDK emitter fixes (js-sdk, python-sdk, csharp-sdk) - #573

Open
johnOC03 wants to merge 54 commits into
mainfrom
combined/sdk-emitter-fixes
Open

johnOC03 wants to merge 54 commits into
mainfrom
combined/sdk-emitter-fixes

Conversation

@johnOC03

Copy link
Copy Markdown
Collaborator

Summary

A combined series of fixes across all three SDK emitters (js-sdk, python-sdk, csharp-sdk), each found and verified by actually running the real generated toolchain (npm install+vitest, poetry install+pytest, dotnet build+test) against a live broker — not just unit tests. Rebased cleanly onto current main (0 conflicts remaining after resolution); full local pre-push checklist (lint, all workspace typechecks, pipeline regeneration, npm test) passes with 994 passed / 12 skipped / 0 failed.

js-sdk

  • Restored globalContextSeeds wiring and agent-connector semantics (seedBinding emits literal '<default>' for tenantIdVar, causing HTTP 409 on every tenant producer #342).
  • Materialized real multipart/@@FILE fixture resolution and seed-binding support (previously silently dropped required fields).
  • Fixed path-param URL derivation (previously always empty due to a step.pathParams dead-field bug — see repo history for the full class of this bug across emitters).
  • Fixed the generated project's tsconfig.json lib setting so Error(message, { cause }) typechecks (ES2022.Error).

python-sdk

  • Materialized real multipart/@@FILE fixture bytes instead of passing an unresolved marker string.
  • Fixed buildPythonUrlExpression's path-param derivation (same class of bug as js-sdk).

csharp-sdk

  • Aligned the emitter with the real, installed Camunda.Orchestration.Sdk 9.2.2 surface (via direct assembly reflection, not guesswork): strongly-typed key .AssumeExists(...) construction, correct request-DTO type names, bare-Task-returning method handling, RequireStringBinding for string? SDK parameters, and 41 missing body-type mappings for search operations.
  • Fixed the CsharpOperationMap type to allow readonly entry arrays (needed for as const test fixtures).

Rebase / cleanup

  • Deduped 3 configs/camunda-oca/ontology/semantics.json semantic-type entries that main had independently reclassified during the time this branch was in flight (AgentDefinitionKey, AgentHistoryItemKey, LoopIterationId) — deferred to main's already-reviewed classification rather than re-litigating.
  • Kept main's superior scripts/e2e/run-oca.sh positive-suite-runner logic (proper PW_FAIL tracking) over this branch's now-superseded fix for the same bug.
  • Kept this branch's Camunda.Orchestration.Sdk 9.2.2 pin in CamundaIntegrationTests.csproj (verified as the current, correct version this branch's csharp-sdk work was built/tested against) over main's stale 9.0.0.

Known blockers (not fixed here — flagged for follow-up)

Live-broker validation surfaced several pre-existing, cross-cutting issues that affect all three SDK targets roughly equally and are out of scope for an emitter-level fix in this PR:

  1. Multi-tenancy disabled on the default local broker causes cascading failures. docker/docker-compose.yml's broker has no multi-tenancy-enabling env var, but the scenario planner/domain-semantics unconditionally populates a tenantId field on createDeployment (and other tenant-scoped ops). Confirmed directly: POST /deployments with a tenantId field → 400 INVALID_ARGUMENT "multi-tenancy is disabled"; the same call without it → 200. This cascades into most createTenantcreateDeployment-chained scenarios across every SDK target.
    • Suggested fix (3-phase plan):
      • Phase 1: add an opt-in docker/docker-compose.multi-tenancy.yml overlay (mirroring the existing docker-compose.rbac.yml pattern) that enables multi-tenancy, so it can be exercised without changing the default broker's behavior.
      • Phase 2: decide, as a repo-level choice, among (a) leave the default broker as-is and treat tenant-scoped scenarios as multi-tenancy-overlay-only, (b) enable multi-tenancy by default, or (c) have the scenario planner suppress tenantId injection per-config when multi-tenancy is known to be disabled.
      • Phase 3: measurement hygiene — any before/after comparison against a live broker must fully recreate it (docker compose down -v && up -d; a plain restart does not reliably clear the H2 in-memory store) and report clean before/after counts.
  2. Scenario dependency-chain gaps (path-analyser, not emitter-specific): several unassign*/delete* RBAC operations chain straight from create* without the intermediate assign* step, producing legitimate 404s. Same root cause likely affects correlateMessage/broadcastSignal scenarios missing a preceding createDeployment+createProcessInstance step.
  3. waitUpToMs: 0 hardcoded for js-sdk's eventually-consistent calls — defeats the SDK's own consistency-retry mechanism, a likely contributor to timing-race NOT_FOUNDs on plain create-then-get chains.
  4. csharp-sdk: 2 operations (getGlobalJobStatistics, getUsageMetrics) need query-parameter modeling in path-analyser (not just a request-body object) to compile — same gap affects js-sdk's getUsageMetrics. Also several response-shape/field-path-extraction gaps and polymorphic/oneOf request-body types remain unresolved for a handful of operations.

None of these are regressions from this PR — they were present before this branch's work began and are documented in detail (with repro steps) for follow-up.

dashka-str and others added 30 commits September 14, 2026 14:59
-requestPlan is used instead of scenario.operations
-no placeholder stub output
-Python literals for booleans and null
…ter (#354)

- buildPythonUrlExpression/renderPythonValue: stop snake_casing the ctx lookup key so ctx.get() matches the ctx.set() key from scenario.bindings (previously always missed, silently falling back to broken literal defaults).
- sdk-mapping.ts: model the real operation-map.json shape ({file,region,label}[]) instead of a fictitious {package,method,qualifiedName} shape, so resolvePythonMethodName's op-map lookup actually resolves SDK method names instead of always falling back to snake_case(operationId).
- regression-invariants.test.ts: add L3 invariants asserting real emitted content (no 'pass  # TODO: implement' stubs, every step comment has a matching client call), and exclude the intentional \ seed token from the unresolved-placeholder invariant (mirrors the JS SDK invariant's existing exception).
- render RANDOM through SeedBinding in generated C#
- add the missing RestSdk.Models import
- switch error assertions to HttpRequestException
- keep PascalCase async operation names unchanged
- move operation lookup to a Map-backed CsharpOperationMapSource
- suffix generated class names with the emit mode (e.g. Variant) so a feature suite and a variant suite for the same operationId no longer emit the same C# type name, which was a CS0101 duplicate-definition error once both files compiled into the same project

- fix TestFixtureBase.cs template: interpolation holes in interpolated strings had escaped quotes around NextCounter args, which is invalid C# syntax inside a non-verbatim interpolated string; corrected to unescaped quotes

- add a regression test asserting feature/variant suites emit distinct class names

- fix the pre-existing HttpRequestException test, which asserted on a 200-status fixture that could never trigger the error-path branch
- csharp-sdk-emitter.test.ts: extract BASE_REQUEST_STEP so the error-path test doesn't spread the optional requestPlan[0] field (was 'Object is possibly undefined').
- csharp-sdk-mapping.test.ts: drop 'as const' on OPERATION_MAP so its readonly array literal type is assignable to the mutable CsharpOperationMap parameter type.

Pre-existing on C#-sdk-emitter-fixes before this merge (confirmed by running tsc on that branch alone).
The real Camunda.Orchestration.Sdk NuGet package is a single flat
namespace (client + request/response DTOs together); RestSdk.Models
only exists in this repo's local vendored reference client and does
not resolve against the published package. Confirmed via a real
dotnet build against the restored 9.0.0 package (CS0234 + cascading
CS0246s on every generated file).
The emitted pyproject.toml required camunda-orchestration-sdk>=10.0.0,
but PyPI's highest stable release is 9.0.1 (10.x only exists as dev
pre-releases pip excludes by default). pip install -e . always failed
with 'No matching distribution found', on any OS. Confirmed real fix
via a real pip install against the actual PyPI package.
Two fixes:
1. materialize-support.ts: add [tool.poetry] package-mode = false. The
   emitted project is a flat pytest suite with no importable package of
   its own; without this, poetry-core's build backend tries to
   build/install a camunda-sdk-tests package, finds no matching
   module/folder, and every install (pip install -e ., poetry install)
   fails. Confirmed via a real poetry install + poetry run pytest run.
2. emitter.ts: sanitize scenario names into valid Python identifiers for
   test function names, prefixed with the scenario's own id for
   uniqueness. Scenario names may contain spaces/dashes/# (e.g.
   'createProcessInstance - bpmn #1'), which produced a SyntaxError on
   every single generated python-sdk file (100% uncollectable by
   pytest), and scenarios sharing a display name silently collided into
   one test function. Confirmed via a real pytest --collect-only run
   (486 tests now collect with zero errors, up from 0).
The published package is @camunda8/cli (npm view confirms @camunda8/c8ctl
404s); it installs the c8ctl binary. Discovered while walking through the
Playwright test setup instructions.
The generated js-sdk test suites previously targeted a fabricated createApiClient/{status,data} contract that never matched the real @camunda8/sdk package, causing 601/601 test failures at runtime with TypeError: createApiClient is not a function.

Real contract (confirmed via live smoke tests against an installed @camunda8/sdk@8.8.13 and a running local broker):
- Client: new Camunda8().getOrchestrationClusterApiClientLoose(), zero-config via ZEEBE_REST_ADDRESS/CAMUNDA_AUTH_STRATEGY env vars.
- Success responses are raw data, not wrapped in {data,...}; no .status on success.
- Errors are thrown Error-like objects with .status.
- ~60-92 eventually-consistent get*/search* methods require a second { consistency: { waitUpToMs } } argument or throw a client-side error; detected via runtime arity (method.length >= 2) rather than a hardcoded operation list.
- Detached method references must be .bind(client)'d before invoking (the SDK's methods read this._client internally).
- Method-name resolution now uses a generic toSdkMethodName() (acronym normalization + lowercase-first-char) instead of the unreliable operation-map.json (whose region values are doc-example identifiers, not real method names, and are ambiguous for oneOf variants).

Also rewrote the emitted package.json dependency version, .env.example, ambient camunda8-sdk.d.ts shim, and README to match the real contract.

Validated end-to-end against a live local broker: getTopology and searchUsers.feature suites pass for real. Known out-of-scope residual gap: some operationIds (e.g. getProcessInstanceStatisticsByError/ByDefinition) collapse to a single real SDK method and are not yet aliased -- flagged as follow-up, same class as the existing C# DTO-naming gap.
Audited all 202 operationIds against the real, installed @camunda8/sdk
client (Object.getOwnPropertyNames(Object.getPrototypeOf(client))):

- Zero true N:1 operationId-to-method name collisions exist. A prior
  assumption that 3 operationIds collapsed onto a single method
  (getProcessInstanceStatistics) was wrong -- each keeps its own
  distinct computed name, which simply has no backing method.
- 56 of 202 operationIds (28%) have no corresponding method at all --
  not a naming mismatch, but whole feature areas missing from the
  installed SDK (Agent Instance, Cluster Variables, Global Task
  Listener, most *Statistics reads, several process-instance lifecycle
  + batch-operation ops, etc). Root cause: spec/SDK version skew --
  configs/camunda-oca/spec-pin.json tracks camunda/camunda@main, while
  @camunda8/sdk's published releases lag behind newer API areas.
  Confirmed not fixable via a version bump in this repo: the
  underlying @camunda8/orchestration-cluster-api@9.1.4 already resolves
  183/202, but @camunda8/sdk (including its alpha channel) pins that
  dependency to \>=8.8.4 <9.0.0\, excluding 9.x entirely.

Since no code change here can call a method that doesn't exist, these
operations previously threw an opaque \TypeError: Cannot read
properties of undefined (reading 'length')\ at the consistency-arg
check. This adds generation-time detection instead:

- materializer/scripts/dump-js-sdk-methods.mjs dumps real client method
  names from the installed @camunda8/sdk into a checked-in
  materializer/src/js-sdk/known-sdk-methods.json (regenerate via
  \
pm run js-sdk:dump-methods --workspace materializer\ whenever the
  @camunda8/sdk devDependency bumps).
- emitter.ts's renderScenarioTest checks every step's resolved SDK
  method name against this list; if any step in a scenario has no
  backing method, the whole scenario emits as \it.skip(...)\ with a
  comment naming the missing method(s), instead of runnable code that
  crashes.

Also fixes a stale API_BASE_URL reference in the generated README's
troubleshooting section (leftover from the old fabricated SDK
contract) and documents the new skip behavior there.
…e alias

a95b227 introduced an inline \�s (...args: unknown[]) => Promise<any>\
cast on every rendered request step. biome.generated.json escalates
noExplicitAny to error for generated/**/*.ts, and js-sdk is an enabled
emitter for camunda-oca -- so \
pm run biome:fix-generated:codegen\
(and CI's lint:generated gate) failed with 800+ errors across the
regenerated suite.

A per-call \// biome-ignore\ comment doesn't reliably suppress this:
the formatter wraps long inline casts across multiple lines, which can
separate the ignore comment (attached to the line above the statement)
from the specific line the any token lands on after wrapping.

Instead, declare a single \	ype SdkCall = (...args: unknown[]) =>
Promise<any>;\ once per test, with one stable, always-short, ignored
line -- every per-step call site then reads \�s SdkCall\ with no any
token to trip the rule regardless of reformatting.
…hem undefined

Ports the Playwright emitter's canonical seed-binding mechanism
(emitCtxSeeding/computeUniqueBindings, reused directly rather than
reimplemented) into the js-sdk emitter. Previously a scenario binding
with no in-scenario producer step (bindings[x] === '__PENDING__') was
emitted as ctx['x'] = undefined, silently dropping required
request-body fields and causing widespread HttpSdkError:
INVALID_ARGUMENT failures against a real broker (932 occurrences,
~291/337 runnable tests failing).

Now uses the already-planner-computed scenario.seedBindings to emit a
deterministic seedBinding() call, vendored into a new
materializer/src/js-sdk/materialize-support.ts support/seeding.ts
scaffolding file (js-sdk suites are nested one level deeper than
Playwright's, so the import path is '../support/seeding').

Adds a regression fixture to tests/codegen/js-sdk-emitter.test.ts.
- Replace mechanical PascalCase(operationId)+Request DTO naming with explicit lookup table (CSHARP_REQUEST_TYPE_BY_OPERATION) that maps to real published SDK type names
- Add resolveRequestTypeName() helper to emitter.ts
- Replace dead step.pathParams fallback with derivePathParamNames() that extracts path params from pathTemplate via regex, matching the approach already used in the js-sdk emitter
- Add toCamelCase() helper for path param variable name generation
- Fix readonly-array typing error in csharp-sdk-mapping.test.ts by restoring explicit CsharpOperationMap type annotation
…reverted by 4d35987

- Restore CSHARP_REQUEST_TYPE_BY_OPERATION lookup table
- Restore resolveRequestTypeName() helper
- Restore derivePathParamNames() and toCamelCase() helpers
- Restore PATH_PARAM_RE regex constant
- Restore both regression tests that were deleted by the revert
- Re-adds throw for unmapped operations instead of silently emitting wrong DTO name
- Replace sparse local 9-entry map with complete v9 published
  SDK map sourced from camunda/orchestration-cluster-api-csharp
  stable/9 at commit d559644
- All 183 mapped regions resolve to real CamundaClient methods
- Convert upstream region labels to actual async method names
- 3 spec operations confirmed to have no SDK implementation:
  getAgentInstance, searchAgentInstances, searchResources
js-sdk hardcoded globalContextSeeds: [], so the omitWhenUnbound tenant
mechanism (already used by playwright + csharp-sdk) never applied.
Every scenario minted a random tenantIdVar, which a single-tenant
broker rejects with INVALID_ARGUMENT on createDeployment and every
op downstream of it.

Threads ctx.globalContextSeeds through renderJsSuite /
computeScenarioSeedLines, with the same assertSafeGlobalContextSeeds
boundary check the C# emitter uses.

Verified against a live broker: js-sdk went from 52 passed/289 failed
to 156 passed/185 failed (671 total).
…pIterationId, AgentHistoryItemKey)

Upstream added AI Agent Connector request-body fields whose ID-like
values were unclassified in the semantics ABox, so the planner's
DomainSemanticsValidationFailure aborted codegen before any suite
(playwright/js-sdk/python-sdk/csharp-sdk) could be emitted.

All three are client-minted filter/correlation attributes, not
server-emitted lifecycle keys, matching the existing Tag/BusinessId
pattern.

Verified: npm run extract-graph completes with no unclassified
semantic types against the current bundled spec.
… (9.2.2)

Version="1.0.0" does not exist on nuget.org for Camunda.Orchestration.Sdk
(published versions start at 8.9.0-alpha.2; 9.0.0 is the lowest stable
release), so dotnet restore could never resolve it from a clean cache.
9.2.2 is the latest stable 9.x release, matching the python-sdk emitter's
>=9.0.0 convention.

Note: this unblocks restore but does not fix compilation — the emitted
call sites still don't match the real SDK's method signatures (missing
required search-query/request-body arguments, object-vs-string? mismatches
in RequireBinding call sites). That's a separate emitter bug.
johnOC03 pushed a commit that referenced this pull request Sep 15, 2026
Split out of PR #573 (combined/sdk-emitter-fixes) into a standalone
per-SDK PR.

- Strongly-typed key-struct construction: CSHARP_PATH_PARAM_KEY_TYPE
  map + `.AssumeExists(...)` codegen for path params, falling back to
  a plain string binding for unmapped names (not every path param is a
  key struct, e.g. getUser's username).
- StringValueObjectConverterFactory (TestFixtureBase.cs): reflection-
  detects any SDK key-struct type (Value property + AssumeExists/
  IsValid) and (de)serializes it as a plain JSON string, fixing both
  request-body round-tripping and response field-path extraction.
- ExtendedDeploymentResponse-style "Raw" wrapper unwrapping in
  ToJsonElement, so field-path extraction matches the real wire shape
  instead of the C#-ergonomic PascalCase mirror properties.
- Fixture path resolution reuses TestFixtureBase's ResolveFixturePath()
  (was building an inline, less-robust path assuming fixtures/ sits
  next to the test binary).
- Missing operationId -> request-DTO mappings (createDocumentLink,
  deleteResource, resolveIncident, modifyProcessInstance, several
  *Statistics reads, etc.) and corrected 3 pre-existing wrong mappings.
- CSHARP_VOID_METHODS: bare-Task-returning SDK methods (GetStatusAsync,
  ResetClockAsync, ~48 Assign*/Unassign*/Delete* RBAC methods, etc.) no
  longer get an implicitly-typed `var result =` assignment (CS0815).
- JSON body rendering gained omitWhenUnbound awareness (previously only
  the multipart-fields loop had it) via
  partitionJsonFields/collectJsonRequestFields/emitJsonRequestDataLines.
- renderTenantExpr uses GetStringBindingOrNull (deployment tenantId is
  always broker-optional) instead of a throwing RequireStringBinding.
- eventualWaitsAfter witness-polling: AwaitEventuallyWitness +
  WitnessPredicateMatches (TestFixtureBase.cs) plus emitter wiring at
  all 3 success-path exits, mirroring the js-sdk emitter's pattern.
- Repo housekeeping swept in from the original combined branch's
  history (unrelated to any single SDK, folded in here rather than a
  separate PR): hashicorp/vault-action v3->v4 + create-github-app-token
  v2->v3 bumps, .dockerignore, README's @camunda8/c8ctl -> @camunda8/cli
  package-name fix, spec-pin bump, and a run-hub.sh fix so a
  generate-only STEPS run no longer requires a live Hub/Keycloak.
…response-shape assertion, auth header, invariant precision; js-sdk dist JSON staging, witness consistency argument)
Copilot AI review requested due to automatic review settings September 15, 2026 04:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate generation, runtime, rendering, E2E, and regression-test issues remain.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (12)

materializer/src/csharp-sdk/emitter.ts:23

  • The request-type table is still incomplete for body-bearing operations that are present in the C# operation map. For example, the planner emits updateUserTask (and the same applies to createAuthorization/createRole and the CRUD update operations), but none has an entry here; resolveRequestTypeName() then returns undefined and the non-empty body path throws No published C# request DTO mapping found during generation. Add the verified DTO mappings for every body operation before generating the full C# suite.
const CSHARP_REQUEST_TYPE_BY_OPERATION: Record<string, string> = {
  createDeployment: 'DeploymentRequest',
  createUser: 'UserRequest',
  createTenant: 'TenantCreateRequest',
  createGroup: 'GroupCreateRequest',
  createMappingRule: 'MappingRuleCreateRequest',

materializer/src/js-sdk/emitter.ts:181

  • initSpecSalt() is still emitted at module scope, but every generated operation file imports the same support/seeding module whose _specSalt is global. Vitest loads multiple files before running their tests, so the last module imported overwrites the salt for all earlier suites; seed values then depend on import order and lose the per-operation isolation this helper is meant to provide. Emit the initialization inside each generated it body immediately before that scenario's seed calls, as the Python emitter now does.
  if (needsSeeding) {
    // Mixed into the seed so parallel vitest workers (sharing one TEST_SEED)
    // don't draw the same sequence for the same binding name.
    lines.push(`initSpecSalt(${JSON.stringify(operationId)});`);
    lines.push('');

materializer/src/js-sdk/emitter.ts:400

  • RequestStep.extract.fieldPath is emitted from the OpenAPI response fields (for example, id), but the SDK call returns an envelope whose payload is under response.data. This assignment therefore reads response.id for generated plans, leaving prerequisite bindings undefined and breaking later path/body inputs. Normalize to response.data before applying the planner accessor, and only assign when the extracted value is not undefined so an absent field cannot erase an earlier binding.
        lines.push(`      ctx['${extract.bind}'] = ${responseVar}${accessor};`);

materializer/src/js-sdk/emitter.ts:452

  • The witness predicate is applied to the raw SDK response wrapper, whose REST payload is under .data, while WitnessPredicate.path is the top-level wire-body field (for example, state). As written, the predicate reads response.state instead of response.data.state, so eventual waits retry until timeout even after the witness reaches the expected state.
    `            const v = (body as Record<string, unknown>)[${JSON.stringify(w.predicate.path)}];`,

materializer/src/js-sdk/sdk-mapping.ts:24

  • Falsy but valid JSON body templates (false, 0, '', or null) are rendered as {}. Since the request plan permits arbitrary JSON values, this silently changes scalar payloads; use an undefined check for absence and preserve the literal value (and update the caller's truthiness check accordingly).
  if (!bodyTemplate) return '{}';

materializer/src/js-sdk/sdk-mapping.ts:32

  • Only whole-string ${...} values are converted to context lookups here, while the planner recognizes embedded and multiple placeholders in a string. A body value like prefix-${nameVar}-${idVar} therefore remains a literal in generated JavaScript even though both bindings are collected and seeded, producing an incorrect request.
      const bindingMatch = value.match(/^\\?\$\{([^}]+)\}$/);
      if (bindingMatch) return `ctx['${bindingMatch[1]}']`;
      return JSON.stringify(value);

materializer/src/python-sdk/emitter.ts:142

  • The Python emitter now keeps planner binding names in their original camelCase (ctx.get('tenantIdVar')), but this doc block still says placeholders become ctx.get('snake_var') and the example above uses id_var. That contradicts the generated code and the regression assertions, so contributors following the documentation can reintroduce the casing bug; update the example/docstring to use idVar/tenantIdVar (or change the implementation consistently).
      // ctx keys are the planner's original binding variable names (e.g.
      // tenantIdVar) — must match the ctx.set(...) calls emitted for
      // scenario.bindings verbatim, so no casing transform here (#354).
      return `ctx.get('${whole[1]}')`;

materializer/src/python-sdk/emitter.ts:138

  • This matcher only substitutes a placeholder when it occupies the entire string. The planner collects embedded and multiple ${...} references, so values such as prefix-${nameVar}-${idVar} are seeded but emitted literally and reach the broker unresolved. Render all placeholder matches as a concatenation of literals and context values instead of limiting substitution to whole-string values.
    const whole = /^\$\{([^}]+)\}$/.exec(value);
    if (whole) {

materializer/src/python-sdk/emitter.ts:556

  • This unconditionally stores get_nested_value(...) in the context. The helper returns None both for a missing response field and for a JSON null, so a missing optional field can erase a previously seeded or extracted binding and make a later request send null. Match the other emitters' extraction contract by distinguishing a missing path and leaving the existing binding untouched.
          `    ctx.set('${extract.bind}', get_nested_value(${responseDataVar}, '${extract.fieldPath}'))`,

materializer/src/python-sdk/emitter.ts:165

  • The renderPythonValue call is preceded by a falsy check that treats valid scalar JSON bodies (false, 0, '', and null) as if no body existed, so this path emits {} instead. Use bodyTemplate === undefined as the no-body check and preserve all other JSON values.
  return renderPythonValue(bodyTemplate);

scripts/e2e/run-hub.sh:66

  • By adding curl to this live-step condition, the script now supports curl-only execution, but the resource-fixture block below still runs only when step run is selected. STEPS=curl therefore reaches curl_compare.py without the valid keys its negative requests need and can report 404/403 instead of the expected validation responses. The fixture setup must also run for curl-only mode.
    tests/regression/generated-suites-typecheck.test.ts:59
  • The PR changes the Python and C# emitters and their generated-project scaffolding, but this regression harness adds a typecheck only for js-sdk. A generated Python syntax/import or C# compile regression can therefore pass the repository test suite even though these are the real-toolchain paths this PR is fixing; add equivalent generated-output checks for both targets (or a separate CI-backed harness).
  • Files reviewed: 35/37 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread materializer/src/csharp-sdk/project-templates/TestFixtureBase.cs Outdated
Comment thread tests/regression/generated-suites-typecheck.test.ts Outdated
…mitter)

- renderPythonValue: mixed literal+\ strings are now rendered as
  Python f-strings so embedded bindings are no longer silently dropped
  when a placeholder doesn't occupy the entire string.
- Scenario materialization now throws at generation time for an empty
  requestPlan instead of emitting a comment-only test body that pytest
  reports as a silent pass.
- Added dedicated test coverage for renderPythonEventualWait's witness
  URL binding, predicate match, timeout, and poll-interval branches.
Copilot AI review requested due to automatic review settings September 16, 2026 04:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical JavaScript response-path issues and additional seed-binding and C# mapping defects block approval.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (4)

Previously missed (1) — in code that hasn't changed since the last review.

materializer/src/js-sdk/sdk-mapping.ts:32

  • seedBindings.ts deliberately recognizes placeholders embedded anywhere in a string and supports multiple placeholders, but this renderer only substitutes a whole-string match. A template such as proc-${processInstanceKeyVar}-${tenantIdVar} is emitted with the literal placeholder text, causing the generated request to send unresolved bindings instead of their seeded values; use an embedded replacement path here and add the same regression coverage as the Python emitter.

materializer/src/csharp-sdk/emitter.ts:104

  • This key-type map is incomplete for the pinned 9.2.2 SDK: methods using {tenantId} take TenantId, and methods using {username} take Username, but both names fall through to RequireStringBinding below. Generated calls such as tenant membership operations then pass strings to strongly typed SDK parameters and fail to compile; add these value-object mappings (and audit the remaining SDK signatures) instead of treating every unmapped path parameter as a string.
  processInstanceKey: 'ProcessInstanceKey',
  resourceKey: 'ResourceKey',
  userTaskKey: 'UserTaskKey',
  variableKey: 'VariableKey',
};

materializer/src/js-sdk/emitter.ts:61

  • emitCtxSeeding writes literal scenario bindings verbatim, so planner values such as proc_${RANDOM} are generated into the JS suite as a quoted literal instead of a runtime-generated identifier. The scenario generator marks these as placeholders consumed by the test runtime; route the RANDOM token through the generated seeding helper (and add a regression case) rather than passing it unchanged.
    bindings: scenario.bindings,
    seedBindings: scenario.seedBindings,
    globalContextSeeds,

materializer/src/python-sdk/emitter.ts:159

  • scenarioGenerator emits literal bindings such as proc_${RANDOM} and jobType_${RANDOM} (path-analyser/src/scenarioGenerator.ts:443-449). This interpolation treats RANDOM as a missing context key and falls back to an empty string, producing proc_/jobType_; these literals should be expanded through seed_binding('RANDOM') (with the corresponding import) just as the C# emitter handles SeedBinding("RANDOM").
  for (const match of value.matchAll(EMBEDDED_PLACEHOLDER_RE)) {
    rendered += escapeLiteral(value.slice(lastIndex, match.index));
    rendered += `{ctx.get('${match[1]}') or ''}`;
    lastIndex = match.index + match[0].length;
  • Files reviewed: 35/37 changed files
  • Comments generated: 2
  • Review effort level: Lite

for (const extract of step.extract) {
const accessor = toOptionalAccessor(extract.fieldPath);
lines.push(` ctx['${extract.bind}'] = ${responseVar}.data${accessor};`);
lines.push(` ctx['${extract.bind}'] = ${responseVar}${accessor};`);
Comment on lines +466 to +469
lines.push(
` const v = (body as Record<string, unknown>)[${JSON.stringify(w.predicate.path)}];`,
);
lines.push(` return v === ${JSON.stringify(w.predicate.equals)};`);
…lbacks in python-sdk emitter (Copilot PR #574 review, round 3)
Copilot AI review requested due to automatic review settings September 16, 2026 04:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate findings remain in token and placeholder rendering, generated-output validation, and specification-pin justification.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (6)

configs/camunda-oca/spec-pin.json:4

  • This changes the camunda-oca baseline to a different upstream spec commit and content hash, but the PR description presents only SDK-emitter fixes and does not document a spec bump or the resulting invariant/output changes. Because this changes the contract that all generated scenarios are validated against, please remove it from this emitter PR or explicitly justify the pin bump and include the required regenerated/invariant review.
  "specRef": "6ea2724e1e03f26508d07748b66a7ca54e9c595b",
  "expectedSpecHash": "sha256:798dfd63863ec3bfa266ccc46e0695082ec9aa1ed45923bd9fb0cb130d4f85db"

materializer/src/js-sdk/emitter.ts:406

  • When the SDK call unexpectedly succeeds, expect.fail(...) throws inside this try and is immediately caught as if it were an HttpSdkError; the test then reports sdkError.status as undefined instead of the intended “expected N but request succeeded” failure. Move the explicit failure outside the try/catch (or return after handling the SDK error) so unexpected success is not misclassified.
  if (isErrorExpected) {
    lines.push('      try {');
    lines.push(
      `        const ${responseVar} = await call${stepNum}(input${stepNum}, consistency${stepNum});`,
    );
    lines.push(`        expect.fail('Expected ${expectedStatus} but request succeeded');`);
    lines.push('      } catch (error) {');
    lines.push('        const sdkError = error as HttpSdkError;');
    lines.push(`        expect(sdkError.status).toBe(${expectedStatus});`);
    lines.push('      }');

materializer/src/python-sdk/emitter.ts:164

  • The embedded-placeholder branch has the same ${RANDOM} problem: proc_${RANDOM} becomes an f-string expression reading an absent RANDOM context key and therefore loses the process/job type suffix. Preserve this token as escaped literal f-string text rather than evaluating it as a binding.
  for (const match of value.matchAll(EMBEDDED_PLACEHOLDER_RE)) {
    rendered += escapeLiteral(value.slice(lastIndex, match.index));
    // `is not None`, not `or`: a falsy-but-bound value like 0 or False must
    // not collapse to '' (Copilot PR #574 review, same class as the
    // path-param fix in buildPythonUrlExpression above).
    rendered += `{ctx.get('${match[1]}') if ctx.get('${match[1]}') is not None else ''}`;

scripts/e2e/run-hub.sh:72

  • This new comment is grammatically incomplete and leaves the inline code span unterminated, so it does not clearly document what header is intentionally omitted.
    tests/regression/generated-suites-typecheck.test.ts:59
  • This test now assumes every active config emits a JS SDK suite, but configs/camunda-hub/codegen/emitters.json:1-3 enables only Playwright. With CONFIG=camunda-hub, getSdkOutDir(..., 'js-sdk')/tsconfig.json is absent and this test throws before checking any suite. Gate this entry on the active config's enabled-emitter registry, consistent with the config-driven target layout.
    tests/regression/generated-suites-typecheck.test.ts:59
  • This new generated-suite guard covers only TypeScript. The PR also changes Python request rendering and several C# DTO/path/return-type mappings, where emitter string tests can pass while the emitted project still fails poetry/dotnet compilation. Add equivalent generated Python syntax/type and C# build checks (or an explicitly scoped CI job) so those real-toolchain regressions are guarded rather than validated only manually.
  • Files reviewed: 35/37 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment on lines +873 to +876
function renderTemplateToken(name: string): string {
if (name === 'RANDOM') {
return 'SeedBinding("RANDOM")';
}
Comment thread materializer/src/js-sdk/sdk-mapping.ts
Comment thread materializer/src/python-sdk/emitter.ts
…d-template rendering (Copilot PR #574 review, round 4)

- renderPythonTemplateString now special-cases the '${RANDOM}' token
  (planner-minted literal seed value, e.g. 'proc_${RANDOM}') as literal
  text instead of resolving it via ctx.get('RANDOM'), for both the
  whole-string and embedded-mixed-string cases -- mirrors how the
  JS/Playwright emitters never resolve this token either.
- Switched mixed-string rendering from an f-string to '+' concatenation
  of string-literal and ctx-lookup segments: an f-string's brace-doubling
  escaping would have corrupted the literal token, tripping
  regression-invariants.test.ts's exact-substring check (#133).
- Replaced the stale SAMPLE_COLLECTION-based whole-string-placeholder
  test (which asserted a negative regex against a fixture with no actual
  placeholder, so it could never fail) with a real widgetIdVar fixture
  asserting the actual ctx.get(...) lookup.
- Added regression tests for both the whole-string and embedded RANDOM
  cases.
Copilot AI review requested due to automatic review settings September 16, 2026 04:59
- Bump documented/enforced minimum Node version to >=18.13.0 (engines
  field in scaffolded package.json + README), matching the `File`
  global's real availability on `node:buffer`.
- Sanitize line-break characters (\r, \n, U+2028, U+2029) out of the
  missing-method skip reason before embedding it in the generated
  `// SKIPPED: ...` line comment, closing a comment-injection risk from
  a hostile/malformed operationId.
- renderJavaScriptBody(): support mixed literal/placeholder strings
  (e.g. 'proc-${a}-${b}') by rendering a real JS template literal
  instead of falling through to an unresolved JSON.stringify() literal,
  mirroring the python-sdk emitter's equivalent fix. Also fixes a
  latent whole-string bug where the planner's literal `${RANDOM}` seed
  token was incorrectly resolved to `ctx['RANDOM']`.
- Gate the `js-sdk` entry in generated-suites-typecheck.test.ts's SUITES
  matrix on the active config's codegen/emitters.json actually
  declaring js-sdk, so camunda-hub (playwright-only) doesn't fail with
  a missing-tsconfig error.
- Confirmed (no code change needed): the require()-based
  known-sdk-methods.json dist-staging concern flagged against
  emitter.ts/materialize-support.ts is already resolved by
  materializer/scripts/copy-support-templates.js, and the File-global
  sdk-mapping.ts:74 concern is already resolved by emitter.ts's
  conditional `import { File } from 'node:buffer'` (Round 2).

New/updated tests in tests/codegen/js-sdk-emitter.test.ts and
tests/regression/generated-suites-typecheck.test.ts.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings September 16, 2026 05:14
… fallback too (Copilot PR #574 review, round 4)

hasMultipartStep previously only checked step.multipartTemplate !== undefined,
missing the bodyTemplate fallback path (payloadTemplate = multipartTemplate ??
bodyTemplate) that renderPythonMultipartFiles also resolves @@file: markers
from. A multipart step relying on that fallback still called resolve_fixture(...)
without the import being emitted, producing a NameError at runtime.

Added containsFixtureMarker() to recursively detect @@file: markers in either
multipartTemplate or bodyTemplate and gate the import on that instead.
…bjectConverter<T> (Copilot PR #576 review)

StringValueObjectConverter<T> is a private nested class; Activator.CreateInstance(Type)
only probes public constructors by default. Pass nonPublic: true so the converter
factory reliably works regardless of compiler/runtime-specific implicit-constructor
accessibility behavior.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate findings remain around generated literal escaping, falsy request bodies, fixture resolution, and exact status validation.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (10)

Previously missed (1) — in code that hasn't changed since the last review.

materializer/src/python-sdk/emitter.ts:228

  • The scalar JSON branch calls renderPythonBody for false, 0, and '', but the preceding truthiness check converts all of them to {}. RequestStep.bodyTemplate is unknown and the C# emitter already handles scalar bodies, so this drops the documented request payload for valid primitive schemas; only an absent (undefined) template should take the no-body path.
 * Substitutes placeholders like "${varName}" with ctx.get('var_name').

materializer/src/csharp-sdk/emitter.ts:564

  • The new bare-Task branch treats any non-throwing completion as success, but these SDK methods only enforce the broad 2xx range and expose no response status. Consequently a step expecting one exact status (for example 204) cannot detect a different successful status. Use a status-returning SDK path for these operations or explicitly model that exact status is unavailable instead of silently passing.
    // to compile with CS0815. A successful (non-throwing) completion is the
    // pass condition for these calls, so there is nothing to assert or
    // extract from.

materializer/src/csharp-sdk/emitter.ts:601

  • This generated assertion claims to check the planner's exact status, but the installed SDK methods return DTOs after EnsureSuccessStatusCode() and do not expose the HTTP response. AssertExpectedStatus therefore falls back to accepting any 2xx, so an expected 201/204 can pass after an unexpected 200/202. The call must use a status-aware SDK response (or the exact-status limitation must be modeled explicitly).
    body.push(`        AssertExpectedStatus(${varName}, ${step.expect.status});`);

materializer/src/js-sdk/emitter.ts:415

  • The successful branch forwards the request but never checks step.expect.status. Because the loose SDK client only throws for non-2xx responses, an expected 201/204 can silently pass when the server returns a different 2xx status. Emit an exact-status assertion using a status-aware SDK response before recording extracts.
      `      const ${responseVar} = await call${stepNum}(input${stepNum}, consistency${stepNum});`,

materializer/src/js-sdk/sdk-mapping.ts:24

  • This helper is also used for each multipart field value, so a valid falsy scalar such as false or 0 is rendered as {} instead of the scalar. Because request templates are typed as unknown and the sibling emitters support scalar values, use an undefined-only no-body check here (and preserve null as null) so multipart fields and scalar JSON bodies retain their wire type.
  if (!bodyTemplate) return '{}';

materializer/src/python-sdk/emitter.ts:186

  • The newly added embedded-placeholder branch interpolates varName directly into a single-quoted Python string literal. A spec-derived binding containing ', \, or a newline makes the generated test invalid or allows source injection; render the key with renderPythonStringLiteral(varName) in both ctx.get(...) calls.
        parts.push(renderPythonStringLiteral(literalBuffer));

materializer/src/python-sdk/emitter.ts:255

  • fieldName is another spec-derived value inserted into a single-quoted Python literal without escaping. The new dictionary-assignment path will emit invalid source for a property such as owner'sName; use renderPythonStringLiteral(fieldName) here, consistently with the multipart fix.
  const deferred: { fieldName: string; binding: string }[] = [];

materializer/src/python-sdk/emitter.ts:262

  • This deferred branch has the same code-generation bug as the inline branch: fieldName is interpolated into a Python string literal without escaping. When an omit-when-unbound field has a non-identifier name, the emitted suite can fail to parse or assign the wrong key.
    inlineEntries.push(`'${fieldName}': ${renderPythonValue(fieldValue)}`);

materializer/src/python-sdk/materialize-support.ts:251

  • The generated project vendors its fixtures specifically so resolution is independent of cwd, but these two candidates search the caller's working directory before the vendored/config fixture roots. Running pytest from the repository (or another directory containing the same relative name) can therefore silently load an unrelated file instead of the fixture selected by the generator. Remove the direct relative-path lookups and resolve only from the generated/configured fixture roots.
    candidates = [
        Path(relative_path),
        Path.cwd() / relative_path,
        Path.cwd() / 'fixtures' / relative_path,

tests/codegen/js-sdk-emitter.test.ts:319

  • This assertion locks in a missing status contract: the planner supplies expect.status, but a successful loose-client call can return 200 for an expected 201/204 without throwing. Generated JS suites therefore accept the wrong successful status and continue to extract data. Remove the negative assertion and make the generated success path verify the exact status (or use an SDK API that exposes it).
  • Files reviewed: 35/37 changed files
  • Comments generated: 3
  • Review effort level: Lite

// biome-ignore lint/suspicious/noTemplateCurlyInString: literal escaped `${RANDOM}` seed token text to embed verbatim in the emitted template literal
parts.push('\\${RANDOM}');
} else {
parts.push(`\${ctx['${match[1]}']}`);
// ctx keys are the planner's original binding variable names (e.g.
// tenantIdVar) — must match the ctx.set(...) calls emitted for
// scenario.bindings verbatim, so no casing transform here (#354).
return `ctx.get('${whole[1]}')`;
if (typeof value === 'string' && value.startsWith('@@FILE:')) {
const fixturePath = value.slice('@@FILE:'.length);
const filename = fixturePath.split('/').pop() || key;
return `'${key}': (${renderPythonStringLiteral(filename)}, resolve_fixture(${renderPythonStringLiteral(fixturePath)}))`;
Copilot AI review requested due to automatic review settings September 16, 2026 05:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

One critical fixture-path safety issue and three moderate request-handling issues remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (2)

materializer/src/js-sdk/emitter.ts:473

  • The loose client returns a response wrapper whose payload is under data (the emitter already emits response?.data for extraction), but this new witness predicate reads state/other planner paths directly from the wrapper. Every eventual wait therefore evaluates undefined === expected and times out even after the API returns the desired payload. Apply the predicate to body.data (or unwrap the response before passing it to awaitEventually).
    `            const v = (body as Record<string, unknown>)[${JSON.stringify(w.predicate.path)}];`,

materializer/src/python-sdk/emitter.ts:235

  • The body renderer's falsy guard above treats every falsy template as absent. Since this path now intentionally renders any non-undefined request body, valid top-level JSON bodies such as false, 0, '', or null reach this return but are emitted as {} instead of their planned value. Change the guard to an undefined check and let renderPythonValue handle scalar/null bodies.
  return renderPythonValue(bodyTemplate);
  • Files reviewed: 35/37 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment on lines +248 to +256
candidates = [
Path(relative_path),
Path.cwd() / relative_path,
Path.cwd() / 'fixtures' / relative_path,
Path.cwd() / 'configs' / active_config / 'fixtures' / relative_path,
here.parent / 'fixtures' / relative_path,
here.parent.parent / 'fixtures' / relative_path,
here.parent.parent.parent / 'fixtures' / relative_path,
]
johnOC03 added a commit that referenced this pull request Sep 21, 2026
* fix: real-toolchain-validated python-sdk emitter fixes

Split out of PR #573 (combined/sdk-emitter-fixes) into a standalone
per-SDK PR.

- Real httpx-based Python SDK request execution (no longer a self-mocked
  AsyncMock suite): multipart @@file fixture resolution, seed-binding
  restoration, f-string quote-nesting fix, path-param derivation off the
  raw path template.
- materializePythonFixtures: vendors BPMN/DMN/form fixtures into the
  generated python-sdk output dir.
- omitWhenUnbound/unique-binding awareness in the seedBindings loop
  (reuses playwright/ctxSeeding.ts, no reimplementation).
- init_spec_salt(...) moved to per-test placement (was module-scope,
  causing cross-module salt collisions under pytest's import-all
  collection).
- get_nested_value bracket-notation field paths; OperationMapSource
  has()/lookup() consistency fix.
- New/updated regression invariants for the emitted Python SDK suite
  (#133, #354): no leftover pass # TODO stubs, every Step N: comment
  has a matching real client call,  runtime seed tokens are
  not misflagged as missing bindings.

* fix: address Copilot PR #574 review comments (response-shape assertion, invariant precision, auth header)

* fix: address round-2 Copilot review comments on PR #574 (python-sdk emitter)

- renderPythonValue: mixed literal+\ strings are now rendered as
  Python f-strings so embedded bindings are no longer silently dropped
  when a placeholder doesn't occupy the entire string.
- Scenario materialization now throws at generation time for an empty
  requestPlan instead of emitting a comment-only test body that pytest
  reports as a silent pass.
- Added dedicated test coverage for renderPythonEventualWait's witness
  URL binding, predicate match, timeout, and poll-interval branches.

* fix: use is-not-None checks instead of or for path-param/template fallbacks in python-sdk emitter (Copilot PR #574 review, round 3)

* fix: preserve literal ${RANDOM} runtime seed token in python-sdk mixed-template rendering (Copilot PR #574 review, round 4)

- renderPythonTemplateString now special-cases the '${RANDOM}' token
  (planner-minted literal seed value, e.g. 'proc_${RANDOM}') as literal
  text instead of resolving it via ctx.get('RANDOM'), for both the
  whole-string and embedded-mixed-string cases -- mirrors how the
  JS/Playwright emitters never resolve this token either.
- Switched mixed-string rendering from an f-string to '+' concatenation
  of string-literal and ctx-lookup segments: an f-string's brace-doubling
  escaping would have corrupted the literal token, tripping
  regression-invariants.test.ts's exact-substring check (#133).
- Replaced the stale SAMPLE_COLLECTION-based whole-string-placeholder
  test (which asserted a negative regex against a fixture with no actual
  placeholder, so it could never fail) with a real widgetIdVar fixture
  asserting the actual ctx.get(...) lookup.
- Added regression tests for both the whole-string and embedded RANDOM
  cases.

* fix: gate python-sdk resolve_fixture import on multipart bodyTemplate fallback too (Copilot PR #574 review, round 4)

hasMultipartStep previously only checked step.multipartTemplate !== undefined,
missing the bodyTemplate fallback path (payloadTemplate = multipartTemplate ??
bodyTemplate) that renderPythonMultipartFiles also resolves @@file: markers
from. A multipart step relying on that fallback still called resolve_fixture(...)
without the import being emitted, producing a NameError at runtime.

Added containsFixtureMarker() to recursively detect @@file: markers in either
multipartTemplate or bodyTemplate and gate the import on that instead.

* fix: harden python-sdk emitter body-verbs, extract-vs-null, 429 retry, fixture order + docs

Addresses the 9 suppressed/low-confidence advisories from PR #574 review
round 4:

- emitter.ts: use client.request(<METHOD>, ...) instead of the
  verb-specific convenience method when a get/delete/options/head step
  carries a json=/data=/files= body, since httpx.AsyncClient's
  convenience methods reject those kwargs.
- emitter.ts: get_nested_value now returns a _MISSING sentinel (not
  None) so a missing/absent field no longer overwrites an existing
  seed or earlier extract via ctx.set; an explicit JSON null is still
  set as a real value.
- emitter.ts: the eventual-wait polling loop now retries on a 429
  response (like 404) instead of treating it as terminal, so a
  transient broker rate-limit doesn't fail the wait early.
- materialize-support.ts: resolve_fixture now prefers the vendored
  <suite>/fixtures/ candidates before cwd-relative fallbacks, so a
  same-named path in the invoking cwd can't shadow the active config's
  materialized fixture.
- README.md: fixed the httpx import missing from the runnable example
  snippet, and corrected the Operation Map section to describe it as
  currently-unused pass-through metadata (renderPythonSuite doesn't
  read it).
- tests/codegen/python-sdk-emitter.test.ts: added a temp-directory
  test for materializePythonFixtures (copy, missing-source, and
  overwrite-not-merge behavior), and updated the extract-call
  assertion for the new _MISSING-gated ctx.set shape.

Declined: query-parameter support in RequestStep/emitter (cross-cutting
path-analyser modeling gap, already tracked as follow-up in an earlier
review thread) and propagating a hard-fail from a skipped
empty-requestPlan scenario in materializer/src/index.ts's runForTarget
(shared across all emitter targets, out of scope for this python-sdk-only
PR).

Signed-off-by: yarm03 <jwoconnor03@gmail.com>

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: yarm03 <jwoconnor03@gmail.com>

---------

Signed-off-by: yarm03 <jwoconnor03@gmail.com>
Co-authored-by: yarm03 <jwoconnor03@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
johnOC03 added a commit that referenced this pull request Sep 21, 2026
* fix: real-toolchain-validated csharp-sdk emitter fixes

Split out of PR #573 (combined/sdk-emitter-fixes) into a standalone
per-SDK PR.

- Strongly-typed key-struct construction: CSHARP_PATH_PARAM_KEY_TYPE
  map + `.AssumeExists(...)` codegen for path params, falling back to
  a plain string binding for unmapped names (not every path param is a
  key struct, e.g. getUser's username).
- StringValueObjectConverterFactory (TestFixtureBase.cs): reflection-
  detects any SDK key-struct type (Value property + AssumeExists/
  IsValid) and (de)serializes it as a plain JSON string, fixing both
  request-body round-tripping and response field-path extraction.
- ExtendedDeploymentResponse-style "Raw" wrapper unwrapping in
  ToJsonElement, so field-path extraction matches the real wire shape
  instead of the C#-ergonomic PascalCase mirror properties.
- Fixture path resolution reuses TestFixtureBase's ResolveFixturePath()
  (was building an inline, less-robust path assuming fixtures/ sits
  next to the test binary).
- Missing operationId -> request-DTO mappings (createDocumentLink,
  deleteResource, resolveIncident, modifyProcessInstance, several
  *Statistics reads, etc.) and corrected 3 pre-existing wrong mappings.
- CSHARP_VOID_METHODS: bare-Task-returning SDK methods (GetStatusAsync,
  ResetClockAsync, ~48 Assign*/Unassign*/Delete* RBAC methods, etc.) no
  longer get an implicitly-typed `var result =` assignment (CS0815).
- JSON body rendering gained omitWhenUnbound awareness (previously only
  the multipart-fields loop had it) via
  partitionJsonFields/collectJsonRequestFields/emitJsonRequestDataLines.
- renderTenantExpr uses GetStringBindingOrNull (deployment tenantId is
  always broker-optional) instead of a throwing RequireStringBinding.
- eventualWaitsAfter witness-polling: AwaitEventuallyWitness +
  WitnessPredicateMatches (TestFixtureBase.cs) plus emitter wiring at
  all 3 success-path exits, mirroring the js-sdk emitter's pattern.
- Repo housekeeping swept in from the original combined branch's
  history (unrelated to any single SDK, folded in here rather than a
  separate PR): hashicorp/vault-action v3->v4 + create-github-app-token
  v2->v3 bumps, .dockerignore, README's @camunda8/c8ctl -> @camunda8/cli
  package-name fix, spec-pin bump, and a run-hub.sh fix so a
  generate-only STEPS run no longer requires a live Hub/Keycloak.

* fix: pass nonPublic:true to Activator.CreateInstance for StringValueObjectConverter<T> (Copilot PR #576 review)

StringValueObjectConverter<T> is a private nested class; Activator.CreateInstance(Type)
only probes public constructors by default. Pass nonPublic: true so the converter
factory reliably works regardless of compiler/runtime-specific implicit-constructor
accessibility behavior.

* fix(csharp-sdk): add missing statistics/path-param SDK type mappings; fix curl-only fixture gate

- CSHARP_REQUEST_TYPE_BY_OPERATION: add getProcessInstanceStatisticsByDefinition,
  getProcessDefinitionInstanceVersionStatistics, getJobErrorStatistics,
  getJobTimeSeriesStatistics, getJobWorkerStatistics (verified against the
  camunda/orchestration-cluster-api-csharp 9.2.2 source/docs) so generated
  no-body statistics calls no longer compile with CS7036.
- CSHARP_PATH_PARAM_KEY_TYPE: add authorizationKey, batchOperationKey,
  decisionEvaluationInstanceKey, decisionEvaluationKey, and
  adHocSubProcessInstanceKey (typed as ElementInstanceKey per the SDK docs)
  so the generated calls pass the SDK's strongly-typed key structs instead
  of RequireStringBinding's raw string, fixing CS1503.
- scripts/e2e/run-hub.sh: provision resource fixtures when either `run` or
  `curl` is selected (not just `run`), so a curl-only invocation still
  exports RV_FIXTURE_* and the curl oracle validates the intended 400s
  instead of surfacing 404/403 mismatches on filler keys.

Addresses Copilot PR review threads on emitter.ts:82 and emitter.ts:104,
and the suppressed advisory on scripts/e2e/run-hub.sh:70.

Signed-off-by: johnOC03 <jwoconnor03@gmail.com>

* fix(csharp-sdk): make RequireStringBinding return non-nullable string

RequireStringBinding used RequireBinding, which already guarantees a
non-null value, but the helper's return type was string?. Under
<Nullable>enable</Nullable> this propagated spurious nullable warnings
into generated SDK calls and AssumeExists(string) call sites. Change
the return type to non-nullable string and use the null-forgiving
operator on ToString() to match the real (non-null) contract.

Addresses Copilot PR #576 review suppressed advisory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: copilot-swe-agent[bot] <copilot-swe-agent[bot]@users.noreply.github.com>

* fix(csharp-sdk): dedupe CsharpOperationMap types by importing from sdk-mapping

emitter.ts re-declared CsharpOperationMapEntry/CsharpOperationMap locally even
though sdk-mapping.ts already defines and exports the same types. Import
and re-export them from sdk-mapping.js instead so there is a single
source of truth; consumers (index.ts, tests) keep importing from
emitter.ts unchanged.

Addresses suppressed Copilot review advisory in PR #576.

Signed-off-by: Copilot <copilot-swe-agent@users.noreply.github.com>

* fix(csharp-sdk): honor pathParams alias in client calls; fix curl-compare string body encoding

- renderClientCallForPath now consults RequestStep.pathParams[].var (the
  planner/emitter alias contract already honored by the JS SDK emitter's
  buildJavaScriptUrlExpression) instead of always deriving the ctx binding
  name from the URL param name, so an aliased path param resolves to the
  correct context value.
- curl_compare.py no longer JSON.stringify()s a primitive-string request
  body, which previously double-quoted the wire payload compared to what
  Playwright's request.data actually sends for a plain string.

Addresses two Copilot PR #576 suppressed advisories (nano-ack threads
posted separately).

Signed-off-by: nano-agent <nano-agent@users.noreply.github.com>

---------

Signed-off-by: johnOC03 <jwoconnor03@gmail.com>
Signed-off-by: copilot-swe-agent[bot] <copilot-swe-agent[bot]@users.noreply.github.com>
Signed-off-by: Copilot <copilot-swe-agent@users.noreply.github.com>
Signed-off-by: nano-agent <nano-agent@users.noreply.github.com>
Co-authored-by: yarm03 <jwoconnor03@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <copilot-swe-agent[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <copilot-swe-agent@users.noreply.github.com>
Co-authored-by: nano-agent <nano-agent@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants