diff --git a/src/renderer/src/store/terminals/workspace-terminal-placeholders.test.ts b/src/renderer/src/store/terminals/workspace-terminal-placeholders.test.ts new file mode 100644 index 00000000000..b4f3f1312a3 --- /dev/null +++ b/src/renderer/src/store/terminals/workspace-terminal-placeholders.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from 'vitest' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' +import { DEFAULT_REPO_BADGE_COLOR } from '../../../../shared/constants' +import { buildRuntimeSessionPlaceholders } from './workspace-terminal-placeholders' + +const repo: Repo = { + id: 'repo-1', + path: '/repos/one', + displayName: 'one', + badgeColor: DEFAULT_REPO_BADGE_COLOR, + addedAt: 0, + connectionId: null, + executionHostId: 'local' +} + +const worktree: Worktree = { + id: 'repo-1::/repos/one', + repoId: 'repo-1', + hostId: 'local', + displayName: 'main', + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + linkedGitLabMR: null, + linkedGitLabIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0, + path: '/repos/one', + head: '', + branch: '', + isBare: false, + isMainWorktree: true +} + +describe('buildRuntimeSessionPlaceholders', () => { + it('returns the original repos array when no placeholder repo is needed', () => { + const repos = [repo] + const worktreesByRepo = { 'repo-1': [worktree] } + + const result = buildRuntimeSessionPlaceholders({ + repos, + runtimeHostIdByWorkspaceSessionKey: {}, + worktreesByRepo + }) + + // Hydration writes these straight to the store; a fresh array would rerender + // every component selecting the whole repo list for no data change. + expect(result.repos).toBe(repos) + expect(result.worktreesByRepo).toBe(worktreesByRepo) + }) + + it('keeps the original repos array when the session only references known repos', () => { + const repos = [repo] + + const result = buildRuntimeSessionPlaceholders({ + repos, + runtimeHostIdByWorkspaceSessionKey: { 'repo-1::/repos/one': 'runtime:host-1' }, + worktreesByRepo: { 'repo-1': [worktree] } + }) + + expect(result.repos).toBe(repos) + }) + + it('still appends a placeholder repo for an unknown runtime workspace', () => { + const repos = [repo] + + const result = buildRuntimeSessionPlaceholders({ + repos, + runtimeHostIdByWorkspaceSessionKey: { 'repo-2::/repos/two': 'runtime:host-1' }, + worktreesByRepo: { 'repo-1': [worktree] } + }) + + expect(result.repos).not.toBe(repos) + expect(result.repos.map((entry) => entry.id)).toEqual(['repo-1', 'repo-2']) + // The caller's array must not be mutated in place. + expect(repos).toHaveLength(1) + }) +}) diff --git a/src/renderer/src/store/terminals/workspace-terminal-placeholders.ts b/src/renderer/src/store/terminals/workspace-terminal-placeholders.ts index e83adf8b937..a2bb0674b7e 100644 --- a/src/renderer/src/store/terminals/workspace-terminal-placeholders.ts +++ b/src/renderer/src/store/terminals/workspace-terminal-placeholders.ts @@ -20,10 +20,15 @@ export function buildRuntimeSessionPlaceholders({ runtimeHostIdByWorkspaceSessionKey: Record worktreesByRepo: Record }): { - repos: Repo[] + repos: readonly Repo[] worktreesByRepo: Record } { - let nextRepos = repos.slice() + // Why not `repos.slice()`: hydration writes this array straight back to the store, and + // sessions that add no placeholder repo are the common case. Copying unconditionally + // gave `repos` a new identity on every workspace hydration, rerendering every component + // that selects the whole array. Appends below already build a new array, as + // nextWorktreesByRepo does for its own copy-on-write. + let nextRepos: readonly Repo[] = repos let nextWorktreesByRepo = worktreesByRepo for (const workspaceSessionKey of Object.keys(runtimeHostIdByWorkspaceSessionKey)) { const hostId = runtimeHostIdByWorkspaceSessionKey[workspaceSessionKey]