diff --git a/apps/server/src/ws.test.ts b/apps/server/src/ws.test.ts index 2be9e61c7c916..c5de0cc789249 100644 --- a/apps/server/src/ws.test.ts +++ b/apps/server/src/ws.test.ts @@ -7,6 +7,7 @@ import { AuthSourceControlWriteScope, EnvironmentId, EventId, + GitManagerError, MessageId, ProjectId, ThreadId, @@ -1346,6 +1347,74 @@ it.effect("on-demand clients get provider config without workspace snapshots", ( ), ); +it.effect("a git action that fails partway still refreshes the repository status", () => + Effect.gen(function* () { + const failure = new GitManagerError({ operation: "test", cwd: "/repo", detail: "push failed" }); + const refreshed = yield* Deferred.make(); + const exits = yield* Queue.unbounded(); + const services = Layer.mergeAll( + layerUnusedConfigRpcServices, + Layer.mock(GitWorkflowService.GitWorkflowService)({ + runStackedAction: () => Effect.fail(failure), + }), + Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({ + refreshStatus: (cwd) => + Deferred.succeed(refreshed, cwd).pipe(Effect.andThen(Effect.fail(failure))), + }), + Layer.mock(ExternalLauncher.ExternalLauncher)({}), + Layer.mock(ProviderRegistry.ProviderRegistry)({}), + Layer.mock(UsageLimitSources.UsageLimitSources)({}), + Layer.mock(RemoteOpenTargets.RemoteOpenTargets)({}), + Layer.mock(DirectEndpoints.DirectEndpoints)({}), + Layer.mock(ManagedProjectFolders.ManagedProjectFolders)({ namedProjectsRoot: "/unused" }), + Layer.mock(ScratchThreadPromotion.ScratchThreadPromotion)({}), + Layer.mock(ServerEnvironment.ServerEnvironment)({}), + Layer.mock(EnvironmentAuth.EnvironmentAuth)({}), + ); + const server = yield* RpcServer.makeNoSerialization(WsRpcGroup, { + ...WS_RPC_SERVER_OPTIONS, + onFromServer: (response) => + response._tag === "Exit" ? Queue.offer(exits, response.exit._tag) : Effect.void, + }).pipe( + Effect.provide( + layerWsRpc( + { + sessionId: AuthSessionId.make("git-test"), + subject: "git-test", + method: "browser-session-cookie", + scopes: [AuthSourceControlWriteScope], + }, + {}, + yield* ServerBrowser.ServerBrowser, + () => Effect.void, + { secretReferences: true }, + ).pipe( + Layer.provide(services), + Layer.merge(RpcAuthorization.layer([AuthSourceControlWriteScope])), + ), + ), + ); + yield* server.write(0, { + _tag: "Request", + id: RpcMessage.RequestId("git"), + tag: WS_METHODS.gitRunStackedAction, + payload: { actionId: "action", cwd: "/repo", action: "commit_push" }, + headers: Headers.empty, + }); + assert.equal(yield* Queue.take(exits), "Failure"); + assert.equal(yield* Deferred.await(refreshed), "/repo"); + }).pipe( + Effect.provide( + Layer.mergeAll( + Layer.fresh(layerConfigSources), + Layer.mock(PreviewAutomationBroker.PreviewAutomationBroker)({}), + Layer.mock(ServerBrowser.ServerBrowser)({}), + ), + ), + Effect.scoped, + ), +); + it.effect("a publish whose push fails still refreshes the new remote", () => Effect.gen(function* () { const refreshes = yield* Queue.unbounded(); diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 035f8b1688d44..1c2cff33bd9e2 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -3138,7 +3138,9 @@ export const layerWsRpc = ( }) .pipe( Effect.matchCauseEffect({ - onFailure: (cause) => Queue.failCause(queue, cause), + // Earlier steps (branch, commit) may have landed before the failing one. + onFailure: (cause) => + refreshGitStatus(input.cwd).pipe(Effect.andThen(Queue.failCause(queue, cause))), onSuccess: (result) => (input.threadId === undefined ? Effect.void