Skip to content

Commit 56732d7

Browse files
V48 Gate 3 (specification-implementation): surface reconnect-required state on deposit branch/commit selects (V48-Gate3-F32)
Live QA: with a repository already selected, Branch and Commit stayed on "Select branch.../Select commit..." forever — no loading spinner, no error, and clicking didn't even open the dropdowns. Root cause: the branch/commit fetch effects in DepositSourceSelection.tsx gate on connectionStatus.valid (a live re-check of the GitHub session), not just .connected. A GitHub App installation token expires roughly hourly, so connected:true/valid:false is a normal, recurring state — the repository list still populates (it doesn't need .valid), but Branch/Commit silently stay empty, and disabled={branches.length === 0} makes the selects inert with nothing telling the depositor why. The legacy TerminalRepositoryContextPanel this component replaced had dedicated "reconnect required" messaging + a Reconnect CTA for exactly this state; it was dropped in the deposit-native rewrite. Restored it: an amber banner explaining Branch/Commit are empty until the live session reconnects, a Reconnect GitHub button (reusing TerminalOpenAuxillariesButton directly — its own logic has no terminal-workspace coupling), and matching inline hints under each select. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e4dccc7 commit 56732d7

4 files changed

Lines changed: 80 additions & 4 deletions

File tree

BITCODE_V48_QA.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,12 @@ Track 3-4 scripts (BTD ledger, settlement, pack journaling) get added when those
520520
- Cause: `POST /api/deposit/synthesize-options` (V48-Gate3-F26-B) dispatches the run then returns `{status:'dispatched'}` immediately, with the actual multi-agent synthesis (`runSynthesis()`) continuing as a bare `void runSynthesis()` promise after the response completes. Vercel Functions (including Fluid Compute) do not guarantee an instance stays alive to finish work started before the response but not awaited by it — the platform is free to freeze/recycle the instance once the response is flushed, unless the continuation is registered via `waitUntil()`. This is the same failure class `execution-orphan-sweep.ts`'s own header comment already names as the reason its sweep exists ("dev-server restarts (and crashed serverless boxes) kill in-process pipelines … leaving them running forever" — QA runs `7f023f23` and the 2026-07-03 pre-restart orphans): that sweep detects and relabels the resulting stuck-`running` rows as `interrupted`, but nothing previously stopped the box from being killed mid-run in the first place. The same bare-`void` pattern was also on the orphan sweep's own fire-and-forget call at dispatch time.
521521
- Repair: added `@vercel/functions` and wrapped both fire-and-forget continuations in `waitUntil()` — the `runSynthesis()` background run and the `sweepOrphanedExecutions()` call in `uapi/app/api/deposit/synthesize-options/route.ts`. This keeps the Function instance alive until the promise settles, so a dispatched run either completes or fails and finalizes its row through the normal `finalizeExecutionRow`/error-banner path, instead of being silently killed mid-flight. Checked the only sibling background-dispatch route (`/api/pipeline-harness/asset-pack`) and confirmed it does NOT have this defect: its continuation runs inside an open `ReadableStream` (SSE), whose lifetime keeps the Function instance alive by construction — no `waitUntil` needed there. Verified: `tsc --noEmit` 0; `depositSynthesizeOptionsRoute.test.ts` 6/6 green.
522522

523+
### V48-Gate3-F32 — Branch/Commit selects on `/deposits` silently stuck on a stale GitHub connection, no explanation (FIXED)
524+
525+
- Severity: high (reported live, 2026-07-08: "branch and commit selects seem broken … not loading lists+defaults nor is clicking opening dropdowns" — with a repository already selected).
526+
- Cause: `DepositSourceSelection.tsx` (the deposit-native replacement for the legacy `TerminalRepositoryContextPanel`/`TerminalSupplySelectionPanel`) fetches the repository list gated only on `connectionStatus.connected`, but gates the Branch and Commit fetch effects on BOTH `connectionStatus.connected` AND `connectionStatus.valid` (same asymmetry as the legacy panel). `valid` comes from `validateStoredConnection`, which re-checks the live GitHub session/token — a GitHub App installation token expires ~hourly and is regenerated lazily, so `connected:true, valid:false` is a normal, recurring state, not an edge case. In that state a repository is still selectable (stored inventory doesn't need `valid`), but Branch/Commit silently stay at `[]` forever: `disabled={... || branches.length === 0}` (and the same for commits) keeps the selects inert with no loading spinner and no error text — the legacy panel this replaced had dedicated "reconnect required" messaging + a Reconnect CTA for exactly this state (`connectionStatus?.connected && !connectionStatus.valid`, `TerminalRepositoryContextPanel.tsx`), but that messaging was dropped when `DepositSourceSelection` replaced it — the refactor's docblock even says it "carries no terminal-UI dependency," which is what left this state unhandled.
527+
- Repair: added a `connectionNeedsReconnect` derived flag to `DepositSourceSelection.tsx`; when true, an amber banner (mirroring the legacy panel's messaging) explains that Branch/Commit stay empty until the live session reconnects, with a "Reconnect GitHub" button (`TerminalOpenAuxillariesButton`, opens Auxillaries → Externals — reused directly since its own logic has no terminal-workspace coupling, only its file location is under `app/terminal/`). The Branch/Commit helper hints below each select also swap to "Reconnect required to load branches/commits" instead of the normal "default is selected when available" copy in this state. `TerminalOpenAuxillariesButton.tsx` was missing an explicit `React` import (never directly rendered by a test before — ts-jest's classic JSX transform needs it in scope); added it. Verified: `tsc --noEmit` 0; new `depositSourceSelection.test.tsx` coverage (valid connection → no banner; invalid → banner + Reconnect button + both hint texts) 5/5 green; full uapi jest suite 639/640 green (1 pre-existing skip).
528+
523529
## Track 1 — Identity / Authentication / Auxillaries — COMPLETE 2026-06-12 (email deferred by decision; F2/F9 and legacy eradication queued for gates)
524530

525531
- [x] Sign up / sign in via Connect Wallet (nav CTA → SignUpWindow → wallet signature on testnet4 → `custom:bitcode-bitcoin` session → `/tps/supabase/callback`) — verified 2026-06-12 after F5 fix; lands on `/packs`. Re-verified from fully nuked state (purged user + cleared site data): created 19:29:21 → session 19:29:25 → binding auto-written 19:29:29 by the bridge on `/packs` mount with no Auxillaries visit; UI consistent across nav, Wallet, and Profile panes.

uapi/app/deposits/DepositSourceSelection.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { VCSRepositorySelector } from "@/components/base/bitcode/vcs/VCSReposito
1919
import { SearchableSelect } from "@/components/base/bitcode/forms/SearchableSelect";
2020
import BitcodeInlineExplainer from "@/components/base/bitcode/execution/BitcodeInlineExplainer";
2121
import { DEPOSIT_SECTION_EXPLAINERS } from "@/app/deposits/deposit-explainers";
22+
import TerminalOpenAuxillariesButton from "@/app/terminal/TerminalOpenAuxillariesButton";
2223
import {
2324
buildTerminalRepositoryAnchorDraft,
2425
type TerminalActivityRecordDraft,
@@ -131,6 +132,14 @@ export default function DepositSourceSelection({
131132
() => deriveSelectedCommit(commits, requestedCommit),
132133
[commits, requestedCommit],
133134
);
135+
// Repositories list from stored inventory regardless of token validity
136+
// (below), but branches/commits require a LIVE, valid session — a stale
137+
// installation token (they expire ~hourly) leaves this true while a
138+
// repository is still selectable, which otherwise reads as "broken":
139+
// Branch/Commit silently stay empty and disabled with no explanation.
140+
const connectionNeedsReconnect = Boolean(
141+
connectionStatus?.connected && !connectionStatus.valid,
142+
);
134143

135144
// Connection posture.
136145
useEffect(() => {
@@ -500,6 +509,22 @@ export default function DepositSourceSelection({
500509
</div>
501510
</div>
502511

512+
{connectionNeedsReconnect ? (
513+
<div className="mt-3 flex flex-wrap items-center justify-between gap-3 border border-amber-300/24 bg-amber-400/10 px-3 py-2.5 text-sm text-amber-100">
514+
<span className="flex items-center gap-2">
515+
<RefreshCw className="h-4 w-4 shrink-0" />
516+
Saved {getProviderLabel(provider)} attachment found, but the live session
517+
needs to reconnect — Branch and Commit stay empty (and non-interactive)
518+
until then; the Repository list above is read from stored inventory only.
519+
</span>
520+
<TerminalOpenAuxillariesButton
521+
step="externals"
522+
label={`Reconnect ${getProviderLabel(provider)}`}
523+
className="shrink-0 border border-amber-300/24 bg-amber-400/12 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.14em] text-amber-50 transition hover:border-amber-300/42 hover:bg-amber-400/18"
524+
/>
525+
</div>
526+
) : null}
527+
503528
<div className="mt-3 grid gap-3 tablet:grid-cols-2 desktop:grid-cols-[minmax(0,180px)_minmax(0,1.4fr)_minmax(0,1fr)_minmax(0,1fr)]">
504529
<div>
505530
<span className="flex items-center gap-2 text-[0.62rem] uppercase tracking-[0.18em] text-neutral-500">
@@ -616,7 +641,9 @@ export default function DepositSourceSelection({
616641
<p className="mt-1.5 text-[0.6rem] uppercase tracking-[0.16em] text-neutral-500">
617642
{isLoadingBranches
618643
? "Loading branches…"
619-
: "Default branch is selected when available"}
644+
: connectionNeedsReconnect
645+
? "Reconnect required to load branches"
646+
: "Default branch is selected when available"}
620647
</p>
621648
</div>
622649

@@ -657,7 +684,9 @@ export default function DepositSourceSelection({
657684
<p className="mt-1.5 text-[0.6rem] uppercase tracking-[0.16em] text-neutral-500">
658685
{isLoadingCommits
659686
? "Loading commits…"
660-
: "Latest branch commit is selected when available"}
687+
: connectionNeedsReconnect
688+
? "Reconnect required to load commits"
689+
: "Latest branch commit is selected when available"}
661690
</p>
662691
</div>
663692
</div>

uapi/app/terminal/TerminalOpenAuxillariesButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client';
22

3+
import React from 'react';
34
import { openAuxillaries } from '@/app/auxillaries/components/AuxillariesProvider';
45
import {
56
getAuxillaryOpenActionLabel,

uapi/tests/depositSourceSelection.test.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ jest.mock("next/navigation", () => ({
1111
useSearchParams: () => new URLSearchParams(""),
1212
}));
1313

14-
function mockVcsFetch() {
14+
function mockVcsFetch(options: { connectionValid?: boolean } = {}) {
15+
const connectionValid = options.connectionValid ?? true;
1516
global.fetch = jest.fn((input: unknown) => {
1617
const url = String(input);
1718
if (url.includes("/connection")) {
1819
return Promise.resolve({
1920
ok: true,
2021
status: 200,
21-
json: async () => ({ connected: true, valid: true }),
22+
json: async () => ({ connected: true, valid: connectionValid }),
2223
});
2324
}
2425
if (url.includes("/repositories")) {
@@ -128,3 +129,42 @@ describe("DepositSourceSelection — V48-Gate3-F17 repository anchoring", () =>
128129
expect(lastHref).not.toContain("sourceCommit=");
129130
});
130131
});
132+
133+
describe("DepositSourceSelection — stale connection surfaces a reconnect notice (repro: branch/commit silently stuck empty)", () => {
134+
afterEach(() => {
135+
jest.restoreAllMocks();
136+
mockReplace.mockReset();
137+
});
138+
139+
it("shows no reconnect notice when the connection is connected and valid", async () => {
140+
mockVcsFetch({ connectionValid: true });
141+
render(
142+
<DepositSourceSelection
143+
routePath="/"
144+
buildRouteHref={(params) => `/deposits?${params?.toString() ?? ""}`}
145+
/>,
146+
);
147+
148+
await screen.findByLabelText("Repository provider");
149+
expect(
150+
screen.queryByText(/needs to reconnect/i),
151+
).not.toBeInTheDocument();
152+
});
153+
154+
it("surfaces a reconnect notice (and why Branch/Commit stay empty) when connected but not valid", async () => {
155+
mockVcsFetch({ connectionValid: false });
156+
render(
157+
<DepositSourceSelection
158+
routePath="/"
159+
buildRouteHref={(params) => `/deposits?${params?.toString() ?? ""}`}
160+
/>,
161+
);
162+
163+
await screen.findByText(/needs to reconnect/i);
164+
expect(
165+
screen.getByRole("button", { name: /Reconnect GitHub/i }),
166+
).toBeInTheDocument();
167+
expect(screen.getByText(/Reconnect required to load branches/i)).toBeInTheDocument();
168+
expect(screen.getByText(/Reconnect required to load commits/i)).toBeInTheDocument();
169+
});
170+
});

0 commit comments

Comments
 (0)