Skip to content
75 changes: 75 additions & 0 deletions apps/server/src/ws.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AuthSourceControlWriteScope,
EnvironmentId,
EventId,
GitManagerError,
MessageId,
ProjectId,
ThreadId,
Expand Down Expand Up @@ -1532,6 +1533,80 @@ it.effect("a missing thread worktree gets its project's skills in the cwd snapsh
),
);

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 refreshFailure = new GitManagerError({
operation: "refresh",
cwd: "/repo",
detail: "refresh failed",
});
const refreshed = yield* Deferred.make<string>();
const exits = yield* Queue.unbounded<Exit.Exit<unknown, unknown>>();
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(refreshFailure))),
}),
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) : 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,
});
const exit = yield* Queue.take(exits);
assert.deepEqual(Exit.isFailure(exit) ? Cause.squash(exit.cause) : exit, 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<string>();
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3172,7 +3172,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
Expand Down
Loading