diff --git a/notes/host-context-implementation-checklist.md b/notes/host-context-implementation-checklist.md index 6357794a0b1..87f87b7f0bb 100644 --- a/notes/host-context-implementation-checklist.md +++ b/notes/host-context-implementation-checklist.md @@ -267,3 +267,4 @@ Done means: reviewers and future agents can understand the vision, implementatio - [x] 2026-06-12: Completed Settings ownership/account rows by adding a shared provider Host scope control with a Change Host action that opens Settings > Active Server from GitHub/GitLab/Linear/Jira account surfaces and GitHub/GitLab API budget panels. Existing ownership diagnostics still distinguish desktop-client versus remote-server credentials, and rate-limit panels show the budget Host scope. Verified with settings ownership, RuntimeEnvironmentsPane, provider account, source-control card, task-tracker card, Jira card, and rate-limit panel tests, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`. - [x] 2026-06-12: Completed Tasks account/host metadata by showing repo-backed account labels in source summaries when GitHub/GitLab accounts differ by Host, while preserving host labels, provider identities, Linear workspace names, Jira site names, and unavailable-host status in labels/titles. Verified with source-summary and repo picker tests, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`. - [x] 2026-06-12: Continued the automation project-id migration by changing CLI automation show output to foreground explicit `runContext` fields (`runProjectId`, `runHostId`, `projectHostSetupId`, `runRepoId`, `runPath`) and label the old repo id as `legacyRepoId`. The checklist row remains partial because the shared `Automation.projectId` compatibility field still exists. Verified with CLI format/index tests, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`. +- [x] 2026-06-12: Aligned Automations manual-run disabled states with the backend run-target resolver so saved run contexts now disable Run Now when the project-host setup is missing or not ready, instead of failing after dispatch. Verified with focused automation availability tests, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`. diff --git a/src/renderer/src/components/automations/AutomationsPage.tsx b/src/renderer/src/components/automations/AutomationsPage.tsx index 6963ebd67c9..87b2b557d66 100644 --- a/src/renderer/src/components/automations/AutomationsPage.tsx +++ b/src/renderer/src/components/automations/AutomationsPage.tsx @@ -497,6 +497,7 @@ export default function AutomationsPage(): React.JSX.Element { automation: selected, repo: selectedRepo, workspace: selectedWorktree, + projectHostSetups, sshConnectionStates }) : null @@ -1248,6 +1249,7 @@ export default function AutomationsPage(): React.JSX.Element { automation, repo, workspace, + projectHostSetups, sshConnectionStates }) if (!availability.canRunNow) { @@ -1830,6 +1832,7 @@ export default function AutomationsPage(): React.JSX.Element { automation, repo: automationRepo, workspace: automationWorktree, + projectHostSetups, sshConnectionStates }) const workspaceLabel = diff --git a/src/renderer/src/components/automations/automation-target-availability.test.ts b/src/renderer/src/components/automations/automation-target-availability.test.ts index 71a575ffa85..97b020ac531 100644 --- a/src/renderer/src/components/automations/automation-target-availability.test.ts +++ b/src/renderer/src/components/automations/automation-target-availability.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { Automation } from '../../../../shared/automations-types' -import type { Repo, Worktree } from '../../../../shared/types' +import type { ProjectHostSetup, Repo, Worktree } from '../../../../shared/types' import { getAutomationTargetAvailability } from './automation-target-availability' function makeAutomation(overrides: Partial = {}): Automation { @@ -53,6 +53,22 @@ function makeWorkspace(overrides: Partial = {}): Worktree { } as Worktree } +function makeProjectHostSetup(overrides: Partial = {}): ProjectHostSetup { + return { + id: 'setup-1', + projectId: 'project-1', + hostId: 'local', + repoId: 'repo-1', + path: '/repo', + displayName: 'Repo', + setupState: 'ready', + setupMethod: 'legacy-repo', + createdAt: 1, + updatedAt: 1, + ...overrides + } +} + describe('automation target availability', () => { it('allows local automations with an available existing workspace', () => { expect( @@ -60,6 +76,7 @@ describe('automation target availability', () => { automation: makeAutomation(), repo: makeRepo(), workspace: makeWorkspace(), + projectHostSetups: [], sshConnectionStates: new Map() }) ).toEqual({ canRunNow: true, reason: 'available', message: null }) @@ -71,6 +88,7 @@ describe('automation target availability', () => { automation: makeAutomation(), repo: null, workspace: makeWorkspace(), + projectHostSetups: [], sshConnectionStates: new Map() }).reason ).toBe('missing-project') @@ -80,6 +98,7 @@ describe('automation target availability', () => { automation: makeAutomation(), repo: makeRepo(), workspace: null, + projectHostSetups: [], sshConnectionStates: new Map() }).reason ).toBe('missing-workspace') @@ -100,11 +119,48 @@ describe('automation target availability', () => { }), repo: makeRepo(), workspace: makeWorkspace(), + projectHostSetups: [makeProjectHostSetup()], sshConnectionStates: new Map() }).reason ).toBe('host-mismatch') }) + it('blocks saved run contexts whose project host setup is missing or not ready', () => { + const automation = makeAutomation({ + runContext: { + kind: 'workspace-run', + projectId: 'project-1', + hostId: 'local', + projectHostSetupId: 'setup-1', + repoId: 'repo-1', + path: '/repo' + } + }) + + expect( + getAutomationTargetAvailability({ + automation, + repo: makeRepo(), + workspace: makeWorkspace(), + projectHostSetups: [], + sshConnectionStates: new Map() + }).reason + ).toBe('missing-project-host-setup') + + expect( + getAutomationTargetAvailability({ + automation, + repo: makeRepo(), + workspace: makeWorkspace(), + projectHostSetups: [makeProjectHostSetup({ setupState: 'error' })], + sshConnectionStates: new Map() + }) + ).toMatchObject({ + reason: 'project-host-setup-not-ready', + message: 'Project setup on the selected automation host is error.' + }) + }) + it('requires SSH hosts to be connected before manual runs', () => { const automation = makeAutomation({ executionTargetType: 'ssh', @@ -125,6 +181,13 @@ describe('automation target availability', () => { automation, repo, workspace: makeWorkspace(), + projectHostSetups: [ + makeProjectHostSetup({ + hostId: 'ssh:devbox', + connectionId: 'devbox', + executionHostId: 'ssh:devbox' + }) + ], sshConnectionStates: new Map([['devbox', { status: 'connected' }]]) }).canRunNow ).toBe(true) @@ -134,6 +197,13 @@ describe('automation target availability', () => { automation, repo, workspace: makeWorkspace(), + projectHostSetups: [ + makeProjectHostSetup({ + hostId: 'ssh:devbox', + connectionId: 'devbox', + executionHostId: 'ssh:devbox' + }) + ], sshConnectionStates: new Map([['devbox', { status: 'disconnected' }]]) }).reason ).toBe('ssh-unavailable') @@ -143,6 +213,13 @@ describe('automation target availability', () => { automation, repo, workspace: makeWorkspace(), + projectHostSetups: [ + makeProjectHostSetup({ + hostId: 'ssh:devbox', + connectionId: 'devbox', + executionHostId: 'ssh:devbox' + }) + ], sshConnectionStates: new Map([['devbox', { status: 'auth-failed' }]]) }).reason ).toBe('ssh-auth-needed') @@ -152,6 +229,13 @@ describe('automation target availability', () => { automation, repo, workspace: makeWorkspace(), + projectHostSetups: [ + makeProjectHostSetup({ + hostId: 'ssh:devbox', + connectionId: 'devbox', + executionHostId: 'ssh:devbox' + }) + ], sshConnectionStates: new Map([['devbox', { status: 'reconnecting' }]]) }).reason ).toBe('ssh-connecting') diff --git a/src/renderer/src/components/automations/automation-target-availability.ts b/src/renderer/src/components/automations/automation-target-availability.ts index 47c1d96b6d9..0a459e4c78a 100644 --- a/src/renderer/src/components/automations/automation-target-availability.ts +++ b/src/renderer/src/components/automations/automation-target-availability.ts @@ -1,7 +1,7 @@ import type { Automation } from '../../../../shared/automations-types' import { getRepoExecutionHostId, parseExecutionHostId } from '../../../../shared/execution-host' import type { SshConnectionState } from '../../../../shared/ssh-types' -import type { Repo, Worktree } from '../../../../shared/types' +import type { ProjectHostSetup, Repo, Worktree } from '../../../../shared/types' export type AutomationTargetAvailability = | { @@ -13,6 +13,8 @@ export type AutomationTargetAvailability = canRunNow: false reason: | 'missing-project' + | 'missing-project-host-setup' + | 'project-host-setup-not-ready' | 'missing-workspace' | 'host-mismatch' | 'unsupported-host' @@ -26,6 +28,7 @@ type AutomationTargetAvailabilityArgs = { automation: Automation repo: Repo | null | undefined workspace: Worktree | null | undefined + projectHostSetups: readonly ProjectHostSetup[] sshConnectionStates: ReadonlyMap> } @@ -33,6 +36,7 @@ export function getAutomationTargetAvailability({ automation, repo, workspace, + projectHostSetups, sshConnectionStates }: AutomationTargetAvailabilityArgs): AutomationTargetAvailability { if (!repo) { @@ -46,7 +50,26 @@ export function getAutomationTargetAvailability({ 'This automation targets a remote server that this client cannot run manually yet.' ) } + const setup = projectHostSetups.find( + (candidate) => candidate.id === automation.runContext?.projectHostSetupId + ) + if (!setup) { + return unavailable( + 'missing-project-host-setup', + 'Project is not set up on the selected automation host anymore.' + ) + } + if (setup.setupState !== 'ready') { + return unavailable( + 'project-host-setup-not-ready', + `Project setup on the selected automation host is ${setup.setupState}.` + ) + } if ( + setup.projectId !== automation.runContext.projectId || + setup.hostId !== automation.runContext.hostId || + setup.repoId !== automation.runContext.repoId || + setup.path !== automation.runContext.path || automation.runContext.repoId !== repo.id || automation.runContext.path !== repo.path || automation.runContext.hostId !== getRepoExecutionHostId(repo)