mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
* fix(mobile): move the last six reply-enum pins where tsc looks mobile/tsconfig.json excludes *.test.ts, so a `Record<HostUnion, true>` coverage record in a schema test is never typechecked: the two that existed (SshConnectionStatus, GitHubProjectOwnerType) checked nothing, and the four closed enums beside them had only a doc citation of the host type. Each arm list moves into its schema module as hostUnionArms<Union>(), which #21269 introduced for the same reason, and each test iterates the exported list instead of holding its own copy: - SSH_CONNECTION_STATUS to SshConnectionStatus - PROJECT_OWNER_TYPE to GitHubProjectOwnerType - DETAIL_FILE_STATUS to GitHubPRFile['status'] - PUSH_TEST_REFUSAL_REASONS and PUSH_REGISTER_REFUSAL_REASONS to the refusal arms of MobilePushTestResult and MobilePushRegisterResult - SETUP_RUN_POLICIES to SetupRunPolicy openEnum's parameter widens from a non-empty tuple to `readonly string[]` so a hostUnionArms list can feed it. z.enum already accepts the same, so the tuple constraint only excluded callers zod itself takes; behaviour unchanged. Twelve mutations prove the pins: dropping one arm and adding a bogus one each fail mobile tsc in all six places. Zero goldens move, the schemas' behaviour being unchanged, and the 21 recording suites pass at the existing baseline. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): fix the type errors in eighteen test files Found by typechecking the tests for the first time (see the config that follows). All mechanical, none weakens a product type: - 67 `act(() => vi.advanceTimersByTime(...))` callbacks return VitestUtils where act wants void, so each becomes a block. The async ones await only a genuinely promise-returning call, so no extra microtask tick is introduced. - Four fixtures were stale against a product type that gained a required member: MobileViewState.alwaysShowDefaultBranch, PrSidebarData.checksError, the branch-compare summary's errorMessage, and SessionOptionDescriptor's transport, which #20884 added precisely so a producer could not inherit the wrong lane's rendering by omission. - `getLastConnectedAt` on the shared relay fake was typed `() => null`, which refused the timestamp two escalation suites assign to it. - Two holders used before assignment take `!`, one `advance!.kind === ...` becomes `advance?.kind`, one widened status arm takes `as const`, and the Expo notification fixture keeps `data` required because the dismissal cases assign through it. 631 test files pass, 6222 tests, unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): typecheck the test files, on a ratchet mobile/tsconfig.json excludes *.test.ts so Metro never compiles tests into the release bundle, and vitest transpiles without checking types. Nothing had ever typechecked a mobile test, which is why a `Record<HostUnion, true>` pin written in one proved nothing and why 144 of the 630 test files had drifted. tsconfig.test.json is that program with the tests put back, behind `typecheck:tests`. Four files stay out: they import the desktop main process or src/shared/child-process, which are written against @types/node, and this program's libs are React Native's, where setTimeout answers a number rather than a NodeJS.Timeout. Pulling that graph in reports ~280 errors about the desktop rather than about mobile; vitest runs those four under Node, which is where they belong. The CI gate is a ratchet rather than the raw typecheck, modelled on check-ts-nocheck-ratchet.mjs: 126 files still fail, so the gate freezes that set and fails when a file that checks today stops checking, or when a baseline entry starts checking and was not pruned. The list may only shrink. Why not zero: 180 of the remaining 510 errors are one seam — tests locate mocked react-native components by string name, which `ElementType` does not admit — and closing it means either 180 casts or a global JSX declaration for the mocked names. That is a design decision, not a mechanical fix, so it is left for a follow-up rather than made here. The rest are smaller clusters of the same kind: vi.fn mocks assigned into typed slots, call-arg tuple indexing, and createElement props fixtures. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile-recorder): correct the corpus counts and the salvage claim The oracle section still quoted the corpus as 368 scenarios and 727 goldens; it is 393 and 778, and the three replay suites report 781 tests. Each number now names the command that measures it. "No golden carries one" was the load-bearing error: 44 goldens carry a recorded `reply-salvage` today, starting with the push-test unknown-reason scenario #21176 added for exactly that purpose. The paragraph claimed the observation pins an absence when on those families it pins a recorded drop. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the tests-typecheck ratchet's parser The gate reads tsc's output, and tsc indents the "Overload 1 of 2, ..." detail under an error. Counting those as filenames would write unparseable entries into the baseline and leave the gate unprunable, so the parser is pinned on that shape as well as on the added/stale diff. Written against the gate itself: it flagged this file before the directive it carried was removed, which is the end-to-end proof the spawn half works. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): await the timer advances the act() rewrite dropped Rewriting `await act(async () => vi.advanceTimersByTimeAsync(n))` into a braced body left the returned promise floating at 27 sites, so the advance was no longer ordered before the assertions that follow it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): unshadow MobileHostCard's .tsx suite A wildcard `include` keeps only the higher-priority extension, so MobileHostCard.test.tsx sat outside every tsc program while MobileHostCard.test.ts existed beside it. Its one error is the same react-test-renderer seam its sibling is baselined for. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): census every test file into the typecheck program The ratchet diffs only files that error, so a test excluded from tsconfig.test.json or shadowed by a sibling extension left the gate silently. Every *.test.ts(x) on disk must now be in the program or named in TESTS_OUTSIDE_PROGRAM with its reason. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(shared): make the enum helpers refuse the ways they can prove nothing openEnum takes a `const` T so a bare literal keeps its arms rather than widening to string. hostUnionArms blocks inference of U with NoInfer and defaults it to never, so a call that omits the host union — where the record would only pin itself — no longer compiles. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): describe the census and correct the baseline count Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): give the push fixture cast its SAFETY rationale Widening the pre-existing cast made the changed-code gate attribute it as a new finding. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): build the push fixtures as typed notifications Replaces the `as unknown as` cast with Expo's own types, filling FirebaseRemoteMessage and its notification once in two builders, and passes the data payload in rather than mutating through an optional member. Typing the fixture showed one assertion comparing the scheduled content against the whole arriving content, which only held while the cast let the fixture omit the two members the presenter drops; it now names the four members the presenter forwards. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): keep the grouped-question advance read non-optional `advance?.kind` let an absent advance take the null-draft branch instead of failing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): run the tests-typecheck ratchet on Windows Spawns tsc's JS entry on this Node instead of the node_modules/.bin shim, which is a POSIX shell script that Windows resolves to tsc.CMD and then appends .exe to. Parsed paths are normalised to POSIX so a Windows run does not read every baseline entry as both stale and added. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): close the ratchet's @ts-nocheck hole and read tsc once tsc exits 0 on a @ts-nocheck file, so a baselined test could be "fixed" with one line, pruned, and never checked again; the census now names any program test file whose leading comment carries the directive. `--noEmit --listFiles` answers both questions in one pass, so the gate spawns tsc once rather than twice. Corrects the two stale counts, and states hostUnionArms' real reason for living in the schema module now that tests are typechecked. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
177 lines
8.5 KiB
TypeScript
177 lines
8.5 KiB
TypeScript
import { z } from 'zod'
|
|
import type { GitHubPRFile } from '../../../src/shared/github/pull-request-types'
|
|
import { hostUnionArms, salvagedOptional, salvagingArray } from '../../../src/shared/zod-salvage'
|
|
import { prCount, prFlag, prNullableText, prText } from '../session/github-pr-entity-reply-schema'
|
|
|
|
// The entities the tasks screen's provider replies are built out of: the mutation envelope every
|
|
// GitHub/GitLab/Linear write answers with, a conversation comment, an assignable user, a review
|
|
// summary, a check row and a changed file. Checked against src/main/github/issue-create.ts,
|
|
// issue-update.ts, issue-comment.ts, client/create/add-pr-review-comment.ts and the shared
|
|
// GitHubCommentResult / GitHubCreateIssueResult types the host returns from them.
|
|
//
|
|
// Two rules run through this file and the four schema modules beside it.
|
|
//
|
|
// 1. A member is required only where a tasks consumer reads it with no guard. Everything the
|
|
// consumer reaches through `?.`, `??` or a `typeof` test stays optional, because main read it
|
|
// that way and a reply without it rendered the same fallback it renders now.
|
|
// 2. No member is required that the site's own recorded `normal` reply does not carry. The corpus
|
|
// is the only evidence of what a host really sends at each site, and requiring a member absent
|
|
// from that control would turn a good reply into an incompatible one. Where that rule holds a
|
|
// schema looser than the host's own type, the schema says so at the member.
|
|
//
|
|
// Member helpers come from the session domain's entity module rather than a second copy: they are
|
|
// plain salvaged-member combinators over zod-salvage, and one definition is what keeps "absent
|
|
// stays absent, malformed reads as absent" identical on both surfaces.
|
|
|
|
// Pinned to the host's own union through hostUnionArms: an arm added or dropped host-side fails tsc.
|
|
export const DETAIL_FILE_STATUS = hostUnionArms<GitHubPRFile['status']>({
|
|
added: true,
|
|
modified: true,
|
|
removed: true,
|
|
renamed: true,
|
|
copied: true,
|
|
changed: true,
|
|
unchanged: true
|
|
})
|
|
|
|
/**
|
|
* One conversation comment, as every task sheet holds it.
|
|
*
|
|
* `id` and `body` are the only required members, and they are the two `DetailComment` declares
|
|
* non-optional: the timeline keys rows by id and renders body unguarded. Every other member is
|
|
* reached through `?.` or `??` — commentAuthor (mobile-tasks-item-comments.tsx:47) is the shape of
|
|
* all of them — so it stays optional and is passed through exactly as the host sent it.
|
|
*
|
|
* A reaction's `content` is forwarded, not matched against an arm set. Three producers feed this
|
|
* one list and they disagree: GitHub sends `GitHubReactionContent` (`'+1'`, `'-1'`, `laugh`, ...,
|
|
* src/shared/github/comment-types.ts:3-17, normalised from GraphQL in
|
|
* src/main/github/comment-reactions.ts:19-27), and GitLab sends `GitLabReaction { name, count }`
|
|
* with no `content` at all (src/shared/gitlab-types.ts:60-72). An arm set drawn from either one
|
|
* drops the other producer's rows outright, so the reader keeps `count` — the only member read
|
|
* unguarded, by the `count > 0` filter at mobile-tasks-item-comments.tsx:145 — and hands `content`
|
|
* to the glyph lookup exactly as it arrived.
|
|
*/
|
|
export const detailCommentSchema = z.looseObject({
|
|
id: z.union([z.string(), z.number().finite()]),
|
|
author: prText('author'),
|
|
authorAvatarUrl: prText('authorAvatarUrl'),
|
|
user: salvagedOptional('user', z.looseObject({ displayName: prText('displayName') })),
|
|
isBot: prFlag('isBot'),
|
|
body: z.string(),
|
|
createdAt: prText('createdAt'),
|
|
url: prText('url'),
|
|
reactions: salvagedOptional(
|
|
'reactions',
|
|
salvagingArray(z.looseObject({ content: prText('content'), count: z.number().finite() }))
|
|
),
|
|
path: prText('path'),
|
|
line: prCount('line'),
|
|
startLine: prCount('startLine'),
|
|
threadId: prText('threadId'),
|
|
isResolved: prFlag('isResolved')
|
|
})
|
|
|
|
export const detailCommentListSchema = salvagingArray(detailCommentSchema)
|
|
|
|
/**
|
|
* A user the item can be assigned to or asked to review.
|
|
*
|
|
* `login` is the identity: the reviewer merge reads `reviewer.login.trim()` with no guard
|
|
* (use-mobile-tasks-hosted-comment-review-actions.tsx:172), so a row without one drops rather than
|
|
* taking the whole picker down. `name` and `avatarUrl` keep an explicit `null` — the recorded
|
|
* `github.listAssignableUsers` reply sends `avatarUrl: null`, and collapsing it to `''` would
|
|
* change what the avatar row renders for a good reply.
|
|
*/
|
|
const assignableUserSchema = z.looseObject({
|
|
login: z.string(),
|
|
name: prNullableText('name'),
|
|
avatarUrl: prNullableText('avatarUrl')
|
|
})
|
|
|
|
export const assignableUserListSchema = salvagingArray(assignableUserSchema)
|
|
|
|
/** One review, keyed by `login` the same way. Nested `author.login` is not accepted here: this
|
|
* surface never read it, and inventing the fallback would widen what the sheet shows. */
|
|
export const reviewSummaryListSchema = salvagingArray(
|
|
z.looseObject({
|
|
login: z.string(),
|
|
state: prNullableText('state'),
|
|
avatarUrl: prNullableText('avatarUrl')
|
|
})
|
|
)
|
|
|
|
/**
|
|
* One check row. `name` labels it and `status` drives its icon, and both are read unguarded by the
|
|
* checks list and by summarizeProviderChecks.
|
|
*
|
|
* `status` is a free string rather than the host's `queued | in_progress | completed` arm set,
|
|
* because this surface is fed by two different producers: the recorded `github.prChecks` reply
|
|
* sends `COMPLETED` / `SUCCESS` in caps, and `GitHubDetailCheck` declares both as strings.
|
|
*/
|
|
export const detailCheckListSchema = salvagingArray(
|
|
z.looseObject({
|
|
name: z.string(),
|
|
status: z.string(),
|
|
conclusion: prNullableText('conclusion'),
|
|
url: prNullableText('url')
|
|
})
|
|
)
|
|
|
|
/**
|
|
* One changed file. `path` is what the expansion, the viewed toggle and the comment anchor are all
|
|
* keyed on, so a row without one can never match and drops.
|
|
*
|
|
* No scenario reply in the corpus carries a file row, so no golden can observe either of the two
|
|
* vocabularies below; both rest on their unit pins and on the host's shared types.
|
|
*
|
|
* `viewerViewedState` is forwarded as text rather than closed against `GitHubPRFileViewedState`
|
|
* (three arms, src/shared/github/pull-request-types.ts:128). Nothing sends it back and both
|
|
* consumers test `=== 'VIEWED'` (mobile-tasks-item-review-sections.tsx:237,
|
|
* mobile-tasks-project-review-panels.tsx:209), so an arm this build predates reaches that test as
|
|
* itself, exactly as main's unchecked reader passed it, instead of vanishing.
|
|
*
|
|
* `status` stays closed, and the arms are not mobile's invention: the host validates this same
|
|
* seven-arm set on the way back in, as a zod enum on `github.prFileContents`' own params
|
|
* (src/shared/rpc-contract/github-pull-request-params.ts:63, matching `GitHubPRFile.status` at
|
|
* github/pull-request-types.ts:133). Its only consumer is that request
|
|
* (use-mobile-tasks-github-check-file-actions.tsx:196,
|
|
* use-mobile-tasks-project-file-merge-actions.tsx:78), so a forwarded eighth arm could not reach
|
|
* the wire without a cast, and if it did the host would refuse the call on its own params. Dropping
|
|
* to absent sends `?? 'modified'` instead, which fetches both sides and renders a diff.
|
|
*/
|
|
export const detailFileListSchema = salvagingArray(
|
|
z.looseObject({
|
|
path: z.string(),
|
|
oldPath: prText('oldPath'),
|
|
status: salvagedOptional('status', z.enum(DETAIL_FILE_STATUS)),
|
|
additions: prCount('additions'),
|
|
deletions: prCount('deletions'),
|
|
isBinary: prFlag('isBinary'),
|
|
viewerViewedState: prText('viewerViewedState')
|
|
})
|
|
)
|
|
|
|
/**
|
|
* The envelope every task mutation answers with, and the reason there is one of it rather than
|
|
* twenty.
|
|
*
|
|
* `ok === false` is the only failure test any of these call sites makes, and `error` is the only
|
|
* text any of them raises. Both stay optional and tri-state: a reply with no `ok` is the success
|
|
* main read, and `ok: false` is a refusal the caller reports with the host's own sentence.
|
|
* `error` is a string because that is what every call site interpolates into `new Error(...)`; a
|
|
* host that answers an object there now reaches the call site's own fallback copy instead of
|
|
* rendering `[object Object]`.
|
|
*/
|
|
export const taskMutationEnvelopeSchema = z.looseObject({
|
|
ok: prFlag('ok'),
|
|
error: prText('error')
|
|
})
|
|
|
|
/** The mutation envelope a comment write adds its created row to. The row is salvaged, so a
|
|
* comment the host could not describe leaves the call site's local echo in place. */
|
|
export const taskCommentWriteEnvelopeSchema = z.looseObject({
|
|
ok: prFlag('ok'),
|
|
error: prText('error'),
|
|
comment: salvagedOptional('comment', detailCommentSchema)
|
|
})
|