Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,20 @@ jobs:
# target must run from repo root.
working-directory: .
run: make swift-check-drift

- name: Run Swift conformance runner unit tests
# Unit-tests the runner's own assertion helpers. Their bounds
# branches never execute against a fixture that passes, so a
# vacuous assertion (#563) survives a fully green conformance run.
working-directory: .
run: make conformance-swift-runner-tests

- name: Run Swift conformance tests
# Same single-source-of-truth pattern as the drift gate: the make
# target carries the platform gate (IS_MACOS), and this macos runner
# satisfies it. The target must run from repo root.
working-directory: .
run: make conformance-swift
test-kotlin:
name: Kotlin Tests
runs-on: ubuntu-latest
Expand Down Expand Up @@ -446,7 +460,7 @@ jobs:
# which includes its conformance step — did not succeed.
name: Conformance Tests
runs-on: ubuntu-latest
needs: [test-go, test-typescript, test-ruby, test-kotlin, test-python]
needs: [test-go, test-typescript, test-ruby, test-kotlin, test-python, test-swift]
if: always()
permissions:
contents: read
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ conformance/runner/typescript/node_modules
# test run cannot dirty the working tree and trip the release clean-tree guard.
node_modules/
.vite/
conformance/runner/swift/.build/
conformance/runner/swift/.swiftpm/
conformance/runner/swift/Package.resolved

# Claude Code session files (except skills, which are committed)
.claude/*
Expand Down
35 changes: 33 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,11 @@ py-clean:
# Conformance Test targets
#------------------------------------------------------------------------------

.PHONY: conformance conformance-runner-tests conformance-go conformance-go-replay conformance-kotlin conformance-kotlin-replay conformance-typescript conformance-typescript-live conformance-ruby conformance-ruby-replay conformance-python conformance-python-replay conformance-build conformance-live conformance-canary oauth-fixtures-check oauth-token-fixtures-check conformance-fixtures-check
.PHONY: conformance conformance-runner-tests conformance-go conformance-go-replay conformance-kotlin conformance-kotlin-replay conformance-typescript conformance-typescript-live conformance-ruby conformance-ruby-replay conformance-python conformance-python-replay conformance-swift conformance-swift-runner-tests conformance-build conformance-live conformance-canary oauth-fixtures-check oauth-token-fixtures-check conformance-fixtures-check

# NOTE: conformance-swift and conformance-swift-runner-tests are defined in the
# Swift SDK targets section below — their IS_MACOS conditional must parse after
# that variable is defined.

# Pinned validator for the data-only OAuth discovery fixtures. Run via uvx so the
# version is reproducible without a global install; the schema is separate from
Expand Down Expand Up @@ -455,6 +459,7 @@ conformance-runner-tests:
cd conformance/runner/python && uv run python -m pytest -q test_delay_gaps.py
cd conformance/runner/ruby && bundle install --quiet && bundle exec ruby delay_gaps_test.rb
cd kotlin && ./gradlew --quiet :conformance:test
@$(MAKE) --no-print-directory conformance-swift-runner-tests

# Build conformance test runner
conformance-build:
Expand Down Expand Up @@ -530,7 +535,7 @@ conformance-python-replay:
cd conformance/runner/python && uv sync && uv run python replay_runner.py

# Run all conformance tests
conformance: oauth-fixtures-check oauth-token-fixtures-check conformance-fixtures-check conformance-runner-tests conformance-go conformance-kotlin conformance-typescript conformance-ruby conformance-python
conformance: oauth-fixtures-check oauth-token-fixtures-check conformance-fixtures-check conformance-runner-tests conformance-go conformance-kotlin conformance-typescript conformance-ruby conformance-python conformance-swift
@echo "==> Conformance tests passed"

# Orchestrate one canary pass against a single backend:
Expand Down Expand Up @@ -730,6 +735,30 @@ else
@echo "SKIP: swift-check (macOS only)"
endif

# Run Swift conformance tests (macOS only — the SDK requires Apple platforms).
# Defined here rather than in the conformance section so the IS_MACOS ifdef
# parses after the variable is defined above.
conformance-swift:
ifdef IS_MACOS
@echo "==> Running Swift conformance tests..."
cd conformance/runner/swift && swift run ConformanceRunner
else
@echo "SKIP: conformance-swift (macOS only)"
endif

# Unit-test the Swift runner's own assertion helpers (macOS only). Same reason
# as the other five: the bounds branches never execute against a fixture that
# passes, so a vacuous assertion survives a fully green conformance run.
# Reached from conformance-runner-tests, which is platform-agnostic and defers
# the gate to this target.
conformance-swift-runner-tests:
ifdef IS_MACOS
@echo "==> Running Swift conformance runner unit tests..."
cd conformance/runner/swift && swift test
else
@echo "SKIP: conformance-swift-runner-tests (macOS only)"
endif

# Regenerate Swift SDK services from OpenAPI spec (needs swift on any platform)
swift-generate:
ifdef HAS_SWIFT
Expand Down Expand Up @@ -999,6 +1028,8 @@ help:
@echo " conformance-ruby-replay Decode TS-captured wire snapshots through Ruby SDK"
@echo " conformance-python Run Python conformance tests"
@echo " conformance-python-replay Decode TS-captured wire snapshots through Python SDK"
@echo " conformance-swift Run Swift conformance tests (macOS only)"
@echo " conformance-swift-runner-tests Unit-test the Swift runner's assertion helpers (macOS only)"
@echo " conformance-build Build Go conformance test runner"
@echo " oauth-fixtures-check Validate OAuth discovery fixtures against their schema"
@echo " oauth-token-fixtures-check Validate OAuth token wire-behavior fixtures against their schema"
Expand Down
13 changes: 11 additions & 2 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1635,7 +1635,7 @@ All wire operations are generated (rubric 1A.6). One narrow exception is sanctio
1. **No hand-written wire I/O.** Every request flows through public generated wire methods (Go: through the shared generated-client transport). No manual path construction or verb selection. Bodies use the generated request types, with one Go-specific carve-out: where zero-value + `omitempty` request structs cannot express always-send-empty semantics, the composite's private transport MAY marshal an explicit body map and call the operation's generated `*WithBody` variant — the generated wrapper still owns path, verb, content type, and response decoding, and the operation identity still reaches hooks and retry. This is the only sanctioned use of hand-marshaled bodies; sparse public methods keep using the generated request types.
2. **Composition, not substitution.** It composes existing generated operations (e.g. GET → overlay → full PUT); it never introduces a wire operation the spec lacks — fix the spec and regenerate instead.
3. **Native hook identities.** Hooks observe the constituent wire operations under their normal per-language identities; composites never mint synthetic operation names.
4. **Conformance-covered.** The composite's behavior is encoded in `conformance/tests/` fixtures run by every runner (with native test mirrors where a runner does not exist yet, e.g. Swift).
4. **Conformance-covered.** The composite's behavior is encoded in `conformance/tests/` fixtures run by every runner. All six SDKs now have one, so a native test mirror is no longer a substitute for fixture coverage.
5. **Declared placement.** The composite lives in the language's designated hand-written extension point (Kotlin generator `EXTENSIBLE_SERVICES`/`HAND_WRITTEN_SERVICES`, TS `src/services/*-extensions.ts` wired in `client.ts`, Ruby zeitwerk `prepend` module, Python service subclass re-exported by the client, Swift same-module extension) so regeneration can never silently drop or fork it.
6. **The raw operation stays reachable.** When a composite takes over the plain method name, the generated single-request method is renamed (via `METHOD_NAME_OVERRIDES`) rather than hidden, and gets its own conformance case asserting it makes exactly one request with no read-before-write. Without that second case, later generator drift could silently turn both public methods into composite behavior and nothing would notice.

Expand Down Expand Up @@ -1753,6 +1753,15 @@ logic is covered by `TestIsSameOrigin` unit tests:
is empty):
- "List operation returns first page with Link header" — skipped via the `link-header` tag branch, not `KOTLIN_SKIPS`: Kotlin auto-paginates by design, so a first-page-only requestCount assertion is inapplicable (architectural).

**Swift** (`conformance/runner/swift/.../Runner.swift` — one tag-based branch;
`temporarySkips` is empty):
- "List operation returns first page with Link header" — skipped via the `link-header` tag branch, not `temporarySkips`: Swift auto-paginates by design, so a first-page-only requestCount assertion is inapplicable (architectural, identical to Kotlin and TypeScript).

Swift carries no capability skips. It is three-gate on retry (status, network,
idempotent POST) and, since #563, retries the authenticated download hop, so
`SWIFT_CONFORMANCE_NO_SKIPS=1` is a no-op today — the mechanism is kept live so
a future temporary skip must be proven genuine before it is added.

The TypeScript live canary additionally reports one placeholder skip when
`BASECAMP_LIVE` is unset (`live-runner.test.ts`) — that is the opt-in gate for
`live-my-surface.json` documented in the category table above, not a
Expand Down Expand Up @@ -1798,7 +1807,7 @@ The following are must-pass criteria from the rubric. Each maps to a spec sectio
| `rb-check` | Ruby: test + rubocop |
| `kt-check` | Kotlin: build + test |
| `swift-check` | Swift: build + test |
| `conformance` | All conformance test categories pass with documented waivers (go, kotlin, python, ruby, typescript runners) |
| `conformance` | All conformance test categories pass with documented waivers (go, kotlin, python, ruby, swift, typescript runners) |

Representative dependency chain (see the Makefile `check:` line for the authoritative, complete list): `check: … sync-api-version-check url-routes-check go-check-drift … kt-check-drift … go-check ts-check rb-check kt-check swift-check py-check conformance …`

Expand Down
57 changes: 57 additions & 0 deletions conformance/runner/swift/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// swift-tools-version: 6.0
import PackageDescription

// Development-only conformance runner. Depends on the Swift SDK via the ROOT
// distribution manifest (../../../Package.swift), whose Basecamp target builds
// the same swift/Sources/Basecamp sources as the development manifest.
//
// Why not swift/Package.swift directly: SwiftPM derives package identity from
// the directory name, so a path dependency on ".../swift" collides with this
// package's own "conformance/runner/swift" identity and is silently treated
// as a self-reference ("product 'Basecamp' ... not found"). The root manifest
// avoids the collision without renaming either directory.
let package = Package(
name: "ConformanceRunner",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(name: "Basecamp", path: "../../..")
],
targets: [
// Assertion contracts with no SDK dependency, split out of the
// executable so their bounds branches can be unit-tested: a target
// carrying @main cannot host XCTest cleanly, and these branches never
// execute against a fixture that passes. #563 shipped a
// delayBetweenRequests check that vacuously passed when the gap it
// named did not exist, in four runners at once.
.target(
name: "ConformanceSupport",
path: "Sources/ConformanceSupport",
swiftSettings: [
.swiftLanguageMode(.v6)
]
),
.executableTarget(
name: "ConformanceRunner",
dependencies: [
"Basecamp",
"ConformanceSupport"
],
path: "Sources/ConformanceRunner",
swiftSettings: [
.swiftLanguageMode(.v6)
]
),
.testTarget(
name: "ConformanceSupportTests",
dependencies: [
"ConformanceSupport"
],
path: "Tests/ConformanceSupportTests",
swiftSettings: [
.swiftLanguageMode(.v6)
]
)
]
)
Loading
Loading