Skip to content

Commit 5f8eaca

Browse files
V48 Gate 3 (implementation-only): lock run config above telemetry in deposit detail
Once a synthesis run is submitted or selected, the page is run detail: repository + Obfuscations configuration render above telemetry and freeze (disabled/read-only). Back unlocks config for a new dispatch. Tests cover layout order and locked controls.
1 parent fed6e59 commit 5f8eaca

6 files changed

Lines changed: 322 additions & 208 deletions

File tree

uapi/app/deposits/DepositPageClient.tsx

Lines changed: 203 additions & 183 deletions
Large diffs are not rendered by default.

uapi/app/deposits/DepositSourceSelection.tsx

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ type DepositSourceSelectionProps = {
6060
repoEarningEstimateSats?: number | null;
6161
/** V48-Gate3-F17: previously anchored repositories, newest first. */
6262
repositoryAnchors?: DepositRepositoryAnchor[];
63+
/**
64+
* When true (run detail / post-submit), freeze repository·branch·commit
65+
* selection — the configuration that produced the loaded run is read-only.
66+
*/
67+
disabled?: boolean;
6368
};
6469

6570
async function readJsonResponse(response: Response) {
@@ -119,6 +124,7 @@ export default function DepositSourceSelection({
119124
buildRouteHref,
120125
repoEarningEstimateSats,
121126
repositoryAnchors = [],
127+
disabled = false,
122128
}: DepositSourceSelectionProps) {
123129
const router = useRouter();
124130
const searchParams = useSearchParams();
@@ -564,8 +570,13 @@ export default function DepositSourceSelection({
564570

565571
return (
566572
<section
567-
className="border border-white/10 bg-white/[0.035] px-4 py-4"
573+
className={`border border-white/10 bg-white/[0.035] px-4 py-4 ${
574+
disabled ? "opacity-80" : ""
575+
}`}
568576
aria-label="Select deposit repository"
577+
data-testid="deposit-source-selection"
578+
data-locked={disabled ? "true" : "false"}
579+
aria-disabled={disabled || undefined}
569580
>
570581
<div className="flex flex-wrap items-start justify-between gap-3">
571582
<div>
@@ -577,8 +588,9 @@ export default function DepositSourceSelection({
577588
<BitcodeInlineExplainer explainer={DEPOSIT_SECTION_EXPLAINERS.repository} />
578589
</h2>
579590
<p className="mt-2 max-w-3xl text-sm leading-6 text-neutral-400">
580-
One connected repository, branch, and commit form the source package
581-
the rest of the Deposit reads.
591+
{disabled
592+
? "Source package that produced this run — locked while reviewing run detail."
593+
: "One connected repository, branch, and commit form the source package the rest of the Deposit reads."}
582594
</p>
583595
</div>
584596
<div className="flex items-center gap-2">
@@ -596,7 +608,9 @@ export default function DepositSourceSelection({
596608
: null,
597609
}))}
598610
value={null}
611+
disabled={disabled}
599612
onSelect={(key) => {
613+
if (disabled) return;
600614
const anchor = repositoryAnchors.find((entry) => entry.id === key);
601615
if (!anchor) return;
602616
updateSourceParams((params) => {
@@ -627,7 +641,7 @@ export default function DepositSourceSelection({
627641
<button
628642
type="button"
629643
aria-label="Anchor repository to the activity ledger"
630-
disabled={!selectedRepository || isRecording}
644+
disabled={disabled || !selectedRepository || isRecording}
631645
onClick={() => {
632646
void handleAnchorRepository();
633647
}}
@@ -682,7 +696,9 @@ export default function DepositSourceSelection({
682696
: null,
683697
}))}
684698
value={provider}
699+
disabled={disabled}
685700
onSelect={(key) => {
701+
if (disabled) return;
686702
const nextProvider = key ?? "github";
687703
updateSourceParams((params) => {
688704
params.set("provider", nextProvider);
@@ -702,6 +718,7 @@ export default function DepositSourceSelection({
702718
<SourceListRefreshButton
703719
ariaLabel="Refresh provider connection"
704720
explainer={DEPOSIT_SECTION_EXPLAINERS.refreshProviderConnection}
721+
disabled={disabled}
705722
loading={isLoadingConnection}
706723
onRefresh={() =>
707724
setConnectionRefreshNonce((nonce) => nonce + 1)
@@ -730,7 +747,9 @@ export default function DepositSourceSelection({
730747
repositories={repositories}
731748
loading={isLoadingRepositories && repositories.length === 0}
732749
value={selectedRepository?.fullName}
733-
onSelect={(repository) =>
750+
disabled={disabled}
751+
onSelect={(repository) => {
752+
if (disabled) return;
734753
updateSourceParams((params) => {
735754
if (repository) {
736755
params.set("repo", repository.fullName);
@@ -741,8 +760,8 @@ export default function DepositSourceSelection({
741760
params.delete("sourceCommit");
742761
params.delete("branch");
743762
params.delete("commit");
744-
})
745-
}
763+
});
764+
}}
746765
placeholder={
747766
connectionStatus?.connected
748767
? "Select repository supply..."
@@ -756,7 +775,7 @@ export default function DepositSourceSelection({
756775
<SourceListRefreshButton
757776
ariaLabel="Refresh repository inventory"
758777
explainer={DEPOSIT_SECTION_EXPLAINERS.refreshRepositoryInventory}
759-
disabled={!connectionStatus?.connected}
778+
disabled={disabled || !connectionStatus?.connected}
760779
loading={isLoadingRepositories}
761780
onRefresh={() => {
762781
// Hard-refresh inventory next to the inventory select (resets
@@ -792,6 +811,7 @@ export default function DepositSourceSelection({
792811
aria-label="Repository source branch"
793812
value={selectedBranch || null}
794813
disabled={
814+
disabled ||
795815
!selectedRepository ||
796816
connectionNeedsReconnect ||
797817
(!isLoadingBranches && branches.length === 0)
@@ -810,7 +830,8 @@ export default function DepositSourceSelection({
810830
? "default"
811831
: null,
812832
}))}
813-
onSelect={(branchName) =>
833+
onSelect={(branchName) => {
834+
if (disabled) return;
814835
updateSourceParams((params) => {
815836
if (selectedRepository)
816837
params.set("repo", selectedRepository.fullName);
@@ -819,15 +840,17 @@ export default function DepositSourceSelection({
819840
params.delete("sourceCommit");
820841
params.delete("branch");
821842
params.delete("commit");
822-
})
823-
}
843+
});
844+
}}
824845
className="h-9 border-white/10 bg-[rgba(10,15,30,0.88)] px-3 text-sm text-white hover:bg-[rgba(10,15,30,0.88)] focus:border-emerald-400/40"
825846
/>
826847
</div>
827848
<SourceListRefreshButton
828849
ariaLabel="Refresh branches list"
829850
explainer={DEPOSIT_SECTION_EXPLAINERS.refreshBranches}
830-
disabled={!selectedRepository || connectionNeedsReconnect}
851+
disabled={
852+
disabled || !selectedRepository || connectionNeedsReconnect
853+
}
831854
loading={isLoadingBranches}
832855
onRefresh={() => setBranchesRefreshNonce((nonce) => nonce + 1)}
833856
/>
@@ -860,6 +883,7 @@ export default function DepositSourceSelection({
860883
: null
861884
}
862885
disabled={
886+
disabled ||
863887
!selectedBranch ||
864888
connectionNeedsReconnect ||
865889
(!isLoadingCommits && commits.length === 0)
@@ -902,7 +926,8 @@ export default function DepositSourceSelection({
902926
searchText: `${commit.sha} ${commit.message}`,
903927
})),
904928
]}
905-
onSelect={(commitKey) =>
929+
onSelect={(commitKey) => {
930+
if (disabled) return;
906931
updateSourceParams((params) => {
907932
if (selectedRepository)
908933
params.set("repo", selectedRepository.fullName);
@@ -915,15 +940,15 @@ export default function DepositSourceSelection({
915940
}
916941
params.delete("branch");
917942
params.delete("commit");
918-
})
919-
}
943+
});
944+
}}
920945
className="h-9 border-white/10 bg-[rgba(10,15,30,0.88)] px-3 text-sm text-white hover:bg-[rgba(10,15,30,0.88)] focus:border-emerald-400/40"
921946
/>
922947
</div>
923948
<SourceListRefreshButton
924949
ariaLabel="Refresh commits list"
925950
explainer={DEPOSIT_SECTION_EXPLAINERS.refreshLatestCommit}
926-
disabled={!selectedBranch || connectionNeedsReconnect}
951+
disabled={disabled || !selectedBranch || connectionNeedsReconnect}
927952
loading={isLoadingCommits}
928953
onRefresh={() => setCommitsRefreshNonce((nonce) => nonce + 1)}
929954
/>

uapi/components/base/bitcode/vcs/VCSFileTreePicker.tsx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ interface VCSFileTreePickerProps {
3232
/** Why a conflicting row is disabled (e.g. 'Already a Forced Inclusion'). */
3333
conflictLabel?: string;
3434
emptyLabel?: string;
35+
/** When true, selection is frozen (run-detail lock). Expand still works. */
36+
disabled?: boolean;
3537
'aria-label'?: string;
3638
}
3739

@@ -75,6 +77,7 @@ export function VCSFileTreePicker({
7577
conflictingPaths = [],
7678
conflictLabel = 'selected in the other picker',
7779
emptyLabel = 'Connect a repository to browse its files.',
80+
disabled = false,
7881
'aria-label': ariaLabel,
7982
}: VCSFileTreePickerProps) {
8083
// Children by directory path ('' = repository root); null while loading.
@@ -291,13 +294,19 @@ export function VCSFileTreePicker({
291294
<button
292295
type="button"
293296
onClick={() => toggleSelected(selectionPath)}
294-
disabled={isConflicting}
295-
title={isConflicting ? conflictLabel : undefined}
297+
disabled={disabled || isConflicting}
298+
title={
299+
disabled
300+
? 'Configuration locked for this run'
301+
: isConflicting
302+
? conflictLabel
303+
: undefined
304+
}
296305
aria-pressed={isSelected}
297306
className={`flex min-w-0 flex-1 items-center gap-1.5 border px-1.5 py-0.5 text-left font-mono text-xs transition ${
298307
isSelected
299308
? 'border-emerald-300/35 bg-emerald-300/10 text-emerald-100'
300-
: isConflicting
309+
: isConflicting || disabled
301310
? 'cursor-not-allowed border-transparent text-neutral-600 line-through'
302311
: 'border-transparent text-neutral-200 hover:border-white/15 hover:bg-white/5'
303312
}`}
@@ -326,15 +335,15 @@ export function VCSFileTreePicker({
326335
<button
327336
type="button"
328337
onClick={handleSelectAll}
329-
disabled={!canSelectAll}
338+
disabled={disabled || !canSelectAll}
330339
className="border border-white/10 px-1.5 py-0.5 text-[0.62rem] uppercase tracking-[0.14em] text-neutral-300 transition hover:border-emerald-300/35 hover:text-emerald-100 disabled:cursor-not-allowed disabled:opacity-40"
331340
>
332341
Select all
333342
</button>
334343
<button
335344
type="button"
336345
onClick={handleClearAll}
337-
disabled={selectedPaths.length === 0}
346+
disabled={disabled || selectedPaths.length === 0}
338347
className="border border-white/10 px-1.5 py-0.5 text-[0.62rem] uppercase tracking-[0.14em] text-neutral-300 transition hover:border-rose-300/35 hover:text-rose-100 disabled:cursor-not-allowed disabled:opacity-40"
339348
>
340349
Clear all
@@ -347,8 +356,13 @@ export function VCSFileTreePicker({
347356
<button
348357
key={path}
349358
type="button"
350-
onClick={() => onChange(selectedPaths.filter((entry) => entry !== path))}
351-
className="border border-emerald-300/25 bg-emerald-300/10 px-1.5 py-0.5 font-mono text-[0.66rem] text-emerald-100 transition hover:border-emerald-200/45 hover:bg-emerald-300/15"
359+
onClick={() =>
360+
disabled
361+
? undefined
362+
: onChange(selectedPaths.filter((entry) => entry !== path))
363+
}
364+
disabled={disabled}
365+
className="border border-emerald-300/25 bg-emerald-300/10 px-1.5 py-0.5 font-mono text-[0.66rem] text-emerald-100 transition hover:border-emerald-200/45 hover:bg-emerald-300/15 disabled:cursor-not-allowed disabled:opacity-60"
352366
aria-label={`Remove ${path}`}
353367
>
354368
{path} ×

uapi/components/base/bitcode/vcs/VCSRepositorySelector.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ interface VCSRepositorySelectorProps {
1515
className?: string;
1616
repositories?: VCSRepository[] | null;
1717
loading?: boolean;
18+
/** When true, the selector is non-interactive (run-detail lock). */
19+
disabled?: boolean;
1820
}
1921

2022
export function VCSRepositorySelector({
@@ -26,6 +28,7 @@ export function VCSRepositorySelector({
2628
className,
2729
repositories: providedRepositories,
2830
loading: providedLoading,
31+
disabled = false,
2932
}: VCSRepositorySelectorProps) {
3033
const [open, setOpen] = useState(false);
3134
const [fetchedRepositories, setFetchedRepositories] = useState<VCSRepository[]>([]);
@@ -117,8 +120,9 @@ export function VCSRepositorySelector({
117120
loading={isLoading}
118121
loadingMessage="Loading repositories..."
119122
className={className}
120-
open={open}
121-
onOpenChange={setOpen}
123+
open={disabled ? false : open}
124+
onOpenChange={disabled ? undefined : setOpen}
125+
disabled={disabled}
122126
/>
123127
);
124128
}

uapi/tests/depositPageClient.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,12 @@ jest.mock("@/app/deposits/DepositSourceSelection", () => ({
9595
onContextChange,
9696
routePath,
9797
repositoryAnchors,
98+
disabled,
9899
}: {
99100
onContextChange: (value: unknown) => void;
100101
routePath?: string;
101102
repositoryAnchors?: Array<{ repositoryFullName: string }>;
103+
disabled?: boolean;
102104
}) => {
103105
React.useEffect(() => {
104106
onContextChange({
@@ -123,7 +125,10 @@ jest.mock("@/app/deposits/DepositSourceSelection", () => ({
123125
return (
124126
<section
125127
aria-label="Deposit source selection"
128+
data-testid="deposit-source-selection"
126129
data-route-path={routePath}
130+
data-locked={disabled ? "true" : "false"}
131+
aria-disabled={disabled || undefined}
127132
>
128133
Deposit source selection
129134
{/* Exposes the repositoryAnchors DERIVATION (DepositPageClient's
@@ -375,6 +380,24 @@ describe("DepositPageClient", () => {
375380
screen.getByTestId("deposit-obfuscations-run-loaded-note"),
376381
).toBeInTheDocument();
377382

383+
// Run configuration is locked and lives above telemetry in run detail.
384+
const configuration = screen.getByTestId("deposit-run-configuration");
385+
expect(configuration).toHaveAttribute("data-locked", "true");
386+
expect(screen.getByTestId("deposit-source-selection")).toHaveAttribute(
387+
"data-locked",
388+
"true",
389+
);
390+
expect(screen.getByLabelText("What to obfuscate or withhold")).toBeDisabled();
391+
expect(
392+
screen.getByRole("button", { name: "Clear obfuscations" }),
393+
).toBeDisabled();
394+
// Configuration DOM precedes telemetry DOM (layout: config above telemetry).
395+
expect(
396+
configuration.compareDocumentPosition(
397+
screen.getByTestId("deposit-synthesis-telemetry"),
398+
) & Node.DOCUMENT_POSITION_FOLLOWING,
399+
).toBeTruthy();
400+
378401
// Back clears the selection; the button returns for a fresh dispatch.
379402
fireEvent.click(
380403
await screen.findByRole("button", { name: "Back to Deposit pipelines" }),
@@ -385,6 +408,17 @@ describe("DepositPageClient", () => {
385408
expect(
386409
screen.queryByTestId("deposit-obfuscations-run-loaded-note"),
387410
).not.toBeInTheDocument();
411+
expect(screen.getByTestId("deposit-run-configuration")).toHaveAttribute(
412+
"data-locked",
413+
"false",
414+
);
415+
expect(screen.getByTestId("deposit-source-selection")).toHaveAttribute(
416+
"data-locked",
417+
"false",
418+
);
419+
expect(
420+
screen.getByLabelText("What to obfuscate or withhold"),
421+
).not.toBeDisabled();
388422
});
389423

390424
it("resumes a completed synthesis run's results when its row is selected (master-detail)", async () => {
@@ -1100,6 +1134,8 @@ describe("DepositPageClient", () => {
11001134

11011135
describe("V48 Gate 3 — repository and Obfuscations anchoring", () => {
11021136
function withAnchorFixtures() {
1137+
// Config editing requires no adopted run (run detail locks configuration).
1138+
mockQuery = "depositStage=review-options";
11031139
mockFetchPipelineExecutionHistory.mockResolvedValue([
11041140
{
11051141
id: "deposit-1",

uapi/tests/depositSourceSelection.test.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,21 @@ describe("DepositSourceSelection — V48-Gate3-F17 repository anchoring", () =>
250250
expect(lastHref).not.toContain("sourceBranch=");
251251
expect(lastHref).not.toContain("sourceCommit=");
252252
});
253+
254+
it("locks provider/repository controls when disabled (run-detail freeze)", async () => {
255+
mockVcsFetch();
256+
render(
257+
<DepositSourceSelection
258+
routePath="/"
259+
buildRouteHref={(params) => `/deposits?${params?.toString() ?? ""}`}
260+
disabled
261+
/>,
262+
);
263+
264+
const root = await screen.findByTestId("deposit-source-selection");
265+
expect(root).toHaveAttribute("data-locked", "true");
266+
expect(screen.getByLabelText("Repository provider")).toBeDisabled();
267+
});
253268
});
254269

255270
describe("DepositSourceSelection — stale connection surfaces a reconnect notice (repro: branch/commit silently stuck empty)", () => {

0 commit comments

Comments
 (0)