diff
May 24, 06:48 PMdiff-artifact/v1{
"artifact": {
"files": [
{
"path": "apps/api/src/infrastructure/database/migrations.ts",
"hunks": [
{
"lines": [
{
"type": "context",
"content": "import { repositoryReviewConfigurationMigration } from \"./migrations/004_repository_review_configuration\";",
"newLineNumber": 4,
"oldLineNumber": 4
},
{
"type": "context",
"content": "import { githubOAuthAndSyncMigration } from \"./migrations/005_github_oauth_and_sync\";",
"newLineNumber": 5,
"oldLineNumber": 5
},
{
"type": "context",
"content": "import { reviewPoliciesMigration } from \"./migrations/006_review_policies\";",
"newLineNumber": 6,
"oldLineNumber": 6
},
{
"type": "addition",
"content": "import { ciFailureArtifactsMigration } from \"./migrations/007_ci_failure_artifacts\";",
"newLineNumber": 7,
"oldLineNumber": null
},
{
"type": "context",
"content": "",
"newLineNumber": 8,
"oldLineNumber": 7
},
{
"type": "context",
"content": "export interface DatabaseQueryResult<Row = unknown> {",
"newLineNumber": 9,
"oldLineNumber": 8
},
{
"type": "context",
"content": " readonly rows: Row[];",
"newLineNumber": 10,
"oldLineNumber": 9
}
],
"newStart": 4,
"oldStart": 4,
"newLineCount": 7,
"oldLineCount": 6,
"sectionHeader": "import { dashboardAuthRetryStateMigration } from \"./migrations/003_dashboard_aut"
},
{
"lines": [
{
"type": "context",
"content": " dashboardAuthRetryStateMigration,",
"newLineNumber": 26,
"oldLineNumber": 25
},
{
"type": "context",
"content": " repositoryReviewConfigurationMigration,",
"newLineNumber": 27,
"oldLineNumber": 26
},
{
"type": "context",
"content": " githubOAuthAndSyncMigration,",
"newLineNumber": 28,
"oldLineNumber": 27
},
{
"type": "deletion",
"content": " reviewPoliciesMigration",
"newLineNumber": null,
"oldLineNumber": 28
},
{
"type": "addition",
"content": " reviewPoliciesMigration,",
"newLineNumber": 29,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ciFailureArtifactsMigration",
"newLineNumber": 30,
"oldLineNumber": null
},
{
"type": "context",
"content": "];",
"newLineNumber": 31,
"oldLineNumber": 29
},
{
"type": "context",
"content": "",
"newLineNumber": 32,
"oldLineNumber": 30
},
{
"type": "context",
"content": "interface AppliedMigrationRow {",
"newLineNumber": 33,
"oldLineNumber": 31
}
],
"newStart": 26,
"oldStart": 25,
"newLineCount": 8,
"oldLineCount": 7,
"sectionHeader": "export const DATABASE_MIGRATIONS: readonly DatabaseMigration[] = ["
}
],
"patch": "@@ -4,6 +4,7 @@ import { dashboardAuthRetryStateMigration } from \"./migrations/003_dashboard_aut\n import { repositoryReviewConfigurationMigration } from \"./migrations/004_repository_review_configuration\";\n import { githubOAuthAndSyncMigration } from \"./migrations/005_github_oauth_and_sync\";\n import { reviewPoliciesMigration } from \"./migrations/006_review_policies\";\n+import { ciFailureArtifactsMigration } from \"./migrations/007_ci_failure_artifacts\";\n \n export interface DatabaseQueryResult<Row = unknown> {\n readonly rows: Row[];\n@@ -25,7 +26,8 @@ export const DATABASE_MIGRATIONS: readonly DatabaseMigration[] = [\n dashboardAuthRetryStateMigration,\n repositoryReviewConfigurationMigration,\n githubOAuthAndSyncMigration,\n- reviewPoliciesMigration\n+ reviewPoliciesMigration,\n+ ciFailureArtifactsMigration\n ];\n \n interface AppliedMigrationRow {",
"status": "modified",
"language": "typescript",
"additions": 3,
"deletions": 1,
"sizeBytes": 2462,
"previousPath": null,
"changedNewLines": [
7,
29,
30
],
"headContentSha256": "1fb9a3edb19a21575d60f8b9922ba04a458546b7759ab57f359c0092801a8abe"
},
{
"path": "apps/api/src/infrastructure/database/migrations/007_ci_failure_artifacts.ts",
"hunks": [
{
"lines": [
{
"type": "addition",
"content": "import type { DatabaseMigration } from \"../migrations\";",
"newLineNumber": 1,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 2,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export const ciFailureArtifactsMigration: DatabaseMigration = {",
"newLineNumber": 3,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: \"007_ci_failure_artifacts\",",
"newLineNumber": 4,
"oldLineNumber": null
},
{
"type": "addition",
"content": " name: \"ci failure explanation artifact type\",",
"newLineNumber": 5,
"oldLineNumber": null
},
{
"type": "addition",
"content": " sql: `",
"newLineNumber": 6,
"oldLineNumber": null
},
{
"type": "addition",
"content": "ALTER TABLE analysis_artifacts",
"newLineNumber": 7,
"oldLineNumber": null
},
{
"type": "addition",
"content": " DROP CONSTRAINT analysis_artifacts_type_check;",
"newLineNumber": 8,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 9,
"oldLineNumber": null
},
{
"type": "addition",
"content": "ALTER TABLE analysis_artifacts",
"newLineNumber": 10,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ADD CONSTRAINT analysis_artifacts_type_check CHECK (",
"newLineNumber": 11,
"oldLineNumber": null
},
{
"type": "addition",
"content": " artifact_type IN ('diff', 'treesitter', 'semgrep', 'context_pack', 'llm_raw', 'ci_log', 'ci_failure_explanation')",
"newLineNumber": 12,
"oldLineNumber": null
},
{
"type": "addition",
"content": " );",
"newLineNumber": 13,
"oldLineNumber": null
},
{
"type": "addition",
"content": "`",
"newLineNumber": 14,
"oldLineNumber": null
},
{
"type": "addition",
"content": "};",
"newLineNumber": 15,
"oldLineNumber": null
}
],
"newStart": 1,
"oldStart": 0,
"newLineCount": 15,
"oldLineCount": 0,
"sectionHeader": ""
}
],
"patch": "@@ -0,0 +1,15 @@\n+import type { DatabaseMigration } from \"../migrations\";\n+\n+export const ciFailureArtifactsMigration: DatabaseMigration = {\n+ id: \"007_ci_failure_artifacts\",\n+ name: \"ci failure explanation artifact type\",\n+ sql: `\n+ALTER TABLE analysis_artifacts\n+ DROP CONSTRAINT analysis_artifacts_type_check;\n+\n+ALTER TABLE analysis_artifacts\n+ ADD CONSTRAINT analysis_artifacts_type_check CHECK (\n+ artifact_type IN ('diff', 'treesitter', 'semgrep', 'context_pack', 'llm_raw', 'ci_log', 'ci_failure_explanation')\n+ );\n+`\n+};",
"status": "added",
"language": "typescript",
"additions": 15,
"deletions": 0,
"sizeBytes": 507,
"previousPath": null,
"changedNewLines": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15
],
"headContentSha256": "18a627c39be6f4e3c1656457245af86e5e64987734306f4a5914a6df8cc3f338"
},
{
"path": "apps/api/src/modules/app.module.ts",
"hunks": [
{
"lines": [
{
"type": "context",
"content": "import { Module } from \"@nestjs/common\";",
"newLineNumber": 1,
"oldLineNumber": 1
},
{
"type": "context",
"content": "import { BillingModule } from \"./billing/billing.module\";",
"newLineNumber": 2,
"oldLineNumber": 2
},
{
"type": "addition",
"content": "import { CiFailuresModule } from \"./ci-failures/ci-failures.module\";",
"newLineNumber": 3,
"oldLineNumber": null
},
{
"type": "context",
"content": "import { GitHubDashboardModule } from \"./github/github.module\";",
"newLineNumber": 4,
"oldLineNumber": 3
},
{
"type": "context",
"content": "import { HealthModule } from \"./health/health.module\";",
"newLineNumber": 5,
"oldLineNumber": 4
},
{
"type": "context",
"content": "import { PullRequestsModule } from \"./pull-requests/pull-requests.module\";",
"newLineNumber": 6,
"oldLineNumber": 5
}
],
"newStart": 1,
"oldStart": 1,
"newLineCount": 6,
"oldLineCount": 5,
"sectionHeader": ""
},
{
"lines": [
{
"type": "context",
"content": " HealthModule,",
"newLineNumber": 15,
"oldLineNumber": 14
},
{
"type": "context",
"content": " GitHubWebhookModule,",
"newLineNumber": 16,
"oldLineNumber": 15
},
{
"type": "context",
"content": " GitHubDashboardModule,",
"newLineNumber": 17,
"oldLineNumber": 16
},
{
"type": "addition",
"content": " CiFailuresModule,",
"newLineNumber": 18,
"oldLineNumber": null
},
{
"type": "context",
"content": " PullRequestsModule,",
"newLineNumber": 19,
"oldLineNumber": 17
},
{
"type": "context",
"content": " RepositoriesModule,",
"newLineNumber": 20,
"oldLineNumber": 18
},
{
"type": "context",
"content": " ReviewRunsModule,",
"newLineNumber": 21,
"oldLineNumber": 19
}
],
"newStart": 15,
"oldStart": 14,
"newLineCount": 7,
"oldLineCount": 6,
"sectionHeader": "import { GitHubWebhookModule } from \"./webhooks/github/github-webhook.module\";"
}
],
"patch": "@@ -1,5 +1,6 @@\n import { Module } from \"@nestjs/common\";\n import { BillingModule } from \"./billing/billing.module\";\n+import { CiFailuresModule } from \"./ci-failures/ci-failures.module\";\n import { GitHubDashboardModule } from \"./github/github.module\";\n import { HealthModule } from \"./health/health.module\";\n import { PullRequestsModule } from \"./pull-requests/pull-requests.module\";\n@@ -14,6 +15,7 @@ import { GitHubWebhookModule } from \"./webhooks/github/github-webhook.module\";\n HealthModule,\n GitHubWebhookModule,\n GitHubDashboardModule,\n+ CiFailuresModule,\n PullRequestsModule,\n RepositoriesModule,\n ReviewRunsModule,",
"status": "modified",
"language": "typescript",
"additions": 2,
"deletions": 0,
"sizeBytes": 970,
"previousPath": null,
"changedNewLines": [
3,
18
],
"headContentSha256": "6ebdb51202d70bcaca6d5a8a9cc21b110e25af7943eefb7f3c315875dd1fae67"
},
{
"path": "apps/api/src/modules/ci-failures/ci-failures.controller.ts",
"hunks": [
{
"lines": [
{
"type": "addition",
"content": "import {",
"newLineNumber": 1,
"oldLineNumber": null
},
{
"type": "addition",
"content": " BadRequestException,",
"newLineNumber": 2,
"oldLineNumber": null
},
{
"type": "addition",
"content": " Controller,",
"newLineNumber": 3,
"oldLineNumber": null
},
{
"type": "addition",
"content": " Get,",
"newLineNumber": 4,
"oldLineNumber": null
},
{
"type": "addition",
"content": " Headers,",
"newLineNumber": 5,
"oldLineNumber": null
},
{
"type": "addition",
"content": " Inject,",
"newLineNumber": 6,
"oldLineNumber": null
},
{
"type": "addition",
"content": " NotFoundException,",
"newLineNumber": 7,
"oldLineNumber": null
},
{
"type": "addition",
"content": " Param,",
"newLineNumber": 8,
"oldLineNumber": null
},
{
"type": "addition",
"content": " Query,",
"newLineNumber": 9,
"oldLineNumber": null
},
{
"type": "addition",
"content": " UnauthorizedException",
"newLineNumber": 10,
"oldLineNumber": null
},
{
"type": "addition",
"content": "} from \"@nestjs/common\";",
"newLineNumber": 11,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import { REVIEW_RUN_STATUSES, type CiFailureDetailResponse, type CiFailureListFilters, type CiFailureListResponse } from \"@firmcode/shared\";",
"newLineNumber": 12,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import {",
"newLineNumber": 13,
"oldLineNumber": null
},
{
"type": "addition",
"content": " DASHBOARD_AUTH_STORE,",
"newLineNumber": 14,
"oldLineNumber": null
},
{
"type": "addition",
"content": " roleHasDashboardCapability,",
"newLineNumber": 15,
"oldLineNumber": null
},
{
"type": "addition",
"content": " type DashboardAuthStore,",
"newLineNumber": 16,
"oldLineNumber": null
},
{
"type": "addition",
"content": " type DashboardMembership",
"newLineNumber": 17,
"oldLineNumber": null
},
{
"type": "addition",
"content": "} from \"../review-runs/dashboard-auth.store\";",
"newLineNumber": 18,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import { CI_FAILURES_STORE, type CiFailuresStore } from \"./ci-failures.store\";",
"newLineNumber": 19,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 20,
"oldLineNumber": null
},
{
"type": "addition",
"content": "@Controller(\"api/ci-failures\")",
"newLineNumber": 21,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export class CiFailuresController {",
"newLineNumber": 22,
"oldLineNumber": null
},
{
"type": "addition",
"content": " constructor(",
"newLineNumber": 23,
"oldLineNumber": null
},
{
"type": "addition",
"content": " @Inject(CI_FAILURES_STORE) private readonly ciFailuresStore: CiFailuresStore,",
"newLineNumber": 24,
"oldLineNumber": null
},
{
"type": "addition",
"content": " @Inject(DASHBOARD_AUTH_STORE) private readonly dashboardAuthStore: DashboardAuthStore",
"newLineNumber": 25,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ) {}",
"newLineNumber": 26,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 27,
"oldLineNumber": null
},
{
"type": "addition",
"content": " @Get()",
"newLineNumber": 28,
"oldLineNumber": null
},
{
"type": "addition",
"content": " async listCiFailures(",
"newLineNumber": 29,
"oldLineNumber": null
},
{
"type": "addition",
"content": " @Query() query: Record<string, string | string[] | undefined>,",
"newLineNumber": 30,
"oldLineNumber": null
},
{
"type": "addition",
"content": " @Headers(\"x-firmcode-workspace-id\") workspaceIdHeader?: string | string[],",
"newLineNumber": 31,
"oldLineNumber": null
},
{
"type": "addition",
"content": " @Headers(\"x-firmcode-user-id\") userIdHeader?: string | string[]",
"newLineNumber": 32,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ): Promise<CiFailureListResponse> {",
"newLineNumber": 33,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const membership = await this.requireMembership(workspaceIdHeader, userIdHeader);",
"newLineNumber": 34,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 35,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return this.ciFailuresStore.listCiFailures({",
"newLineNumber": 36,
"oldLineNumber": null
},
{
"type": "addition",
"content": " workspaceId: membership.workspaceId,",
"newLineNumber": 37,
"oldLineNumber": null
},
{
"type": "addition",
"content": " canAccessRawArtifacts: roleHasDashboardCapability(membership.role, \"access_raw_artifacts\"),",
"newLineNumber": 38,
"oldLineNumber": null
},
{
"type": "addition",
"content": " filters: parseCiFailureListFilters(query)",
"newLineNumber": 39,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 40,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 41,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 42,
"oldLineNumber": null
},
{
"type": "addition",
"content": " @Get(\":id\")",
"newLineNumber": 43,
"oldLineNumber": null
},
{
"type": "addition",
"content": " async getCiFailureDetail(",
"newLineNumber": 44,
"oldLineNumber": null
},
{
"type": "addition",
"content": " @Param(\"id\") id: string,",
"newLineNumber": 45,
"oldLineNumber": null
},
{
"type": "addition",
"content": " @Headers(\"x-firmcode-workspace-id\") workspaceIdHeader?: string | string[],",
"newLineNumber": 46,
"oldLineNumber": null
},
{
"type": "addition",
"content": " @Headers(\"x-firmcode-user-id\") userIdHeader?: string | string[]",
"newLineNumber": 47,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ): Promise<CiFailureDetailResponse> {",
"newLineNumber": 48,
"oldLineNumber": null
},
{
"type": "addition",
"content": " assertCiFailureId(id);",
"newLineNumber": 49,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const membership = await this.requireMembership(workspaceIdHeader, userIdHeader);",
"newLineNumber": 50,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const detail = await this.ciFailuresStore.getCiFailureDetail({",
"newLineNumber": 51,
"oldLineNumber": null
},
{
"type": "addition",
"content": " workspaceId: membership.workspaceId,",
"newLineNumber": 52,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ciFailureId: id,",
"newLineNumber": 53,
"oldLineNumber": null
},
{
"type": "addition",
"content": " canAccessRawArtifacts: roleHasDashboardCapability(membership.role, \"access_raw_artifacts\")",
"newLineNumber": 54,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 55,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 56,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (detail === null) {",
"newLineNumber": 57,
"oldLineNumber": null
},
{
"type": "addition",
"content": " throw new NotFoundException(\"CI failure not found\");",
"newLineNumber": 58,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 59,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 60,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return detail;",
"newLineNumber": 61,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 62,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 63,
"oldLineNumber": null
},
{
"type": "addition",
"content": " private async requireMembership(",
"newLineNumber": 64,
"oldLineNumber": null
},
{
"type": "addition",
"content": " workspaceIdHeader: string | string[] | undefined,",
"newLineNumber": 65,
"oldLineNumber": null
},
{
"type": "addition",
"content": " userIdHeader: string | string[] | undefined",
"newLineNumber": 66,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ): Promise<DashboardMembership> {",
"newLineNumber": 67,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const workspaceId = readSingleValue(workspaceIdHeader) ?? null;",
"newLineNumber": 68,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const clerkUserId = readSingleValue(userIdHeader) ?? null;",
"newLineNumber": 69,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 70,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (workspaceId === null || clerkUserId === null) {",
"newLineNumber": 71,
"oldLineNumber": null
},
{
"type": "addition",
"content": " throw new UnauthorizedException(\"Dashboard authentication is required\");",
"newLineNumber": 72,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 73,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 74,
"oldLineNumber": null
},
{
"type": "addition",
"content": " assertUuid(\"workspace ID\", workspaceId);",
"newLineNumber": 75,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 76,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const membership = await this.dashboardAuthStore.findActiveMembership({ workspaceId, clerkUserId });",
"newLineNumber": 77,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 78,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (membership === null) {",
"newLineNumber": 79,
"oldLineNumber": null
},
{
"type": "addition",
"content": " throw new NotFoundException(\"CI failure not found\");",
"newLineNumber": 80,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 81,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 82,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return membership;",
"newLineNumber": 83,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 84,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 85,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 86,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function parseCiFailureListFilters(query: Record<string, string | string[] | undefined>): CiFailureListFilters {",
"newLineNumber": 87,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const repositoryId = readSingleValue(query.repositoryId);",
"newLineNumber": 88,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const repository = readSingleValue(query.repository);",
"newLineNumber": 89,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const status = readSingleValue(query.status);",
"newLineNumber": 90,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const flaky = readSingleValue(query.flaky);",
"newLineNumber": 91,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const dateFrom = readSingleValue(query.dateFrom);",
"newLineNumber": 92,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const dateTo = readSingleValue(query.dateTo);",
"newLineNumber": 93,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const limit = readSingleValue(query.limit);",
"newLineNumber": 94,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 95,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (repositoryId !== undefined) {",
"newLineNumber": 96,
"oldLineNumber": null
},
{
"type": "addition",
"content": " assertUuid(\"repository ID\", repositoryId);",
"newLineNumber": 97,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 98,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 99,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (status !== undefined && !REVIEW_RUN_STATUSES.includes(status as (typeof REVIEW_RUN_STATUSES)[number])) {",
"newLineNumber": 100,
"oldLineNumber": null
},
{
"type": "addition",
"content": " throw new BadRequestException(\"status must be a supported review run status\");",
"newLineNumber": 101,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 102,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 103,
"oldLineNumber": null
},
{
"type": "addition",
"content": " validateIsoDateFilter(\"dateFrom\", dateFrom);",
"newLineNumber": 104,
"oldLineNumber": null
},
{
"type": "addition",
"content": " validateIsoDateFilter(\"dateTo\", dateTo);",
"newLineNumber": 105,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 106,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (dateFrom !== undefined && dateTo !== undefined && new Date(dateFrom).getTime() > new Date(dateTo).getTime()) {",
"newLineNumber": 107,
"oldLineNumber": null
},
{
"type": "addition",
"content": " throw new BadRequestException(\"dateFrom must be before or equal to dateTo\");",
"newLineNumber": 108,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 109,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 110,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return {",
"newLineNumber": 111,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repositoryId,",
"newLineNumber": 112,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repository,",
"newLineNumber": 113,
"oldLineNumber": null
},
{
"type": "addition",
"content": " status: status as CiFailureListFilters[\"status\"],",
"newLineNumber": 114,
"oldLineNumber": null
},
{
"type": "addition",
"content": " flaky: parseBooleanFilter(flaky),",
"newLineNumber": 115,
"oldLineNumber": null
},
{
"type": "addition",
"content": " dateFrom,",
"newLineNumber": 116,
"oldLineNumber": null
},
{
"type": "addition",
"content": " dateTo,",
"newLineNumber": 117,
"oldLineNumber": null
},
{
"type": "addition",
"content": " limit: parseLimit(limit)",
"newLineNumber": 118,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 119,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 120,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 121,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function parseBooleanFilter(value: string | undefined): boolean | undefined {",
"newLineNumber": 122,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (value === undefined) {",
"newLineNumber": 123,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return undefined;",
"newLineNumber": 124,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 125,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 126,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (value === \"true\") {",
"newLineNumber": 127,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return true;",
"newLineNumber": 128,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 129,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 130,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (value === \"false\") {",
"newLineNumber": 131,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return false;",
"newLineNumber": 132,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 133,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 134,
"oldLineNumber": null
},
{
"type": "addition",
"content": " throw new BadRequestException(\"flaky must be true or false\");",
"newLineNumber": 135,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 136,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 137,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function validateIsoDateFilter(name: string, value: string | undefined): void {",
"newLineNumber": 138,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (value !== undefined && Number.isNaN(Date.parse(value))) {",
"newLineNumber": 139,
"oldLineNumber": null
},
{
"type": "addition",
"content": " throw new BadRequestException(`${name} must be a valid date`);",
"newLineNumber": 140,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 141,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 142,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 143,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function parseLimit(value: string | undefined): number | undefined {",
"newLineNumber": 144,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (value === undefined) {",
"newLineNumber": 145,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return undefined;",
"newLineNumber": 146,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 147,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 148,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const parsed = Number(value);",
"newLineNumber": 149,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 150,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {",
"newLineNumber": 151,
"oldLineNumber": null
},
{
"type": "addition",
"content": " throw new BadRequestException(\"limit must be an integer between 1 and 100\");",
"newLineNumber": 152,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 153,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 154,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return parsed;",
"newLineNumber": 155,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 156,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 157,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function readSingleValue(value: string | string[] | undefined): string | undefined {",
"newLineNumber": 158,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (Array.isArray(value)) {",
"newLineNumber": 159,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return value[0];",
"newLineNumber": 160,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 161,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 162,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return value === \"\" ? undefined : value;",
"newLineNumber": 163,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 164,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 165,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function assertCiFailureId(value: string): void {",
"newLineNumber": 166,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const [artifactId, groupId] = value.split(\":\");",
"newLineNumber": 167,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 168,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (artifactId === undefined || groupId === undefined || groupId.length === 0) {",
"newLineNumber": 169,
"oldLineNumber": null
},
{
"type": "addition",
"content": " throw new BadRequestException(\"CI failure ID must include an artifact and group\");",
"newLineNumber": 170,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 171,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 172,
"oldLineNumber": null
},
{
"type": "addition",
"content": " assertUuid(\"CI failure artifact ID\", artifactId);",
"newLineNumber": 173,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 174,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 175,
"oldLineNumber": null
},
{
"type": "addition",
"content": "const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;",
"newLineNumber": 176,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 177,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function assertUuid(label: string, value: string): void {",
"newLineNumber": 178,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (!UUID_PATTERN.test(value)) {",
"newLineNumber": 179,
"oldLineNumber": null
},
{
"type": "addition",
"content": " throw new BadRequestException(`${label} must be a UUID`);",
"newLineNumber": 180,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 181,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 182,
"oldLineNumber": null
}
],
"newStart": 1,
"oldStart": 0,
"newLineCount": 182,
"oldLineCount": 0,
"sectionHeader": ""
}
],
"patch": "@@ -0,0 +1,182 @@\n+import {\n+ BadRequestException,\n+ Controller,\n+ Get,\n+ Headers,\n+ Inject,\n+ NotFoundException,\n+ Param,\n+ Query,\n+ UnauthorizedException\n+} from \"@nestjs/common\";\n+import { REVIEW_RUN_STATUSES, type CiFailureDetailResponse, type CiFailureListFilters, type CiFailureListResponse } from \"@firmcode/shared\";\n+import {\n+ DASHBOARD_AUTH_STORE,\n+ roleHasDashboardCapability,\n+ type DashboardAuthStore,\n+ type DashboardMembership\n+} from \"../review-runs/dashboard-auth.store\";\n+import { CI_FAILURES_STORE, type CiFailuresStore } from \"./ci-failures.store\";\n+\n+@Controller(\"api/ci-failures\")\n+export class CiFailuresController {\n+ constructor(\n+ @Inject(CI_FAILURES_STORE) private readonly ciFailuresStore: CiFailuresStore,\n+ @Inject(DASHBOARD_AUTH_STORE) private readonly dashboardAuthStore: DashboardAuthStore\n+ ) {}\n+\n+ @Get()\n+ async listCiFailures(\n+ @Query() query: Record<string, string | string[] | undefined>,\n+ @Headers(\"x-firmcode-workspace-id\") workspaceIdHeader?: string | string[],\n+ @Headers(\"x-firmcode-user-id\") userIdHeader?: string | string[]\n+ ): Promise<CiFailureListResponse> {\n+ const membership = await this.requireMembership(workspaceIdHeader, userIdHeader);\n+\n+ return this.ciFailuresStore.listCiFailures({\n+ workspaceId: membership.workspaceId,\n+ canAccessRawArtifacts: roleHasDashboardCapability(membership.role, \"access_raw_artifacts\"),\n+ filters: parseCiFailureListFilters(query)\n+ });\n+ }\n+\n+ @Get(\":id\")\n+ async getCiFailureDetail(\n+ @Param(\"id\") id: string,\n+ @Headers(\"x-firmcode-workspace-id\") workspaceIdHeader?: string | string[],\n+ @Headers(\"x-firmcode-user-id\") userIdHeader?: string | string[]\n+ ): Promise<CiFailureDetailResponse> {\n+ assertCiFailureId(id);\n+ const membership = await this.requireMembership(workspaceIdHeader, userIdHeader);\n+ const detail = await this.ciFailuresStore.getCiFailureDetail({\n+ workspaceId: membership.workspaceId,\n+ ciFailureId: id,\n+ canAccessRawArtifacts: roleHasDashboardCapability(membership.role, \"access_raw_artifacts\")\n+ });\n+\n+ if (detail === null) {\n+ throw new NotFoundException(\"CI failure not found\");\n+ }\n+\n+ return detail;\n+ }\n+\n+ private async requireMembership(\n+ workspaceIdHeader: string | string[] | undefined,\n+ userIdHeader: string | string[] | undefined\n+ ): Promise<DashboardMembership> {\n+ const workspaceId = readSingleValue(workspaceIdHeader) ?? null;\n+ const clerkUserId = readSingleValue(userIdHeader) ?? null;\n+\n+ if (workspaceId === null || clerkUserId === null) {\n+ throw new UnauthorizedException(\"Dashboard authentication is required\");\n+ }\n+\n+ assertUuid(\"workspace ID\", workspaceId);\n+\n+ const membership = await this.dashboardAuthStore.findActiveMembership({ workspaceId, clerkUserId });\n+\n+ if (membership === null) {\n+ throw new NotFoundException(\"CI failure not found\");\n+ }\n+\n+ return membership;\n+ }\n+}\n+\n+function parseCiFailureListFilters(query: Record<string, string | string[] | undefined>): CiFailureListFilters {\n+ const repositoryId = readSingleValue(query.repositoryId);\n+ const repository = readSingleValue(query.repository);\n+ const status = readSingleValue(query.status);\n+ const flaky = readSingleValue(query.flaky);\n+ const dateFrom = readSingleValue(query.dateFrom);\n+ const dateTo = readSingleValue(query.dateTo);\n+ const limit = readSingleValue(query.limit);\n+\n+ if (repositoryId !== undefined) {\n+ assertUuid(\"repository ID\", repositoryId);\n+ }\n+\n+ if (status !== undefined && !REVIEW_RUN_STATUSES.includes(status as (typeof REVIEW_RUN_STATUSES)[number])) {\n+ throw new BadRequestException(\"status must be a supported review run status\");\n+ }\n+\n+ validateIsoDateFilter(\"dateFrom\", dateFrom);\n+ validateIsoDateFilter(\"dateTo\", dateTo);\n+\n+ if (dateFrom !== undefined && dateTo !== undefined && new Date(dateFrom).getTime() > new Date(dateTo).getTime()) {\n+ throw new BadRequestException(\"dateFrom must be before or equal to dateTo\");\n+ }\n+\n+ return {\n+ repositoryId,\n+ repository,\n+ status: status as CiFailureListFilters[\"status\"],\n+ flaky: parseBooleanFilter(flaky),\n+ dateFrom,\n+ dateTo,\n+ limit: parseLimit(limit)\n+ };\n+}\n+\n+function parseBooleanFilter(value: string | undefined): boolean | undefined {\n+ if (value === undefined) {\n+ return undefined;\n+ }\n+\n+ if (value === \"true\") {\n+ return true;\n+ }\n+\n+ if (value === \"false\") {\n+ return false;\n+ }\n+\n+ throw new BadRequestException(\"flaky must be true or false\");\n+}\n+\n+function validateIsoDateFilter(name: string, value: string | undefined): void {\n+ if (value !== undefined && Number.isNaN(Date.parse(value))) {\n+ throw new BadRequestException(`${name} must be a valid date`);\n+ }\n+}\n+\n+function parseLimit(value: string | undefined): number | undefined {\n+ if (value === undefined) {\n+ return undefined;\n+ }\n+\n+ const parsed = Number(value);\n+\n+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {\n+ throw new BadRequestException(\"limit must be an integer between 1 and 100\");\n+ }\n+\n+ return parsed;\n+}\n+\n+function readSingleValue(value: string | string[] | undefined): string | undefined {\n+ if (Array.isArray(value)) {\n+ return value[0];\n+ }\n+\n+ return value === \"\" ? undefined : value;\n+}\n+\n+function assertCiFailureId(value: string): void {\n+ const [artifactId, groupId] = value.split(\":\");\n+\n+ if (artifactId === undefined || groupId === undefined || groupId.length === 0) {\n+ throw new BadRequestException(\"CI failure ID must include an artifact and group\");\n+ }\n+\n+ assertUuid(\"CI failure artifact ID\", artifactId);\n+}\n+\n+const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n+\n+function assertUuid(label: string, value: string): void {\n+ if (!UUID_PATTERN.test(value)) {\n+ throw new BadRequestException(`${label} must be a UUID`);\n+ }\n+}",
"status": "added",
"language": "typescript",
"additions": 182,
"deletions": 0,
"sizeBytes": 5760,
"previousPath": null,
"changedNewLines": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
96,
97,
98,
99,
100,
101,
102,
103,
104,
105,
106,
107,
108,
109,
110,
111,
112,
113,
114,
115,
116,
117,
118,
119,
120,
121,
122,
123,
124,
125,
126,
127,
128,
129,
130,
131,
132,
133,
134,
135,
136,
137,
138,
139,
140,
141,
142,
143,
144,
145,
146,
147,
148,
149,
150,
151,
152,
153,
154,
155,
156,
157,
158,
159,
160,
161,
162,
163,
164,
165,
166,
167,
168,
169,
170,
171,
172,
173,
174,
175,
176,
177,
178,
179,
180,
181,
182
],
"headContentSha256": "79190d448275d02210515fdabb68f135e643b948702c2eb34bfbbc057f94e3f8"
},
{
"path": "apps/api/src/modules/ci-failures/ci-failures.module.ts",
"hunks": [
{
"lines": [
{
"type": "addition",
"content": "import { Module } from \"@nestjs/common\";",
"newLineNumber": 1,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import { Pool } from \"pg\";",
"newLineNumber": 2,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import type { ApiRuntimeConfig } from \"@firmcode/shared\";",
"newLineNumber": 3,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import { API_RUNTIME_CONFIG, apiRuntimeConfigProvider } from \"../../config/api-config.provider\";",
"newLineNumber": 4,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import {",
"newLineNumber": 5,
"oldLineNumber": null
},
{
"type": "addition",
"content": " DASHBOARD_AUTH_STORE,",
"newLineNumber": 6,
"oldLineNumber": null
},
{
"type": "addition",
"content": " EmptyDashboardAuthStore,",
"newLineNumber": 7,
"oldLineNumber": null
},
{
"type": "addition",
"content": " PostgresDashboardAuthStore",
"newLineNumber": 8,
"oldLineNumber": null
},
{
"type": "addition",
"content": "} from \"../review-runs/dashboard-auth.store\";",
"newLineNumber": 9,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import { CiFailuresController } from \"./ci-failures.controller\";",
"newLineNumber": 10,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import { CI_FAILURES_STORE, EmptyCiFailuresStore, PostgresCiFailuresStore } from \"./ci-failures.store\";",
"newLineNumber": 11,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 12,
"oldLineNumber": null
},
{
"type": "addition",
"content": "@Module({",
"newLineNumber": 13,
"oldLineNumber": null
},
{
"type": "addition",
"content": " controllers: [CiFailuresController],",
"newLineNumber": 14,
"oldLineNumber": null
},
{
"type": "addition",
"content": " providers: [",
"newLineNumber": 15,
"oldLineNumber": null
},
{
"type": "addition",
"content": " apiRuntimeConfigProvider,",
"newLineNumber": 16,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 17,
"oldLineNumber": null
},
{
"type": "addition",
"content": " provide: CI_FAILURES_STORE,",
"newLineNumber": 18,
"oldLineNumber": null
},
{
"type": "addition",
"content": " useFactory: (config: ApiRuntimeConfig) => {",
"newLineNumber": 19,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (config.nodeEnv === \"test\") {",
"newLineNumber": 20,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return new EmptyCiFailuresStore();",
"newLineNumber": 21,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 22,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 23,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return new PostgresCiFailuresStore(",
"newLineNumber": 24,
"oldLineNumber": null
},
{
"type": "addition",
"content": " new Pool({",
"newLineNumber": 25,
"oldLineNumber": null
},
{
"type": "addition",
"content": " connectionString: config.database.url,",
"newLineNumber": 26,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ssl: config.database.ssl ? { rejectUnauthorized: false } : false",
"newLineNumber": 27,
"oldLineNumber": null
},
{
"type": "addition",
"content": " })",
"newLineNumber": 28,
"oldLineNumber": null
},
{
"type": "addition",
"content": " );",
"newLineNumber": 29,
"oldLineNumber": null
},
{
"type": "addition",
"content": " },",
"newLineNumber": 30,
"oldLineNumber": null
},
{
"type": "addition",
"content": " inject: [API_RUNTIME_CONFIG]",
"newLineNumber": 31,
"oldLineNumber": null
},
{
"type": "addition",
"content": " },",
"newLineNumber": 32,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 33,
"oldLineNumber": null
},
{
"type": "addition",
"content": " provide: DASHBOARD_AUTH_STORE,",
"newLineNumber": 34,
"oldLineNumber": null
},
{
"type": "addition",
"content": " useFactory: (config: ApiRuntimeConfig) => {",
"newLineNumber": 35,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (config.nodeEnv === \"test\") {",
"newLineNumber": 36,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return new EmptyDashboardAuthStore();",
"newLineNumber": 37,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 38,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 39,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return new PostgresDashboardAuthStore(",
"newLineNumber": 40,
"oldLineNumber": null
},
{
"type": "addition",
"content": " new Pool({",
"newLineNumber": 41,
"oldLineNumber": null
},
{
"type": "addition",
"content": " connectionString: config.database.url,",
"newLineNumber": 42,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ssl: config.database.ssl ? { rejectUnauthorized: false } : false",
"newLineNumber": 43,
"oldLineNumber": null
},
{
"type": "addition",
"content": " })",
"newLineNumber": 44,
"oldLineNumber": null
},
{
"type": "addition",
"content": " );",
"newLineNumber": 45,
"oldLineNumber": null
},
{
"type": "addition",
"content": " },",
"newLineNumber": 46,
"oldLineNumber": null
},
{
"type": "addition",
"content": " inject: [API_RUNTIME_CONFIG]",
"newLineNumber": 47,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 48,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ]",
"newLineNumber": 49,
"oldLineNumber": null
},
{
"type": "addition",
"content": "})",
"newLineNumber": 50,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export class CiFailuresModule {}",
"newLineNumber": 51,
"oldLineNumber": null
}
],
"newStart": 1,
"oldStart": 0,
"newLineCount": 51,
"oldLineCount": 0,
"sectionHeader": ""
}
],
"patch": "@@ -0,0 +1,51 @@\n+import { Module } from \"@nestjs/common\";\n+import { Pool } from \"pg\";\n+import type { ApiRuntimeConfig } from \"@firmcode/shared\";\n+import { API_RUNTIME_CONFIG, apiRuntimeConfigProvider } from \"../../config/api-config.provider\";\n+import {\n+ DASHBOARD_AUTH_STORE,\n+ EmptyDashboardAuthStore,\n+ PostgresDashboardAuthStore\n+} from \"../review-runs/dashboard-auth.store\";\n+import { CiFailuresController } from \"./ci-failures.controller\";\n+import { CI_FAILURES_STORE, EmptyCiFailuresStore, PostgresCiFailuresStore } from \"./ci-failures.store\";\n+\n+@Module({\n+ controllers: [CiFailuresController],\n+ providers: [\n+ apiRuntimeConfigProvider,\n+ {\n+ provide: CI_FAILURES_STORE,\n+ useFactory: (config: ApiRuntimeConfig) => {\n+ if (config.nodeEnv === \"test\") {\n+ return new EmptyCiFailuresStore();\n+ }\n+\n+ return new PostgresCiFailuresStore(\n+ new Pool({\n+ connectionString: config.database.url,\n+ ssl: config.database.ssl ? { rejectUnauthorized: false } : false\n+ })\n+ );\n+ },\n+ inject: [API_RUNTIME_CONFIG]\n+ },\n+ {\n+ provide: DASHBOARD_AUTH_STORE,\n+ useFactory: (config: ApiRuntimeConfig) => {\n+ if (config.nodeEnv === \"test\") {\n+ return new EmptyDashboardAuthStore();\n+ }\n+\n+ return new PostgresDashboardAuthStore(\n+ new Pool({\n+ connectionString: config.database.url,\n+ ssl: config.database.ssl ? { rejectUnauthorized: false } : false\n+ })\n+ );\n+ },\n+ inject: [API_RUNTIME_CONFIG]\n+ }\n+ ]\n+})\n+export class CiFailuresModule {}",
"status": "added",
"language": "typescript",
"additions": 51,
"deletions": 0,
"sizeBytes": 1580,
"previousPath": null,
"changedNewLines": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51
],
"headContentSha256": "24e062dee6867300dbff14d8312cc055859030fa2cb1473077a16c8895e3be75"
},
{
"path": "apps/api/src/modules/ci-failures/ci-failures.store.ts",
"hunks": [
{
"lines": [
{
"type": "addition",
"content": "import type {",
"newLineNumber": 1,
"oldLineNumber": null
},
{
"type": "addition",
"content": " CiFailureDetailResponse,",
"newLineNumber": 2,
"oldLineNumber": null
},
{
"type": "addition",
"content": " CiFailureFailedJob,",
"newLineNumber": 3,
"oldLineNumber": null
},
{
"type": "addition",
"content": " CiFailureListFilters,",
"newLineNumber": 4,
"oldLineNumber": null
},
{
"type": "addition",
"content": " CiFailureListItem,",
"newLineNumber": 5,
"oldLineNumber": null
},
{
"type": "addition",
"content": " CiFailureListResponse,",
"newLineNumber": 6,
"oldLineNumber": null
},
{
"type": "addition",
"content": " CiFailureRelatedReviewRun,",
"newLineNumber": 7,
"oldLineNumber": null
},
{
"type": "addition",
"content": " CiFailureSuggestedFix,",
"newLineNumber": 8,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ReviewRunArtifact,",
"newLineNumber": 9,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ReviewRunArtifactType,",
"newLineNumber": 10,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ReviewRunLogExcerpt,",
"newLineNumber": 11,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ReviewRunStatus",
"newLineNumber": 12,
"oldLineNumber": null
},
{
"type": "addition",
"content": "} from \"@firmcode/shared\";",
"newLineNumber": 13,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import type { DatabaseExecutor } from \"../../infrastructure/database/migrations\";",
"newLineNumber": 14,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 15,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export const CI_FAILURES_STORE = Symbol(\"CI_FAILURES_STORE\");",
"newLineNumber": 16,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 17,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export interface CiFailuresStore {",
"newLineNumber": 18,
"oldLineNumber": null
},
{
"type": "addition",
"content": " listCiFailures(input: CiFailureListInput): Promise<CiFailureListResponse>;",
"newLineNumber": 19,
"oldLineNumber": null
},
{
"type": "addition",
"content": " getCiFailureDetail(input: CiFailureDetailLookup): Promise<CiFailureDetailResponse | null>;",
"newLineNumber": 20,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 21,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 22,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export interface CiFailureListInput {",
"newLineNumber": 23,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly workspaceId: string;",
"newLineNumber": 24,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly canAccessRawArtifacts: boolean;",
"newLineNumber": 25,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly filters: CiFailureListFilters;",
"newLineNumber": 26,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 27,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 28,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export interface CiFailureDetailLookup {",
"newLineNumber": 29,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly workspaceId: string;",
"newLineNumber": 30,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly ciFailureId: string;",
"newLineNumber": 31,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly canAccessRawArtifacts: boolean;",
"newLineNumber": 32,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 33,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 34,
"oldLineNumber": null
},
{
"type": "addition",
"content": "interface CiFailureArtifactRow {",
"newLineNumber": 35,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly review_run_id: string;",
"newLineNumber": 36,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly repository_id: string;",
"newLineNumber": 37,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly pull_request_id: string;",
"newLineNumber": 38,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly repository_full_name: string;",
"newLineNumber": 39,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly pull_request_number: number;",
"newLineNumber": 40,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly pull_request_title: string;",
"newLineNumber": 41,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly status: ReviewRunStatus;",
"newLineNumber": 42,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly review_run_created_at: Date | string | null;",
"newLineNumber": 43,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly artifact_id: string;",
"newLineNumber": 44,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly artifact_type: ReviewRunArtifactType;",
"newLineNumber": 45,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly storage_key: string;",
"newLineNumber": 46,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly metadata_json: unknown;",
"newLineNumber": 47,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly artifact_created_at: Date | string | null;",
"newLineNumber": 48,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 49,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 50,
"oldLineNumber": null
},
{
"type": "addition",
"content": "interface GroupedRun {",
"newLineNumber": 51,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly reviewRunId: string;",
"newLineNumber": 52,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly repositoryId: string;",
"newLineNumber": 53,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly pullRequestId: string;",
"newLineNumber": 54,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly repositoryFullName: string;",
"newLineNumber": 55,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly pullRequestNumber: number;",
"newLineNumber": 56,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly pullRequestTitle: string;",
"newLineNumber": 57,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly status: ReviewRunStatus;",
"newLineNumber": 58,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly reviewRunCreatedAt: string;",
"newLineNumber": 59,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly artifacts: CiFailureArtifactRow[];",
"newLineNumber": 60,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 61,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 62,
"oldLineNumber": null
},
{
"type": "addition",
"content": "interface CiFailureRecord {",
"newLineNumber": 63,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly run: GroupedRun;",
"newLineNumber": 64,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly explanationArtifact: CiFailureArtifactRow;",
"newLineNumber": 65,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly explanation: Record<string, unknown>;",
"newLineNumber": 66,
"oldLineNumber": null
},
{
"type": "addition",
"content": " readonly primaryGroup: Record<string, unknown>;",
"newLineNumber": 67,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 68,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 69,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export class EmptyCiFailuresStore implements CiFailuresStore {",
"newLineNumber": 70,
"oldLineNumber": null
},
{
"type": "addition",
"content": " async listCiFailures(input: CiFailureListInput): Promise<CiFailureListResponse> {",
"newLineNumber": 71,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return {",
"newLineNumber": 72,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ciFailures: [],",
"newLineNumber": 73,
"oldLineNumber": null
},
{
"type": "addition",
"content": " filters: input.filters,",
"newLineNumber": 74,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pagination: {",
"newLineNumber": 75,
"oldLineNumber": null
},
{
"type": "addition",
"content": " limit: input.filters.limit ?? DEFAULT_CI_FAILURE_LIMIT,",
"newLineNumber": 76,
"oldLineNumber": null
},
{
"type": "addition",
"content": " returned: 0",
"newLineNumber": 77,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 78,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 79,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 80,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 81,
"oldLineNumber": null
},
{
"type": "addition",
"content": " async getCiFailureDetail(_input: CiFailureDetailLookup): Promise<CiFailureDetailResponse | null> {",
"newLineNumber": 82,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return null;",
"newLineNumber": 83,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 84,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 85,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 86,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export class PostgresCiFailuresStore implements CiFailuresStore {",
"newLineNumber": 87,
"oldLineNumber": null
},
{
"type": "addition",
"content": " constructor(private readonly database: DatabaseExecutor) {}",
"newLineNumber": 88,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 89,
"oldLineNumber": null
},
{
"type": "addition",
"content": " async listCiFailures(input: CiFailureListInput): Promise<CiFailureListResponse> {",
"newLineNumber": 90,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const limit = input.filters.limit ?? DEFAULT_CI_FAILURE_LIMIT;",
"newLineNumber": 91,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const rows = await this.loadCandidateRows(input.workspaceId, input.filters);",
"newLineNumber": 92,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const ciFailures = buildCiFailureRecords(rows)",
"newLineNumber": 93,
"oldLineNumber": null
},
{
"type": "addition",
"content": " .map((record) => toCiFailureListItem(record))",
"newLineNumber": 94,
"oldLineNumber": null
},
{
"type": "addition",
"content": " .filter((item) => matchesPostQueryFilters(item, input.filters))",
"newLineNumber": 95,
"oldLineNumber": null
},
{
"type": "addition",
"content": " .slice(0, limit);",
"newLineNumber": 96,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 97,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return {",
"newLineNumber": 98,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ciFailures,",
"newLineNumber": 99,
"oldLineNumber": null
},
{
"type": "addition",
"content": " filters: input.filters,",
"newLineNumber": 100,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pagination: {",
"newLineNumber": 101,
"oldLineNumber": null
},
{
"type": "addition",
"content": " limit,",
"newLineNumber": 102,
"oldLineNumber": null
},
{
"type": "addition",
"content": " returned: ciFailures.length",
"newLineNumber": 103,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 104,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 105,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 106,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 107,
"oldLineNumber": null
},
{
"type": "addition",
"content": " async getCiFailureDetail(input: CiFailureDetailLookup): Promise<CiFailureDetailResponse | null> {",
"newLineNumber": 108,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const rows = await this.loadCandidateRows(input.workspaceId, {});",
"newLineNumber": 109,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const record = buildCiFailureRecords(rows).find((candidate) => toCiFailureId(candidate) === input.ciFailureId);",
"newLineNumber": 110,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 111,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (record === undefined) {",
"newLineNumber": 112,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return null;",
"newLineNumber": 113,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 114,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 115,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return toCiFailureDetail(record, input.canAccessRawArtifacts);",
"newLineNumber": 116,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 117,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 118,
"oldLineNumber": null
},
{
"type": "addition",
"content": " private async loadCandidateRows(workspaceId: string, filters: CiFailureListFilters): Promise<CiFailureArtifactRow[]> {",
"newLineNumber": 119,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const { whereSql, values } = buildCiFailureWhereClause(workspaceId, filters);",
"newLineNumber": 120,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const result = await this.database.query<CiFailureArtifactRow>(",
"newLineNumber": 121,
"oldLineNumber": null
},
{
"type": "addition",
"content": " `",
"newLineNumber": 122,
"oldLineNumber": null
},
{
"type": "addition",
"content": "SELECT",
"newLineNumber": 123,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rr.id AS review_run_id,",
"newLineNumber": 124,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rr.repository_id,",
"newLineNumber": 125,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rr.pull_request_id,",
"newLineNumber": 126,
"oldLineNumber": null
},
{
"type": "addition",
"content": " r.full_name AS repository_full_name,",
"newLineNumber": 127,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pr.number AS pull_request_number,",
"newLineNumber": 128,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pr.title AS pull_request_title,",
"newLineNumber": 129,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rr.status,",
"newLineNumber": 130,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rr.created_at AS review_run_created_at,",
"newLineNumber": 131,
"oldLineNumber": null
},
{
"type": "addition",
"content": " aa.id AS artifact_id,",
"newLineNumber": 132,
"oldLineNumber": null
},
{
"type": "addition",
"content": " aa.artifact_type,",
"newLineNumber": 133,
"oldLineNumber": null
},
{
"type": "addition",
"content": " aa.storage_key,",
"newLineNumber": 134,
"oldLineNumber": null
},
{
"type": "addition",
"content": " aa.metadata_json,",
"newLineNumber": 135,
"oldLineNumber": null
},
{
"type": "addition",
"content": " aa.created_at AS artifact_created_at",
"newLineNumber": 136,
"oldLineNumber": null
},
{
"type": "addition",
"content": "FROM review_runs rr",
"newLineNumber": 137,
"oldLineNumber": null
},
{
"type": "addition",
"content": "JOIN repositories r ON r.id = rr.repository_id",
"newLineNumber": 138,
"oldLineNumber": null
},
{
"type": "addition",
"content": "JOIN github_installations gi ON gi.id = r.installation_id",
"newLineNumber": 139,
"oldLineNumber": null
},
{
"type": "addition",
"content": "JOIN pull_requests pr ON pr.id = rr.pull_request_id",
"newLineNumber": 140,
"oldLineNumber": null
},
{
"type": "addition",
"content": "JOIN analysis_artifacts aa ON aa.review_run_id = rr.id",
"newLineNumber": 141,
"oldLineNumber": null
},
{
"type": "addition",
"content": "${whereSql}",
"newLineNumber": 142,
"oldLineNumber": null
},
{
"type": "addition",
"content": "ORDER BY rr.created_at DESC, aa.created_at ASC, aa.artifact_type ASC",
"newLineNumber": 143,
"oldLineNumber": null
},
{
"type": "addition",
"content": "LIMIT 1000",
"newLineNumber": 144,
"oldLineNumber": null
},
{
"type": "addition",
"content": "`,",
"newLineNumber": 145,
"oldLineNumber": null
},
{
"type": "addition",
"content": " values",
"newLineNumber": 146,
"oldLineNumber": null
},
{
"type": "addition",
"content": " );",
"newLineNumber": 147,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 148,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return result.rows;",
"newLineNumber": 149,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 150,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 151,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 152,
"oldLineNumber": null
},
{
"type": "addition",
"content": "const DEFAULT_CI_FAILURE_LIMIT = 50;",
"newLineNumber": 153,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 154,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function buildCiFailureWhereClause(",
"newLineNumber": 155,
"oldLineNumber": null
},
{
"type": "addition",
"content": " workspaceId: string,",
"newLineNumber": 156,
"oldLineNumber": null
},
{
"type": "addition",
"content": " filters: CiFailureListFilters",
"newLineNumber": 157,
"oldLineNumber": null
},
{
"type": "addition",
"content": "): { whereSql: string; values: unknown[] } {",
"newLineNumber": 158,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const conditions = [\"gi.workspace_id = $1\"];",
"newLineNumber": 159,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const values: unknown[] = [workspaceId];",
"newLineNumber": 160,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 161,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (filters.repositoryId !== undefined) {",
"newLineNumber": 162,
"oldLineNumber": null
},
{
"type": "addition",
"content": " values.push(filters.repositoryId);",
"newLineNumber": 163,
"oldLineNumber": null
},
{
"type": "addition",
"content": " conditions.push(`rr.repository_id = $${values.length}`);",
"newLineNumber": 164,
"oldLineNumber": null
},
{
"type": "addition",
"content": " } else if (filters.repository !== undefined) {",
"newLineNumber": 165,
"oldLineNumber": null
},
{
"type": "addition",
"content": " values.push(filters.repository);",
"newLineNumber": 166,
"oldLineNumber": null
},
{
"type": "addition",
"content": " conditions.push(`lower(r.full_name) = lower($${values.length})`);",
"newLineNumber": 167,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 168,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 169,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (filters.status !== undefined) {",
"newLineNumber": 170,
"oldLineNumber": null
},
{
"type": "addition",
"content": " values.push(filters.status);",
"newLineNumber": 171,
"oldLineNumber": null
},
{
"type": "addition",
"content": " conditions.push(`rr.status = $${values.length}`);",
"newLineNumber": 172,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 173,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 174,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (filters.dateFrom !== undefined) {",
"newLineNumber": 175,
"oldLineNumber": null
},
{
"type": "addition",
"content": " values.push(filters.dateFrom);",
"newLineNumber": 176,
"oldLineNumber": null
},
{
"type": "addition",
"content": " conditions.push(`rr.created_at >= $${values.length}`);",
"newLineNumber": 177,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 178,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 179,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (filters.dateTo !== undefined) {",
"newLineNumber": 180,
"oldLineNumber": null
},
{
"type": "addition",
"content": " values.push(filters.dateTo);",
"newLineNumber": 181,
"oldLineNumber": null
},
{
"type": "addition",
"content": " conditions.push(`rr.created_at <= $${values.length}`);",
"newLineNumber": 182,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 183,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 184,
"oldLineNumber": null
},
{
"type": "addition",
"content": " conditions.push(\"aa.artifact_type IN ('ci_failure_explanation', 'ci_log', 'llm_raw')\");",
"newLineNumber": 185,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 186,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return {",
"newLineNumber": 187,
"oldLineNumber": null
},
{
"type": "addition",
"content": " whereSql: `WHERE ${conditions.join(\" AND \")}`,",
"newLineNumber": 188,
"oldLineNumber": null
},
{
"type": "addition",
"content": " values",
"newLineNumber": 189,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 190,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 191,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 192,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function buildCiFailureRecords(rows: CiFailureArtifactRow[]): CiFailureRecord[] {",
"newLineNumber": 193,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const grouped = new Map<string, GroupedRun>();",
"newLineNumber": 194,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 195,
"oldLineNumber": null
},
{
"type": "addition",
"content": " for (const row of rows) {",
"newLineNumber": 196,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const existing = grouped.get(row.review_run_id);",
"newLineNumber": 197,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 198,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (existing !== undefined) {",
"newLineNumber": 199,
"oldLineNumber": null
},
{
"type": "addition",
"content": " existing.artifacts.push(row);",
"newLineNumber": 200,
"oldLineNumber": null
},
{
"type": "addition",
"content": " continue;",
"newLineNumber": 201,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 202,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 203,
"oldLineNumber": null
},
{
"type": "addition",
"content": " grouped.set(row.review_run_id, {",
"newLineNumber": 204,
"oldLineNumber": null
},
{
"type": "addition",
"content": " reviewRunId: row.review_run_id,",
"newLineNumber": 205,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repositoryId: row.repository_id,",
"newLineNumber": 206,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pullRequestId: row.pull_request_id,",
"newLineNumber": 207,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repositoryFullName: row.repository_full_name,",
"newLineNumber": 208,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pullRequestNumber: row.pull_request_number,",
"newLineNumber": 209,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pullRequestTitle: row.pull_request_title,",
"newLineNumber": 210,
"oldLineNumber": null
},
{
"type": "addition",
"content": " status: row.status,",
"newLineNumber": 211,
"oldLineNumber": null
},
{
"type": "addition",
"content": " reviewRunCreatedAt: toRequiredIsoString(row.review_run_created_at),",
"newLineNumber": 212,
"oldLineNumber": null
},
{
"type": "addition",
"content": " artifacts: [row]",
"newLineNumber": 213,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 214,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 215,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 216,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return [...grouped.values()]",
"newLineNumber": 217,
"oldLineNumber": null
},
{
"type": "addition",
"content": " .map(toCiFailureRecord)",
"newLineNumber": 218,
"oldLineNumber": null
},
{
"type": "addition",
"content": " .filter((record): record is CiFailureRecord => record !== null)",
"newLineNumber": 219,
"oldLineNumber": null
},
{
"type": "addition",
"content": " .sort((left, right) => right.explanationArtifact.artifact_created_at!.toString().localeCompare(left.explanationArtifact.artifact_created_at!.toString()));",
"newLineNumber": 220,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 221,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 222,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function toCiFailureRecord(run: GroupedRun): CiFailureRecord | null {",
"newLineNumber": 223,
"oldLineNumber": null
},
{
"type": "addition",
"content": " for (const artifact of run.artifacts) {",
"newLineNumber": 224,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const artifactBody = unwrapArtifact(artifact.metadata_json);",
"newLineNumber": 225,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 226,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (artifactBody?.schemaVersion === \"ci-failure-explanation/v1\") {",
"newLineNumber": 227,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const groups = readGroups(artifactBody);",
"newLineNumber": 228,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const primaryGroup = groups[0];",
"newLineNumber": 229,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 230,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (primaryGroup === undefined) {",
"newLineNumber": 231,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return null;",
"newLineNumber": 232,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 233,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 234,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return {",
"newLineNumber": 235,
"oldLineNumber": null
},
{
"type": "addition",
"content": " run,",
"newLineNumber": 236,
"oldLineNumber": null
},
{
"type": "addition",
"content": " explanationArtifact: artifact,",
"newLineNumber": 237,
"oldLineNumber": null
},
{
"type": "addition",
"content": " explanation: artifactBody,",
"newLineNumber": 238,
"oldLineNumber": null
},
{
"type": "addition",
"content": " primaryGroup",
"newLineNumber": 239,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 240,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 241,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 242,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 243,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return null;",
"newLineNumber": 244,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 245,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 246,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function toCiFailureListItem(record: CiFailureRecord): CiFailureListItem {",
"newLineNumber": 247,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return {",
"newLineNumber": 248,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: toCiFailureId(record),",
"newLineNumber": 249,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repositoryId: record.run.repositoryId,",
"newLineNumber": 250,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repositoryFullName: record.run.repositoryFullName,",
"newLineNumber": 251,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pullRequestId: record.run.pullRequestId,",
"newLineNumber": 252,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pullRequestNumber: record.run.pullRequestNumber,",
"newLineNumber": 253,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pullRequestTitle: record.run.pullRequestTitle,",
"newLineNumber": 254,
"oldLineNumber": null
},
{
"type": "addition",
"content": " reviewRunId: record.run.reviewRunId,",
"newLineNumber": 255,
"oldLineNumber": null
},
{
"type": "addition",
"content": " failedJob: toFailedJob(record.primaryGroup, record.run.artifacts),",
"newLineNumber": 256,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rootCauseSummary: readString(record.primaryGroup.rootCauseSummary) ?? readString(record.explanation.summary) ?? \"CI failed without a root cause summary.\",",
"newLineNumber": 257,
"oldLineNumber": null
},
{
"type": "addition",
"content": " flakySuspected: readGroups(record.explanation).some((group) => group.flaky === true),",
"newLineNumber": 258,
"oldLineNumber": null
},
{
"type": "addition",
"content": " suggestedFix: readStringArray(record.primaryGroup.suggestedFixes)[0] ?? null,",
"newLineNumber": 259,
"oldLineNumber": null
},
{
"type": "addition",
"content": " status: record.run.status,",
"newLineNumber": 260,
"oldLineNumber": null
},
{
"type": "addition",
"content": " createdAt: toRequiredIsoString(record.explanationArtifact.artifact_created_at)",
"newLineNumber": 261,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 262,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 263,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 264,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function toCiFailureDetail(record: CiFailureRecord, canAccessRawArtifacts: boolean): CiFailureDetailResponse {",
"newLineNumber": 265,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const listItem = toCiFailureListItem(record);",
"newLineNumber": 266,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const groups = readGroups(record.explanation);",
"newLineNumber": 267,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const suggestedFixes = uniqueStrings(groups.flatMap((group) => readStringArray(group.suggestedFixes))).map<CiFailureSuggestedFix>(",
"newLineNumber": 268,
"oldLineNumber": null
},
{
"type": "addition",
"content": " (text, index) => ({",
"newLineNumber": 269,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: `${listItem.id}:fix:${index + 1}`,",
"newLineNumber": 270,
"oldLineNumber": null
},
{
"type": "addition",
"content": " text",
"newLineNumber": 271,
"oldLineNumber": null
},
{
"type": "addition",
"content": " })",
"newLineNumber": 272,
"oldLineNumber": null
},
{
"type": "addition",
"content": " );",
"newLineNumber": 273,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const relatedReviewRun: CiFailureRelatedReviewRun = {",
"newLineNumber": 274,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: record.run.reviewRunId,",
"newLineNumber": 275,
"oldLineNumber": null
},
{
"type": "addition",
"content": " status: record.run.status,",
"newLineNumber": 276,
"oldLineNumber": null
},
{
"type": "addition",
"content": " createdAt: record.run.reviewRunCreatedAt,",
"newLineNumber": 277,
"oldLineNumber": null
},
{
"type": "addition",
"content": " detailUrl: `/api/review-runs/${record.run.reviewRunId}`",
"newLineNumber": 278,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 279,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 280,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return {",
"newLineNumber": 281,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ...listItem,",
"newLineNumber": 282,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rootCause: listItem.rootCauseSummary,",
"newLineNumber": 283,
"oldLineNumber": null
},
{
"type": "addition",
"content": " suggestedFixes,",
"newLineNumber": 284,
"oldLineNumber": null
},
{
"type": "addition",
"content": " failedJobs: groups.map((group) => toFailedJob(group, record.run.artifacts)),",
"newLineNumber": 285,
"oldLineNumber": null
},
{
"type": "addition",
"content": " relatedReviewRun,",
"newLineNumber": 286,
"oldLineNumber": null
},
{
"type": "addition",
"content": " relatedArtifacts: sortRelatedArtifacts(record.run.artifacts).map((artifact) => toArtifact(artifact, canAccessRawArtifacts)),",
"newLineNumber": 287,
"oldLineNumber": null
},
{
"type": "addition",
"content": " logExcerpts: deriveRedactedLogExcerpts(record, groups),",
"newLineNumber": 288,
"oldLineNumber": null
},
{
"type": "addition",
"content": " unavailableLogNotes: Array.isArray(record.explanation.unavailableLogNotes) ? record.explanation.unavailableLogNotes : []",
"newLineNumber": 289,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 290,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 291,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 292,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function sortRelatedArtifacts(artifacts: readonly CiFailureArtifactRow[]): CiFailureArtifactRow[] {",
"newLineNumber": 293,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return [...artifacts].sort((left, right) => {",
"newLineNumber": 294,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const leftRank = left.artifact_type === \"ci_failure_explanation\" ? 0 : 1;",
"newLineNumber": 295,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const rightRank = right.artifact_type === \"ci_failure_explanation\" ? 0 : 1;",
"newLineNumber": 296,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 297,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (leftRank !== rightRank) {",
"newLineNumber": 298,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return leftRank - rightRank;",
"newLineNumber": 299,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 300,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 301,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return toRequiredIsoString(left.artifact_created_at).localeCompare(toRequiredIsoString(right.artifact_created_at));",
"newLineNumber": 302,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 303,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 304,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 305,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function toCiFailureId(record: CiFailureRecord): string {",
"newLineNumber": 306,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const groupId = readString(record.primaryGroup.id) ?? \"primary\";",
"newLineNumber": 307,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return `${record.explanationArtifact.artifact_id}:${encodeURIComponent(groupId)}`;",
"newLineNumber": 308,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 309,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 310,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function toFailedJob(group: Record<string, unknown>, artifacts: CiFailureArtifactRow[]): CiFailureFailedJob {",
"newLineNumber": 311,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const checkRunId = readNumber(group.checkRunId) ?? 0;",
"newLineNumber": 312,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const checkRun = findCheckRun(artifacts, checkRunId);",
"newLineNumber": 313,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 314,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return {",
"newLineNumber": 315,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: readString(group.id) ?? `ci:${checkRunId}`,",
"newLineNumber": 316,
"oldLineNumber": null
},
{
"type": "addition",
"content": " workflowName: readString(checkRun?.workflowName) ?? null,",
"newLineNumber": 317,
"oldLineNumber": null
},
{
"type": "addition",
"content": " jobName: readString(group.jobName) ?? readString(checkRun?.name) ?? \"Unknown CI job\",",
"newLineNumber": 318,
"oldLineNumber": null
},
{
"type": "addition",
"content": " checkRunId,",
"newLineNumber": 319,
"oldLineNumber": null
},
{
"type": "addition",
"content": " conclusion: readString(group.conclusion) ?? readString(checkRun?.conclusion) ?? \"failure\",",
"newLineNumber": 320,
"oldLineNumber": null
},
{
"type": "addition",
"content": " stepName: readString(group.stepName),",
"newLineNumber": 321,
"oldLineNumber": null
},
{
"type": "addition",
"content": " category: readString(group.category) ?? \"unknown\",",
"newLineNumber": 322,
"oldLineNumber": null
},
{
"type": "addition",
"content": " detailsUrl: readString(checkRun?.detailsUrl) ?? readString(checkRun?.htmlUrl) ?? null",
"newLineNumber": 323,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 324,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 325,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 326,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function findCheckRun(artifacts: CiFailureArtifactRow[], checkRunId: number): Record<string, unknown> | null {",
"newLineNumber": 327,
"oldLineNumber": null
},
{
"type": "addition",
"content": " for (const artifact of artifacts) {",
"newLineNumber": 328,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const body = unwrapArtifact(artifact.metadata_json);",
"newLineNumber": 329,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const checkRuns = Array.isArray(body?.checkRuns) ? body.checkRuns : [];",
"newLineNumber": 330,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const checkRun = checkRuns.find((candidate): candidate is Record<string, unknown> => {",
"newLineNumber": 331,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return isRecord(candidate) && readNumber(candidate.id) === checkRunId;",
"newLineNumber": 332,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 333,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 334,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (checkRun !== undefined) {",
"newLineNumber": 335,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return checkRun;",
"newLineNumber": 336,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 337,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 338,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 339,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return null;",
"newLineNumber": 340,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 341,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 342,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function toArtifact(row: CiFailureArtifactRow, canAccessRawArtifacts: boolean): ReviewRunArtifact {",
"newLineNumber": 343,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return {",
"newLineNumber": 344,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: row.artifact_id,",
"newLineNumber": 345,
"oldLineNumber": null
},
{
"type": "addition",
"content": " artifactType: row.artifact_type,",
"newLineNumber": 346,
"oldLineNumber": null
},
{
"type": "addition",
"content": " storageKey: canAccessRawArtifacts ? row.storage_key : null,",
"newLineNumber": 347,
"oldLineNumber": null
},
{
"type": "addition",
"content": " metadata: sanitizeArtifactMetadata(row.metadata_json),",
"newLineNumber": 348,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rawAccessAllowed: canAccessRawArtifacts,",
"newLineNumber": 349,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rawAccessRequiredRole: \"developer\",",
"newLineNumber": 350,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rawAccessUrl: canAccessRawArtifacts ? `/api/review-runs/${row.review_run_id}/artifacts/${row.artifact_id}/raw` : null,",
"newLineNumber": 351,
"oldLineNumber": null
},
{
"type": "addition",
"content": " createdAt: toRequiredIsoString(row.artifact_created_at)",
"newLineNumber": 352,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 353,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 354,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 355,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function sanitizeArtifactMetadata(value: unknown): Record<string, unknown> {",
"newLineNumber": 356,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const metadata = isRecord(value) ? { ...value } : {};",
"newLineNumber": 357,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const artifact = unwrapArtifact(value);",
"newLineNumber": 358,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 359,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (artifact?.schemaVersion === \"ci-log-artifact/v1\") {",
"newLineNumber": 360,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return {",
"newLineNumber": 361,
"oldLineNumber": null
},
{
"type": "addition",
"content": " schemaVersion: artifact.schemaVersion,",
"newLineNumber": 362,
"oldLineNumber": null
},
{
"type": "addition",
"content": " redacted: true,",
"newLineNumber": 363,
"oldLineNumber": null
},
{
"type": "addition",
"content": " logsCount: Array.isArray(artifact.logs) ? artifact.logs.length : 0,",
"newLineNumber": 364,
"oldLineNumber": null
},
{
"type": "addition",
"content": " unavailableLogsCount: Array.isArray(artifact.unavailableLogs) ? artifact.unavailableLogs.length : 0",
"newLineNumber": 365,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 366,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 367,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 368,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return metadata;",
"newLineNumber": 369,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 370,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 371,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function deriveRedactedLogExcerpts(record: CiFailureRecord, groups: Record<string, unknown>[]): Array<ReviewRunLogExcerpt & { collapsed: true }> {",
"newLineNumber": 372,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const excerpts: Array<ReviewRunLogExcerpt & { collapsed: true }> = [];",
"newLineNumber": 373,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 374,
"oldLineNumber": null
},
{
"type": "addition",
"content": " groups.forEach((group, groupIndex) => {",
"newLineNumber": 375,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const evidence = Array.isArray(group.evidence) ? group.evidence : [];",
"newLineNumber": 376,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 377,
"oldLineNumber": null
},
{
"type": "addition",
"content": " evidence.forEach((entry, entryIndex) => {",
"newLineNumber": 378,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (!isRecord(entry)) {",
"newLineNumber": 379,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return;",
"newLineNumber": 380,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 381,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 382,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const excerpt = readString(entry.excerpt);",
"newLineNumber": 383,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 384,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (excerpt === null) {",
"newLineNumber": 385,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return;",
"newLineNumber": 386,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 387,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 388,
"oldLineNumber": null
},
{
"type": "addition",
"content": " excerpts.push({",
"newLineNumber": 389,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: `${toCiFailureId(record)}:excerpt:${groupIndex + 1}:${entryIndex + 1}`,",
"newLineNumber": 390,
"oldLineNumber": null
},
{
"type": "addition",
"content": " source: \"ci_log\",",
"newLineNumber": 391,
"oldLineNumber": null
},
{
"type": "addition",
"content": " title: readString(group.stepName) ?? readString(group.jobName) ?? `CI log excerpt ${entryIndex + 1}`,",
"newLineNumber": 392,
"oldLineNumber": null
},
{
"type": "addition",
"content": " excerpt,",
"newLineNumber": 393,
"oldLineNumber": null
},
{
"type": "addition",
"content": " artifactId: findCiLogArtifactId(record.run.artifacts, readNumber(entry.checkRunId)),",
"newLineNumber": 394,
"oldLineNumber": null
},
{
"type": "addition",
"content": " storageKey: null,",
"newLineNumber": 395,
"oldLineNumber": null
},
{
"type": "addition",
"content": " redacted: true,",
"newLineNumber": 396,
"oldLineNumber": null
},
{
"type": "addition",
"content": " truncated: false,",
"newLineNumber": 397,
"oldLineNumber": null
},
{
"type": "addition",
"content": " collapsed: true,",
"newLineNumber": 398,
"oldLineNumber": null
},
{
"type": "addition",
"content": " createdAt: toRequiredIsoString(record.explanationArtifact.artifact_created_at)",
"newLineNumber": 399,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 400,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 401,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 402,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 403,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return excerpts;",
"newLineNumber": 404,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 405,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 406,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function findCiLogArtifactId(artifacts: CiFailureArtifactRow[], checkRunId: number | null): string | null {",
"newLineNumber": 407,
"oldLineNumber": null
},
{
"type": "addition",
"content": " for (const artifact of artifacts) {",
"newLineNumber": 408,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (artifact.artifact_type !== \"ci_log\") {",
"newLineNumber": 409,
"oldLineNumber": null
},
{
"type": "addition",
"content": " continue;",
"newLineNumber": 410,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 411,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 412,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (checkRunId === null) {",
"newLineNumber": 413,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return artifact.artifact_id;",
"newLineNumber": 414,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 415,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 416,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const body = unwrapArtifact(artifact.metadata_json);",
"newLineNumber": 417,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const logs = Array.isArray(body?.logs) ? body.logs : [];",
"newLineNumber": 418,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const hasMatchingLog = logs.some((log) => isRecord(log) && readNumber(log.checkRunId) === checkRunId);",
"newLineNumber": 419,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 420,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (hasMatchingLog) {",
"newLineNumber": 421,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return artifact.artifact_id;",
"newLineNumber": 422,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 423,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 424,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 425,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return null;",
"newLineNumber": 426,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 427,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 428,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function matchesPostQueryFilters(item: CiFailureListItem, filters: CiFailureListFilters): boolean {",
"newLineNumber": 429,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return filters.flaky === undefined || item.flakySuspected === filters.flaky;",
"newLineNumber": 430,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 431,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 432,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function unwrapArtifact(value: unknown): Record<string, unknown> | null {",
"newLineNumber": 433,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (!isRecord(value)) {",
"newLineNumber": 434,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return null;",
"newLineNumber": 435,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 436,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 437,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (isRecord(value.artifact)) {",
"newLineNumber": 438,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return value.artifact;",
"newLineNumber": 439,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 440,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 441,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return value;",
"newLineNumber": 442,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 443,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 444,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function readGroups(value: Record<string, unknown>): Record<string, unknown>[] {",
"newLineNumber": 445,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return Array.isArray(value.groups) ? value.groups.filter(isRecord) : [];",
"newLineNumber": 446,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 447,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 448,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function readString(value: unknown): string | null {",
"newLineNumber": 449,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return typeof value === \"string\" && value.length > 0 ? value : null;",
"newLineNumber": 450,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 451,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 452,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function readNumber(value: unknown): number | null {",
"newLineNumber": 453,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return typeof value === \"number\" && Number.isFinite(value) ? value : null;",
"newLineNumber": 454,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 455,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 456,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function readStringArray(value: unknown): string[] {",
"newLineNumber": 457,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\" && item.length > 0) : [];",
"newLineNumber": 458,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 459,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 460,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function uniqueStrings(values: string[]): string[] {",
"newLineNumber": 461,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return [...new Set(values)];",
"newLineNumber": 462,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 463,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 464,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function isRecord(value: unknown): value is Record<string, unknown> {",
"newLineNumber": 465,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return typeof value === \"object\" && value !== null && !Array.isArray(value);",
"newLineNumber": 466,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 467,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 468,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function toRequiredIsoString(value: Date | string | null): string {",
"newLineNumber": 469,
"oldLineNumber": null
},
{
"type": "addition",
"content": " if (value === null) {",
"newLineNumber": 470,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return new Date(0).toISOString();",
"newLineNumber": 471,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 472,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 473,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return typeof value === \"string\" ? new Date(value).toISOString() : value.toISOString();",
"newLineNumber": 474,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 475,
"oldLineNumber": null
}
],
"newStart": 1,
"oldStart": 0,
"newLineCount": 475,
"oldLineCount": 0,
"sectionHeader": ""
}
],
"patch": "@@ -0,0 +1,475 @@\n+import type {\n+ CiFailureDetailResponse,\n+ CiFailureFailedJob,\n+ CiFailureListFilters,\n+ CiFailureListItem,\n+ CiFailureListResponse,\n+ CiFailureRelatedReviewRun,\n+ CiFailureSuggestedFix,\n+ ReviewRunArtifact,\n+ ReviewRunArtifactType,\n+ ReviewRunLogExcerpt,\n+ ReviewRunStatus\n+} from \"@firmcode/shared\";\n+import type { DatabaseExecutor } from \"../../infrastructure/database/migrations\";\n+\n+export const CI_FAILURES_STORE = Symbol(\"CI_FAILURES_STORE\");\n+\n+export interface CiFailuresStore {\n+ listCiFailures(input: CiFailureListInput): Promise<CiFailureListResponse>;\n+ getCiFailureDetail(input: CiFailureDetailLookup): Promise<CiFailureDetailResponse | null>;\n+}\n+\n+export interface CiFailureListInput {\n+ readonly workspaceId: string;\n+ readonly canAccessRawArtifacts: boolean;\n+ readonly filters: CiFailureListFilters;\n+}\n+\n+export interface CiFailureDetailLookup {\n+ readonly workspaceId: string;\n+ readonly ciFailureId: string;\n+ readonly canAccessRawArtifacts: boolean;\n+}\n+\n+interface CiFailureArtifactRow {\n+ readonly review_run_id: string;\n+ readonly repository_id: string;\n+ readonly pull_request_id: string;\n+ readonly repository_full_name: string;\n+ readonly pull_request_number: number;\n+ readonly pull_request_title: string;\n+ readonly status: ReviewRunStatus;\n+ readonly review_run_created_at: Date | string | null;\n+ readonly artifact_id: string;\n+ readonly artifact_type: ReviewRunArtifactType;\n+ readonly storage_key: string;\n+ readonly metadata_json: unknown;\n+ readonly artifact_created_at: Date | string | null;\n+}\n+\n+interface GroupedRun {\n+ readonly reviewRunId: string;\n+ readonly repositoryId: string;\n+ readonly pullRequestId: string;\n+ readonly repositoryFullName: string;\n+ readonly pullRequestNumber: number;\n+ readonly pullRequestTitle: string;\n+ readonly status: ReviewRunStatus;\n+ readonly reviewRunCreatedAt: string;\n+ readonly artifacts: CiFailureArtifactRow[];\n+}\n+\n+interface CiFailureRecord {\n+ readonly run: GroupedRun;\n+ readonly explanationArtifact: CiFailureArtifactRow;\n+ readonly explanation: Record<string, unknown>;\n+ readonly primaryGroup: Record<string, unknown>;\n+}\n+\n+export class EmptyCiFailuresStore implements CiFailuresStore {\n+ async listCiFailures(input: CiFailureListInput): Promise<CiFailureListResponse> {\n+ return {\n+ ciFailures: [],\n+ filters: input.filters,\n+ pagination: {\n+ limit: input.filters.limit ?? DEFAULT_CI_FAILURE_LIMIT,\n+ returned: 0\n+ }\n+ };\n+ }\n+\n+ async getCiFailureDetail(_input: CiFailureDetailLookup): Promise<CiFailureDetailResponse | null> {\n+ return null;\n+ }\n+}\n+\n+export class PostgresCiFailuresStore implements CiFailuresStore {\n+ constructor(private readonly database: DatabaseExecutor) {}\n+\n+ async listCiFailures(input: CiFailureListInput): Promise<CiFailureListResponse> {\n+ const limit = input.filters.limit ?? DEFAULT_CI_FAILURE_LIMIT;\n+ const rows = await this.loadCandidateRows(input.workspaceId, input.filters);\n+ const ciFailures = buildCiFailureRecords(rows)\n+ .map((record) => toCiFailureListItem(record))\n+ .filter((item) => matchesPostQueryFilters(item, input.filters))\n+ .slice(0, limit);\n+\n+ return {\n+ ciFailures,\n+ filters: input.filters,\n+ pagination: {\n+ limit,\n+ returned: ciFailures.length\n+ }\n+ };\n+ }\n+\n+ async getCiFailureDetail(input: CiFailureDetailLookup): Promise<CiFailureDetailResponse | null> {\n+ const rows = await this.loadCandidateRows(input.workspaceId, {});\n+ const record = buildCiFailureRecords(rows).find((candidate) => toCiFailureId(candidate) === input.ciFailureId);\n+\n+ if (record === undefined) {\n+ return null;\n+ }\n+\n+ return toCiFailureDetail(record, input.canAccessRawArtifacts);\n+ }\n+\n+ private async loadCandidateRows(workspaceId: string, filters: CiFailureListFilters): Promise<CiFailureArtifactRow[]> {\n+ const { whereSql, values } = buildCiFailureWhereClause(workspaceId, filters);\n+ const result = await this.database.query<CiFailureArtifactRow>(\n+ `\n+SELECT\n+ rr.id AS review_run_id,\n+ rr.repository_id,\n+ rr.pull_request_id,\n+ r.full_name AS repository_full_name,\n+ pr.number AS pull_request_number,\n+ pr.title AS pull_request_title,\n+ rr.status,\n+ rr.created_at AS review_run_created_at,\n+ aa.id AS artifact_id,\n+ aa.artifact_type,\n+ aa.storage_key,\n+ aa.metadata_json,\n+ aa.created_at AS artifact_created_at\n+FROM review_runs rr\n+JOIN repositories r ON r.id = rr.repository_id\n+JOIN github_installations gi ON gi.id = r.installation_id\n+JOIN pull_requests pr ON pr.id = rr.pull_request_id\n+JOIN analysis_artifacts aa ON aa.review_run_id = rr.id\n+${whereSql}\n+ORDER BY rr.created_at DESC, aa.created_at ASC, aa.artifact_type ASC\n+LIMIT 1000\n+`,\n+ values\n+ );\n+\n+ return result.rows;\n+ }\n+}\n+\n+const DEFAULT_CI_FAILURE_LIMIT = 50;\n+\n+function buildCiFailureWhereClause(\n+ workspaceId: string,\n+ filters: CiFailureListFilters\n+): { whereSql: string; values: unknown[] } {\n+ const conditions = [\"gi.workspace_id = $1\"];\n+ const values: unknown[] = [workspaceId];\n+\n+ if (filters.repositoryId !== undefined) {\n+ values.push(filters.repositoryId);\n+ conditions.push(`rr.repository_id = $${values.length}`);\n+ } else if (filters.repository !== undefined) {\n+ values.push(filters.repository);\n+ conditions.push(`lower(r.full_name) = lower($${values.length})`);\n+ }\n+\n+ if (filters.status !== undefined) {\n+ values.push(filters.status);\n+ conditions.push(`rr.status = $${values.length}`);\n+ }\n+\n+ if (filters.dateFrom !== undefined) {\n+ values.push(filters.dateFrom);\n+ conditions.push(`rr.created_at >= $${values.length}`);\n+ }\n+\n+ if (filters.dateTo !== undefined) {\n+ values.push(filters.dateTo);\n+ conditions.push(`rr.created_at <= $${values.length}`);\n+ }\n+\n+ conditions.push(\"aa.artifact_type IN ('ci_failure_explanation', 'ci_log', 'llm_raw')\");\n+\n+ return {\n+ whereSql: `WHERE ${conditions.join(\" AND \")}`,\n+ values\n+ };\n+}\n+\n+function buildCiFailureRecords(rows: CiFailureArtifactRow[]): CiFailureRecord[] {\n+ const grouped = new Map<string, GroupedRun>();\n+\n+ for (const row of rows) {\n+ const existing = grouped.get(row.review_run_id);\n+\n+ if (existing !== undefined) {\n+ existing.artifacts.push(row);\n+ continue;\n+ }\n+\n+ grouped.set(row.review_run_id, {\n+ reviewRunId: row.review_run_id,\n+ repositoryId: row.repository_id,\n+ pullRequestId: row.pull_request_id,\n+ repositoryFullName: row.repository_full_name,\n+ pullRequestNumber: row.pull_request_number,\n+ pullRequestTitle: row.pull_request_title,\n+ status: row.status,\n+ reviewRunCreatedAt: toRequiredIsoString(row.review_run_created_at),\n+ artifacts: [row]\n+ });\n+ }\n+\n+ return [...grouped.values()]\n+ .map(toCiFailureRecord)\n+ .filter((record): record is CiFailureRecord => record !== null)\n+ .sort((left, right) => right.explanationArtifact.artifact_created_at!.toString().localeCompare(left.explanationArtifact.artifact_created_at!.toString()));\n+}\n+\n+function toCiFailureRecord(run: GroupedRun): CiFailureRecord | null {\n+ for (const artifact of run.artifacts) {\n+ const artifactBody = unwrapArtifact(artifact.metadata_json);\n+\n+ if (artifactBody?.schemaVersion === \"ci-failure-explanation/v1\") {\n+ const groups = readGroups(artifactBody);\n+ const primaryGroup = groups[0];\n+\n+ if (primaryGroup === undefined) {\n+ return null;\n+ }\n+\n+ return {\n+ run,\n+ explanationArtifact: artifact,\n+ explanation: artifactBody,\n+ primaryGroup\n+ };\n+ }\n+ }\n+\n+ return null;\n+}\n+\n+function toCiFailureListItem(record: CiFailureRecord): CiFailureListItem {\n+ return {\n+ id: toCiFailureId(record),\n+ repositoryId: record.run.repositoryId,\n+ repositoryFullName: record.run.repositoryFullName,\n+ pullRequestId: record.run.pullRequestId,\n+ pullRequestNumber: record.run.pullRequestNumber,\n+ pullRequestTitle: record.run.pullRequestTitle,\n+ reviewRunId: record.run.reviewRunId,\n+ failedJob: toFailedJob(record.primaryGroup, record.run.artifacts),\n+ rootCauseSummary: readString(record.primaryGroup.rootCauseSummary) ?? readString(record.explanation.summary) ?? \"CI failed without a root cause summary.\",\n+ flakySuspected: readGroups(record.explanation).some((group) => group.flaky === true),\n+ suggestedFix: readStringArray(record.primaryGroup.suggestedFixes)[0] ?? null,\n+ status: record.run.status,\n+ createdAt: toRequiredIsoString(record.explanationArtifact.artifact_created_at)\n+ };\n+}\n+\n+function toCiFailureDetail(record: CiFailureRecord, canAccessRawArtifacts: boolean): CiFailureDetailResponse {\n+ const listItem = toCiFailureListItem(record);\n+ const groups = readGroups(record.explanation);\n+ const suggestedFixes = uniqueStrings(groups.flatMap((group) => readStringArray(group.suggestedFixes))).map<CiFailureSuggestedFix>(\n+ (text, index) => ({\n+ id: `${listItem.id}:fix:${index + 1}`,\n+ text\n+ })\n+ );\n+ const relatedReviewRun: CiFailureRelatedReviewRun = {\n+ id: record.run.reviewRunId,\n+ status: record.run.status,\n+ createdAt: record.run.reviewRunCreatedAt,\n+ detailUrl: `/api/review-runs/${record.run.reviewRunId}`\n+ };\n+\n+ return {\n+ ...listItem,\n+ rootCause: listItem.rootCauseSummary,\n+ suggestedFixes,\n+ failedJobs: groups.map((group) => toFailedJob(group, record.run.artifacts)),\n+ relatedReviewRun,\n+ relatedArtifacts: sortRelatedArtifacts(record.run.artifacts).map((artifact) => toArtifact(artifact, canAccessRawArtifacts)),\n+ logExcerpts: deriveRedactedLogExcerpts(record, groups),\n+ unavailableLogNotes: Array.isArray(record.explanation.unavailableLogNotes) ? record.explanation.unavailableLogNotes : []\n+ };\n+}\n+\n+function sortRelatedArtifacts(artifacts: readonly CiFailureArtifactRow[]): CiFailureArtifactRow[] {\n+ return [...artifacts].sort((left, right) => {\n+ const leftRank = left.artifact_type === \"ci_failure_explanation\" ? 0 : 1;\n+ const rightRank = right.artifact_type === \"ci_failure_explanation\" ? 0 : 1;\n+\n+ if (leftRank !== rightRank) {\n+ return leftRank - rightRank;\n+ }\n+\n+ return toRequiredIsoString(left.artifact_created_at).localeCompare(toRequiredIsoString(right.artifact_created_at));\n+ });\n+}\n+\n+function toCiFailureId(record: CiFailureRecord): string {\n+ const groupId = readString(record.primaryGroup.id) ?? \"primary\";\n+ return `${record.explanationArtifact.artifact_id}:${encodeURIComponent(groupId)}`;\n+}\n+\n+function toFailedJob(group: Record<string, unknown>, artifacts: CiFailureArtifactRow[]): CiFailureFailedJob {\n+ const checkRunId = readNumber(group.checkRunId) ?? 0;\n+ const checkRun = findCheckRun(artifacts, checkRunId);\n+\n+ return {\n+ id: readString(group.id) ?? `ci:${checkRunId}`,\n+ workflowName: readString(checkRun?.workflowName) ?? null,\n+ jobName: readString(group.jobName) ?? readString(checkRun?.name) ?? \"Unknown CI job\",\n+ checkRunId,\n+ conclusion: readString(group.conclusion) ?? readString(checkRun?.conclusion) ?? \"failure\",\n+ stepName: readString(group.stepName),\n+ category: readString(group.category) ?? \"unknown\",\n+ detailsUrl: readString(checkRun?.detailsUrl) ?? readString(checkRun?.htmlUrl) ?? null\n+ };\n+}\n+\n+function findCheckRun(artifacts: CiFailureArtifactRow[], checkRunId: number): Record<string, unknown> | null {\n+ for (const artifact of artifacts) {\n+ const body = unwrapArtifact(artifact.metadata_json);\n+ const checkRuns = Array.isArray(body?.checkRuns) ? body.checkRuns : [];\n+ const checkRun = checkRuns.find((candidate): candidate is Record<string, unknown> => {\n+ return isRecord(candidate) && readNumber(candidate.id) === checkRunId;\n+ });\n+\n+ if (checkRun !== undefined) {\n+ return checkRun;\n+ }\n+ }\n+\n+ return null;\n+}\n+\n+function toArtifact(row: CiFailureArtifactRow, canAccessRawArtifacts: boolean): ReviewRunArtifact {\n+ return {\n+ id: row.artifact_id,\n+ artifactType: row.artifact_type,\n+ storageKey: canAccessRawArtifacts ? row.storage_key : null,\n+ metadata: sanitizeArtifactMetadata(row.metadata_json),\n+ rawAccessAllowed: canAccessRawArtifacts,\n+ rawAccessRequiredRole: \"developer\",\n+ rawAccessUrl: canAccessRawArtifacts ? `/api/review-runs/${row.review_run_id}/artifacts/${row.artifact_id}/raw` : null,\n+ createdAt: toRequiredIsoString(row.artifact_created_at)\n+ };\n+}\n+\n+function sanitizeArtifactMetadata(value: unknown): Record<string, unknown> {\n+ const metadata = isRecord(value) ? { ...value } : {};\n+ const artifact = unwrapArtifact(value);\n+\n+ if (artifact?.schemaVersion === \"ci-log-artifact/v1\") {\n+ return {\n+ schemaVersion: artifact.schemaVersion,\n+ redacted: true,\n+ logsCount: Array.isArray(artifact.logs) ? artifact.logs.length : 0,\n+ unavailableLogsCount: Array.isArray(artifact.unavailableLogs) ? artifact.unavailableLogs.length : 0\n+ };\n+ }\n+\n+ return metadata;\n+}\n+\n+function deriveRedactedLogExcerpts(record: CiFailureRecord, groups: Record<string, unknown>[]): Array<ReviewRunLogExcerpt & { collapsed: true }> {\n+ const excerpts: Array<ReviewRunLogExcerpt & { collapsed: true }> = [];\n+\n+ groups.forEach((group, groupIndex) => {\n+ const evidence = Array.isArray(group.evidence) ? group.evidence : [];\n+\n+ evidence.forEach((entry, entryIndex) => {\n+ if (!isRecord(entry)) {\n+ return;\n+ }\n+\n+ const excerpt = readString(entry.excerpt);\n+\n+ if (excerpt === null) {\n+ return;\n+ }\n+\n+ excerpts.push({\n+ id: `${toCiFailureId(record)}:excerpt:${groupIndex + 1}:${entryIndex + 1}`,\n+ source: \"ci_log\",\n+ title: readString(group.stepName) ?? readString(group.jobName) ?? `CI log excerpt ${entryIndex + 1}`,\n+ excerpt,\n+ artifactId: findCiLogArtifactId(record.run.artifacts, readNumber(entry.checkRunId)),\n+ storageKey: null,\n+ redacted: true,\n+ truncated: false,\n+ collapsed: true,\n+ createdAt: toRequiredIsoString(record.explanationArtifact.artifact_created_at)\n+ });\n+ });\n+ });\n+\n+ return excerpts;\n+}\n+\n+function findCiLogArtifactId(artifacts: CiFailureArtifactRow[], checkRunId: number | null): string | null {\n+ for (const artifact of artifacts) {\n+ if (artifact.artifact_type !== \"ci_log\") {\n+ continue;\n+ }\n+\n+ if (checkRunId === null) {\n+ return artifact.artifact_id;\n+ }\n+\n+ const body = unwrapArtifact(artifact.metadata_json);\n+ const logs = Array.isArray(body?.logs) ? body.logs : [];\n+ const hasMatchingLog = logs.some((log) => isRecord(log) && readNumber(log.checkRunId) === checkRunId);\n+\n+ if (hasMatchingLog) {\n+ return artifact.artifact_id;\n+ }\n+ }\n+\n+ return null;\n+}\n+\n+function matchesPostQueryFilters(item: CiFailureListItem, filters: CiFailureListFilters): boolean {\n+ return filters.flaky === undefined || item.flakySuspected === filters.flaky;\n+}\n+\n+function unwrapArtifact(value: unknown): Record<string, unknown> | null {\n+ if (!isRecord(value)) {\n+ return null;\n+ }\n+\n+ if (isRecord(value.artifact)) {\n+ return value.artifact;\n+ }\n+\n+ return value;\n+}\n+\n+function readGroups(value: Record<string, unknown>): Record<string, unknown>[] {\n+ return Array.isArray(value.groups) ? value.groups.filter(isRecord) : [];\n+}\n+\n+function readString(value: unknown): string | null {\n+ return typeof value === \"string\" && value.length > 0 ? value : null;\n+}\n+\n+function readNumber(value: unknown): number | null {\n+ return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n+}\n+\n+function readStringArray(value: unknown): string[] {\n+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\" && item.length > 0) : [];\n+}\n+\n+function uniqueStrings(values: string[]): string[] {\n+ return [...new Set(values)];\n+}\n+\n+function isRecord(value: unknown): value is Record<string, unknown> {\n+ return typeof value === \"object\" && value !== null && !Array.isArray(value);\n+}\n+\n+function toRequiredIsoString(value: Date | string | null): string {\n+ if (value === null) {\n+ return new Date(0).toISOString();\n+ }\n+\n+ return typeof value === \"string\" ? new Date(value).toISOString() : value.toISOString();\n+}",
"status": "added",
"language": "typescript",
"additions": 475,
"deletions": 0,
"sizeBytes": 15857,
"previousPath": null,
"changedNewLines": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
96,
97,
98,
99,
100,
101,
102,
103,
104,
105,
106,
107,
108,
109,
110,
111,
112,
113,
114,
115,
116,
117,
118,
119,
120,
121,
122,
123,
124,
125,
126,
127,
128,
129,
130,
131,
132,
133,
134,
135,
136,
137,
138,
139,
140,
141,
142,
143,
144,
145,
146,
147,
148,
149,
150,
151,
152,
153,
154,
155,
156,
157,
158,
159,
160,
161,
162,
163,
164,
165,
166,
167,
168,
169,
170,
171,
172,
173,
174,
175,
176,
177,
178,
179,
180,
181,
182,
183,
184,
185,
186,
187,
188,
189,
190,
191,
192,
193,
194,
195,
196,
197,
198,
199,
200,
201,
202,
203,
204,
205,
206,
207,
208,
209,
210,
211,
212,
213,
214,
215,
216,
217,
218,
219,
220,
221,
222,
223,
224,
225,
226,
227,
228,
229,
230,
231,
232,
233,
234,
235,
236,
237,
238,
239,
240,
241,
242,
243,
244,
245,
246,
247,
248,
249,
250,
251,
252,
253,
254,
255,
256,
257,
258,
259,
260,
261,
262,
263,
264,
265,
266,
267,
268,
269,
270,
271,
272,
273,
274,
275,
276,
277,
278,
279,
280,
281,
282,
283,
284,
285,
286,
287,
288,
289,
290,
291,
292,
293,
294,
295,
296,
297,
298,
299,
300,
301,
302,
303,
304,
305,
306,
307,
308,
309,
310,
311,
312,
313,
314,
315,
316,
317,
318,
319,
320,
321,
322,
323,
324,
325,
326,
327,
328,
329,
330,
331,
332,
333,
334,
335,
336,
337,
338,
339,
340,
341,
342,
343,
344,
345,
346,
347,
348,
349,
350,
351,
352,
353,
354,
355,
356,
357,
358,
359,
360,
361,
362,
363,
364,
365,
366,
367,
368,
369,
370,
371,
372,
373,
374,
375,
376,
377,
378,
379,
380,
381,
382,
383,
384,
385,
386,
387,
388,
389,
390,
391,
392,
393,
394,
395,
396,
397,
398,
399,
400,
401,
402,
403,
404,
405,
406,
407,
408,
409,
410,
411,
412,
413,
414,
415,
416,
417,
418,
419,
420,
421,
422,
423,
424,
425,
426,
427,
428,
429,
430,
431,
432,
433,
434,
435,
436,
437,
438,
439,
440,
441,
442,
443,
444,
445,
446,
447,
448,
449,
450,
451,
452,
453,
454,
455,
456,
457,
458,
459,
460,
461,
462,
463,
464,
465,
466,
467,
468,
469,
470,
471,
472,
473,
474,
475
],
"headContentSha256": "ced24ca13a60fc06c13751c14e61ced9d344a184eddc0e406a9d33501050bcf6"
},
{
"path": "apps/api/test/ci-failures-dashboard-api.spec.ts",
"hunks": [
{
"lines": [
{
"type": "addition",
"content": "import { BadRequestException, NotFoundException, UnauthorizedException } from \"@nestjs/common\";",
"newLineNumber": 1,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import { newDb } from \"pg-mem\";",
"newLineNumber": 2,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import { runDatabaseMigrations } from \"../src/infrastructure/database/migrations\";",
"newLineNumber": 3,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import { CiFailuresController } from \"../src/modules/ci-failures/ci-failures.controller\";",
"newLineNumber": 4,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import { PostgresCiFailuresStore } from \"../src/modules/ci-failures/ci-failures.store\";",
"newLineNumber": 5,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import { PostgresDashboardAuthStore } from \"../src/modules/review-runs/dashboard-auth.store\";",
"newLineNumber": 6,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import { ReviewRunsController } from \"../src/modules/review-runs/review-runs.controller\";",
"newLineNumber": 7,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import { PostgresReviewRunsStore } from \"../src/modules/review-runs/review-runs.store\";",
"newLineNumber": 8,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 9,
"oldLineNumber": null
},
{
"type": "addition",
"content": "interface PgPoolLike {",
"newLineNumber": 10,
"oldLineNumber": null
},
{
"type": "addition",
"content": " query<Row = unknown>(sql: string, values?: readonly unknown[]): Promise<{ rows: Row[] }>;",
"newLineNumber": 11,
"oldLineNumber": null
},
{
"type": "addition",
"content": " end(): Promise<void>;",
"newLineNumber": 12,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 13,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 14,
"oldLineNumber": null
},
{
"type": "addition",
"content": "const WORKSPACE_ID = \"00000000-0000-4000-8000-000000000101\";",
"newLineNumber": 15,
"oldLineNumber": null
},
{
"type": "addition",
"content": "const OTHER_WORKSPACE_ID = \"00000000-0000-4000-8000-000000000102\";",
"newLineNumber": 16,
"oldLineNumber": null
},
{
"type": "addition",
"content": "const DEVELOPER_USER_ID = \"user_developer\";",
"newLineNumber": 17,
"oldLineNumber": null
},
{
"type": "addition",
"content": "const VIEWER_USER_ID = \"user_viewer\";",
"newLineNumber": 18,
"oldLineNumber": null
},
{
"type": "addition",
"content": "const OTHER_VIEWER_USER_ID = \"user_other_viewer\";",
"newLineNumber": 19,
"oldLineNumber": null
},
{
"type": "addition",
"content": "const EXPLANATION_ARTIFACT_ID = \"00000000-0000-4000-8000-000000000501\";",
"newLineNumber": 20,
"oldLineNumber": null
},
{
"type": "addition",
"content": "const CI_LOG_ARTIFACT_ID = \"00000000-0000-4000-8000-000000000502\";",
"newLineNumber": 21,
"oldLineNumber": null
},
{
"type": "addition",
"content": "const FLAKY_EXPLANATION_ARTIFACT_ID = \"00000000-0000-4000-8000-000000000503\";",
"newLineNumber": 22,
"oldLineNumber": null
},
{
"type": "addition",
"content": "const OTHER_EXPLANATION_ARTIFACT_ID = \"00000000-0000-4000-8000-000000000504\";",
"newLineNumber": 23,
"oldLineNumber": null
},
{
"type": "addition",
"content": "const CI_FAILURE_ID = `${EXPLANATION_ARTIFACT_ID}:ci%3A101%3Anpm-test%3Aabc123def456`;",
"newLineNumber": 24,
"oldLineNumber": null
},
{
"type": "addition",
"content": "const OTHER_CI_FAILURE_ID = `${OTHER_EXPLANATION_ARTIFACT_ID}:ci%3A303%3Adeploy%3Aprivate`;",
"newLineNumber": 25,
"oldLineNumber": null
},
{
"type": "addition",
"content": "const RAW_SECRET = \"RAW_TOKEN=super-secret-value\";",
"newLineNumber": 26,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 27,
"oldLineNumber": null
},
{
"type": "addition",
"content": "function createTestPool(): PgPoolLike {",
"newLineNumber": 28,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const db = newDb({ autoCreateForeignKeyIndices: true, noAstCoverageCheck: true });",
"newLineNumber": 29,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const adapters = db.adapters.createPg();",
"newLineNumber": 30,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 31,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return new adapters.Pool();",
"newLineNumber": 32,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 33,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 34,
"oldLineNumber": null
},
{
"type": "addition",
"content": "describe(\"CI failures dashboard API\", () => {",
"newLineNumber": 35,
"oldLineNumber": null
},
{
"type": "addition",
"content": " let pool: PgPoolLike;",
"newLineNumber": 36,
"oldLineNumber": null
},
{
"type": "addition",
"content": " let controller: CiFailuresController;",
"newLineNumber": 37,
"oldLineNumber": null
},
{
"type": "addition",
"content": " let reviewRunsController: ReviewRunsController;",
"newLineNumber": 38,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 39,
"oldLineNumber": null
},
{
"type": "addition",
"content": " beforeEach(async () => {",
"newLineNumber": 40,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pool = createTestPool();",
"newLineNumber": 41,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await runDatabaseMigrations(pool);",
"newLineNumber": 42,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await seedCiFailureDashboardData(pool);",
"newLineNumber": 43,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 44,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const dashboardAuthStore = new PostgresDashboardAuthStore(pool);",
"newLineNumber": 45,
"oldLineNumber": null
},
{
"type": "addition",
"content": " controller = new CiFailuresController(new PostgresCiFailuresStore(pool), dashboardAuthStore);",
"newLineNumber": 46,
"oldLineNumber": null
},
{
"type": "addition",
"content": " reviewRunsController = new ReviewRunsController(new PostgresReviewRunsStore(pool), dashboardAuthStore);",
"newLineNumber": 47,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 48,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 49,
"oldLineNumber": null
},
{
"type": "addition",
"content": " afterEach(async () => {",
"newLineNumber": 50,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await pool.end();",
"newLineNumber": 51,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 52,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 53,
"oldLineNumber": null
},
{
"type": "addition",
"content": " it(\"lists workspace-scoped CI failures with repository, PR, failed job, root cause, flaky status, suggested fix, status, and created time\", async () => {",
"newLineNumber": 54,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const response = await controller.listCiFailures(",
"newLineNumber": 55,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 56,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repository: \"openclaw/firmcode\",",
"newLineNumber": 57,
"oldLineNumber": null
},
{
"type": "addition",
"content": " status: \"succeeded\",",
"newLineNumber": 58,
"oldLineNumber": null
},
{
"type": "addition",
"content": " flaky: \"false\",",
"newLineNumber": 59,
"oldLineNumber": null
},
{
"type": "addition",
"content": " dateFrom: \"2026-05-22T00:00:00.000Z\",",
"newLineNumber": 60,
"oldLineNumber": null
},
{
"type": "addition",
"content": " dateTo: \"2026-05-24T00:00:00.000Z\",",
"newLineNumber": 61,
"oldLineNumber": null
},
{
"type": "addition",
"content": " limit: \"1\"",
"newLineNumber": 62,
"oldLineNumber": null
},
{
"type": "addition",
"content": " },",
"newLineNumber": 63,
"oldLineNumber": null
},
{
"type": "addition",
"content": " WORKSPACE_ID,",
"newLineNumber": 64,
"oldLineNumber": null
},
{
"type": "addition",
"content": " VIEWER_USER_ID",
"newLineNumber": 65,
"oldLineNumber": null
},
{
"type": "addition",
"content": " );",
"newLineNumber": 66,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 67,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(response).toMatchObject({",
"newLineNumber": 68,
"oldLineNumber": null
},
{
"type": "addition",
"content": " filters: {",
"newLineNumber": 69,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repository: \"openclaw/firmcode\",",
"newLineNumber": 70,
"oldLineNumber": null
},
{
"type": "addition",
"content": " status: \"succeeded\",",
"newLineNumber": 71,
"oldLineNumber": null
},
{
"type": "addition",
"content": " flaky: false,",
"newLineNumber": 72,
"oldLineNumber": null
},
{
"type": "addition",
"content": " limit: 1",
"newLineNumber": 73,
"oldLineNumber": null
},
{
"type": "addition",
"content": " },",
"newLineNumber": 74,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pagination: {",
"newLineNumber": 75,
"oldLineNumber": null
},
{
"type": "addition",
"content": " limit: 1,",
"newLineNumber": 76,
"oldLineNumber": null
},
{
"type": "addition",
"content": " returned: 1",
"newLineNumber": 77,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 78,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 79,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(response.ciFailures).toEqual([",
"newLineNumber": 80,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect.objectContaining({",
"newLineNumber": 81,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: CI_FAILURE_ID,",
"newLineNumber": 82,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repositoryFullName: \"openclaw/firmcode\",",
"newLineNumber": 83,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pullRequestNumber: 77,",
"newLineNumber": 84,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pullRequestTitle: \"Fix payment tests\",",
"newLineNumber": 85,
"oldLineNumber": null
},
{
"type": "addition",
"content": " reviewRunId: \"00000000-0000-4000-8000-000000000401\",",
"newLineNumber": 86,
"oldLineNumber": null
},
{
"type": "addition",
"content": " failedJob: expect.objectContaining({",
"newLineNumber": 87,
"oldLineNumber": null
},
{
"type": "addition",
"content": " jobName: \"unit tests\",",
"newLineNumber": 88,
"oldLineNumber": null
},
{
"type": "addition",
"content": " checkRunId: 101,",
"newLineNumber": 89,
"oldLineNumber": null
},
{
"type": "addition",
"content": " stepName: \"npm test\",",
"newLineNumber": 90,
"oldLineNumber": null
},
{
"type": "addition",
"content": " detailsUrl: \"https://github.com/openclaw/firmcode/actions/runs/202/jobs/303\"",
"newLineNumber": 91,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }),",
"newLineNumber": 92,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rootCauseSummary:",
"newLineNumber": 93,
"oldLineNumber": null
},
{
"type": "addition",
"content": " \"The unit tests job failed in step `npm test` because the log reports: AssertionError: expected 201 to equal 200.\",",
"newLineNumber": 94,
"oldLineNumber": null
},
{
"type": "addition",
"content": " flakySuspected: false,",
"newLineNumber": 95,
"oldLineNumber": null
},
{
"type": "addition",
"content": " suggestedFix: \"Reproduce the failing test command locally: `npm test`.\",",
"newLineNumber": 96,
"oldLineNumber": null
},
{
"type": "addition",
"content": " status: \"succeeded\",",
"newLineNumber": 97,
"oldLineNumber": null
},
{
"type": "addition",
"content": " createdAt: \"2026-05-23T10:06:00.000Z\"",
"newLineNumber": 98,
"oldLineNumber": null
},
{
"type": "addition",
"content": " })",
"newLineNumber": 99,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ]);",
"newLineNumber": 100,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(JSON.stringify(response)).not.toContain(RAW_SECRET);",
"newLineNumber": 101,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 102,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 103,
"oldLineNumber": null
},
{
"type": "addition",
"content": " it(\"filters flaky CI failures independently from SQL-backed filters\", async () => {",
"newLineNumber": 104,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const response = await controller.listCiFailures({ flaky: \"true\" }, WORKSPACE_ID, VIEWER_USER_ID);",
"newLineNumber": 105,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 106,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(response.ciFailures).toHaveLength(1);",
"newLineNumber": 107,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(response.ciFailures[0]).toMatchObject({",
"newLineNumber": 108,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: `${FLAKY_EXPLANATION_ARTIFACT_ID}:ci%3A202%3Apytest%3Aflaky`,",
"newLineNumber": 109,
"oldLineNumber": null
},
{
"type": "addition",
"content": " flakySuspected: true,",
"newLineNumber": 110,
"oldLineNumber": null
},
{
"type": "addition",
"content": " suggestedFix: \"Rerun the failed job once; if it passes, harden or quarantine the unstable test instead of masking it.\"",
"newLineNumber": 111,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 112,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 113,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 114,
"oldLineNumber": null
},
{
"type": "addition",
"content": " it(\"returns CI failure detail with suggested fixes, failed jobs, related links, and collapsed redacted log excerpts\", async () => {",
"newLineNumber": 115,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const detail = await controller.getCiFailureDetail(CI_FAILURE_ID, WORKSPACE_ID, VIEWER_USER_ID);",
"newLineNumber": 116,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 117,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(detail).toMatchObject({",
"newLineNumber": 118,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: CI_FAILURE_ID,",
"newLineNumber": 119,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rootCause:",
"newLineNumber": 120,
"oldLineNumber": null
},
{
"type": "addition",
"content": " \"The unit tests job failed in step `npm test` because the log reports: AssertionError: expected 201 to equal 200.\",",
"newLineNumber": 121,
"oldLineNumber": null
},
{
"type": "addition",
"content": " suggestedFixes: [",
"newLineNumber": 122,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 123,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: `${CI_FAILURE_ID}:fix:1`,",
"newLineNumber": 124,
"oldLineNumber": null
},
{
"type": "addition",
"content": " text: \"Reproduce the failing test command locally: `npm test`.\"",
"newLineNumber": 125,
"oldLineNumber": null
},
{
"type": "addition",
"content": " },",
"newLineNumber": 126,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 127,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: `${CI_FAILURE_ID}:fix:2`,",
"newLineNumber": 128,
"oldLineNumber": null
},
{
"type": "addition",
"content": " text: \"Inspect the failing assertion and update either the changed behavior or the expected value.\"",
"newLineNumber": 129,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 130,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ],",
"newLineNumber": 131,
"oldLineNumber": null
},
{
"type": "addition",
"content": " failedJobs: [",
"newLineNumber": 132,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 133,
"oldLineNumber": null
},
{
"type": "addition",
"content": " jobName: \"unit tests\",",
"newLineNumber": 134,
"oldLineNumber": null
},
{
"type": "addition",
"content": " checkRunId: 101,",
"newLineNumber": 135,
"oldLineNumber": null
},
{
"type": "addition",
"content": " conclusion: \"failure\",",
"newLineNumber": 136,
"oldLineNumber": null
},
{
"type": "addition",
"content": " category: \"test_failure\"",
"newLineNumber": 137,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 138,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ],",
"newLineNumber": 139,
"oldLineNumber": null
},
{
"type": "addition",
"content": " relatedReviewRun: {",
"newLineNumber": 140,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: \"00000000-0000-4000-8000-000000000401\",",
"newLineNumber": 141,
"oldLineNumber": null
},
{
"type": "addition",
"content": " status: \"succeeded\",",
"newLineNumber": 142,
"oldLineNumber": null
},
{
"type": "addition",
"content": " detailUrl: \"/api/review-runs/00000000-0000-4000-8000-000000000401\"",
"newLineNumber": 143,
"oldLineNumber": null
},
{
"type": "addition",
"content": " },",
"newLineNumber": 144,
"oldLineNumber": null
},
{
"type": "addition",
"content": " relatedArtifacts: [",
"newLineNumber": 145,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 146,
"oldLineNumber": null
},
{
"type": "addition",
"content": " artifactType: \"ci_failure_explanation\",",
"newLineNumber": 147,
"oldLineNumber": null
},
{
"type": "addition",
"content": " storageKey: null,",
"newLineNumber": 148,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rawAccessAllowed: false,",
"newLineNumber": 149,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rawAccessUrl: null",
"newLineNumber": 150,
"oldLineNumber": null
},
{
"type": "addition",
"content": " },",
"newLineNumber": 151,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 152,
"oldLineNumber": null
},
{
"type": "addition",
"content": " artifactType: \"ci_log\",",
"newLineNumber": 153,
"oldLineNumber": null
},
{
"type": "addition",
"content": " storageKey: null,",
"newLineNumber": 154,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rawAccessAllowed: false,",
"newLineNumber": 155,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rawAccessUrl: null",
"newLineNumber": 156,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 157,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ],",
"newLineNumber": 158,
"oldLineNumber": null
},
{
"type": "addition",
"content": " logExcerpts: [",
"newLineNumber": 159,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 160,
"oldLineNumber": null
},
{
"type": "addition",
"content": " excerpt: \"FAIL src/payments.test.ts\\nAssertionError: expected 201 to equal 200\\nTOKEN=[REDACTED_SECRET]\",",
"newLineNumber": 161,
"oldLineNumber": null
},
{
"type": "addition",
"content": " redacted: true,",
"newLineNumber": 162,
"oldLineNumber": null
},
{
"type": "addition",
"content": " collapsed: true,",
"newLineNumber": 163,
"oldLineNumber": null
},
{
"type": "addition",
"content": " storageKey: null,",
"newLineNumber": 164,
"oldLineNumber": null
},
{
"type": "addition",
"content": " artifactId: CI_LOG_ARTIFACT_ID",
"newLineNumber": 165,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 166,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ]",
"newLineNumber": 167,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 168,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(JSON.stringify(detail)).not.toContain(RAW_SECRET);",
"newLineNumber": 169,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 170,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 171,
"oldLineNumber": null
},
{
"type": "addition",
"content": " it(\"keeps raw logs out of default detail responses even for raw-artifact-capable roles\", async () => {",
"newLineNumber": 172,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const detail = await controller.getCiFailureDetail(CI_FAILURE_ID, WORKSPACE_ID, DEVELOPER_USER_ID);",
"newLineNumber": 173,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 174,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(detail.relatedArtifacts.find((artifact) => artifact.artifactType === \"ci_log\")).toMatchObject({",
"newLineNumber": 175,
"oldLineNumber": null
},
{
"type": "addition",
"content": " storageKey: \"artifacts/run-401/ci-log.json\",",
"newLineNumber": 176,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rawAccessAllowed: true,",
"newLineNumber": 177,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rawAccessUrl: `/api/review-runs/00000000-0000-4000-8000-000000000401/artifacts/${CI_LOG_ARTIFACT_ID}/raw`",
"newLineNumber": 178,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 179,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(JSON.stringify(detail)).not.toContain(RAW_SECRET);",
"newLineNumber": 180,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(JSON.stringify(detail)).toContain(\"[REDACTED_SECRET]\");",
"newLineNumber": 181,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 182,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 183,
"oldLineNumber": null
},
{
"type": "addition",
"content": " it(\"denies raw CI log artifact metadata to non-elevated roles through the raw artifact endpoint\", async () => {",
"newLineNumber": 184,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await expect(",
"newLineNumber": 185,
"oldLineNumber": null
},
{
"type": "addition",
"content": " reviewRunsController.getRawArtifactAccess(",
"newLineNumber": 186,
"oldLineNumber": null
},
{
"type": "addition",
"content": " \"00000000-0000-4000-8000-000000000401\",",
"newLineNumber": 187,
"oldLineNumber": null
},
{
"type": "addition",
"content": " CI_LOG_ARTIFACT_ID,",
"newLineNumber": 188,
"oldLineNumber": null
},
{
"type": "addition",
"content": " WORKSPACE_ID,",
"newLineNumber": 189,
"oldLineNumber": null
},
{
"type": "addition",
"content": " VIEWER_USER_ID",
"newLineNumber": 190,
"oldLineNumber": null
},
{
"type": "addition",
"content": " )",
"newLineNumber": 191,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ).rejects.toMatchObject({ status: 403 });",
"newLineNumber": 192,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 193,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 194,
"oldLineNumber": null
},
{
"type": "addition",
"content": " it(\"rejects missing failures, malformed filters, and malformed IDs\", async () => {",
"newLineNumber": 195,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await expect(",
"newLineNumber": 196,
"oldLineNumber": null
},
{
"type": "addition",
"content": " controller.getCiFailureDetail(",
"newLineNumber": 197,
"oldLineNumber": null
},
{
"type": "addition",
"content": " \"00000000-0000-4000-8000-000000009999:ci%3Amissing\",",
"newLineNumber": 198,
"oldLineNumber": null
},
{
"type": "addition",
"content": " WORKSPACE_ID,",
"newLineNumber": 199,
"oldLineNumber": null
},
{
"type": "addition",
"content": " VIEWER_USER_ID",
"newLineNumber": 200,
"oldLineNumber": null
},
{
"type": "addition",
"content": " )",
"newLineNumber": 201,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ).rejects.toThrow(NotFoundException);",
"newLineNumber": 202,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await expect(controller.getCiFailureDetail(\"not-a-ci-failure-id\", WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(",
"newLineNumber": 203,
"oldLineNumber": null
},
{
"type": "addition",
"content": " BadRequestException",
"newLineNumber": 204,
"oldLineNumber": null
},
{
"type": "addition",
"content": " );",
"newLineNumber": 205,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await expect(controller.listCiFailures({ status: \"done\" }, WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(",
"newLineNumber": 206,
"oldLineNumber": null
},
{
"type": "addition",
"content": " BadRequestException",
"newLineNumber": 207,
"oldLineNumber": null
},
{
"type": "addition",
"content": " );",
"newLineNumber": 208,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await expect(controller.listCiFailures({ flaky: \"sometimes\" }, WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(",
"newLineNumber": 209,
"oldLineNumber": null
},
{
"type": "addition",
"content": " BadRequestException",
"newLineNumber": 210,
"oldLineNumber": null
},
{
"type": "addition",
"content": " );",
"newLineNumber": 211,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await expect(controller.listCiFailures({ dateFrom: \"not-a-date\" }, WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(",
"newLineNumber": 212,
"oldLineNumber": null
},
{
"type": "addition",
"content": " BadRequestException",
"newLineNumber": 213,
"oldLineNumber": null
},
{
"type": "addition",
"content": " );",
"newLineNumber": 214,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await expect(",
"newLineNumber": 215,
"oldLineNumber": null
},
{
"type": "addition",
"content": " controller.listCiFailures(",
"newLineNumber": 216,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 217,
"oldLineNumber": null
},
{
"type": "addition",
"content": " dateFrom: \"2026-05-24T00:00:00.000Z\",",
"newLineNumber": 218,
"oldLineNumber": null
},
{
"type": "addition",
"content": " dateTo: \"2026-05-23T00:00:00.000Z\"",
"newLineNumber": 219,
"oldLineNumber": null
},
{
"type": "addition",
"content": " },",
"newLineNumber": 220,
"oldLineNumber": null
},
{
"type": "addition",
"content": " WORKSPACE_ID,",
"newLineNumber": 221,
"oldLineNumber": null
},
{
"type": "addition",
"content": " VIEWER_USER_ID",
"newLineNumber": 222,
"oldLineNumber": null
},
{
"type": "addition",
"content": " )",
"newLineNumber": 223,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ).rejects.toThrow(BadRequestException);",
"newLineNumber": 224,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await expect(controller.listCiFailures({ limit: \"101\" }, WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(",
"newLineNumber": 225,
"oldLineNumber": null
},
{
"type": "addition",
"content": " BadRequestException",
"newLineNumber": 226,
"oldLineNumber": null
},
{
"type": "addition",
"content": " );",
"newLineNumber": 227,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await expect(controller.listCiFailures({ repositoryId: \"not-a-uuid\" }, WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(",
"newLineNumber": 228,
"oldLineNumber": null
},
{
"type": "addition",
"content": " BadRequestException",
"newLineNumber": 229,
"oldLineNumber": null
},
{
"type": "addition",
"content": " );",
"newLineNumber": 230,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 231,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 232,
"oldLineNumber": null
},
{
"type": "addition",
"content": " it(\"enforces dashboard authentication, ownership, and cross-workspace isolation\", async () => {",
"newLineNumber": 233,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await expect(controller.listCiFailures({}, WORKSPACE_ID, undefined)).rejects.toThrow(UnauthorizedException);",
"newLineNumber": 234,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await expect(controller.listCiFailures({}, OTHER_WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(NotFoundException);",
"newLineNumber": 235,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await expect(controller.getCiFailureDetail(OTHER_CI_FAILURE_ID, WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(",
"newLineNumber": 236,
"oldLineNumber": null
},
{
"type": "addition",
"content": " NotFoundException",
"newLineNumber": 237,
"oldLineNumber": null
},
{
"type": "addition",
"content": " );",
"newLineNumber": 238,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 239,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const otherWorkspaceResponse = await controller.listCiFailures({}, OTHER_WORKSPACE_ID, OTHER_VIEWER_USER_ID);",
"newLineNumber": 240,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const workspaceResponse = await controller.listCiFailures({}, WORKSPACE_ID, VIEWER_USER_ID);",
"newLineNumber": 241,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 242,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(otherWorkspaceResponse.ciFailures).toEqual([",
"newLineNumber": 243,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect.objectContaining({",
"newLineNumber": 244,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: OTHER_CI_FAILURE_ID,",
"newLineNumber": 245,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repositoryFullName: \"other/private-roadmap\"",
"newLineNumber": 246,
"oldLineNumber": null
},
{
"type": "addition",
"content": " })",
"newLineNumber": 247,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ]);",
"newLineNumber": 248,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(JSON.stringify(workspaceResponse)).not.toContain(\"other/private-roadmap\");",
"newLineNumber": 249,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(JSON.stringify(workspaceResponse)).not.toContain(\"Deploy private roadmap\");",
"newLineNumber": 250,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 251,
"oldLineNumber": null
},
{
"type": "addition",
"content": "});",
"newLineNumber": 252,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 253,
"oldLineNumber": null
},
{
"type": "addition",
"content": "async function seedCiFailureDashboardData(pool: PgPoolLike): Promise<void> {",
"newLineNumber": 254,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const ciExplanation = {",
"newLineNumber": 255,
"oldLineNumber": null
},
{
"type": "addition",
"content": " schemaVersion: \"ci-failure-explanation/v1\",",
"newLineNumber": 256,
"oldLineNumber": null
},
{
"type": "addition",
"content": " reviewRunId: \"00000000-0000-4000-8000-000000000401\",",
"newLineNumber": 257,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repositoryFullName: \"openclaw/firmcode\",",
"newLineNumber": 258,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pullRequestNumber: 77,",
"newLineNumber": 259,
"oldLineNumber": null
},
{
"type": "addition",
"content": " headSha: \"head-sha-ci\",",
"newLineNumber": 260,
"oldLineNumber": null
},
{
"type": "addition",
"content": " summary:",
"newLineNumber": 261,
"oldLineNumber": null
},
{
"type": "addition",
"content": " \"Found 1 CI failure group across 1 failed job. Most likely: The unit tests job failed in step `npm test` because the log reports: AssertionError: expected 201 to equal 200.\",",
"newLineNumber": 262,
"oldLineNumber": null
},
{
"type": "addition",
"content": " groups: [",
"newLineNumber": 263,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 264,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: \"ci:101:npm-test:abc123def456\",",
"newLineNumber": 265,
"oldLineNumber": null
},
{
"type": "addition",
"content": " jobName: \"unit tests\",",
"newLineNumber": 266,
"oldLineNumber": null
},
{
"type": "addition",
"content": " checkRunId: 101,",
"newLineNumber": 267,
"oldLineNumber": null
},
{
"type": "addition",
"content": " conclusion: \"failure\",",
"newLineNumber": 268,
"oldLineNumber": null
},
{
"type": "addition",
"content": " stepName: \"npm test\",",
"newLineNumber": 269,
"oldLineNumber": null
},
{
"type": "addition",
"content": " category: \"test_failure\",",
"newLineNumber": 270,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rootCauseSummary:",
"newLineNumber": 271,
"oldLineNumber": null
},
{
"type": "addition",
"content": " \"The unit tests job failed in step `npm test` because the log reports: AssertionError: expected 201 to equal 200.\",",
"newLineNumber": 272,
"oldLineNumber": null
},
{
"type": "addition",
"content": " suggestedFixes: [",
"newLineNumber": 273,
"oldLineNumber": null
},
{
"type": "addition",
"content": " \"Reproduce the failing test command locally: `npm test`.\",",
"newLineNumber": 274,
"oldLineNumber": null
},
{
"type": "addition",
"content": " \"Inspect the failing assertion and update either the changed behavior or the expected value.\"",
"newLineNumber": 275,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ],",
"newLineNumber": 276,
"oldLineNumber": null
},
{
"type": "addition",
"content": " flaky: false,",
"newLineNumber": 277,
"oldLineNumber": null
},
{
"type": "addition",
"content": " flakySignals: [],",
"newLineNumber": 278,
"oldLineNumber": null
},
{
"type": "addition",
"content": " evidence: [",
"newLineNumber": 279,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 280,
"oldLineNumber": null
},
{
"type": "addition",
"content": " checkRunId: 101,",
"newLineNumber": 281,
"oldLineNumber": null
},
{
"type": "addition",
"content": " workflowJobId: 303,",
"newLineNumber": 282,
"oldLineNumber": null
},
{
"type": "addition",
"content": " stepName: \"npm test\",",
"newLineNumber": 283,
"oldLineNumber": null
},
{
"type": "addition",
"content": " excerpt: \"FAIL src/payments.test.ts\\nAssertionError: expected 201 to equal 200\\nTOKEN=[REDACTED_SECRET]\"",
"newLineNumber": 284,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 285,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ]",
"newLineNumber": 286,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 287,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ],",
"newLineNumber": 288,
"oldLineNumber": null
},
{
"type": "addition",
"content": " unavailableLogNotes: []",
"newLineNumber": 289,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 290,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const flakyExplanation = {",
"newLineNumber": 291,
"oldLineNumber": null
},
{
"type": "addition",
"content": " schemaVersion: \"ci-failure-explanation/v1\",",
"newLineNumber": 292,
"oldLineNumber": null
},
{
"type": "addition",
"content": " reviewRunId: \"00000000-0000-4000-8000-000000000402\",",
"newLineNumber": 293,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repositoryFullName: \"openclaw/firmcode\",",
"newLineNumber": 294,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pullRequestNumber: 78,",
"newLineNumber": 295,
"oldLineNumber": null
},
{
"type": "addition",
"content": " headSha: \"head-sha-flaky\",",
"newLineNumber": 296,
"oldLineNumber": null
},
{
"type": "addition",
"content": " summary: \"Found 1 CI failure group across 1 failed job. Most likely: an integration timeout may be flaky.\",",
"newLineNumber": 297,
"oldLineNumber": null
},
{
"type": "addition",
"content": " groups: [",
"newLineNumber": 298,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 299,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: \"ci:202:pytest:flaky\",",
"newLineNumber": 300,
"oldLineNumber": null
},
{
"type": "addition",
"content": " jobName: \"integration tests\",",
"newLineNumber": 301,
"oldLineNumber": null
},
{
"type": "addition",
"content": " checkRunId: 202,",
"newLineNumber": 302,
"oldLineNumber": null
},
{
"type": "addition",
"content": " conclusion: \"timed_out\",",
"newLineNumber": 303,
"oldLineNumber": null
},
{
"type": "addition",
"content": " stepName: \"pytest apps/api/tests\",",
"newLineNumber": 304,
"oldLineNumber": null
},
{
"type": "addition",
"content": " category: \"timeout\",",
"newLineNumber": 305,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rootCauseSummary: \"The integration tests job timed out waiting for a service health check.\",",
"newLineNumber": 306,
"oldLineNumber": null
},
{
"type": "addition",
"content": " suggestedFixes: [",
"newLineNumber": 307,
"oldLineNumber": null
},
{
"type": "addition",
"content": " \"Rerun the failed job once; if it passes, harden or quarantine the unstable test instead of masking it.\"",
"newLineNumber": 308,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ],",
"newLineNumber": 309,
"oldLineNumber": null
},
{
"type": "addition",
"content": " flaky: true,",
"newLineNumber": 310,
"oldLineNumber": null
},
{
"type": "addition",
"content": " flakySignals: [{ signal: \"explicit_flaky\", detail: \"The same test passed on retry.\", confidence: 0.8 }],",
"newLineNumber": 311,
"oldLineNumber": null
},
{
"type": "addition",
"content": " evidence: [",
"newLineNumber": 312,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 313,
"oldLineNumber": null
},
{
"type": "addition",
"content": " checkRunId: 202,",
"newLineNumber": 314,
"oldLineNumber": null
},
{
"type": "addition",
"content": " workflowJobId: 404,",
"newLineNumber": 315,
"oldLineNumber": null
},
{
"type": "addition",
"content": " stepName: \"pytest apps/api/tests\",",
"newLineNumber": 316,
"oldLineNumber": null
},
{
"type": "addition",
"content": " excerpt: \"TimeoutError: waited 60000ms for http://localhost:5432/health\"",
"newLineNumber": 317,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 318,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ]",
"newLineNumber": 319,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 320,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ],",
"newLineNumber": 321,
"oldLineNumber": null
},
{
"type": "addition",
"content": " unavailableLogNotes: []",
"newLineNumber": 322,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 323,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const otherExplanation = {",
"newLineNumber": 324,
"oldLineNumber": null
},
{
"type": "addition",
"content": " schemaVersion: \"ci-failure-explanation/v1\",",
"newLineNumber": 325,
"oldLineNumber": null
},
{
"type": "addition",
"content": " reviewRunId: \"00000000-0000-4000-8000-000000000403\",",
"newLineNumber": 326,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repositoryFullName: \"other/private-roadmap\",",
"newLineNumber": 327,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pullRequestNumber: 99,",
"newLineNumber": 328,
"oldLineNumber": null
},
{
"type": "addition",
"content": " headSha: \"head-sha-private\",",
"newLineNumber": 329,
"oldLineNumber": null
},
{
"type": "addition",
"content": " summary: \"Deploy private roadmap failed.\",",
"newLineNumber": 330,
"oldLineNumber": null
},
{
"type": "addition",
"content": " groups: [",
"newLineNumber": 331,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 332,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: \"ci:303:deploy:private\",",
"newLineNumber": 333,
"oldLineNumber": null
},
{
"type": "addition",
"content": " jobName: \"Deploy private roadmap\",",
"newLineNumber": 334,
"oldLineNumber": null
},
{
"type": "addition",
"content": " checkRunId: 303,",
"newLineNumber": 335,
"oldLineNumber": null
},
{
"type": "addition",
"content": " conclusion: \"failure\",",
"newLineNumber": 336,
"oldLineNumber": null
},
{
"type": "addition",
"content": " stepName: \"deploy\",",
"newLineNumber": 337,
"oldLineNumber": null
},
{
"type": "addition",
"content": " category: \"infrastructure\",",
"newLineNumber": 338,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rootCauseSummary: \"Deploy private roadmap failed.\",",
"newLineNumber": 339,
"oldLineNumber": null
},
{
"type": "addition",
"content": " suggestedFixes: [\"Check the private deployment credentials.\"],",
"newLineNumber": 340,
"oldLineNumber": null
},
{
"type": "addition",
"content": " flaky: false,",
"newLineNumber": 341,
"oldLineNumber": null
},
{
"type": "addition",
"content": " flakySignals: [],",
"newLineNumber": 342,
"oldLineNumber": null
},
{
"type": "addition",
"content": " evidence: [{ checkRunId: 303, workflowJobId: 505, stepName: \"deploy\", excerpt: \"Deployment failed.\" }]",
"newLineNumber": 343,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 344,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ],",
"newLineNumber": 345,
"oldLineNumber": null
},
{
"type": "addition",
"content": " unavailableLogNotes: []",
"newLineNumber": 346,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 347,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const ciLogArtifact = {",
"newLineNumber": 348,
"oldLineNumber": null
},
{
"type": "addition",
"content": " schemaVersion: \"ci-log-artifact/v1\",",
"newLineNumber": 349,
"oldLineNumber": null
},
{
"type": "addition",
"content": " reviewRunId: \"00000000-0000-4000-8000-000000000401\",",
"newLineNumber": 350,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repositoryFullName: \"openclaw/firmcode\",",
"newLineNumber": 351,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pullRequestNumber: 77,",
"newLineNumber": 352,
"oldLineNumber": null
},
{
"type": "addition",
"content": " headSha: \"head-sha-ci\",",
"newLineNumber": 353,
"oldLineNumber": null
},
{
"type": "addition",
"content": " checkRuns: [",
"newLineNumber": 354,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 355,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: 101,",
"newLineNumber": 356,
"oldLineNumber": null
},
{
"type": "addition",
"content": " name: \"unit tests\",",
"newLineNumber": 357,
"oldLineNumber": null
},
{
"type": "addition",
"content": " status: \"completed\",",
"newLineNumber": 358,
"oldLineNumber": null
},
{
"type": "addition",
"content": " conclusion: \"failure\",",
"newLineNumber": 359,
"oldLineNumber": null
},
{
"type": "addition",
"content": " appSlug: \"github-actions\",",
"newLineNumber": 360,
"oldLineNumber": null
},
{
"type": "addition",
"content": " detailsUrl: \"https://github.com/openclaw/firmcode/actions/runs/202/jobs/303\",",
"newLineNumber": 361,
"oldLineNumber": null
},
{
"type": "addition",
"content": " htmlUrl: \"https://github.com/openclaw/firmcode/actions/runs/202/job/303\",",
"newLineNumber": 362,
"oldLineNumber": null
},
{
"type": "addition",
"content": " workflowRunId: 202,",
"newLineNumber": 363,
"oldLineNumber": null
},
{
"type": "addition",
"content": " workflowJobId: 303,",
"newLineNumber": 364,
"oldLineNumber": null
},
{
"type": "addition",
"content": " startedAt: \"2026-05-23T10:00:00Z\",",
"newLineNumber": 365,
"oldLineNumber": null
},
{
"type": "addition",
"content": " completedAt: \"2026-05-23T10:05:00Z\"",
"newLineNumber": 366,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 367,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ],",
"newLineNumber": 368,
"oldLineNumber": null
},
{
"type": "addition",
"content": " logs: [",
"newLineNumber": 369,
"oldLineNumber": null
},
{
"type": "addition",
"content": " {",
"newLineNumber": 370,
"oldLineNumber": null
},
{
"type": "addition",
"content": " checkRunId: 101,",
"newLineNumber": 371,
"oldLineNumber": null
},
{
"type": "addition",
"content": " name: \"unit tests\",",
"newLineNumber": 372,
"oldLineNumber": null
},
{
"type": "addition",
"content": " source: \"github_actions_job\",",
"newLineNumber": 373,
"oldLineNumber": null
},
{
"type": "addition",
"content": " workflowRunId: 202,",
"newLineNumber": 374,
"oldLineNumber": null
},
{
"type": "addition",
"content": " workflowJobId: 303,",
"newLineNumber": 375,
"oldLineNumber": null
},
{
"type": "addition",
"content": " content: `${RAW_SECRET}\\nFAIL src/payments.test.ts\\nAssertionError: expected 201 to equal 200`,",
"newLineNumber": 376,
"oldLineNumber": null
},
{
"type": "addition",
"content": " originalBytes: 91,",
"newLineNumber": 377,
"oldLineNumber": null
},
{
"type": "addition",
"content": " redactedBytes: 72,",
"newLineNumber": 378,
"oldLineNumber": null
},
{
"type": "addition",
"content": " storedBytes: 72,",
"newLineNumber": 379,
"oldLineNumber": null
},
{
"type": "addition",
"content": " truncated: false,",
"newLineNumber": 380,
"oldLineNumber": null
},
{
"type": "addition",
"content": " redacted: true",
"newLineNumber": 381,
"oldLineNumber": null
},
{
"type": "addition",
"content": " }",
"newLineNumber": 382,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ],",
"newLineNumber": 383,
"oldLineNumber": null
},
{
"type": "addition",
"content": " unavailableLogs: []",
"newLineNumber": 384,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 385,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 386,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await pool.query(",
"newLineNumber": 387,
"oldLineNumber": null
},
{
"type": "addition",
"content": " `",
"newLineNumber": 388,
"oldLineNumber": null
},
{
"type": "addition",
"content": "INSERT INTO workspaces (id, clerk_org_id, name) VALUES",
"newLineNumber": 389,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('${WORKSPACE_ID}', 'org_firmcode', 'Firmcode'),",
"newLineNumber": 390,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('${OTHER_WORKSPACE_ID}', 'org_other', 'Other');",
"newLineNumber": 391,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 392,
"oldLineNumber": null
},
{
"type": "addition",
"content": "INSERT INTO workspace_memberships (workspace_id, clerk_user_id, role, active) VALUES",
"newLineNumber": 393,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('${WORKSPACE_ID}', '${DEVELOPER_USER_ID}', 'developer', true),",
"newLineNumber": 394,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('${WORKSPACE_ID}', '${VIEWER_USER_ID}', 'viewer', true),",
"newLineNumber": 395,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('${OTHER_WORKSPACE_ID}', '${OTHER_VIEWER_USER_ID}', 'viewer', true);",
"newLineNumber": 396,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 397,
"oldLineNumber": null
},
{
"type": "addition",
"content": "INSERT INTO github_installations (id, workspace_id, installation_id, account_login, permissions_json) VALUES",
"newLineNumber": 398,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('00000000-0000-4000-8000-000000000111', '${WORKSPACE_ID}', 111, 'openclaw', '{\"pull_requests\":\"write\",\"checks\":\"read\",\"actions\":\"read\"}'),",
"newLineNumber": 399,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('00000000-0000-4000-8000-000000000112', '${OTHER_WORKSPACE_ID}', 112, 'other', '{\"pull_requests\":\"write\",\"checks\":\"read\",\"actions\":\"read\"}');",
"newLineNumber": 400,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 401,
"oldLineNumber": null
},
{
"type": "addition",
"content": "INSERT INTO repositories (id, installation_id, github_repository_id, owner, name, full_name, private, default_branch, enabled) VALUES",
"newLineNumber": 402,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('00000000-0000-4000-8000-000000000201', '00000000-0000-4000-8000-000000000111', 201, 'openclaw', 'firmcode', 'openclaw/firmcode', false, 'main', true),",
"newLineNumber": 403,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('00000000-0000-4000-8000-000000000202', '00000000-0000-4000-8000-000000000112', 202, 'other', 'private-roadmap', 'other/private-roadmap', true, 'main', true);",
"newLineNumber": 404,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 405,
"oldLineNumber": null
},
{
"type": "addition",
"content": "INSERT INTO pull_requests (",
"newLineNumber": 406,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id,",
"newLineNumber": 407,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repository_id,",
"newLineNumber": 408,
"oldLineNumber": null
},
{
"type": "addition",
"content": " github_pr_id,",
"newLineNumber": 409,
"oldLineNumber": null
},
{
"type": "addition",
"content": " number,",
"newLineNumber": 410,
"oldLineNumber": null
},
{
"type": "addition",
"content": " title,",
"newLineNumber": 411,
"oldLineNumber": null
},
{
"type": "addition",
"content": " author_login,",
"newLineNumber": 412,
"oldLineNumber": null
},
{
"type": "addition",
"content": " base_ref,",
"newLineNumber": 413,
"oldLineNumber": null
},
{
"type": "addition",
"content": " head_ref,",
"newLineNumber": 414,
"oldLineNumber": null
},
{
"type": "addition",
"content": " base_sha,",
"newLineNumber": 415,
"oldLineNumber": null
},
{
"type": "addition",
"content": " head_sha,",
"newLineNumber": 416,
"oldLineNumber": null
},
{
"type": "addition",
"content": " state,",
"newLineNumber": 417,
"oldLineNumber": null
},
{
"type": "addition",
"content": " draft,",
"newLineNumber": 418,
"oldLineNumber": null
},
{
"type": "addition",
"content": " created_at,",
"newLineNumber": 419,
"oldLineNumber": null
},
{
"type": "addition",
"content": " updated_at",
"newLineNumber": 420,
"oldLineNumber": null
},
{
"type": "addition",
"content": ") VALUES",
"newLineNumber": 421,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('00000000-0000-4000-8000-000000000301', '00000000-0000-4000-8000-000000000201', 301, 77, 'Fix payment tests', 'kelly', 'main', 'feature/payments', 'base-sha', 'head-sha-ci', 'open', false, '2026-05-23T09:55:00.000Z', '2026-05-23T10:05:00.000Z'),",
"newLineNumber": 422,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('00000000-0000-4000-8000-000000000302', '00000000-0000-4000-8000-000000000201', 302, 78, 'Harden integration tests', 'kelly', 'main', 'feature/flaky', 'base-sha', 'head-sha-flaky', 'open', false, '2026-05-22T09:55:00.000Z', '2026-05-22T10:05:00.000Z'),",
"newLineNumber": 423,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('00000000-0000-4000-8000-000000000303', '00000000-0000-4000-8000-000000000202', 303, 99, 'Secret acquisition roadmap', 'mallory', 'main', 'feature/private', 'base-sha', 'head-sha-private', 'open', false, '2026-05-23T08:00:00.000Z', '2026-05-23T08:05:00.000Z');",
"newLineNumber": 424,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 425,
"oldLineNumber": null
},
{
"type": "addition",
"content": "INSERT INTO github_deliveries (delivery_id, event_name, action) VALUES",
"newLineNumber": 426,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('delivery-ci', 'check_run', 'completed'),",
"newLineNumber": 427,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('delivery-flaky', 'check_run', 'completed'),",
"newLineNumber": 428,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('delivery-other-ci', 'check_run', 'completed');",
"newLineNumber": 429,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 430,
"oldLineNumber": null
},
{
"type": "addition",
"content": "INSERT INTO review_runs (",
"newLineNumber": 431,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id,",
"newLineNumber": 432,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repository_id,",
"newLineNumber": 433,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pull_request_id,",
"newLineNumber": 434,
"oldLineNumber": null
},
{
"type": "addition",
"content": " delivery_id,",
"newLineNumber": 435,
"oldLineNumber": null
},
{
"type": "addition",
"content": " trigger_event,",
"newLineNumber": 436,
"oldLineNumber": null
},
{
"type": "addition",
"content": " head_sha,",
"newLineNumber": 437,
"oldLineNumber": null
},
{
"type": "addition",
"content": " status,",
"newLineNumber": 438,
"oldLineNumber": null
},
{
"type": "addition",
"content": " started_at,",
"newLineNumber": 439,
"oldLineNumber": null
},
{
"type": "addition",
"content": " finished_at,",
"newLineNumber": 440,
"oldLineNumber": null
},
{
"type": "addition",
"content": " created_at,",
"newLineNumber": 441,
"oldLineNumber": null
},
{
"type": "addition",
"content": " updated_at",
"newLineNumber": 442,
"oldLineNumber": null
},
{
"type": "addition",
"content": ") VALUES",
"newLineNumber": 443,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('00000000-0000-4000-8000-000000000401', '00000000-0000-4000-8000-000000000201', '00000000-0000-4000-8000-000000000301', 'delivery-ci', 'check_run.completed', 'head-sha-ci', 'succeeded', '2026-05-23T10:00:00.000Z', '2026-05-23T10:06:00.000Z', '2026-05-23T10:00:00.000Z', '2026-05-23T10:06:00.000Z'),",
"newLineNumber": 444,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('00000000-0000-4000-8000-000000000402', '00000000-0000-4000-8000-000000000201', '00000000-0000-4000-8000-000000000302', 'delivery-flaky', 'check_run.completed', 'head-sha-flaky', 'succeeded', '2026-05-22T10:00:00.000Z', '2026-05-22T10:06:00.000Z', '2026-05-22T10:00:00.000Z', '2026-05-22T10:06:00.000Z'),",
"newLineNumber": 445,
"oldLineNumber": null
},
{
"type": "addition",
"content": "('00000000-0000-4000-8000-000000000403', '00000000-0000-4000-8000-000000000202', '00000000-0000-4000-8000-000000000303', 'delivery-other-ci', 'check_run.completed', 'head-sha-private', 'succeeded', '2026-05-23T08:00:00.000Z', '2026-05-23T08:06:00.000Z', '2026-05-23T08:00:00.000Z', '2026-05-23T08:06:00.000Z');",
"newLineNumber": 446,
"oldLineNumber": null
},
{
"type": "addition",
"content": "`",
"newLineNumber": 447,
"oldLineNumber": null
},
{
"type": "addition",
"content": " );",
"newLineNumber": 448,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 449,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await pool.query(",
"newLineNumber": 450,
"oldLineNumber": null
},
{
"type": "addition",
"content": " `",
"newLineNumber": 451,
"oldLineNumber": null
},
{
"type": "addition",
"content": "INSERT INTO analysis_artifacts (id, review_run_id, artifact_type, storage_key, metadata_json, created_at) VALUES",
"newLineNumber": 452,
"oldLineNumber": null
},
{
"type": "addition",
"content": "($1, '00000000-0000-4000-8000-000000000401', 'ci_failure_explanation', 'artifacts/run-401/ci-failure-explanation.json', $2::jsonb, '2026-05-23T10:06:00.000Z'),",
"newLineNumber": 453,
"oldLineNumber": null
},
{
"type": "addition",
"content": "($3, '00000000-0000-4000-8000-000000000401', 'ci_log', 'artifacts/run-401/ci-log.json', $4::jsonb, '2026-05-23T10:05:00.000Z'),",
"newLineNumber": 454,
"oldLineNumber": null
},
{
"type": "addition",
"content": "($5, '00000000-0000-4000-8000-000000000402', 'ci_failure_explanation', 'artifacts/run-402/ci-failure-explanation.json', $6::jsonb, '2026-05-22T10:06:00.000Z'),",
"newLineNumber": 455,
"oldLineNumber": null
},
{
"type": "addition",
"content": "($7, '00000000-0000-4000-8000-000000000403', 'ci_failure_explanation', 'artifacts/run-403/ci-failure-explanation.json', $8::jsonb, '2026-05-23T08:06:00.000Z')",
"newLineNumber": 456,
"oldLineNumber": null
},
{
"type": "addition",
"content": "`,",
"newLineNumber": 457,
"oldLineNumber": null
},
{
"type": "addition",
"content": " [",
"newLineNumber": 458,
"oldLineNumber": null
},
{
"type": "addition",
"content": " EXPLANATION_ARTIFACT_ID,",
"newLineNumber": 459,
"oldLineNumber": null
},
{
"type": "addition",
"content": " JSON.stringify({ artifact: ciExplanation }),",
"newLineNumber": 460,
"oldLineNumber": null
},
{
"type": "addition",
"content": " CI_LOG_ARTIFACT_ID,",
"newLineNumber": 461,
"oldLineNumber": null
},
{
"type": "addition",
"content": " JSON.stringify({ artifact: ciLogArtifact }),",
"newLineNumber": 462,
"oldLineNumber": null
},
{
"type": "addition",
"content": " FLAKY_EXPLANATION_ARTIFACT_ID,",
"newLineNumber": 463,
"oldLineNumber": null
},
{
"type": "addition",
"content": " JSON.stringify({ artifact: flakyExplanation }),",
"newLineNumber": 464,
"oldLineNumber": null
},
{
"type": "addition",
"content": " OTHER_EXPLANATION_ARTIFACT_ID,",
"newLineNumber": 465,
"oldLineNumber": null
},
{
"type": "addition",
"content": " JSON.stringify({ artifact: otherExplanation })",
"newLineNumber": 466,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ]",
"newLineNumber": 467,
"oldLineNumber": null
},
{
"type": "addition",
"content": " );",
"newLineNumber": 468,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 469,
"oldLineNumber": null
}
],
"newStart": 1,
"oldStart": 0,
"newLineCount": 469,
"oldLineCount": 0,
"sectionHeader": ""
}
],
"patch": "@@ -0,0 +1,469 @@\n+import { BadRequestException, NotFoundException, UnauthorizedException } from \"@nestjs/common\";\n+import { newDb } from \"pg-mem\";\n+import { runDatabaseMigrations } from \"../src/infrastructure/database/migrations\";\n+import { CiFailuresController } from \"../src/modules/ci-failures/ci-failures.controller\";\n+import { PostgresCiFailuresStore } from \"../src/modules/ci-failures/ci-failures.store\";\n+import { PostgresDashboardAuthStore } from \"../src/modules/review-runs/dashboard-auth.store\";\n+import { ReviewRunsController } from \"../src/modules/review-runs/review-runs.controller\";\n+import { PostgresReviewRunsStore } from \"../src/modules/review-runs/review-runs.store\";\n+\n+interface PgPoolLike {\n+ query<Row = unknown>(sql: string, values?: readonly unknown[]): Promise<{ rows: Row[] }>;\n+ end(): Promise<void>;\n+}\n+\n+const WORKSPACE_ID = \"00000000-0000-4000-8000-000000000101\";\n+const OTHER_WORKSPACE_ID = \"00000000-0000-4000-8000-000000000102\";\n+const DEVELOPER_USER_ID = \"user_developer\";\n+const VIEWER_USER_ID = \"user_viewer\";\n+const OTHER_VIEWER_USER_ID = \"user_other_viewer\";\n+const EXPLANATION_ARTIFACT_ID = \"00000000-0000-4000-8000-000000000501\";\n+const CI_LOG_ARTIFACT_ID = \"00000000-0000-4000-8000-000000000502\";\n+const FLAKY_EXPLANATION_ARTIFACT_ID = \"00000000-0000-4000-8000-000000000503\";\n+const OTHER_EXPLANATION_ARTIFACT_ID = \"00000000-0000-4000-8000-000000000504\";\n+const CI_FAILURE_ID = `${EXPLANATION_ARTIFACT_ID}:ci%3A101%3Anpm-test%3Aabc123def456`;\n+const OTHER_CI_FAILURE_ID = `${OTHER_EXPLANATION_ARTIFACT_ID}:ci%3A303%3Adeploy%3Aprivate`;\n+const RAW_SECRET = \"RAW_TOKEN=super-secret-value\";\n+\n+function createTestPool(): PgPoolLike {\n+ const db = newDb({ autoCreateForeignKeyIndices: true, noAstCoverageCheck: true });\n+ const adapters = db.adapters.createPg();\n+\n+ return new adapters.Pool();\n+}\n+\n+describe(\"CI failures dashboard API\", () => {\n+ let pool: PgPoolLike;\n+ let controller: CiFailuresController;\n+ let reviewRunsController: ReviewRunsController;\n+\n+ beforeEach(async () => {\n+ pool = createTestPool();\n+ await runDatabaseMigrations(pool);\n+ await seedCiFailureDashboardData(pool);\n+\n+ const dashboardAuthStore = new PostgresDashboardAuthStore(pool);\n+ controller = new CiFailuresController(new PostgresCiFailuresStore(pool), dashboardAuthStore);\n+ reviewRunsController = new ReviewRunsController(new PostgresReviewRunsStore(pool), dashboardAuthStore);\n+ });\n+\n+ afterEach(async () => {\n+ await pool.end();\n+ });\n+\n+ it(\"lists workspace-scoped CI failures with repository, PR, failed job, root cause, flaky status, suggested fix, status, and created time\", async () => {\n+ const response = await controller.listCiFailures(\n+ {\n+ repository: \"openclaw/firmcode\",\n+ status: \"succeeded\",\n+ flaky: \"false\",\n+ dateFrom: \"2026-05-22T00:00:00.000Z\",\n+ dateTo: \"2026-05-24T00:00:00.000Z\",\n+ limit: \"1\"\n+ },\n+ WORKSPACE_ID,\n+ VIEWER_USER_ID\n+ );\n+\n+ expect(response).toMatchObject({\n+ filters: {\n+ repository: \"openclaw/firmcode\",\n+ status: \"succeeded\",\n+ flaky: false,\n+ limit: 1\n+ },\n+ pagination: {\n+ limit: 1,\n+ returned: 1\n+ }\n+ });\n+ expect(response.ciFailures).toEqual([\n+ expect.objectContaining({\n+ id: CI_FAILURE_ID,\n+ repositoryFullName: \"openclaw/firmcode\",\n+ pullRequestNumber: 77,\n+ pullRequestTitle: \"Fix payment tests\",\n+ reviewRunId: \"00000000-0000-4000-8000-000000000401\",\n+ failedJob: expect.objectContaining({\n+ jobName: \"unit tests\",\n+ checkRunId: 101,\n+ stepName: \"npm test\",\n+ detailsUrl: \"https://github.com/openclaw/firmcode/actions/runs/202/jobs/303\"\n+ }),\n+ rootCauseSummary:\n+ \"The unit tests job failed in step `npm test` because the log reports: AssertionError: expected 201 to equal 200.\",\n+ flakySuspected: false,\n+ suggestedFix: \"Reproduce the failing test command locally: `npm test`.\",\n+ status: \"succeeded\",\n+ createdAt: \"2026-05-23T10:06:00.000Z\"\n+ })\n+ ]);\n+ expect(JSON.stringify(response)).not.toContain(RAW_SECRET);\n+ });\n+\n+ it(\"filters flaky CI failures independently from SQL-backed filters\", async () => {\n+ const response = await controller.listCiFailures({ flaky: \"true\" }, WORKSPACE_ID, VIEWER_USER_ID);\n+\n+ expect(response.ciFailures).toHaveLength(1);\n+ expect(response.ciFailures[0]).toMatchObject({\n+ id: `${FLAKY_EXPLANATION_ARTIFACT_ID}:ci%3A202%3Apytest%3Aflaky`,\n+ flakySuspected: true,\n+ suggestedFix: \"Rerun the failed job once; if it passes, harden or quarantine the unstable test instead of masking it.\"\n+ });\n+ });\n+\n+ it(\"returns CI failure detail with suggested fixes, failed jobs, related links, and collapsed redacted log excerpts\", async () => {\n+ const detail = await controller.getCiFailureDetail(CI_FAILURE_ID, WORKSPACE_ID, VIEWER_USER_ID);\n+\n+ expect(detail).toMatchObject({\n+ id: CI_FAILURE_ID,\n+ rootCause:\n+ \"The unit tests job failed in step `npm test` because the log reports: AssertionError: expected 201 to equal 200.\",\n+ suggestedFixes: [\n+ {\n+ id: `${CI_FAILURE_ID}:fix:1`,\n+ text: \"Reproduce the failing test command locally: `npm test`.\"\n+ },\n+ {\n+ id: `${CI_FAILURE_ID}:fix:2`,\n+ text: \"Inspect the failing assertion and update either the changed behavior or the expected value.\"\n+ }\n+ ],\n+ failedJobs: [\n+ {\n+ jobName: \"unit tests\",\n+ checkRunId: 101,\n+ conclusion: \"failure\",\n+ category: \"test_failure\"\n+ }\n+ ],\n+ relatedReviewRun: {\n+ id: \"00000000-0000-4000-8000-000000000401\",\n+ status: \"succeeded\",\n+ detailUrl: \"/api/review-runs/00000000-0000-4000-8000-000000000401\"\n+ },\n+ relatedArtifacts: [\n+ {\n+ artifactType: \"ci_failure_explanation\",\n+ storageKey: null,\n+ rawAccessAllowed: false,\n+ rawAccessUrl: null\n+ },\n+ {\n+ artifactType: \"ci_log\",\n+ storageKey: null,\n+ rawAccessAllowed: false,\n+ rawAccessUrl: null\n+ }\n+ ],\n+ logExcerpts: [\n+ {\n+ excerpt: \"FAIL src/payments.test.ts\\nAssertionError: expected 201 to equal 200\\nTOKEN=[REDACTED_SECRET]\",\n+ redacted: true,\n+ collapsed: true,\n+ storageKey: null,\n+ artifactId: CI_LOG_ARTIFACT_ID\n+ }\n+ ]\n+ });\n+ expect(JSON.stringify(detail)).not.toContain(RAW_SECRET);\n+ });\n+\n+ it(\"keeps raw logs out of default detail responses even for raw-artifact-capable roles\", async () => {\n+ const detail = await controller.getCiFailureDetail(CI_FAILURE_ID, WORKSPACE_ID, DEVELOPER_USER_ID);\n+\n+ expect(detail.relatedArtifacts.find((artifact) => artifact.artifactType === \"ci_log\")).toMatchObject({\n+ storageKey: \"artifacts/run-401/ci-log.json\",\n+ rawAccessAllowed: true,\n+ rawAccessUrl: `/api/review-runs/00000000-0000-4000-8000-000000000401/artifacts/${CI_LOG_ARTIFACT_ID}/raw`\n+ });\n+ expect(JSON.stringify(detail)).not.toContain(RAW_SECRET);\n+ expect(JSON.stringify(detail)).toContain(\"[REDACTED_SECRET]\");\n+ });\n+\n+ it(\"denies raw CI log artifact metadata to non-elevated roles through the raw artifact endpoint\", async () => {\n+ await expect(\n+ reviewRunsController.getRawArtifactAccess(\n+ \"00000000-0000-4000-8000-000000000401\",\n+ CI_LOG_ARTIFACT_ID,\n+ WORKSPACE_ID,\n+ VIEWER_USER_ID\n+ )\n+ ).rejects.toMatchObject({ status: 403 });\n+ });\n+\n+ it(\"rejects missing failures, malformed filters, and malformed IDs\", async () => {\n+ await expect(\n+ controller.getCiFailureDetail(\n+ \"00000000-0000-4000-8000-000000009999:ci%3Amissing\",\n+ WORKSPACE_ID,\n+ VIEWER_USER_ID\n+ )\n+ ).rejects.toThrow(NotFoundException);\n+ await expect(controller.getCiFailureDetail(\"not-a-ci-failure-id\", WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(\n+ BadRequestException\n+ );\n+ await expect(controller.listCiFailures({ status: \"done\" }, WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(\n+ BadRequestException\n+ );\n+ await expect(controller.listCiFailures({ flaky: \"sometimes\" }, WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(\n+ BadRequestException\n+ );\n+ await expect(controller.listCiFailures({ dateFrom: \"not-a-date\" }, WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(\n+ BadRequestException\n+ );\n+ await expect(\n+ controller.listCiFailures(\n+ {\n+ dateFrom: \"2026-05-24T00:00:00.000Z\",\n+ dateTo: \"2026-05-23T00:00:00.000Z\"\n+ },\n+ WORKSPACE_ID,\n+ VIEWER_USER_ID\n+ )\n+ ).rejects.toThrow(BadRequestException);\n+ await expect(controller.listCiFailures({ limit: \"101\" }, WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(\n+ BadRequestException\n+ );\n+ await expect(controller.listCiFailures({ repositoryId: \"not-a-uuid\" }, WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(\n+ BadRequestException\n+ );\n+ });\n+\n+ it(\"enforces dashboard authentication, ownership, and cross-workspace isolation\", async () => {\n+ await expect(controller.listCiFailures({}, WORKSPACE_ID, undefined)).rejects.toThrow(UnauthorizedException);\n+ await expect(controller.listCiFailures({}, OTHER_WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(NotFoundException);\n+ await expect(controller.getCiFailureDetail(OTHER_CI_FAILURE_ID, WORKSPACE_ID, VIEWER_USER_ID)).rejects.toThrow(\n+ NotFoundException\n+ );\n+\n+ const otherWorkspaceResponse = await controller.listCiFailures({}, OTHER_WORKSPACE_ID, OTHER_VIEWER_USER_ID);\n+ const workspaceResponse = await controller.listCiFailures({}, WORKSPACE_ID, VIEWER_USER_ID);\n+\n+ expect(otherWorkspaceResponse.ciFailures).toEqual([\n+ expect.objectContaining({\n+ id: OTHER_CI_FAILURE_ID,\n+ repositoryFullName: \"other/private-roadmap\"\n+ })\n+ ]);\n+ expect(JSON.stringify(workspaceResponse)).not.toContain(\"other/private-roadmap\");\n+ expect(JSON.stringify(workspaceResponse)).not.toContain(\"Deploy private roadmap\");\n+ });\n+});\n+\n+async function seedCiFailureDashboardData(pool: PgPoolLike): Promise<void> {\n+ const ciExplanation = {\n+ schemaVersion: \"ci-failure-explanation/v1\",\n+ reviewRunId: \"00000000-0000-4000-8000-000000000401\",\n+ repositoryFullName: \"openclaw/firmcode\",\n+ pullRequestNumber: 77,\n+ headSha: \"head-sha-ci\",\n+ summary:\n+ \"Found 1 CI failure group across 1 failed job. Most likely: The unit tests job failed in step `npm test` because the log reports: AssertionError: expected 201 to equal 200.\",\n+ groups: [\n+ {\n+ id: \"ci:101:npm-test:abc123def456\",\n+ jobName: \"unit tests\",\n+ checkRunId: 101,\n+ conclusion: \"failure\",\n+ stepName: \"npm test\",\n+ category: \"test_failure\",\n+ rootCauseSummary:\n+ \"The unit tests job failed in step `npm test` because the log reports: AssertionError: expected 201 to equal 200.\",\n+ suggestedFixes: [\n+ \"Reproduce the failing test command locally: `npm test`.\",\n+ \"Inspect the failing assertion and update either the changed behavior or the expected value.\"\n+ ],\n+ flaky: false,\n+ flakySignals: [],\n+ evidence: [\n+ {\n+ checkRunId: 101,\n+ workflowJobId: 303,\n+ stepName: \"npm test\",\n+ excerpt: \"FAIL src/payments.test.ts\\nAssertionError: expected 201 to equal 200\\nTOKEN=[REDACTED_SECRET]\"\n+ }\n+ ]\n+ }\n+ ],\n+ unavailableLogNotes: []\n+ };\n+ const flakyExplanation = {\n+ schemaVersion: \"ci-failure-explanation/v1\",\n+ reviewRunId: \"00000000-0000-4000-8000-000000000402\",\n+ repositoryFullName: \"openclaw/firmcode\",\n+ pullRequestNumber: 78,\n+ headSha: \"head-sha-flaky\",\n+ summary: \"Found 1 CI failure group across 1 failed job. Most likely: an integration timeout may be flaky.\",\n+ groups: [\n+ {\n+ id: \"ci:202:pytest:flaky\",\n+ jobName: \"integration tests\",\n+ checkRunId: 202,\n+ conclusion: \"timed_out\",\n+ stepName: \"pytest apps/api/tests\",\n+ category: \"timeout\",\n+ rootCauseSummary: \"The integration tests job timed out waiting for a service health check.\",\n+ suggestedFixes: [\n+ \"Rerun the failed job once; if it passes, harden or quarantine the unstable test instead of masking it.\"\n+ ],\n+ flaky: true,\n+ flakySignals: [{ signal: \"explicit_flaky\", detail: \"The same test passed on retry.\", confidence: 0.8 }],\n+ evidence: [\n+ {\n+ checkRunId: 202,\n+ workflowJobId: 404,\n+ stepName: \"pytest apps/api/tests\",\n+ excerpt: \"TimeoutError: waited 60000ms for http://localhost:5432/health\"\n+ }\n+ ]\n+ }\n+ ],\n+ unavailableLogNotes: []\n+ };\n+ const otherExplanation = {\n+ schemaVersion: \"ci-failure-explanation/v1\",\n+ reviewRunId: \"00000000-0000-4000-8000-000000000403\",\n+ repositoryFullName: \"other/private-roadmap\",\n+ pullRequestNumber: 99,\n+ headSha: \"head-sha-private\",\n+ summary: \"Deploy private roadmap failed.\",\n+ groups: [\n+ {\n+ id: \"ci:303:deploy:private\",\n+ jobName: \"Deploy private roadmap\",\n+ checkRunId: 303,\n+ conclusion: \"failure\",\n+ stepName: \"deploy\",\n+ category: \"infrastructure\",\n+ rootCauseSummary: \"Deploy private roadmap failed.\",\n+ suggestedFixes: [\"Check the private deployment credentials.\"],\n+ flaky: false,\n+ flakySignals: [],\n+ evidence: [{ checkRunId: 303, workflowJobId: 505, stepName: \"deploy\", excerpt: \"Deployment failed.\" }]\n+ }\n+ ],\n+ unavailableLogNotes: []\n+ };\n+ const ciLogArtifact = {\n+ schemaVersion: \"ci-log-artifact/v1\",\n+ reviewRunId: \"00000000-0000-4000-8000-000000000401\",\n+ repositoryFullName: \"openclaw/firmcode\",\n+ pullRequestNumber: 77,\n+ headSha: \"head-sha-ci\",\n+ checkRuns: [\n+ {\n+ id: 101,\n+ name: \"unit tests\",\n+ status: \"completed\",\n+ conclusion: \"failure\",\n+ appSlug: \"github-actions\",\n+ detailsUrl: \"https://github.com/openclaw/firmcode/actions/runs/202/jobs/303\",\n+ htmlUrl: \"https://github.com/openclaw/firmcode/actions/runs/202/job/303\",\n+ workflowRunId: 202,\n+ workflowJobId: 303,\n+ startedAt: \"2026-05-23T10:00:00Z\",\n+ completedAt: \"2026-05-23T10:05:00Z\"\n+ }\n+ ],\n+ logs: [\n+ {\n+ checkRunId: 101,\n+ name: \"unit tests\",\n+ source: \"github_actions_job\",\n+ workflowRunId: 202,\n+ workflowJobId: 303,\n+ content: `${RAW_SECRET}\\nFAIL src/payments.test.ts\\nAssertionError: expected 201 to equal 200`,\n+ originalBytes: 91,\n+ redactedBytes: 72,\n+ storedBytes: 72,\n+ truncated: false,\n+ redacted: true\n+ }\n+ ],\n+ unavailableLogs: []\n+ };\n+\n+ await pool.query(\n+ `\n+INSERT INTO workspaces (id, clerk_org_id, name) VALUES\n+('${WORKSPACE_ID}', 'org_firmcode', 'Firmcode'),\n+('${OTHER_WORKSPACE_ID}', 'org_other', 'Other');\n+\n+INSERT INTO workspace_memberships (workspace_id, clerk_user_id, role, active) VALUES\n+('${WORKSPACE_ID}', '${DEVELOPER_USER_ID}', 'developer', true),\n+('${WORKSPACE_ID}', '${VIEWER_USER_ID}', 'viewer', true),\n+('${OTHER_WORKSPACE_ID}', '${OTHER_VIEWER_USER_ID}', 'viewer', true);\n+\n+INSERT INTO github_installations (id, workspace_id, installation_id, account_login, permissions_json) VALUES\n+('00000000-0000-4000-8000-000000000111', '${WORKSPACE_ID}', 111, 'openclaw', '{\"pull_requests\":\"write\",\"checks\":\"read\",\"actions\":\"read\"}'),\n+('00000000-0000-4000-8000-000000000112', '${OTHER_WORKSPACE_ID}', 112, 'other', '{\"pull_requests\":\"write\",\"checks\":\"read\",\"actions\":\"read\"}');\n+\n+INSERT INTO repositories (id, installation_id, github_repository_id, owner, name, full_name, private, default_branch, enabled) VALUES\n+('00000000-0000-4000-8000-000000000201', '00000000-0000-4000-8000-000000000111', 201, 'openclaw', 'firmcode', 'openclaw/firmcode', false, 'main', true),\n+('00000000-0000-4000-8000-000000000202', '00000000-0000-4000-8000-000000000112', 202, 'other', 'private-roadmap', 'other/private-roadmap', true, 'main', true);\n+\n+INSERT INTO pull_requests (\n+ id,\n+ repository_id,\n+ github_pr_id,\n+ number,\n+ title,\n+ author_login,\n+ base_ref,\n+ head_ref,\n+ base_sha,\n+ head_sha,\n+ state,\n+ draft,\n+ created_at,\n+ updated_at\n+) VALUES\n+('00000000-0000-4000-8000-000000000301', '00000000-0000-4000-8000-000000000201', 301, 77, 'Fix payment tests', 'kelly', 'main', 'feature/payments', 'base-sha', 'head-sha-ci', 'open', false, '2026-05-23T09:55:00.000Z', '2026-05-23T10:05:00.000Z'),\n+('00000000-0000-4000-8000-000000000302', '00000000-0000-4000-8000-000000000201', 302, 78, 'Harden integration tests', 'kelly', 'main', 'feature/flaky', 'base-sha', 'head-sha-flaky', 'open', false, '2026-05-22T09:55:00.000Z', '2026-05-22T10:05:00.000Z'),\n+('00000000-0000-4000-8000-000000000303', '00000000-0000-4000-8000-000000000202', 303, 99, 'Secret acquisition roadmap', 'mallory', 'main', 'feature/private', 'base-sha', 'head-sha-private', 'open', false, '2026-05-23T08:00:00.000Z', '2026-05-23T08:05:00.000Z');\n+\n+INSERT INTO github_deliveries (delivery_id, event_name, action) VALUES\n+('delivery-ci', 'check_run', 'completed'),\n+('delivery-flaky', 'check_run', 'completed'),\n+('delivery-other-ci', 'check_run', 'completed');\n+\n+INSERT INTO review_runs (\n+ id,\n+ repository_id,\n+ pull_request_id,\n+ delivery_id,\n+ trigger_event,\n+ head_sha,\n+ status,\n+ started_at,\n+ finished_at,\n+ created_at,\n+ updated_at\n+) VALUES\n+('00000000-0000-4000-8000-000000000401', '00000000-0000-4000-8000-000000000201', '00000000-0000-4000-8000-000000000301', 'delivery-ci', 'check_run.completed', 'head-sha-ci', 'succeeded', '2026-05-23T10:00:00.000Z', '2026-05-23T10:06:00.000Z', '2026-05-23T10:00:00.000Z', '2026-05-23T10:06:00.000Z'),\n+('00000000-0000-4000-8000-000000000402', '00000000-0000-4000-8000-000000000201', '00000000-0000-4000-8000-000000000302', 'delivery-flaky', 'check_run.completed', 'head-sha-flaky', 'succeeded', '2026-05-22T10:00:00.000Z', '2026-05-22T10:06:00.000Z', '2026-05-22T10:00:00.000Z', '2026-05-22T10:06:00.000Z'),\n+('00000000-0000-4000-8000-000000000403', '00000000-0000-4000-8000-000000000202', '00000000-0000-4000-8000-000000000303', 'delivery-other-ci', 'check_run.completed', 'head-sha-private', 'succeeded', '2026-05-23T08:00:00.000Z', '2026-05-23T08:06:00.000Z', '2026-05-23T08:00:00.000Z', '2026-05-23T08:06:00.000Z');\n+`\n+ );\n+\n+ await pool.query(\n+ `\n+INSERT INTO analysis_artifacts (id, review_run_id, artifact_type, storage_key, metadata_json, created_at) VALUES\n+($1, '00000000-0000-4000-8000-000000000401', 'ci_failure_explanation', 'artifacts/run-401/ci-failure-explanation.json', $2::jsonb, '2026-05-23T10:06:00.000Z'),\n+($3, '00000000-0000-4000-8000-000000000401', 'ci_log', 'artifacts/run-401/ci-log.json', $4::jsonb, '2026-05-23T10:05:00.000Z'),\n+($5, '00000000-0000-4000-8000-000000000402', 'ci_failure_explanation', 'artifacts/run-402/ci-failure-explanation.json', $6::jsonb, '2026-05-22T10:06:00.000Z'),\n+($7, '00000000-0000-4000-8000-000000000403', 'ci_failure_explanation', 'artifacts/run-403/ci-failure-explanation.json', $8::jsonb, '2026-05-23T08:06:00.000Z')\n+`,\n+ [\n+ EXPLANATION_ARTIFACT_ID,\n+ JSON.stringify({ artifact: ciExplanation }),\n+ CI_LOG_ARTIFACT_ID,\n+ JSON.stringify({ artifact: ciLogArtifact }),\n+ FLAKY_EXPLANATION_ARTIFACT_ID,\n+ JSON.stringify({ artifact: flakyExplanation }),\n+ OTHER_EXPLANATION_ARTIFACT_ID,\n+ JSON.stringify({ artifact: otherExplanation })\n+ ]\n+ );\n+}",
"status": "added",
"language": "typescript",
"additions": 469,
"deletions": 0,
"sizeBytes": 19433,
"previousPath": null,
"changedNewLines": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
96,
97,
98,
99,
100,
101,
102,
103,
104,
105,
106,
107,
108,
109,
110,
111,
112,
113,
114,
115,
116,
117,
118,
119,
120,
121,
122,
123,
124,
125,
126,
127,
128,
129,
130,
131,
132,
133,
134,
135,
136,
137,
138,
139,
140,
141,
142,
143,
144,
145,
146,
147,
148,
149,
150,
151,
152,
153,
154,
155,
156,
157,
158,
159,
160,
161,
162,
163,
164,
165,
166,
167,
168,
169,
170,
171,
172,
173,
174,
175,
176,
177,
178,
179,
180,
181,
182,
183,
184,
185,
186,
187,
188,
189,
190,
191,
192,
193,
194,
195,
196,
197,
198,
199,
200,
201,
202,
203,
204,
205,
206,
207,
208,
209,
210,
211,
212,
213,
214,
215,
216,
217,
218,
219,
220,
221,
222,
223,
224,
225,
226,
227,
228,
229,
230,
231,
232,
233,
234,
235,
236,
237,
238,
239,
240,
241,
242,
243,
244,
245,
246,
247,
248,
249,
250,
251,
252,
253,
254,
255,
256,
257,
258,
259,
260,
261,
262,
263,
264,
265,
266,
267,
268,
269,
270,
271,
272,
273,
274,
275,
276,
277,
278,
279,
280,
281,
282,
283,
284,
285,
286,
287,
288,
289,
290,
291,
292,
293,
294,
295,
296,
297,
298,
299,
300,
301,
302,
303,
304,
305,
306,
307,
308,
309,
310,
311,
312,
313,
314,
315,
316,
317,
318,
319,
320,
321,
322,
323,
324,
325,
326,
327,
328,
329,
330,
331,
332,
333,
334,
335,
336,
337,
338,
339,
340,
341,
342,
343,
344,
345,
346,
347,
348,
349,
350,
351,
352,
353,
354,
355,
356,
357,
358,
359,
360,
361,
362,
363,
364,
365,
366,
367,
368,
369,
370,
371,
372,
373,
374,
375,
376,
377,
378,
379,
380,
381,
382,
383,
384,
385,
386,
387,
388,
389,
390,
391,
392,
393,
394,
395,
396,
397,
398,
399,
400,
401,
402,
403,
404,
405,
406,
407,
408,
409,
410,
411,
412,
413,
414,
415,
416,
417,
418,
419,
420,
421,
422,
423,
424,
425,
426,
427,
428,
429,
430,
431,
432,
433,
434,
435,
436,
437,
438,
439,
440,
441,
442,
443,
444,
445,
446,
447,
448,
449,
450,
451,
452,
453,
454,
455,
456,
457,
458,
459,
460,
461,
462,
463,
464,
465,
466,
467,
468,
469
],
"headContentSha256": "62750a157f420228598b2fc978115a87fd6dc6aaad42817701146392ec5f9e85"
},
{
"path": "apps/api/test/infrastructure/database/database-migrations.spec.ts",
"hunks": [
{
"lines": [
{
"type": "context",
"content": " \"003_dashboard_auth_retry_state\",",
"newLineNumber": 165,
"oldLineNumber": 165
},
{
"type": "context",
"content": " \"004_repository_review_configuration\",",
"newLineNumber": 166,
"oldLineNumber": 166
},
{
"type": "context",
"content": " \"005_github_oauth_and_sync\",",
"newLineNumber": 167,
"oldLineNumber": 167
},
{
"type": "deletion",
"content": " \"006_review_policies\"",
"newLineNumber": null,
"oldLineNumber": 168
},
{
"type": "addition",
"content": " \"006_review_policies\",",
"newLineNumber": 168,
"oldLineNumber": null
},
{
"type": "addition",
"content": " \"007_ci_failure_artifacts\"",
"newLineNumber": 169,
"oldLineNumber": null
},
{
"type": "context",
"content": " ]);",
"newLineNumber": 170,
"oldLineNumber": 169
},
{
"type": "context",
"content": " expect(secondRunMigrationIds).toEqual([]);",
"newLineNumber": 171,
"oldLineNumber": 170
},
{
"type": "context",
"content": " expect(tables.rows.map((row) => row.table_name)).toEqual(EXPECTED_TABLES);",
"newLineNumber": 172,
"oldLineNumber": 171
}
],
"newStart": 165,
"oldStart": 165,
"newLineCount": 8,
"oldLineCount": 7,
"sectionHeader": "ORDER BY table_name"
},
{
"lines": [
{
"type": "context",
"content": " { id: \"003_dashboard_auth_retry_state\" },",
"newLineNumber": 176,
"oldLineNumber": 175
},
{
"type": "context",
"content": " { id: \"004_repository_review_configuration\" },",
"newLineNumber": 177,
"oldLineNumber": 176
},
{
"type": "context",
"content": " { id: \"005_github_oauth_and_sync\" },",
"newLineNumber": 178,
"oldLineNumber": 177
},
{
"type": "deletion",
"content": " { id: \"006_review_policies\" }",
"newLineNumber": null,
"oldLineNumber": 178
},
{
"type": "addition",
"content": " { id: \"006_review_policies\" },",
"newLineNumber": 179,
"oldLineNumber": null
},
{
"type": "addition",
"content": " { id: \"007_ci_failure_artifacts\" }",
"newLineNumber": 180,
"oldLineNumber": null
},
{
"type": "context",
"content": " ]);",
"newLineNumber": 181,
"oldLineNumber": 179
},
{
"type": "context",
"content": " });",
"newLineNumber": 182,
"oldLineNumber": 180
},
{
"type": "context",
"content": "",
"newLineNumber": 183,
"oldLineNumber": 181
}
],
"newStart": 176,
"oldStart": 175,
"newLineCount": 8,
"oldLineCount": 7,
"sectionHeader": "ORDER BY table_name"
}
],
"patch": "@@ -165,7 +165,8 @@ ORDER BY table_name\n \"003_dashboard_auth_retry_state\",\n \"004_repository_review_configuration\",\n \"005_github_oauth_and_sync\",\n- \"006_review_policies\"\n+ \"006_review_policies\",\n+ \"007_ci_failure_artifacts\"\n ]);\n expect(secondRunMigrationIds).toEqual([]);\n expect(tables.rows.map((row) => row.table_name)).toEqual(EXPECTED_TABLES);\n@@ -175,7 +176,8 @@ ORDER BY table_name\n { id: \"003_dashboard_auth_retry_state\" },\n { id: \"004_repository_review_configuration\" },\n { id: \"005_github_oauth_and_sync\" },\n- { id: \"006_review_policies\" }\n+ { id: \"006_review_policies\" },\n+ { id: \"007_ci_failure_artifacts\" }\n ]);\n });\n ",
"status": "modified",
"language": "typescript",
"additions": 4,
"deletions": 2,
"sizeBytes": 10290,
"previousPath": null,
"changedNewLines": [
168,
169,
179,
180
],
"headContentSha256": "4470c6efb43c3ae9b3b41781f5ad3ec269dc5f98e3893389139ccfb75df4dee0"
},
{
"path": "apps/web/app/api/ci-failures/[id]/route.ts",
"hunks": [
{
"lines": [
{
"type": "addition",
"content": "import { forwardDashboardApiMutation } from \"../../../../lib/dashboard-api-proxy\";",
"newLineNumber": 1,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 2,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export async function GET(_request: Request, context: { params: { id: string } }): Promise<Response> {",
"newLineNumber": 3,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return forwardDashboardApiMutation({",
"newLineNumber": 4,
"oldLineNumber": null
},
{
"type": "addition",
"content": " method: \"GET\",",
"newLineNumber": 5,
"oldLineNumber": null
},
{
"type": "addition",
"content": " path: `/api/ci-failures/${encodeURIComponent(context.params.id)}`",
"newLineNumber": 6,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 7,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 8,
"oldLineNumber": null
}
],
"newStart": 1,
"oldStart": 0,
"newLineCount": 8,
"oldLineCount": 0,
"sectionHeader": ""
}
],
"patch": "@@ -0,0 +1,8 @@\n+import { forwardDashboardApiMutation } from \"../../../../lib/dashboard-api-proxy\";\n+\n+export async function GET(_request: Request, context: { params: { id: string } }): Promise<Response> {\n+ return forwardDashboardApiMutation({\n+ method: \"GET\",\n+ path: `/api/ci-failures/${encodeURIComponent(context.params.id)}`\n+ });\n+}",
"status": "added",
"language": "typescript",
"additions": 8,
"deletions": 0,
"sizeBytes": 323,
"previousPath": null,
"changedNewLines": [
1,
2,
3,
4,
5,
6,
7,
8
],
"headContentSha256": "12a15f5a566f87348357ffe91e85d02a41ffa1a87cc0977e7d6a49c599babdc0"
},
{
"path": "apps/web/app/api/ci-failures/route.ts",
"hunks": [
{
"lines": [
{
"type": "addition",
"content": "import { forwardDashboardApiMutation } from \"../../../lib/dashboard-api-proxy\";",
"newLineNumber": 1,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 2,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export async function GET(request: Request): Promise<Response> {",
"newLineNumber": 3,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const url = new URL(request.url);",
"newLineNumber": 4,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const path = `/api/ci-failures${url.search}`;",
"newLineNumber": 5,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 6,
"oldLineNumber": null
},
{
"type": "addition",
"content": " return forwardDashboardApiMutation({",
"newLineNumber": 7,
"oldLineNumber": null
},
{
"type": "addition",
"content": " method: \"GET\",",
"newLineNumber": 8,
"oldLineNumber": null
},
{
"type": "addition",
"content": " path",
"newLineNumber": 9,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 10,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 11,
"oldLineNumber": null
}
],
"newStart": 1,
"oldStart": 0,
"newLineCount": 11,
"oldLineCount": 0,
"sectionHeader": ""
}
],
"patch": "@@ -0,0 +1,11 @@\n+import { forwardDashboardApiMutation } from \"../../../lib/dashboard-api-proxy\";\n+\n+export async function GET(request: Request): Promise<Response> {\n+ const url = new URL(request.url);\n+ const path = `/api/ci-failures${url.search}`;\n+\n+ return forwardDashboardApiMutation({\n+ method: \"GET\",\n+ path\n+ });\n+}",
"status": "added",
"language": "typescript",
"additions": 11,
"deletions": 0,
"sizeBytes": 306,
"previousPath": null,
"changedNewLines": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11
],
"headContentSha256": "0a5f445ca5cb74fca53643b4508463ef7bbcf1b5a14f33abdd97a3ebb753a89e"
},
{
"path": "apps/web/packages/shared/test/github/firmcodeai-activity.spec.ts",
"hunks": [
{
"lines": [
{
"type": "context",
"content": " it(\"renders a branded summary activity comment\", () => {",
"newLineNumber": 9,
"oldLineNumber": 9
},
{
"type": "context",
"content": " const markdown = renderFirmcodeAiSummaryActivity({",
"newLineNumber": 10,
"oldLineNumber": 10
},
{
"type": "context",
"content": " reviewRunId: \"run-1\",",
"newLineNumber": 11,
"oldLineNumber": 11
},
{
"type": "deletion",
"content": " repositoryFullName: \"firmcloudapps/firmcode-tester\",",
"newLineNumber": null,
"oldLineNumber": 12
},
{
"type": "addition",
"content": " repositoryFullName: \"kelly-oriabure/firmcode-tester\",",
"newLineNumber": 12,
"oldLineNumber": null
},
{
"type": "context",
"content": " pullRequestNumber: 13,",
"newLineNumber": 13,
"oldLineNumber": 13
},
{
"type": "context",
"content": " headSha: \"5ccce2d5f1b0e7bedd6239418c39bb28740b741e\",",
"newLineNumber": 14,
"oldLineNumber": 14
},
{
"type": "context",
"content": " summaryBody: \"This PR changes Semgrep scan workspace behavior.\",",
"newLineNumber": 15,
"oldLineNumber": 15
}
],
"newStart": 9,
"oldStart": 9,
"newLineCount": 7,
"oldLineCount": 7,
"sectionHeader": "describe(\"FirmcodeAI GitHub activity Markdown\", () => {"
},
{
"lines": [
{
"type": "context",
"content": " it(\"matches the summary Markdown snapshot\", () => {",
"newLineNumber": 46,
"oldLineNumber": 46
},
{
"type": "context",
"content": " const markdown = renderFirmcodeAiSummaryActivity({",
"newLineNumber": 47,
"oldLineNumber": 47
},
{
"type": "context",
"content": " reviewRunId: \"run-snapshot\",",
"newLineNumber": 48,
"oldLineNumber": 48
},
{
"type": "deletion",
"content": " repositoryFullName: \"firmcloudapps/firmcode-tester\",",
"newLineNumber": null,
"oldLineNumber": 49
},
{
"type": "addition",
"content": " repositoryFullName: \"kelly-oriabure/firmcode-tester\",",
"newLineNumber": 49,
"oldLineNumber": null
},
{
"type": "context",
"content": " pullRequestNumber: 21,",
"newLineNumber": 50,
"oldLineNumber": 50
},
{
"type": "context",
"content": " headSha: \"5ccce2d5f1b0e7bedd6239418c39bb28740b741e\",",
"newLineNumber": 51,
"oldLineNumber": 51
},
{
"type": "context",
"content": " summaryBody: \"This PR updates webhook ingestion and review publishing.\",",
"newLineNumber": 52,
"oldLineNumber": 52
}
],
"newStart": 46,
"oldStart": 46,
"newLineCount": 7,
"oldLineCount": 7,
"sectionHeader": "describe(\"FirmcodeAI GitHub activity Markdown\", () => {"
}
],
"patch": "@@ -9,7 +9,7 @@ describe(\"FirmcodeAI GitHub activity Markdown\", () => {\n it(\"renders a branded summary activity comment\", () => {\n const markdown = renderFirmcodeAiSummaryActivity({\n reviewRunId: \"run-1\",\n- repositoryFullName: \"firmcloudapps/firmcode-tester\",\n+ repositoryFullName: \"kelly-oriabure/firmcode-tester\",\n pullRequestNumber: 13,\n headSha: \"5ccce2d5f1b0e7bedd6239418c39bb28740b741e\",\n summaryBody: \"This PR changes Semgrep scan workspace behavior.\",\n@@ -46,7 +46,7 @@ describe(\"FirmcodeAI GitHub activity Markdown\", () => {\n it(\"matches the summary Markdown snapshot\", () => {\n const markdown = renderFirmcodeAiSummaryActivity({\n reviewRunId: \"run-snapshot\",\n- repositoryFullName: \"firmcloudapps/firmcode-tester\",\n+ repositoryFullName: \"kelly-oriabure/firmcode-tester\",\n pullRequestNumber: 21,\n headSha: \"5ccce2d5f1b0e7bedd6239418c39bb28740b741e\",\n summaryBody: \"This PR updates webhook ingestion and review publishing.\",",
"status": "modified",
"language": "typescript",
"additions": 2,
"deletions": 2,
"sizeBytes": 3309,
"previousPath": null,
"changedNewLines": [
12,
49
],
"headContentSha256": "7bf6a7934c3e8cf5316872b7debaadd38bfd6b77d681309b71fabb13d0ca8166"
},
{
"path": "apps/web/tests/github-sync-routes.spec.ts",
"hunks": [
{
"lines": [
{
"type": "context",
"content": "import { GET as startGitHubOAuth } from \"../app/auth/github/route\";",
"newLineNumber": 1,
"oldLineNumber": 1
},
{
"type": "addition",
"content": "import { GET as listCiFailures } from \"../app/api/ci-failures/route\";",
"newLineNumber": 2,
"oldLineNumber": null
},
{
"type": "addition",
"content": "import { GET as readCiFailure } from \"../app/api/ci-failures/[id]/route\";",
"newLineNumber": 3,
"oldLineNumber": null
},
{
"type": "context",
"content": "import { POST as syncInstallations } from \"../app/api/github/installations/sync/route\";",
"newLineNumber": 4,
"oldLineNumber": 2
},
{
"type": "context",
"content": "import { GET as readRules, PATCH as saveRules } from \"../app/api/rules/route\";",
"newLineNumber": 5,
"oldLineNumber": 3
},
{
"type": "context",
"content": "import { POST as syncRepository } from \"../app/api/repositories/[id]/sync/route\";",
"newLineNumber": 6,
"oldLineNumber": 4
}
],
"newStart": 1,
"oldStart": 1,
"newLineCount": 6,
"oldLineCount": 4,
"sectionHeader": ""
},
{
"lines": [
{
"type": "context",
"content": " expect(headers.get(\"x-firmcode-workspace-id\")).toBe(\"workspace-1\");",
"newLineNumber": 131,
"oldLineNumber": 129
},
{
"type": "context",
"content": " expect(headers.get(\"x-firmcode-user-id\")).toBe(\"user-1\");",
"newLineNumber": 132,
"oldLineNumber": 130
},
{
"type": "context",
"content": " });",
"newLineNumber": 133,
"oldLineNumber": 131
},
{
"type": "addition",
"content": "",
"newLineNumber": 134,
"oldLineNumber": null
},
{
"type": "addition",
"content": " it(\"routes CI failure list and detail reads to the authenticated dashboard API\", async () => {",
"newLineNumber": 135,
"oldLineNumber": null
},
{
"type": "addition",
"content": " process.env.NEXT_PUBLIC_API_URL = \"http://dashboard-api.test\";",
"newLineNumber": 136,
"oldLineNumber": null
},
{
"type": "addition",
"content": " process.env.FIRMCODE_DASHBOARD_WORKSPACE_ID = \"workspace-1\";",
"newLineNumber": 137,
"oldLineNumber": null
},
{
"type": "addition",
"content": " process.env.FIRMCODE_DASHBOARD_CLERK_USER_ID = \"user-1\";",
"newLineNumber": 138,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const fetcher = vi.fn(async () => jsonResponse({ ciFailures: [], filters: {}, pagination: { limit: 50, returned: 0 } }));",
"newLineNumber": 139,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 140,
"oldLineNumber": null
},
{
"type": "addition",
"content": " vi.stubGlobal(\"fetch\", fetcher);",
"newLineNumber": 141,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 142,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await listCiFailures(new Request(\"http://localhost/api/ci-failures?repository=openclaw%2Ffirmcode\"));",
"newLineNumber": 143,
"oldLineNumber": null
},
{
"type": "addition",
"content": " await readCiFailure(new Request(\"http://localhost/api/ci-failures/failure-1\"), {",
"newLineNumber": 144,
"oldLineNumber": null
},
{
"type": "addition",
"content": " params: { id: \"failure-1\" }",
"newLineNumber": 145,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 146,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 147,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const listUrl = new URL(String(fetcher.mock.calls[0]?.[0]));",
"newLineNumber": 148,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const detailUrl = new URL(String(fetcher.mock.calls[1]?.[0]));",
"newLineNumber": 149,
"oldLineNumber": null
},
{
"type": "addition",
"content": " const listHeaders = new Headers((fetcher.mock.calls[0]?.[1] as RequestInit | undefined)?.headers);",
"newLineNumber": 150,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 151,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(listUrl.pathname).toBe(\"/api/ci-failures\");",
"newLineNumber": 152,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(listUrl.searchParams.get(\"repository\")).toBe(\"openclaw/firmcode\");",
"newLineNumber": 153,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(detailUrl.pathname).toBe(\"/api/ci-failures/failure-1\");",
"newLineNumber": 154,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(listHeaders.get(\"x-firmcode-workspace-id\")).toBe(\"workspace-1\");",
"newLineNumber": 155,
"oldLineNumber": null
},
{
"type": "addition",
"content": " expect(listHeaders.get(\"x-firmcode-user-id\")).toBe(\"user-1\");",
"newLineNumber": 156,
"oldLineNumber": null
},
{
"type": "addition",
"content": " });",
"newLineNumber": 157,
"oldLineNumber": null
},
{
"type": "context",
"content": "});",
"newLineNumber": 158,
"oldLineNumber": 132
},
{
"type": "context",
"content": "",
"newLineNumber": 159,
"oldLineNumber": 133
},
{
"type": "context",
"content": "function jsonResponse(body: unknown, status = 200): Response {",
"newLineNumber": 160,
"oldLineNumber": 134
}
],
"newStart": 131,
"oldStart": 129,
"newLineCount": 30,
"oldLineCount": 6,
"sectionHeader": "describe(\"GitHub sync routes\", () => {"
}
],
"patch": "@@ -1,4 +1,6 @@\n import { GET as startGitHubOAuth } from \"../app/auth/github/route\";\n+import { GET as listCiFailures } from \"../app/api/ci-failures/route\";\n+import { GET as readCiFailure } from \"../app/api/ci-failures/[id]/route\";\n import { POST as syncInstallations } from \"../app/api/github/installations/sync/route\";\n import { GET as readRules, PATCH as saveRules } from \"../app/api/rules/route\";\n import { POST as syncRepository } from \"../app/api/repositories/[id]/sync/route\";\n@@ -129,6 +131,30 @@ describe(\"GitHub sync routes\", () => {\n expect(headers.get(\"x-firmcode-workspace-id\")).toBe(\"workspace-1\");\n expect(headers.get(\"x-firmcode-user-id\")).toBe(\"user-1\");\n });\n+\n+ it(\"routes CI failure list and detail reads to the authenticated dashboard API\", async () => {\n+ process.env.NEXT_PUBLIC_API_URL = \"http://dashboard-api.test\";\n+ process.env.FIRMCODE_DASHBOARD_WORKSPACE_ID = \"workspace-1\";\n+ process.env.FIRMCODE_DASHBOARD_CLERK_USER_ID = \"user-1\";\n+ const fetcher = vi.fn(async () => jsonResponse({ ciFailures: [], filters: {}, pagination: { limit: 50, returned: 0 } }));\n+\n+ vi.stubGlobal(\"fetch\", fetcher);\n+\n+ await listCiFailures(new Request(\"http://localhost/api/ci-failures?repository=openclaw%2Ffirmcode\"));\n+ await readCiFailure(new Request(\"http://localhost/api/ci-failures/failure-1\"), {\n+ params: { id: \"failure-1\" }\n+ });\n+\n+ const listUrl = new URL(String(fetcher.mock.calls[0]?.[0]));\n+ const detailUrl = new URL(String(fetcher.mock.calls[1]?.[0]));\n+ const listHeaders = new Headers((fetcher.mock.calls[0]?.[1] as RequestInit | undefined)?.headers);\n+\n+ expect(listUrl.pathname).toBe(\"/api/ci-failures\");\n+ expect(listUrl.searchParams.get(\"repository\")).toBe(\"openclaw/firmcode\");\n+ expect(detailUrl.pathname).toBe(\"/api/ci-failures/failure-1\");\n+ expect(listHeaders.get(\"x-firmcode-workspace-id\")).toBe(\"workspace-1\");\n+ expect(listHeaders.get(\"x-firmcode-user-id\")).toBe(\"user-1\");\n+ });\n });\n \n function jsonResponse(body: unknown, status = 200): Response {",
"status": "modified",
"language": "typescript",
"additions": 26,
"deletions": 0,
"sizeBytes": 7908,
"previousPath": null,
"changedNewLines": [
2,
3,
134,
135,
136,
137,
138,
139,
140,
141,
142,
143,
144,
145,
146,
147,
148,
149,
150,
151,
152,
153,
154,
155,
156,
157
],
"headContentSha256": "6962faa18d8ae07bcb49a3a59557430ee75a5d09cb2c2065b33f44a257daab08"
},
{
"path": "packages/shared/src/contracts/review.ts",
"hunks": [
{
"lines": [
{
"type": "context",
"content": " };",
"newLineNumber": 577,
"oldLineNumber": 577
},
{
"type": "context",
"content": "}",
"newLineNumber": 578,
"oldLineNumber": 578
},
{
"type": "context",
"content": "",
"newLineNumber": 579,
"oldLineNumber": 579
},
{
"type": "deletion",
"content": "export type ReviewRunArtifactType = \"diff\" | \"treesitter\" | \"semgrep\" | \"context_pack\" | \"llm_raw\" | \"ci_log\";",
"newLineNumber": null,
"oldLineNumber": 580
},
{
"type": "addition",
"content": "export type ReviewRunArtifactType =",
"newLineNumber": 580,
"oldLineNumber": null
},
{
"type": "addition",
"content": " | \"diff\"",
"newLineNumber": 581,
"oldLineNumber": null
},
{
"type": "addition",
"content": " | \"treesitter\"",
"newLineNumber": 582,
"oldLineNumber": null
},
{
"type": "addition",
"content": " | \"semgrep\"",
"newLineNumber": 583,
"oldLineNumber": null
},
{
"type": "addition",
"content": " | \"context_pack\"",
"newLineNumber": 584,
"oldLineNumber": null
},
{
"type": "addition",
"content": " | \"llm_raw\"",
"newLineNumber": 585,
"oldLineNumber": null
},
{
"type": "addition",
"content": " | \"ci_log\"",
"newLineNumber": 586,
"oldLineNumber": null
},
{
"type": "addition",
"content": " | \"ci_failure_explanation\";",
"newLineNumber": 587,
"oldLineNumber": null
},
{
"type": "context",
"content": "",
"newLineNumber": 588,
"oldLineNumber": 581
},
{
"type": "context",
"content": "export interface ReviewRunArtifact {",
"newLineNumber": 589,
"oldLineNumber": 582
},
{
"type": "context",
"content": " id: string;",
"newLineNumber": 590,
"oldLineNumber": 583
}
],
"newStart": 577,
"oldStart": 577,
"newLineCount": 14,
"oldLineCount": 7,
"sectionHeader": "export interface WorkspaceSettingsResponse {"
},
{
"lines": [
{
"type": "context",
"content": " };",
"newLineNumber": 655,
"oldLineNumber": 648
},
{
"type": "context",
"content": "}",
"newLineNumber": 656,
"oldLineNumber": 649
},
{
"type": "context",
"content": "",
"newLineNumber": 657,
"oldLineNumber": 650
},
{
"type": "addition",
"content": "export interface CiFailureListFilters {",
"newLineNumber": 658,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repositoryId?: string;",
"newLineNumber": 659,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repository?: string;",
"newLineNumber": 660,
"oldLineNumber": null
},
{
"type": "addition",
"content": " status?: ReviewRunStatus;",
"newLineNumber": 661,
"oldLineNumber": null
},
{
"type": "addition",
"content": " flaky?: boolean;",
"newLineNumber": 662,
"oldLineNumber": null
},
{
"type": "addition",
"content": " dateFrom?: string;",
"newLineNumber": 663,
"oldLineNumber": null
},
{
"type": "addition",
"content": " dateTo?: string;",
"newLineNumber": 664,
"oldLineNumber": null
},
{
"type": "addition",
"content": " limit?: number;",
"newLineNumber": 665,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 666,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 667,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export interface CiFailureFailedJob {",
"newLineNumber": 668,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: string;",
"newLineNumber": 669,
"oldLineNumber": null
},
{
"type": "addition",
"content": " workflowName: string | null;",
"newLineNumber": 670,
"oldLineNumber": null
},
{
"type": "addition",
"content": " jobName: string;",
"newLineNumber": 671,
"oldLineNumber": null
},
{
"type": "addition",
"content": " checkRunId: number;",
"newLineNumber": 672,
"oldLineNumber": null
},
{
"type": "addition",
"content": " conclusion: string;",
"newLineNumber": 673,
"oldLineNumber": null
},
{
"type": "addition",
"content": " stepName: string | null;",
"newLineNumber": 674,
"oldLineNumber": null
},
{
"type": "addition",
"content": " category: string;",
"newLineNumber": 675,
"oldLineNumber": null
},
{
"type": "addition",
"content": " detailsUrl: string | null;",
"newLineNumber": 676,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 677,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 678,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export interface CiFailureListItem {",
"newLineNumber": 679,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: string;",
"newLineNumber": 680,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repositoryId: string;",
"newLineNumber": 681,
"oldLineNumber": null
},
{
"type": "addition",
"content": " repositoryFullName: string;",
"newLineNumber": 682,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pullRequestId: string;",
"newLineNumber": 683,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pullRequestNumber: number;",
"newLineNumber": 684,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pullRequestTitle: string;",
"newLineNumber": 685,
"oldLineNumber": null
},
{
"type": "addition",
"content": " reviewRunId: string;",
"newLineNumber": 686,
"oldLineNumber": null
},
{
"type": "addition",
"content": " failedJob: CiFailureFailedJob;",
"newLineNumber": 687,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rootCauseSummary: string;",
"newLineNumber": 688,
"oldLineNumber": null
},
{
"type": "addition",
"content": " flakySuspected: boolean;",
"newLineNumber": 689,
"oldLineNumber": null
},
{
"type": "addition",
"content": " suggestedFix: string | null;",
"newLineNumber": 690,
"oldLineNumber": null
},
{
"type": "addition",
"content": " status: ReviewRunStatus;",
"newLineNumber": 691,
"oldLineNumber": null
},
{
"type": "addition",
"content": " createdAt: string;",
"newLineNumber": 692,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 693,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 694,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export interface CiFailureListResponse {",
"newLineNumber": 695,
"oldLineNumber": null
},
{
"type": "addition",
"content": " ciFailures: CiFailureListItem[];",
"newLineNumber": 696,
"oldLineNumber": null
},
{
"type": "addition",
"content": " filters: CiFailureListFilters;",
"newLineNumber": 697,
"oldLineNumber": null
},
{
"type": "addition",
"content": " pagination: {",
"newLineNumber": 698,
"oldLineNumber": null
},
{
"type": "addition",
"content": " limit: number;",
"newLineNumber": 699,
"oldLineNumber": null
},
{
"type": "addition",
"content": " returned: number;",
"newLineNumber": 700,
"oldLineNumber": null
},
{
"type": "addition",
"content": " };",
"newLineNumber": 701,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 702,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 703,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export interface CiFailureSuggestedFix {",
"newLineNumber": 704,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: string;",
"newLineNumber": 705,
"oldLineNumber": null
},
{
"type": "addition",
"content": " text: string;",
"newLineNumber": 706,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 707,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 708,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export interface CiFailureRelatedReviewRun {",
"newLineNumber": 709,
"oldLineNumber": null
},
{
"type": "addition",
"content": " id: string;",
"newLineNumber": 710,
"oldLineNumber": null
},
{
"type": "addition",
"content": " status: ReviewRunStatus;",
"newLineNumber": 711,
"oldLineNumber": null
},
{
"type": "addition",
"content": " createdAt: string;",
"newLineNumber": 712,
"oldLineNumber": null
},
{
"type": "addition",
"content": " detailUrl: string;",
"newLineNumber": 713,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 714,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 715,
"oldLineNumber": null
},
{
"type": "addition",
"content": "export interface CiFailureDetailResponse extends CiFailureListItem {",
"newLineNumber": 716,
"oldLineNumber": null
},
{
"type": "addition",
"content": " rootCause: string;",
"newLineNumber": 717,
"oldLineNumber": null
},
{
"type": "addition",
"content": " suggestedFixes: CiFailureSuggestedFix[];",
"newLineNumber": 718,
"oldLineNumber": null
},
{
"type": "addition",
"content": " failedJobs: CiFailureFailedJob[];",
"newLineNumber": 719,
"oldLineNumber": null
},
{
"type": "addition",
"content": " relatedReviewRun: CiFailureRelatedReviewRun;",
"newLineNumber": 720,
"oldLineNumber": null
},
{
"type": "addition",
"content": " relatedArtifacts: ReviewRunArtifact[];",
"newLineNumber": 721,
"oldLineNumber": null
},
{
"type": "addition",
"content": " logExcerpts: Array<ReviewRunLogExcerpt & { collapsed: true }>;",
"newLineNumber": 722,
"oldLineNumber": null
},
{
"type": "addition",
"content": " unavailableLogNotes: unknown[];",
"newLineNumber": 723,
"oldLineNumber": null
},
{
"type": "addition",
"content": "}",
"newLineNumber": 724,
"oldLineNumber": null
},
{
"type": "addition",
"content": "",
"newLineNumber": 725,
"oldLineNumber": null
},
{
"type": "context",
"content": "export interface WorkspaceBillingResponse {",
"newLineNumber": 726,
"oldLineNumber": 651
},
{
"type": "context",
"content": " workspace: {",
"newLineNumber": 727,
"oldLineNumber": 652
},
{
"type": "context",
"content": " id: string;",
"newLineNumber": 728,
"oldLineNumber": 653
}
],
"newStart": 655,
"oldStart": 648,
"newLineCount": 74,
"oldLineCount": 6,
"sectionHeader": "export interface ReviewRunDetail extends ReviewRunSummary {"
}
],
"patch": "@@ -577,7 +577,14 @@ export interface WorkspaceSettingsResponse {\n };\n }\n \n-export type ReviewRunArtifactType = \"diff\" | \"treesitter\" | \"semgrep\" | \"context_pack\" | \"llm_raw\" | \"ci_log\";\n+export type ReviewRunArtifactType =\n+ | \"diff\"\n+ | \"treesitter\"\n+ | \"semgrep\"\n+ | \"context_pack\"\n+ | \"llm_raw\"\n+ | \"ci_log\"\n+ | \"ci_failure_explanation\";\n \n export interface ReviewRunArtifact {\n id: string;\n@@ -648,6 +655,74 @@ export interface ReviewRunDetail extends ReviewRunSummary {\n };\n }\n \n+export interface CiFailureListFilters {\n+ repositoryId?: string;\n+ repository?: string;\n+ status?: ReviewRunStatus;\n+ flaky?: boolean;\n+ dateFrom?: string;\n+ dateTo?: string;\n+ limit?: number;\n+}\n+\n+export interface CiFailureFailedJob {\n+ id: string;\n+ workflowName: string | null;\n+ jobName: string;\n+ checkRunId: number;\n+ conclusion: string;\n+ stepName: string | null;\n+ category: string;\n+ detailsUrl: string | null;\n+}\n+\n+export interface CiFailureListItem {\n+ id: string;\n+ repositoryId: string;\n+ repositoryFullName: string;\n+ pullRequestId: string;\n+ pullRequestNumber: number;\n+ pullRequestTitle: string;\n+ reviewRunId: string;\n+ failedJob: CiFailureFailedJob;\n+ rootCauseSummary: string;\n+ flakySuspected: boolean;\n+ suggestedFix: string | null;\n+ status: ReviewRunStatus;\n+ createdAt: string;\n+}\n+\n+export interface CiFailureListResponse {\n+ ciFailures: CiFailureListItem[];\n+ filters: CiFailureListFilters;\n+ pagination: {\n+ limit: number;\n+ returned: number;\n+ };\n+}\n+\n+export interface CiFailureSuggestedFix {\n+ id: string;\n+ text: string;\n+}\n+\n+export interface CiFailureRelatedReviewRun {\n+ id: string;\n+ status: ReviewRunStatus;\n+ createdAt: string;\n+ detailUrl: string;\n+}\n+\n+export interface CiFailureDetailResponse extends CiFailureListItem {\n+ rootCause: string;\n+ suggestedFixes: CiFailureSuggestedFix[];\n+ failedJobs: CiFailureFailedJob[];\n+ relatedReviewRun: CiFailureRelatedReviewRun;\n+ relatedArtifacts: ReviewRunArtifact[];\n+ logExcerpts: Array<ReviewRunLogExcerpt & { collapsed: true }>;\n+ unavailableLogNotes: unknown[];\n+}\n+\n export interface WorkspaceBillingResponse {\n workspace: {\n id: string;",
"status": "modified",
"language": "typescript",
"additions": 76,
"deletions": 1,
"sizeBytes": 21349,
"previousPath": null,
"changedNewLines": [
580,
581,
582,
583,
584,
585,
586,
587,
658,
659,
660,
661,
662,
663,
664,
665,
666,
667,
668,
669,
670,
671,
672,
673,
674,
675,
676,
677,
678,
679,
680,
681,
682,
683,
684,
685,
686,
687,
688,
689,
690,
691,
692,
693,
694,
695,
696,
697,
698,
699,
700,
701,
702,
703,
704,
705,
706,
707,
708,
709,
710,
711,
712,
713,
714,
715,
716,
717,
718,
719,
720,
721,
722,
723,
724,
725
],
"headContentSha256": "0d8c477a503a0d72d7cb8853e22487f2bd82bfbb423500ef5804657a711fac33"
}
],
"baseSha": "6d51fd7cf5fbdf63a5aaa1eb6b4b2688cbcd3ca0",
"headSha": "5abd541d3d87c238379d846f68d770e954f57620",
"reviewRunId": "82d47c43-e4df-4de2-a6fe-c9b589f3d34e",
"skippedFiles": [
{
"path": "apps/web/packages/shared/test/github/__snapshots__/firmcodeai-activity.spec.ts.snap",
"detail": "File extension is not enabled yet.",
"reason": "unsupported",
"status": "modified",
"previousPath": null,
"excludedFromSemgrep": true,
"excludedFromLlmContext": true,
"excludedFromTreeSitter": true
}
],
"schemaVersion": "diff-artifact/v1",
"pullRequestNumber": 81,
"repositoryFullName": "firmcloudapps/firmcode-tester"
}
}