authorgravatar for mwolson@member.fsf.orgMike Olson <mwolson@member.fsf.org> 2026-06-18 23:20:53-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-18 20:20:53-07:00
log1fcc57af136e00633c5d149c3d8564e98f7823c3
treeb8c0a21b23f9253fae4f832d6bf3347dd3fba148
parente95b57dc268471c882f734546fdcd92e9a833e45
signature Signed by PGP key B5690EEEBB952194

fix(web): Remove saved environments atomically (#2917)


2 files changed, 52 insertions(+), 0 deletions(-)

apps/desktop/src/settings/DesktopSavedEnvironments.test.ts+20
...@@ -272,6 +272,26 @@ describe("DesktopSavedEnvironments", () => {...@@ -272,6 +272,26 @@ describe("DesktopSavedEnvironments", () => {
272 ),272 ),
273 );273 );
274274
275 it.effect("removes saved environment metadata and its embedded secret atomically", () =>
276 withSavedEnvironments(
277 Effect.gen(function* () {
278 const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments;
279 yield* savedEnvironments.setRegistry([savedRegistryRecord]);
280 yield* savedEnvironments.setSecret({
281 environmentId: savedRegistryRecord.environmentId,
282 secret: "bearer-token",
283 });
284
285 yield* savedEnvironments.removeEnvironment(savedRegistryRecord.environmentId);
286
287 assert.deepEqual(yield* savedEnvironments.getRegistry, []);
288 assert.isTrue(
289 Option.isNone(yield* savedEnvironments.getSecret(savedRegistryRecord.environmentId)),
290 );
291 }),
292 ),
293 );
294
275 it.effect("treats empty saved environment documents as empty", () =>295 it.effect("treats empty saved environment documents as empty", () =>
276 withSavedEnvironments(296 withSavedEnvironments(
277 Effect.gen(function* () {297 Effect.gen(function* () {
apps/desktop/src/settings/DesktopSavedEnvironments.ts+32
...@@ -121,6 +121,9 @@ export interface DesktopSavedEnvironmentsShape {...@@ -121,6 +121,9 @@ export interface DesktopSavedEnvironmentsShape {
121 readonly setRegistry: (121 readonly setRegistry: (
122 records: readonly PersistedSavedEnvironmentRecord[],122 records: readonly PersistedSavedEnvironmentRecord[],
123 ) => Effect.Effect<void, DesktopSavedEnvironmentsWriteError>;123 ) => Effect.Effect<void, DesktopSavedEnvironmentsWriteError>;
124 readonly removeEnvironment: (
125 environmentId: string,
126 ) => Effect.Effect<void, DesktopSavedEnvironmentsWriteError>;
124 readonly getSecret: (127 readonly getSecret: (
125 environmentId: string,128 environmentId: string,
126 ) => Effect.Effect<Option.Option<string>, DesktopSavedEnvironmentsGetSecretError>;129 ) => Effect.Effect<Option.Option<string>, DesktopSavedEnvironmentsGetSecretError>;
...@@ -293,6 +296,23 @@ export const layer = Layer.effect(...@@ -293,6 +296,23 @@ export const layer = Layer.effect(
293 ).pipe(Effect.mapError((cause) => new DesktopSavedEnvironmentsWriteError({ cause })));296 ).pipe(Effect.mapError((cause) => new DesktopSavedEnvironmentsWriteError({ cause })));
294 yield* writeDocument(preserveExistingSecrets(currentDocument, records));297 yield* writeDocument(preserveExistingSecrets(currentDocument, records));
295 }),298 }),
299 removeEnvironment: Effect.fn("desktop.savedEnvironments.removeEnvironment")(
300 function* (environmentId) {
301 yield* Effect.annotateCurrentSpan({ environmentId });
302 const document = yield* readRegistryDocument(
303 fileSystem,
304 environment.savedEnvironmentRegistryPath,
305 ).pipe(Effect.mapError((cause) => new DesktopSavedEnvironmentsWriteError({ cause })));
306 if (!document.records.some((record) => record.environmentId === environmentId)) {
307 return;
308 }
309
310 yield* writeDocument({
311 version: document.version,
312 records: document.records.filter((record) => record.environmentId !== environmentId),
313 });
314 },
315 ),
296 getSecret: Effect.fn("desktop.savedEnvironments.getSecret")(function* (environmentId) {316 getSecret: Effect.fn("desktop.savedEnvironments.getSecret")(function* (environmentId) {
297 yield* Effect.annotateCurrentSpan({ environmentId });317 yield* Effect.annotateCurrentSpan({ environmentId });
298 const document = yield* readRegistryDocument(318 const document = yield* readRegistryDocument(
...@@ -385,6 +405,18 @@ export const layerTest = (input?: {...@@ -385,6 +405,18 @@ export const layerTest = (input?: {
385 return DesktopSavedEnvironments.of({405 return DesktopSavedEnvironments.of({
386 getRegistry: Ref.get(recordsRef),406 getRegistry: Ref.get(recordsRef),
387 setRegistry: (records) => Ref.set(recordsRef, records),407 setRegistry: (records) => Ref.set(recordsRef, records),
408 removeEnvironment: (environmentId) =>
409 Ref.update(recordsRef, (records) =>
410 records.filter((record) => record.environmentId !== environmentId),
411 ).pipe(
412 Effect.andThen(
413 Ref.update(secretsRef, (secrets) => {
414 const nextSecrets = new Map(secrets);
415 nextSecrets.delete(environmentId);
416 return nextSecrets;
417 }),
418 ),
419 ),
388 getSecret: (environmentId) =>420 getSecret: (environmentId) =>
389 Ref.get(secretsRef).pipe(421 Ref.get(secretsRef).pipe(
390 Effect.map((secrets) => Option.fromNullishOr(secrets.get(environmentId))),422 Effect.map((secrets) => Option.fromNullishOr(secrets.get(environmentId))),