Skip to content

Commit 3835d74

Browse files
committed
sync(4.29): fidelity scope extension, divergence rows, final test ports
- MAPPING: four new chat@4.29.0 core test files mapped (callback-url, thread-history, transcripts, transcripts-wiring) — 12 of 19 files now in scope; stale 4.26-era references in the script/template updated - fidelity_baseline.json regenerated at chat@4.29.0 (731 TS tests in scope, 0 missing; strict mode green) - docs/UPSTREAM_SYNC.md: Known Non-Parity rows from the port wave — GitHub octokit / Linear linear_client getters, @chat-adapter/tests kit, Teams modal-submit webhook options slice, jsx-runtime callbackUrl props, Transcripts API Python adaptations, Slack legacy mrkdwn renderer scope note, Discord gateway-only interactions surface, and the Teams microsoft-teams-apps deferral to 0.4.30 - tests/test_chat_faithful.py: the [Slash Commands] duplicate-title openModal-unsupported port (matcher counts names as a multiset) and the #459 subject-wiring port (skipif-gated on BaseAdapter.fetch_subject, activates when PR #131 lands) https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64
1 parent 39432c3 commit 3835d74

4 files changed

Lines changed: 86 additions & 9 deletions

File tree

docs/UPSTREAM_SYNC.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,10 @@ stay explicit instead of being rediscovered in code review.
639639
| `GitHubAdapter.octokit` native client getter (vercel/chat#459, #478) | Not exposed | `get octokit(): Octokit` (plus deprecated `client` alias) returns the underlying Octokit — fixed instance in PAT/single-tenant App mode, per-installation client resolved from `AsyncLocalStorage` inside a webhook handler in multi-tenant mode | The Python adapter is hand-rolled over raw `aiohttp` (`_github_api_request`) with PyJWT for App JWTs and an installation-token cache; the `github` extra is `pyjwt[crypto]` only — there is no Octokit-equivalent object to return, and exposing the raw session or an invented facade under the name `octokit` would misrepresent the surface. Revisit if the adapter adopts an octokit-style SDK (e.g. `githubkit`) as an optional dependency per hazard #10's "prefer official SDKs" sub-rule; the getter (and the GitHub `fetch_subject` half of #459) ports cleanly then. |
640640
| `LinearAdapter.linear_client` native client getter (vercel/chat#459, #478) | Not exposed | `get linearClient(): LinearClient` (plus deprecated `client` alias) returns the `@linear/sdk` `LinearClient`, per-org from `AsyncLocalStorage` in multi-tenant OAuth mode | `@linear/sdk` is TypeScript-only and no official Linear Python SDK exists; the adapter issues GraphQL directly over `aiohttp` (`_graphql_query`) and already documents that stance. Nothing honest to put behind the name. Revisit only if Linear ships an official Python SDK (the Linear `fetch_subject` half of #459 is blocked on the same). |
641641
| `@chat-adapter/tests` adapter test kit (vercel/chat#470) | Not ported | New TS package with test utilities for adapter authors | Python already ships `chat_sdk.testing` (`MockAdapter`, `MockStateAdapter`, `create_test_message()`) covering the same surface for this repo's adapter tests; mirroring the TS kit verbatim would duplicate it. Revisit if upstream's kit grows capabilities ours lacks (e.g. recorded replay fixtures for third-party adapter authors). |
642+
| Teams modal-submit webhook options (vercel/chat#454 adapter-teams slice) | Not ported — the Python Teams adapter has no task-module/modal-submit flow (`handleTaskSubmit`/`processModalSubmit` are absent), so upstream's change passing `bridgeAdapter.getWebhookOptions(activity.id)` into `processModalSubmit` has no landing site | `TeamsAdapter.handleTaskSubmit` forwards webhook options so modal callbackUrl POSTs are registered with `waitUntil` | Pre-existing gap: Teams modals are unported. The Slack adapter already forwards options to `process_modal_submit`, so the new waitUntil plumbing is exercised there. Add the Teams call when Teams modal support lands. |
643+
| jsx-runtime `callbackUrl` props (vercel/chat#454 slice) | Not ported | `ButtonProps`/`ModalProps` gain `callbackUrl`; `resolveJSXElement` forwards it | Covered by the existing "JSX Card/Modal elements" row — Python has no JSX runtime; `Button()`/`Modal()` builders accept `callback_url` directly. |
644+
| Transcripts API Python adaptations (vercel/chat#448) | `transcripts.delete()` returns a `DeleteResult` dataclass; misconfiguration raises `ValueError` (constructor/`AppendInput` guards, invalid duration) or `ChatError` (`chat.transcripts` accessor); guard messages name the Python kwarg (`options.user_key`); `DurationString` is a `str` alias validated at runtime by `_parse_duration` | Inline `{ deleted: number }`; generic `Error` for all of the above; template-literal `` `${number}${"s"\|"m"\|"h"\|"d"}` `` type | Port rules: typed dataclasses over raw dicts; repo error-type conventions (constructor misconfig → `ValueError`, runtime API misuse → `ChatError`) with upstream-matching message wording; Python has no template-literal types. Same shapes and values throughout. |
645+
| Slack legacy mrkdwn renderer (response_url surface only, post-#440) | `_node_to_mrkdwn` renders headings as `*bold*` and images as `{alt} ({url})` / bare URL | TS `nodeToMrkdwn` has no heading/image branches — both fall through to `defaultNodeToText`, dropping heading emphasis and image URLs | Pre-existing Python improvement; after vercel/chat#440 it affects only `to_response_url_text` (ephemeral edits via response_url). Preserves visual hierarchy and image URLs Slack would otherwise lose. |
642646

643647
### Platform-specific gaps
644648

@@ -648,6 +652,7 @@ stay explicit instead of being rediscovered in code review.
648652
| Teams `dialog_open_timeout_ms` config | Not implemented | Configurable | Low demand |
649653
| Google Chat file uploads | Ignored in message parse | Supported | API complexity; can add later |
650654
| Discord Gateway WebSocket | HTTP interactions only | Both HTTP and Gateway | Gateway requires persistent connection |
655+
| Discord gateway-only interactions (vercel/chat#490) | Handled on the forwarded-event surface: a `GATEWAY_INTERACTION_CREATE` envelope (raw INTERACTION_CREATE dispatch payload in `data`) is deferred via `POST /interactions/{id}/{token}/callback` (type 5 slash / type 6 component) and routed through the existing HTTP interaction handlers; a malformed forward missing `id`/`token` is logged and skipped | Upstream handles `Events.InteractionCreate` directly on the resident discord.js client via `deferReply()`/`deferUpdate()`; the upstream forwarder never forwards interactions | Python has no resident Gateway client (row above), so gateway-only deployments run an external listener shim that forwards raw dispatch payloads (`x-discord-gateway-token`). Observable wire behavior is identical — same callback REST calls, same handler routing, same `@original` deferred-response resolution. |
651656
| Teams `User-Agent: Vercel.ChatSDK` outbound header | Not set on `aiohttp` calls | Propagated by `botbuilder` 2.0.8 | Python Teams adapter doesn't use `botbuilder` (raw `aiohttp`). Upstream's vercel/chat#415 was a JS-only `botbuilder` SDK bump that flipped `X-User-Agent``User-Agent`. No equivalent dependency to bump on the Python side. Setting a `User-Agent` on the ~9 outbound `aiohttp` call sites would be a defense-in-depth nice-to-have; deferred to a follow-up. |
652657
| Teams adapter on `microsoft-teams-apps` (official MS Python SDK) | Hand-rolled Bot Framework REST + JWT verification (see the transitional native-streaming rows above) | `@microsoft/teams.apps` owns the wire format, throttling, and activity routing | **Explicitly deferred to the 0.4.30 cycle** (issue #93). The Python SDK only went GA 2026-05-01; the migration is a 4–6 day restructuring of `adapter.py` (~2,300 LOC) + 6 test files and was too large to land inside the 4.29 wave's tail. The 3.12 floor bump (#111) — the migration's prerequisite — already landed in 0.4.29. |
653658
| Telegram `get_user().is_bot` | Always `False` (matches upstream — `getChat` does not expose `is_bot`) | Always `false` (same caveat documented in upstream code comment) | The Telegram Bot API's `getChat` endpoint does not surface the `is_bot` field that's available on the `User` object inside incoming `Message` updates. Callers needing bot detection must use `message.author.is_bot` from webhooks instead of `chat.get_user(...).is_bot`. |

scripts/fidelity_baseline.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"_comment": "Ratchet-down baseline for scripts/verify_test_fidelity.py. This repo ships at strict fidelity for mapped core files (0 missing) against chat@4.26.0, so the baseline is empty. Scope: the MAPPING dict in scripts/verify_test_fidelity.py is the authoritative list of TS files checked; it currently covers 8 of the 17 packages/chat/src/*.test.ts files. Default CI mode runs --strict via .github/workflows/lint.yml; this file is retained for local workflows that want to opt back into baseline mode (e.g. during an upstream sync where several ports land in flight). To baseline genuinely-divergent tests, run scripts/verify_test_fidelity.py --update-baseline after documenting the divergence in docs/UPSTREAM_SYNC.md.",
3-
"ts_parity": "chat@4.26.0",
4-
"total_ts_tests": 588,
3+
"ts_parity": "chat@4.29.0",
4+
"total_ts_tests": 731,
55
"total_missing": 0,
66
"missing": {}
77
}

scripts/verify_test_fidelity.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@
1414
``--strict`` is the current CI contract (see ``.github/workflows/lint.yml``):
1515
the baseline is ignored and any missing translation — or a missing upstream
1616
checkout — fails the build. This repo ships at strict fidelity for mapped
17-
core files (0 missing) against ``chat@4.26.0``. The ``MAPPING`` dict below
18-
is the authoritative scope list; it currently covers 8 of the 17
19-
``packages/chat/src/*.test.ts`` files (extending it is tracked as a
20-
follow-up).
17+
core files (0 missing) against ``chat@4.29.0``. The ``MAPPING`` dict below
18+
is the authoritative scope list (extending it to the remaining unmapped
19+
``packages/chat/src/*.test.ts`` files is tracked as issue #78).
2120
2221
Baseline mode (the default without ``--strict``) is retained for local
2322
workflows where a few ports land in flight: it succeeds iff the set of
@@ -48,6 +47,11 @@
4847
# chat@4.29.0 moved ai.test.ts into ai/ and split it (vercel/chat#492)
4948
"packages/chat/src/ai/messages.test.ts": "tests/test_ai_messages.py",
5049
"packages/chat/src/ai/index.test.ts": "tests/test_ai_tools.py",
50+
# New core test files in chat@4.29.0
51+
"packages/chat/src/callback-url.test.ts": "tests/test_callback_url.py",
52+
"packages/chat/src/thread-history.test.ts": "tests/test_thread_history.py",
53+
"packages/chat/src/transcripts.test.ts": "tests/test_transcripts.py",
54+
"packages/chat/src/transcripts-wiring.test.ts": "tests/test_transcripts_wiring.py",
5155
"packages/chat/src/from-full-stream.test.ts": "tests/test_from_full_stream.py",
5256
}
5357

@@ -277,7 +281,7 @@ def load_baseline(path: Path) -> dict[str, set[tuple[str, str]]]:
277281
"against the current UPSTREAM_PARITY tag, so the baseline is "
278282
"normally empty. Scope: the MAPPING dict in "
279283
"scripts/verify_test_fidelity.py is the authoritative list of TS "
280-
"files checked; it currently covers 8 of the 17 "
284+
"files checked (extending to the remaining unmapped files is issue #78) "
281285
"packages/chat/src/*.test.ts files. Default CI mode runs --strict "
282286
"via .github/workflows/lint.yml; this file is retained for local "
283287
"workflows that want to opt back into baseline mode (e.g. during "
@@ -313,7 +317,7 @@ def write_baseline(path: Path, all_missing: dict[str, list], total_ts: int) -> N
313317
current_parity = _current_parity_tag()
314318
payload = {
315319
"_comment": existing_comment if existing_comment is not None else _DEFAULT_BASELINE_COMMENT,
316-
"ts_parity": current_parity if current_parity is not None else "chat@4.26.0",
320+
"ts_parity": current_parity if current_parity is not None else "chat@4.29.0",
317321
"total_ts_tests": total_ts,
318322
"total_missing": sum(len(v) for v in all_missing.values()),
319323
"missing": {
@@ -442,7 +446,7 @@ def main() -> int:
442446
print(f" - {path}")
443447
print(
444448
"\nClone the upstream repo at the pinned parity tag, e.g.:\n"
445-
" git clone --depth 1 --branch chat@4.26.0 "
449+
" git clone --depth 1 --branch chat@4.29.0 "
446450
"https://github.com/vercel/chat.git /tmp/vercel-chat\n"
447451
"then re-run with TS_ROOT=/tmp/vercel-chat."
448452
)

tests/test_chat_faithful.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4014,6 +4014,7 @@ async def test_persists_when_both_persistthreadhistory_and_persistmessagehistory
40144014
assert stored is not None
40154015
assert stored[0]["id"] == "msg-1"
40164016

4017+
40174018
# ============================================================================
40184019
# 21. processMessage return value (core slice of vercel/chat#444)
40194020
# ============================================================================
@@ -4179,6 +4180,7 @@ async def handler(thread, message, context=None):
41794180
assert received_skipped_subject == mock_subject
41804181
assert adapter.fetch_subject.await_count == 2 # type: ignore[attr-defined]
41814182

4183+
41824184
# ============================================================================
41834185
# 24. Action callbackUrl handling (vercel/chat#454)
41844186
# ============================================================================
@@ -4565,3 +4567,69 @@ async def _handler(event):
45654567
await _process_action_and_wait(chat, event)
45664568

45674569
assert any(call_args[0] == "Button callbackUrl POST failed" for call_args in logger.error.calls)
4570+
4571+
4572+
# ============================================================================
4573+
# 25. Slash-command openModal without adapter support + subject wiring
4574+
# ============================================================================
4575+
4576+
4577+
class TestSlashCommandOpenModalUnsupported:
4578+
"""[Slash Commands] flavor of the openModal-unsupported test.
4579+
4580+
Upstream chat.test.ts has two tests with this exact title (one under
4581+
the actions describe, one under slash commands); the fidelity matcher
4582+
counts Python names as a multiset, so each needs its own def.
4583+
"""
4584+
4585+
# TS: "should return undefined from openModal when adapter does not support modals"
4586+
async def test_should_return_undefined_from_openmodal_when_adapter_does_not_support_modals(
4587+
self,
4588+
):
4589+
adapter = create_mock_adapter("slack")
4590+
adapter.open_modal = None # type: ignore[assignment]
4591+
4592+
chat, _, _ = await _init_chat(adapter=adapter)
4593+
captured_event: list[SlashCommandEvent] = []
4594+
4595+
async def _handler(event):
4596+
captured_event.append(event)
4597+
4598+
chat.on_slash_command("/feedback", _handler)
4599+
4600+
event = _make_slash_event(adapter, command="/feedback", text="", trigger_id="trigger-123")
4601+
chat.process_slash_command(event)
4602+
await asyncio.sleep(0.02)
4603+
4604+
assert len(captured_event) == 1
4605+
4606+
modal = {
4607+
"type": "modal",
4608+
"callback_id": "test_modal",
4609+
"title": "Test Modal",
4610+
"children": [],
4611+
}
4612+
result = await captured_event[0].open_modal(modal)
4613+
assert result is None
4614+
4615+
4616+
class TestSubjectAdapterWiring:
4617+
# TS: "should wire adapter on message for subject access"
4618+
@requires_fetch_subject
4619+
async def test_should_wire_adapter_on_message_for_subject_access(self):
4620+
chat, adapter, _state = await _init_chat()
4621+
received: list[Any] = []
4622+
4623+
@chat.on_mention
4624+
async def handler(thread, message, context=None):
4625+
received.append(message)
4626+
4627+
await chat.handle_incoming_message(
4628+
adapter,
4629+
"slack:C123:1234.5678",
4630+
create_test_message("msg-subject", "Hey @slack-bot test"),
4631+
)
4632+
4633+
assert len(received) == 1
4634+
# The mock adapter exposes no fetch_subject hook -> resolves None
4635+
assert await received[0].subject is None

0 commit comments

Comments
 (0)