From 5f64876bc4345d679052d9cd28334898544a226c Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Tue, 3 Nov 2026 09:41:41 +1100 Subject: [PATCH 1/3] fix(server): a publish whose first push fails still shows the new remote Co-Authored-By: Claude Opus 5.5 --- apps/server/src/ws.test.ts | 88 ++++++++++++++++++++++++++++++++++++++ apps/server/src/ws.ts | 8 +++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/apps/server/src/ws.test.ts b/apps/server/src/ws.test.ts index caa06c2e97a80..513029019cc77 100644 --- a/apps/server/src/ws.test.ts +++ b/apps/server/src/ws.test.ts @@ -3,12 +3,15 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; import { AuthSessionId, + AuthSourceControlWriteScope, EnvironmentId, ProjectId, ThreadId, ProviderDriverKind, ProviderInstanceId, + SourceControlRepositoryError, WS_METHODS, + WsRpcGroup, WsServerRefreshProvidersRpc, WsSubscribeServerConfigRpc, type OrchestrationV2StoredEvent, @@ -58,6 +61,7 @@ import * as ModelManifest from "./provider/ModelManifest.ts"; import * as Orchestrator from "./orchestration-v2/Orchestrator.ts"; import * as OtlpEndpointCheck from "./observability/OtlpEndpointCheck.ts"; import * as PairingGrantStore from "./auth/PairingGrantStore.ts"; +import * as RpcAuthorization from "./auth/RpcAuthorization.ts"; import * as PeerLinkRequests from "./peer/PeerLinkRequests.ts"; import * as PeerLinks from "./peer/PeerLinks.ts"; import * as PortScanner from "./preview/PortScanner.ts"; @@ -1035,6 +1039,90 @@ it.effect("on-demand clients get provider config without workspace snapshots", ( ), ); +it.effect("a publish whose push fails still refreshes the new remote", () => + Effect.gen(function* () { + const refreshes = yield* Queue.unbounded(); + const exits = yield* Queue.unbounded(); + const services = Layer.mergeAll( + layerUnusedConfigRpcServices, + Layer.mock(SourceControlRepositoryService.SourceControlRepositoryService)({ + publishRepository: (input) => + Effect.fail( + new SourceControlRepositoryError({ + provider: input.provider, + operation: "publishRepository", + detail: 'Created owner/repo and added it as remote "origin", but the push failed.', + }), + ), + }), + Layer.mock(RepositoryIdentityResolver.RepositoryIdentityResolver)({ + resolve: (cwd) => Queue.offer(refreshes, `identity ${cwd}`).pipe(Effect.as(null)), + }), + Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({ + refreshStatus: (cwd) => + Queue.offer(refreshes, `status ${cwd}`).pipe(Effect.andThen(Effect.die("unused"))), + }), + 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("publish-test"), + subject: "publish-test", + method: "browser-session-cookie", + scopes: [AuthSourceControlWriteScope], + }, + {}, + yield* ServerBrowser.ServerBrowser, + () => Effect.void, + ).pipe( + Layer.provide(services), + Layer.merge(RpcAuthorization.layer([AuthSourceControlWriteScope])), + ), + ), + ); + yield* server.write(0, { + _tag: "Request", + id: RpcMessage.RequestId("publish"), + tag: WS_METHODS.sourceControlPublishRepository, + payload: { + cwd: "/repo", + provider: "github", + repository: "owner/repo", + visibility: "private", + }, + headers: Headers.empty, + }); + assert.equal(yield* Queue.take(exits), "Failure"); + assert.deepEqual( + [yield* Queue.take(refreshes), yield* Queue.take(refreshes)], + ["identity /repo", "status /repo"], + ); + }).pipe( + Effect.provide( + Layer.mergeAll( + Layer.fresh(layerConfigSources), + Layer.mock(PreviewAutomationBroker.PreviewAutomationBroker)({}), + Layer.mock(ServerBrowser.ServerBrowser)({}), + ), + ), + Effect.scoped, + ), +); + // Compaction keeps the oldest and newest event per message, so a cursor older // than the deleted rows still replays each message where the client would // first have seen it (reducers append on first sight) and reaches the same diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 00842afde41b7..7cea54681e8c8 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -2815,8 +2815,12 @@ export const layerWsRpc = ( [WS_METHODS.subscribeProjectClones]: () => projectCloneTracker.stream, [WS_METHODS.sourceControlPublishRepository]: (input) => sourceControlRepositories.publishRepository(input).pipe( - Effect.tap(() => repositoryIdentityResolver.resolve(input.cwd, { refresh: true })), - Effect.tap(() => refreshGitStatus(input.cwd)), + // A failed push still leaves the new remote added. + Effect.ensuring( + repositoryIdentityResolver + .resolve(input.cwd, { refresh: true }) + .pipe(Effect.andThen(refreshGitStatus(input.cwd))), + ), ), [WS_METHODS.projectsSearchEntries]: (input) => workspaceEntries.search(input).pipe( From ce3f393c6686109cdc4101465b33c0c4df5745db Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Wed, 4 Nov 2026 07:48:27 +1100 Subject: [PATCH 2/3] test(server): failed publish test checks the identity refresh is forced Co-Authored-By: Claude Opus 5.5 --- apps/server/src/ws.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/server/src/ws.test.ts b/apps/server/src/ws.test.ts index a8d80a2c5c371..97a9a12de7eb9 100644 --- a/apps/server/src/ws.test.ts +++ b/apps/server/src/ws.test.ts @@ -1182,7 +1182,10 @@ it.effect("a publish whose push fails still refreshes the new remote", () => ), }), Layer.mock(RepositoryIdentityResolver.RepositoryIdentityResolver)({ - resolve: (cwd) => Queue.offer(refreshes, `identity ${cwd}`).pipe(Effect.as(null)), + resolve: (cwd, options) => + Queue.offer(refreshes, `identity ${cwd} refresh=${options?.refresh === true}`).pipe( + Effect.as(null), + ), }), Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({ refreshStatus: (cwd) => @@ -1235,7 +1238,7 @@ it.effect("a publish whose push fails still refreshes the new remote", () => assert.equal(yield* Queue.take(exits), "Failure"); assert.deepEqual( [yield* Queue.take(refreshes), yield* Queue.take(refreshes)], - ["identity /repo", "status /repo"], + ["identity /repo refresh=true", "status /repo"], ); }).pipe( Effect.provide( From 799d458e9a2ffbfd7f9ebed9698d2245c063eef3 Mon Sep 17 00:00:00 2001 From: maria-rcks <254055478+maria-rcks@users.noreply.github.com> Date: Fri, 6 Nov 2026 03:14:49 +0000 Subject: [PATCH 3/3] test(server): pass settings client to layerWsRpc in the publish test --- apps/server/src/ws.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/src/ws.test.ts b/apps/server/src/ws.test.ts index 9ce3ea69b61a0..855e59440ace4 100644 --- a/apps/server/src/ws.test.ts +++ b/apps/server/src/ws.test.ts @@ -1296,6 +1296,7 @@ it.effect("a publish whose push fails still refreshes the new remote", () => {}, yield* ServerBrowser.ServerBrowser, () => Effect.void, + { secretReferences: true }, ).pipe( Layer.provide(services), Layer.merge(RpcAuthorization.layer([AuthSourceControlWriteScope])),