Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions apps/server/src/ws.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,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,
Expand Down Expand Up @@ -63,6 +66,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";
Expand Down Expand Up @@ -1240,6 +1244,94 @@ 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<string>();
const exits = yield* Queue.unbounded<string>();
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, options) =>
Queue.offer(refreshes, `identity ${cwd} refresh=${options?.refresh === true}`).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,
{ secretReferences: true },
).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 refresh=true", "status /repo"],
);
}).pipe(
Effect.provide(
Layer.mergeAll(
Layer.fresh(layerConfigSources),
Layer.mock(PreviewAutomationBroker.PreviewAutomationBroker)({}),
Layer.mock(ServerBrowser.ServerBrowser)({}),
),
),
Effect.scoped,
),
);

// Pruned history cannot be replayed: a cursor below the boundary must resync from a snapshot.
it.effect.each(["thread", "shell"] as const)(
"%s resume from a cursor below the pruned boundary loads a snapshot",
Expand Down
8 changes: 6 additions & 2 deletions apps/server/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2921,8 +2921,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(
Comment thread
saphid marked this conversation as resolved.
Resolved
repositoryIdentityResolver
.resolve(input.cwd, { refresh: true })
.pipe(Effect.andThen(refreshGitStatus(input.cwd))),
),
),
[WS_METHODS.projectsSearchEntries]: (input) =>
workspaceEntries.search(input).pipe(
Expand Down
Loading