From cdbe11f64c9ed02e8346d9aed2e0aaf0c9d7fe41 Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:22:30 +0200 Subject: [PATCH] fix(server): resolve missing project identity before PR lookups Co-authored-by: Claude Opus 5.5 --- .../pullRequest/PullRequestService.test.ts | 32 +++++++++++++++++++ .../src/pullRequest/PullRequestService.ts | 14 ++++++++ 2 files changed, 46 insertions(+) diff --git a/apps/server/src/pullRequest/PullRequestService.test.ts b/apps/server/src/pullRequest/PullRequestService.test.ts index 005b3293b218..b6db92eac549 100644 --- a/apps/server/src/pullRequest/PullRequestService.test.ts +++ b/apps/server/src/pullRequest/PullRequestService.test.ts @@ -22,6 +22,7 @@ import { PullRequestOperationError } from "@t3tools/contracts"; import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts"; import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; import * as PullRequestFilesViewed from "../persistence/PullRequestFilesViewed.ts"; +import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts"; import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts"; import * as SourceControlRateLimit from "../sourceControl/SourceControlRateLimit.ts"; import { ForgejoCli } from "../sourceControl/ForgejoCli.ts"; @@ -402,6 +403,7 @@ function makeService(input: { readonly projects: ReadonlyArray; readonly providers: ReadonlyArray; readonly resolveHandle?: SourceControlProviderRegistry.SourceControlProviderRegistry["Service"]["resolveHandle"]; + readonly resolveRepositoryIdentity?: RepositoryIdentityResolver.RepositoryIdentityResolver["Service"]["resolve"]; }) { // Built into the test's own scope rather than provided call by call: the marks store owns a // database, and `Effect.provide` would close it the moment the service was handed back. @@ -422,6 +424,9 @@ function makeService(input: { getProjectShellById: (projectId) => Effect.succeed(Option.fromNullishOr(input.projects.find((p) => p.id === projectId))), }), + Layer.mock(RepositoryIdentityResolver.RepositoryIdentityResolver)({ + resolve: input.resolveRepositoryIdentity ?? (() => Effect.succeed(null)), + }), SourceControlRateLimit.layer, // The real store over a database of its own, so the environment-kept marks are exercised // through the SQL that holds them rather than through a stand-in that agrees with itself. @@ -2184,6 +2189,33 @@ it.effect("rejects a different Forgejo HTTP port for an HTTP checkout", () => }), ); +it.effect("resolves a project's repository identity when its shell has none cached", () => + Effect.gen(function* () { + const resolved = project({ + id: "web", + title: "web", + workspaceRoot: "/web", + repository: "acme/web", + }); + const service = yield* makeService({ + projects: [{ ...resolved, repositoryIdentity: null }], + providers: [ + fakeProvider("github", { + getChangeRequestSummary: () => Effect.succeed(changeRequest(7, "2026-07-02T00:00:00Z")), + }), + ], + resolveRepositoryIdentity: () => Effect.succeed(resolved.repositoryIdentity ?? null), + }); + + const summary = yield* service.summary( + { projectId: "web" as ProjectId, host: "github.com", repository: "acme/web", number: 7 }, + { recoverTransientFailure: false }, + ); + + assert.strictEqual(summary.number, 7); + }), +); + it.effect("routes a hosted reference to another repository through a project on that host", () => Effect.gen(function* () { const seen: Array<{ cwd: string; repository: string; host: string }> = []; diff --git a/apps/server/src/pullRequest/PullRequestService.ts b/apps/server/src/pullRequest/PullRequestService.ts index c17451a0b5a7..ac3ecd8d9d09 100644 --- a/apps/server/src/pullRequest/PullRequestService.ts +++ b/apps/server/src/pullRequest/PullRequestService.ts @@ -76,6 +76,7 @@ import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/source import { AllowGitHubReserve } from "../sourceControl/GitHubCli.ts"; import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts"; import * as PullRequestFilesViewed from "../persistence/PullRequestFilesViewed.ts"; +import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts"; import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts"; import * as SourceControlRateLimit from "../sourceControl/SourceControlRateLimit.ts"; import { @@ -629,6 +630,7 @@ export const make = Effect.gen(function* () { const pullRequestRefreshes = yield* SubscriptionRef.make(0); const registry = yield* PullRequestProviderRegistry; const projections = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; + const repositoryIdentities = yield* RepositoryIdentityResolver.RepositoryIdentityResolver; const sourceControlProviders = yield* SourceControlProviderRegistry.SourceControlProviderRegistry; const rateLimits = yield* SourceControlRateLimit.SourceControlRateLimit; const filesViewedStore = yield* PullRequestFilesViewed.PullRequestFilesViewedRepository; @@ -726,6 +728,18 @@ export const make = Effect.gen(function* () { cause: error, }), ), + Effect.flatMap((projects) => + Effect.forEach( + projects, + (project) => + project.repositoryIdentity != null + ? Effect.succeed(project) + : repositoryIdentities + .resolve(project.workspaceRoot) + .pipe(Effect.map((repositoryIdentity) => ({ ...project, repositoryIdentity }))), + { concurrency: REPOSITORY_CONCURRENCY }, + ), + ), Effect.flatMap((projects) => refineUnknownProjectKinds(projects, filter).pipe( Effect.map((refinedProviders) => ({ refinedProviders, projects })),