fix(mobile): keep the two advisory task probes total so a nullish payload cannot unhydrate the screen

`preflight.check` and `linear.status` are read under `success-result-or-skip` and documented as
advisory. That policy accepts an envelope whose `result` is absent or null, then asks the reader to
decode it; a `looseObject` refuses, the throw leaves the reader, and the caller's catch discards the
entire hydration. The corpus records the difference: on the `result-absent` and `result-null`
partitions main hydrates the Tasks screen and lists one provider, and the checked readers left it
unhydrated with no providers.

`.catch` restores main's answer exactly. Every consumer guards to the leaf and compares to `true`,
so absence, null and a garbage payload have always meant "not installed" and "not connected".

Four cases pin it, and removing either catch fails all four.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-17 03:30:34 -04:00
parent 55173fe1f7
commit b354d1338a
2 changed files with 40 additions and 9 deletions
@@ -87,6 +87,24 @@ describe('the two advisory probes', () => {
gh: { installed: true, authenticated: true }
})
})
// `success-result-or-skip` accepts an envelope whose `result` is absent or null and then asks
// the reader to decode it, so a refusal here throws out of the caller's whole hydration. Main
// hydrated on both of these replies; the recorded settings.task-hydration and
// settings.workspace-context partitions are what caught the difference.
it.each([
['absent', undefined],
['null', null],
['a bare string', 'preflight'],
['an array', []]
])('reads a %s payload as "nothing advertised" rather than refusing it', (_label, reply) => {
const preflight = taskPreflightSchema.safeParse(reply)
expect(preflight.success).toBe(true)
expect(preflight.success && preflight.data.glab?.installed).toBeUndefined()
const linear = taskLinearStatusSchema.safeParse(reply)
expect(linear.success).toBe(true)
expect(linear.success && linear.data.connected).toBeUndefined()
})
})
describe('the three preference writes', () => {
+22 -9
View File
@@ -64,13 +64,21 @@ export const taskUiStateSchema = z
* (use-mobile-tasks-runtime-hydration.tsx:295, mobile-home-host-requests.ts:95) and the
* `readProbeMember` pair in use-new-workspace-runtime-context.ts:92. `git` and `gh` are declared
* non-optional by PreflightStatus but no mobile consumer reads them, so they pass through.
*
* Total on purpose. This read is advertised as advisory, but `success-result-or-skip` accepts an
* envelope whose `result` is absent or null and then asks the reader to decode it, and a refusal
* there throws out of the whole hydration -- the corpus records main hydrating the Tasks screen on
* that reply and this reader leaving it unhydrated. `.catch` restores main's answer exactly,
* because every consumer guards to the leaf and reads absence as "not installed".
*/
export const taskPreflightSchema = z.looseObject({
glab: salvagedOptional(
'glab',
z.looseObject({ installed: salvagedOptional('installed', z.boolean()) })
)
})
export const taskPreflightSchema = z
.looseObject({
glab: salvagedOptional(
'glab',
z.looseObject({ installed: salvagedOptional('installed', z.boolean()) })
)
})
.catch({ glab: undefined })
/**
* Whether Linear is connected.
@@ -80,10 +88,15 @@ export const taskPreflightSchema = z.looseObject({
* `false` already mean the same thing and nothing is required. `workspaces` is not declared: the
* picker reads it through a different operation on this same method, and listing a member ahead of
* a reader is how a schema starts refusing replies no consumer here would have noticed.
*
* Total for the same reason as the preflight read above: absence and `false` already mean the same
* thing here, so a nullish payload must read as "not connected" rather than throw.
*/
export const taskLinearStatusSchema = z.looseObject({
connected: salvagedOptional('connected', z.boolean())
})
export const taskLinearStatusSchema = z
.looseObject({
connected: salvagedOptional('connected', z.boolean())
})
.catch({ connected: undefined })
/**
* The three writes whose reply body no call site reads.