test(vertical): composition eval suite over the assembled chat modules (#188)#193
Merged
Conversation
bulkDeleteThreads iterated workspaces in DB row order of random hex thread ids, so which workspace's denial surfaced first varied per run — the unit test asserting the check sequence was flaky (caught by the vertical composition suite runs). Sort the distinct workspace ids before checking; fail-closed semantics are unchanged.
interactionToPersistedPart/noticePart returned Record<string, unknown>, so every product pushing them into a ChatMessagePart[] transcript needed a cast, and the 'byte-matches' claim between the codecs and chat-store's ChatInteractionPart/ChatNoticePart was prose only. Return the precise InteractionPersistedPart/NoticePersistedPart shapes (type aliases, so Record<string, unknown> consumers stay source-compatible) and enforce mutual assignability with the stored part types at compile time in chat-store/parts.ts.
ProvisionPayloadSections claimed to be structural but typed backend.profile as the SDK's AgentProfile, so even this module's own call site cast into its own gate type, and the SDK's string profile ref was rejected. Type the slice the gate actually reads (ProvisionProfileSection: resources.files) plus the string ref arm, and drop the internal cast.
A mini chat app assembled ONLY from the shipped subpaths — createAppAuth (better-auth memoryAdapter) + createChatTables/createChatStore (:memory: better-sqlite3) + the #190 turn-boundary gates + a fake sandbox producer emitting canonical sidecar part streams pumped through /stream's normalizer into both the NDJSON client lane and the persisted parts lane + createInteractionAnswerRoute over a fake sidecar broker the producer genuinely blocks on. 15 scenario evals assert user-visible outcomes only: full turn persists parts + token receipts; failed turn keeps the user message and surfaces the error; interaction ask blocks the turn, rejects invalid answers fail-closed, unblocks on answer, and persists the answered card; re-emitted duplicate questions render one card and one answer resolves both; cross-workspace reads/answers/lists/deletes are denied with zero side effects; the three #190 gates fire client-visible 400s with their breakdown messages before any row is written. This suite is the composition gate for #189/#190/#191/#192: a subpath change that passes its unit tests but breaks assembly fails here.
tangletools
approved these changes
Jul 15, 2026
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — f12bea43
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-07-15T18:52:13Z
tangletools
approved these changes
Jul 15, 2026
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — f9d6530b
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-07-15T18:55:27Z
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Assembles the just-merged #189 (app-auth), #190 (incident gates), #191 (chat-store), and #192 (interactions) into ONE mini chat app under
tests/vertical/and evaluates whether the modules actually compose into a product — not whether their unit tests pass. The suite runs in normal CI (in-memory sqlite + memoryAdapter, no network) and becomes the permanent composition gate for issue #188: a future subpath change that keeps its unit tests green but breaks assembly fails here.The assembled app (
tests/vertical/mini-app.ts)createAppAuth(real better-auth, memory adapter) → guard quartet → product membership check →createChatStoreover:memory:better-sqlite3 with real DDL/FKs → the three #190 gates at the turn boundary → a fake sandbox producer emitting canonical sidecar part streams (text/reasoning snapshots+deltas, tool state transitions,step-finishtoken receipts, interaction asks, error) pumped through/stream's normalizer into both lanes (NDJSON client stream consumed with/web-react'sconsumeChatStream, persistedmessage.parts) →createInteractionAnswerRouteover a fake sidecar broker the producer genuinely blocks on (tests/vertical/fake-sidecar.ts).Scenario verdicts (15 evals, all user-visible outcomes)
auth.unauthenticated; non-member turn → 403 before any row is writtenonErrorEventis wired (#190), user message retained, partial output +noticepart persistedonErrorEventwired → routed to the callback, no transcript duplicateINVALID_INTERACTION_ANSWERand run stays blocked; valid answer unblocks; answered card persisted and round-trips throughpersistedPartToInteraction; replay → 410INTERACTION_EXPIREDover the 40000-byte budget+ section breakdown, fired pre-writeBreakdown: profile=…(files=…), env=…, secrets=…Seams fixed in this branch (S-cost, all additive)
src/chat-store/store.ts:236—bulkDeleteThreadschecked workspaces in DB row order of random hex ids: which denial surfaced first was nondeterministic and the unit test asserting the check sequence (tests/chat-store/chat-store.test.ts:221) was flaky — it failed 2/5 local runs. Distinct workspace ids are now sorted before checking; fail-closed semantics unchanged.src/interactions/contract.ts—interactionToPersistedPart/noticePartreturnedRecord<string, unknown>; the "byte-matches chat-store'sChatInteractionPart/ChatNoticePart" claim was prose only. They now return preciseInteractionPersistedPart/NoticePersistedParttypes (aliases, soRecord<string, unknown>consumers stay source-compatible) andsrc/chat-store/parts.tsenforces mutual assignability at compile time — a field added on one side without the other now failstsc.src/sandbox/index.ts:1040—ProvisionPayloadSectionsclaimed to be structural but typedbackend.profileas the SDK'sAgentProfile; even the module's own call site (ensureWorkspaceSandbox) had to cast into its own gate type, and the SDK's string profile ref was rejected. Now a structuralProvisionProfileSection(+ thestringref arm); internal cast dropped.Filed (larger than S-cost)
/streamand/chat-store:finalizeAssistantParts/mergePersistedPart(src/stream/stream-normalizer.ts:165,211) produceJsonRecord[]whileappendMessage(src/chat-store/store.ts:70) takesChatMessagePart[]— every product persisting a streamed turn writesas unknown as ChatMessagePart[](seetests/vertical/mini-app.ts, persistence lane). Needs a typed projection (aChatMessagePart-typed finalize overload, or a validatingtoChatMessagePartsin chat-store); deciding where that boundary lives is a design call, not a one-line export.normalizePersistedPartreturnsnullfor every canonical kind other than text/reasoning/tool (src/stream/stream-normalizer.ts:87-150) — an integrator feeding all parts through it silently dropsstep-finishreceipts andinteractionparts. Documented in/chat-store/parts.ts, but anormalizePersistedPart-adjacent passthrough for the storable kinds would remove the trap.Responses whilecreateInteractionAnswerRoute.resolveConnectionexpects{ ok: false, response }returns — every product writes the same try/catch adapter (tests/vertical/mini-app.ts,resolveConnection).Verification
pnpm typecheckclean;pnpm test149 files / 2416 tests green;NODE_OPTIONS=--max-old-space-size=8192 pnpm buildgreen on a cleanpnpm install --frozen-lockfile.tests/chat-storerun 3× consecutively green after fix 1 (was flaky before).🤖 Generated with Claude Code