From 74c3981de7aef34b4d8f9690a6bddb4842eae613 Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Mon, 31 Aug 2026 23:37:25 -0700 Subject: [PATCH] test(composer): cover the forced source reset behaviourally, not by source text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The boundaries suite pinned the project switch by matching the literal `handleRepoChange(nextRepoId, ...)` call, which the setup-choice refactor renamed. The invariant it protected — returning from a folder target to a repo with the same id must force the repo-scoped reset past handleRepoChange's same-id early return — now runs against the real handleRepoChange, so a rename cannot break it and a dropped flag cannot pass it. --- .../project-target-actions.test.ts | 96 +++++++++++++++++++ ...poserState-host-context-boundaries.test.ts | 11 +-- 2 files changed, 101 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/hooks/composer-state/project-target-actions.test.ts b/src/renderer/src/hooks/composer-state/project-target-actions.test.ts index 6a4e6cc4230..1fec0870d2e 100644 --- a/src/renderer/src/hooks/composer-state/project-target-actions.test.ts +++ b/src/renderer/src/hooks/composer-state/project-target-actions.test.ts @@ -5,6 +5,7 @@ import type { ExecutionHostId } from '../../../../shared/execution-host' import type { Project, ProjectHostSetup } from '../../../../shared/project-types' import type { Repo } from '../../../../shared/repo-types' import { useProjectTargetActions } from './project-target-actions' +import { useTargetChangeActions } from './target-change-actions' const PROJECT_ID = 'github:stablyai/orca' @@ -114,3 +115,98 @@ describe('composer project switch with several ready setups (STA-6080)', () => { expect(spies.setRepoId).not.toHaveBeenCalled() }) }) + +/** + * The folder branch of the composer leaves `repoId` on the group's source repo, so switching back to + * a project whose setup uses that same repo id hits `handleRepoChange`'s same-id early return and + * would keep the folder's source state. `forceResetStartFrom: isProjectGroupTarget` is what forces + * the reset through. Exercised against the real `handleRepoChange`, not its source text. + */ +function renderProjectSwitchFromFolderTarget(isProjectGroupTarget: boolean) { + const spies = { + setBaseBranch: vi.fn(), + setLinkedIssue: vi.fn(), + setRepoId: vi.fn(), + setSparseEnabled: vi.fn(), + setStartFromResetHint: vi.fn() + } + const noop = vi.fn() + const shared = { + linkedWorkItem: null, + setBranchNameOverride: noop, + setBranchNameOverridePreservesNameEdits: noop, + setForkPushWarning: noop, + setLinkedGitLabIssue: noop, + setLinkedGitLabMR: noop, + setLinkedPR: noop, + setLinkedTaskSourceContext: noop, + setLinkedWorkItem: noop, + setProjectError: noop, + setPushTarget: noop, + setReuseEligibleBranch: noop, + setReuseSelectedBranch: noop, + setSelectedProjectHostSetupOverrideId: noop, + setSelectedProjectIdOverride: noop, + setSparseDirectories: noop, + setSparseSelectedPresetId: noop, + ...spies + } + const { result } = renderHook(() => { + const { handleRepoChange } = useTargetChangeActions({ + baseBranch: undefined, + branchAutoNameRef: { current: null }, + decisions: { retargetGitHubPrStartPointSelection: (selection: unknown) => selection }, + folderSourceRepos: [], + hostOptions: [], + projectHostSetupOptions: [], + // Why: the folder branch already parked the composer on this repo id, so the project switch + // resolves to the same value and only the forced reset can clear the folder's state. + repoId: 'orca-main', + selectedRepoProjectId: PROJECT_ID, + setCompareBaseRef: noop, + smartGitHubPrStartPointSelectionRef: { current: null }, + ...shared + } as unknown as Parameters[0]) + return useProjectTargetActions({ + actionableHostIds: new Set(['local']), + eligibleRepos: [makeRepo('orca-main', '/checkouts/main')], + handleRepoChange, + initialProjectGroupAppliedRef: { current: false }, + isProjectGroupTarget, + projectGroups: [], + projectHostSetups: [makeSetup('setup-main', 'local', 'orca-main', '/checkouts/main')], + projects: [project], + repos: [], + selectedWorkspaceTarget: { status: 'unavailable', reason: 'project-has-no-ready-setup' }, + workspaceHostScope: 'all', + setSelectedProjectGroupId: noop, + ...shared + } as unknown as Parameters[0]) + }) + return { handleProjectChange: result.current.handleProjectChange, spies } +} + +describe('returning from a folder target to a repo with the same id', () => { + it('forces the repo-scoped source reset', () => { + const { handleProjectChange, spies } = renderProjectSwitchFromFolderTarget(true) + + handleProjectChange(PROJECT_ID) + + expect(spies.setRepoId).toHaveBeenCalledWith('orca-main') + expect(spies.setSparseEnabled).toHaveBeenCalledWith(false) + expect(spies.setLinkedIssue).toHaveBeenCalledWith('') + expect(spies.setBaseBranch).toHaveBeenCalledWith(undefined) + expect(spies.setStartFromResetHint).toHaveBeenCalledWith(null) + }) + + it('leaves the source state alone when the composer was already on that repo', () => { + const { handleProjectChange, spies } = renderProjectSwitchFromFolderTarget(false) + + handleProjectChange(PROJECT_ID) + + expect(spies.setRepoId).toHaveBeenCalledWith('orca-main') + expect(spies.setSparseEnabled).not.toHaveBeenCalled() + expect(spies.setLinkedIssue).not.toHaveBeenCalled() + expect(spies.setBaseBranch).not.toHaveBeenCalled() + }) +}) diff --git a/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts b/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts index d3ddbc34e3d..cb9e2731138 100644 --- a/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts +++ b/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts @@ -453,15 +453,14 @@ describe('useComposerState host-context boundaries', () => { expect(section).toContain("branchAutoNameRef.current = ''") }) - it('forces repo-scoped source reset when returning from folder target to a repo with the same id', () => { + // The caller half — that a project switch passes `forceResetStartFrom: isProjectGroupTarget`, and + // that the reset actually runs — is covered behaviourally against the real `handleRepoChange` in + // composer-state/project-target-actions.test.ts ("returning from a folder target to a repo with + // the same id"), so it survives renaming the local that holds the resolved repo id. + it('keeps the same-id early return escapable', () => { const handleRepoChange = COMPOSER_SOURCE.targetChange expect(handleRepoChange).toContain('forceResetStartFrom?: boolean') expect(handleRepoChange).toContain('value === repoId && !options.forceResetStartFrom') - - const handleProjectChange = COMPOSER_SOURCE.projectTarget - expect(handleProjectChange).toContain( - 'handleRepoChange(nextRepoId, { forceResetStartFrom: isProjectGroupTarget })' - ) }) it('keeps a Linear branch override when its workspace-scoped issue survives a repo change', () => {