From 5d993ab35a9226574ec657e7dbe28d132e86a158 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 16:42:51 -0500 Subject: [PATCH 1/8] test(web): cover commit action when upstream is ahead Co-Authored-By: GPT-6.1 Sol --- .../GitActionsControl.logic.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/apps/web/src/components/GitActionsControl.logic.test.ts b/apps/web/src/components/GitActionsControl.logic.test.ts index a1b7d3ea5642a..2f29fa3e4cda3 100644 --- a/apps/web/src/components/GitActionsControl.logic.test.ts +++ b/apps/web/src/components/GitActionsControl.logic.test.ts @@ -28,6 +28,31 @@ function status(overrides: Partial = {}): VcsStatusResult { }; } +describe("when: local changes are behind upstream", () => { + it("keeps the commit action available without pushing, with or without an open PR", () => { + const behind = status({ hasWorkingTreeChanges: true, behindCount: 2 }); + const commit = { label: "Commit", disabled: false, kind: "run_action", action: "commit" }; + assert.deepEqual(resolveQuickAction(behind, false), commit); + assert.deepEqual( + resolveQuickAction( + { + ...behind, + pr: { + number: 13, + title: "Open PR", + url: "https://example.com/pr/13", + baseRef: "main", + headRef: "feature/test", + state: "open", + }, + }, + false, + ), + commit, + ); + }); +}); + describe("when: ref is clean, ahead, and has an open PR", () => { it("resolveQuickAction prefers push", () => { const quick = resolveQuickAction( From 665bb24116d07c39e2c063e886750c68cbc82e7b Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 16:54:54 -0500 Subject: [PATCH 2/8] fix(web): offer commit when the branch is behind upstream Co-Authored-By: GPT-6.1 Sol --- apps/web/src/components/GitActionsControl.logic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/GitActionsControl.logic.ts b/apps/web/src/components/GitActionsControl.logic.ts index 1ef11206338b6..a263d50f1d2f2 100644 --- a/apps/web/src/components/GitActionsControl.logic.ts +++ b/apps/web/src/components/GitActionsControl.logic.ts @@ -222,7 +222,7 @@ export function resolveQuickAction( } if (hasChanges) { - if (!gitStatus.hasUpstream && !hasPrimaryRemote) { + if (isBehind || (!gitStatus.hasUpstream && !hasPrimaryRemote)) { return { label: "Commit", disabled: false, kind: "run_action", action: "commit" }; } if (hasOpenPr || isDefaultRef) { From 3b83434d74585a7928c53189f9d2e8fd9a067c79 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 16:58:43 -0500 Subject: [PATCH 3/8] test(web): preserve literal types in the expected git action Co-Authored-By: GPT-6.1 Sol --- apps/web/src/components/GitActionsControl.logic.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/GitActionsControl.logic.test.ts b/apps/web/src/components/GitActionsControl.logic.test.ts index 2f29fa3e4cda3..4c42b84624a50 100644 --- a/apps/web/src/components/GitActionsControl.logic.test.ts +++ b/apps/web/src/components/GitActionsControl.logic.test.ts @@ -31,7 +31,12 @@ function status(overrides: Partial = {}): VcsStatusResult { describe("when: local changes are behind upstream", () => { it("keeps the commit action available without pushing, with or without an open PR", () => { const behind = status({ hasWorkingTreeChanges: true, behindCount: 2 }); - const commit = { label: "Commit", disabled: false, kind: "run_action", action: "commit" }; + const commit = { + label: "Commit", + disabled: false, + kind: "run_action", + action: "commit", + } as const; assert.deepEqual(resolveQuickAction(behind, false), commit); assert.deepEqual( resolveQuickAction( From e6bbf132efd1089bfae7e3587831370ce4f3e73c Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Fri, 23 Oct 2026 03:33:59 -0500 Subject: [PATCH 4/8] test(web): group the behind-upstream case with other quick actions Co-Authored-By: Claude Opus 5.5 --- .../GitActionsControl.logic.test.ts | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/apps/web/src/components/GitActionsControl.logic.test.ts b/apps/web/src/components/GitActionsControl.logic.test.ts index 4c42b84624a50..f126dea879952 100644 --- a/apps/web/src/components/GitActionsControl.logic.test.ts +++ b/apps/web/src/components/GitActionsControl.logic.test.ts @@ -28,36 +28,6 @@ function status(overrides: Partial = {}): VcsStatusResult { }; } -describe("when: local changes are behind upstream", () => { - it("keeps the commit action available without pushing, with or without an open PR", () => { - const behind = status({ hasWorkingTreeChanges: true, behindCount: 2 }); - const commit = { - label: "Commit", - disabled: false, - kind: "run_action", - action: "commit", - } as const; - assert.deepEqual(resolveQuickAction(behind, false), commit); - assert.deepEqual( - resolveQuickAction( - { - ...behind, - pr: { - number: 13, - title: "Open PR", - url: "https://example.com/pr/13", - baseRef: "main", - headRef: "feature/test", - state: "open", - }, - }, - false, - ), - commit, - ); - }); -}); - describe("when: ref is clean, ahead, and has an open PR", () => { it("resolveQuickAction prefers push", () => { const quick = resolveQuickAction( @@ -159,6 +129,36 @@ describe("when: ref has no upstream configured", () => { }); }); +describe("when: local changes are behind upstream", () => { + it("keeps the commit action available without pushing, with or without an open PR", () => { + const behind = status({ hasWorkingTreeChanges: true, behindCount: 2 }); + const commit = { + label: "Commit", + disabled: false, + kind: "run_action", + action: "commit", + } as const; + assert.deepEqual(resolveQuickAction(behind, false), commit); + assert.deepEqual( + resolveQuickAction( + { + ...behind, + pr: { + number: 13, + title: "Open PR", + url: "https://example.com/pr/13", + baseRef: "main", + headRef: "feature/test", + state: "open", + }, + }, + false, + ), + commit, + ); + }); +}); + describe("requiresDefaultBranchConfirmation", () => { it("requires confirmation for push actions on default ref", () => { assert.isFalse(requiresDefaultBranchConfirmation("commit", true)); From 00440d0b4e5e10a5711c9eaafc297c9bedb77474 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Fri, 23 Oct 2026 03:33:59 -0500 Subject: [PATCH 5/8] test(client-runtime): cover commit action when upstream is ahead Co-Authored-By: Claude Opus 5.5 --- .../src/state/gitActions.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 packages/client-runtime/src/state/gitActions.test.ts diff --git a/packages/client-runtime/src/state/gitActions.test.ts b/packages/client-runtime/src/state/gitActions.test.ts new file mode 100644 index 0000000000000..4224290a9946e --- /dev/null +++ b/packages/client-runtime/src/state/gitActions.test.ts @@ -0,0 +1,48 @@ +import type { VcsStatusResult } from "@t3tools/contracts"; +import { describe, expect, it } from "vite-plus/test"; + +import { resolveQuickAction } from "./gitActions.ts"; + +function status(overrides: Partial = {}): VcsStatusResult { + return { + isRepo: true, + hasPrimaryRemote: true, + isDefaultRef: false, + refName: "feature/test", + hasWorkingTreeChanges: false, + workingTree: { + files: [], + insertions: 0, + deletions: 0, + }, + hasUpstream: true, + aheadCount: 0, + behindCount: 0, + pr: null, + ...overrides, + }; +} + +describe("resolveQuickAction", () => { + it("commits local changes without pushing when the branch is behind upstream", () => { + const behind = status({ hasWorkingTreeChanges: true, behindCount: 2 }); + const commit = { label: "Commit", disabled: false, kind: "run_action", action: "commit" }; + expect(resolveQuickAction(behind, false)).toEqual(commit); + expect( + resolveQuickAction( + { + ...behind, + pr: { + number: 13, + title: "Open PR", + url: "https://example.com/pr/13", + baseRef: "main", + headRef: "feature/test", + state: "open", + }, + }, + false, + ), + ).toEqual(commit); + }); +}); From f642b17937b93f511c53dbc3a7729b06bd908099 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Fri, 23 Oct 2026 03:34:29 -0500 Subject: [PATCH 6/8] fix(mobile): offer commit when the branch is behind upstream Co-Authored-By: Claude Opus 5.5 --- packages/client-runtime/src/state/gitActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-runtime/src/state/gitActions.ts b/packages/client-runtime/src/state/gitActions.ts index f708e9ca9b98c..4817218550a13 100644 --- a/packages/client-runtime/src/state/gitActions.ts +++ b/packages/client-runtime/src/state/gitActions.ts @@ -143,7 +143,7 @@ export function resolveQuickAction( } if (hasChanges) { - if (!gitStatus.hasUpstream && !hasOriginRemote) { + if (isBehind || (!gitStatus.hasUpstream && !hasOriginRemote)) { return { label: "Commit", disabled: false, kind: "run_action", action: "commit" }; } if (hasOpenPr || isDefaultBranch) { From a6bce15e6de9a5b78fc8b3644900da0bb9a0667c Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Fri, 23 Oct 2026 03:41:34 -0500 Subject: [PATCH 7/8] test(web): place the behind-upstream case before the no-upstream cases Co-Authored-By: Claude Opus 5.5 --- .../GitActionsControl.logic.test.ts | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/apps/web/src/components/GitActionsControl.logic.test.ts b/apps/web/src/components/GitActionsControl.logic.test.ts index f126dea879952..a7c2cd43e23d5 100644 --- a/apps/web/src/components/GitActionsControl.logic.test.ts +++ b/apps/web/src/components/GitActionsControl.logic.test.ts @@ -92,6 +92,36 @@ describe("when: on default ref without open PR", () => { }); }); +describe("when: local changes are behind upstream", () => { + it("keeps the commit action available without pushing, with or without an open PR", () => { + const behind = status({ hasWorkingTreeChanges: true, behindCount: 2 }); + const commit = { + label: "Commit", + disabled: false, + kind: "run_action", + action: "commit", + } as const; + assert.deepEqual(resolveQuickAction(behind, false), commit); + assert.deepEqual( + resolveQuickAction( + { + ...behind, + pr: { + number: 13, + title: "Open PR", + url: "https://example.com/pr/13", + baseRef: "main", + headRef: "feature/test", + state: "open", + }, + }, + false, + ), + commit, + ); + }); +}); + describe("when: ref has no upstream configured", () => { it("resolveQuickAction runs push and create PR when no upstream and commits are ahead", () => { const quick = resolveQuickAction( @@ -129,36 +159,6 @@ describe("when: ref has no upstream configured", () => { }); }); -describe("when: local changes are behind upstream", () => { - it("keeps the commit action available without pushing, with or without an open PR", () => { - const behind = status({ hasWorkingTreeChanges: true, behindCount: 2 }); - const commit = { - label: "Commit", - disabled: false, - kind: "run_action", - action: "commit", - } as const; - assert.deepEqual(resolveQuickAction(behind, false), commit); - assert.deepEqual( - resolveQuickAction( - { - ...behind, - pr: { - number: 13, - title: "Open PR", - url: "https://example.com/pr/13", - baseRef: "main", - headRef: "feature/test", - state: "open", - }, - }, - false, - ), - commit, - ); - }); -}); - describe("requiresDefaultBranchConfirmation", () => { it("requires confirmation for push actions on default ref", () => { assert.isFalse(requiresDefaultBranchConfirmation("commit", true)); From 52dc47004a616f6313cf8fa8c364077c12abdd32 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Fri, 23 Oct 2026 03:41:34 -0500 Subject: [PATCH 8/8] test(client-runtime): cover the behind-upstream quick action beside the vcs tests Co-Authored-By: Claude Opus 5.5 --- .../src/state/gitActions.test.ts | 48 ------------------- packages/client-runtime/src/state/vcs.test.ts | 46 ++++++++++++++++++ 2 files changed, 46 insertions(+), 48 deletions(-) delete mode 100644 packages/client-runtime/src/state/gitActions.test.ts diff --git a/packages/client-runtime/src/state/gitActions.test.ts b/packages/client-runtime/src/state/gitActions.test.ts deleted file mode 100644 index 4224290a9946e..0000000000000 --- a/packages/client-runtime/src/state/gitActions.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { VcsStatusResult } from "@t3tools/contracts"; -import { describe, expect, it } from "vite-plus/test"; - -import { resolveQuickAction } from "./gitActions.ts"; - -function status(overrides: Partial = {}): VcsStatusResult { - return { - isRepo: true, - hasPrimaryRemote: true, - isDefaultRef: false, - refName: "feature/test", - hasWorkingTreeChanges: false, - workingTree: { - files: [], - insertions: 0, - deletions: 0, - }, - hasUpstream: true, - aheadCount: 0, - behindCount: 0, - pr: null, - ...overrides, - }; -} - -describe("resolveQuickAction", () => { - it("commits local changes without pushing when the branch is behind upstream", () => { - const behind = status({ hasWorkingTreeChanges: true, behindCount: 2 }); - const commit = { label: "Commit", disabled: false, kind: "run_action", action: "commit" }; - expect(resolveQuickAction(behind, false)).toEqual(commit); - expect( - resolveQuickAction( - { - ...behind, - pr: { - number: 13, - title: "Open PR", - url: "https://example.com/pr/13", - baseRef: "main", - headRef: "feature/test", - state: "open", - }, - }, - false, - ), - ).toEqual(commit); - }); -}); diff --git a/packages/client-runtime/src/state/vcs.test.ts b/packages/client-runtime/src/state/vcs.test.ts index 0c839e188e106..518589ba212bf 100644 --- a/packages/client-runtime/src/state/vcs.test.ts +++ b/packages/client-runtime/src/state/vcs.test.ts @@ -5,6 +5,7 @@ import { WS_METHODS, VcsProcessExitError, type VcsListRefsResult, + type VcsStatusResult, } from "@t3tools/contracts"; import { describe, expect, it } from "@effect/vitest"; import * as Deferred from "effect/Deferred"; @@ -37,6 +38,7 @@ import { commitVcsRefsRefresh, createVcsEnvironmentAtoms, makeCachedVcsRefsChanges, + resolveQuickAction, } from "./vcs.ts"; import { invalidateCachedVcsRefs, @@ -617,3 +619,47 @@ const grantedSessions = Atom.family((_id: EnvironmentId) => }), ), ); + +function status(overrides: Partial = {}): VcsStatusResult { + return { + isRepo: true, + hasPrimaryRemote: true, + isDefaultRef: false, + refName: "feature/test", + hasWorkingTreeChanges: false, + workingTree: { + files: [], + insertions: 0, + deletions: 0, + }, + hasUpstream: true, + aheadCount: 0, + behindCount: 0, + pr: null, + ...overrides, + }; +} + +describe("resolveQuickAction", () => { + it("commits local changes without pushing when the branch is behind upstream", () => { + const behind = status({ hasWorkingTreeChanges: true, behindCount: 2 }); + const commit = { label: "Commit", disabled: false, kind: "run_action", action: "commit" }; + expect(resolveQuickAction(behind, false)).toEqual(commit); + expect( + resolveQuickAction( + { + ...behind, + pr: { + number: 13, + title: "Open PR", + url: "https://example.com/pr/13", + baseRef: "main", + headRef: "feature/test", + state: "open", + }, + }, + false, + ), + ).toEqual(commit); + }); +});