| ... | ... | @@ -5,6 +5,7 @@ import * as Effect from "effect/Effect"; |
| 5 | 5 | import * as Layer from "effect/Layer"; |
| 6 | 6 | import * as PubSub from "effect/PubSub"; |
| 7 | 7 | import * as Ref from "effect/Ref"; |
| 8 | import { TestClock } from "effect/testing"; |
| 8 | 9 | |
| 9 | 10 | import * as ProjectEnrichment from "./ProjectEnrichmentService.ts"; |
| 10 | 11 | import * as ProjectFaviconResolver from "./ProjectFaviconResolver.ts"; |
| ... | ... | @@ -341,3 +342,92 @@ it.effect("deduplicates requests, bounds pending work, and reloads invalidated r |
| 341 | 342 | ); |
| 342 | 343 | }), |
| 343 | 344 | ); |
| 345 | |
| 346 | it.effect("rescans a favicon only after 15 minutes", () => |
| 347 | Effect.gen(function* () { |
| 348 | const faviconScans = yield* Ref.make(0); |
| 349 | const layerMetadata = Layer.merge( |
| 350 | Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, { |
| 351 | resolve: (workspaceRoot) => Effect.succeed(identity(workspaceRoot)), |
| 352 | }), |
| 353 | Layer.succeed(ProjectFaviconResolver.ProjectFaviconResolver, { |
| 354 | // Each scan returns a new path, so a wait can tell a rescan from the cached value. |
| 355 | resolvePath: (workspaceRoot) => |
| 356 | Ref.updateAndGet(faviconScans, (count) => count + 1).pipe( |
| 357 | Effect.map((scan) => `${workspaceRoot}/favicon-${scan}.svg`), |
| 358 | ), |
| 359 | }), |
| 360 | ); |
| 361 | |
| 362 | yield* Effect.gen(function* () { |
| 363 | const service = yield* ProjectEnrichment.ProjectEnrichmentService; |
| 364 | yield* service.getAvailable("/repo"); |
| 365 | yield* waitForAvailable( |
| 366 | service, |
| 367 | "/repo", |
| 368 | (value) => value.faviconPath === "/repo/favicon-1.svg", |
| 369 | ); |
| 370 | |
| 371 | // Callers such as the shell stream read projects far more often than this. |
| 372 | for (let minute = 1; minute < 15; minute += 1) { |
| 373 | yield* TestClock.adjust("1 minute"); |
| 374 | yield* service.getAvailable("/repo"); |
| 375 | } |
| 376 | assert.equal(yield* Ref.get(faviconScans), 1); |
| 377 | |
| 378 | yield* TestClock.adjust("1 minute"); |
| 379 | yield* service.getAvailable("/repo"); |
| 380 | yield* waitForAvailable( |
| 381 | service, |
| 382 | "/repo", |
| 383 | (value) => value.faviconPath === "/repo/favicon-2.svg", |
| 384 | ); |
| 385 | assert.equal(yield* Ref.get(faviconScans), 2); |
| 386 | }).pipe(Effect.provide(layer(layerMetadata))); |
| 387 | }), |
| 388 | ); |
| 389 | |
| 390 | it.effect( |
| 391 | "follows repository identity changes within a minute without rescanning the favicon", |
| 392 | () => |
| 393 | Effect.gen(function* () { |
| 394 | // 0: no repository yet, then one version per remote. |
| 395 | const remoteVersion = yield* Ref.make(0); |
| 396 | const faviconScans = yield* Ref.make(0); |
| 397 | const layerMetadata = Layer.merge( |
| 398 | Layer.succeed(RepositoryIdentityResolver.RepositoryIdentityResolver, { |
| 399 | resolve: (workspaceRoot) => |
| 400 | Ref.get(remoteVersion).pipe( |
| 401 | Effect.map((version) => (version === 0 ? null : identity(workspaceRoot, version))), |
| 402 | ), |
| 403 | }), |
| 404 | Layer.succeed(ProjectFaviconResolver.ProjectFaviconResolver, { |
| 405 | resolvePath: () => Ref.update(faviconScans, (count) => count + 1).pipe(Effect.as(null)), |
| 406 | }), |
| 407 | ); |
| 408 | |
| 409 | yield* Effect.gen(function* () { |
| 410 | const service = yield* ProjectEnrichment.ProjectEnrichmentService; |
| 411 | yield* service.getAvailable("/folder"); |
| 412 | yield* waitForAvailable(service, "/folder", (value) => value.repositoryIdentityResolved); |
| 413 | assert.equal((yield* service.peek("/folder")).repositoryIdentity, null); |
| 414 | |
| 415 | // The folder is published as a repository, then its remote moves to another host. |
| 416 | for (const version of [1, 2]) { |
| 417 | yield* Ref.set(remoteVersion, version); |
| 418 | yield* TestClock.adjust("1 minute"); |
| 419 | yield* service.getAvailable("/folder"); |
| 420 | const available = yield* waitForAvailable( |
| 421 | service, |
| 422 | "/folder", |
| 423 | (value) => value.repositoryIdentity?.canonicalKey === `example.test/v${version}/folder`, |
| 424 | ); |
| 425 | assert.equal( |
| 426 | available.repositoryIdentity?.canonicalKey, |
| 427 | `example.test/v${version}/folder`, |
| 428 | ); |
| 429 | } |
| 430 | assert.equal(yield* Ref.get(faviconScans), 1); |
| 431 | }).pipe(Effect.provide(layer(layerMetadata))); |
| 432 | }), |
| 433 | ); |