diff --git a/AGENTS.md b/AGENTS.md index 21483738..580e7a7c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -196,7 +196,7 @@ Nuxt file-based routing: - **Failure clustering / fingerprints**: The grouping key is computed in `shared/error-fingerprint.ts` (`computeErrorFingerprint`) over error type + normalized message + masked locator — the **stack frame is intentionally NOT hashed** (kept as `topFrameFile` for display only) so the same root cause groups across spec files. Add new volatile-token masking inside `maskVolatile` (message) / `maskSelector` (locator). When you change normalization, bump `FINGERPRINT_VERSION` and keep the demo mirror in `scripts/generate-demo-seed.mjs#computeDemoFingerprint` in sync. A version bump is **non-destructive**: `reclusterFailureFingerprints()` (`shared/handlers/failure-cluster-recluster.ts`) re-fingerprints existing clusters from their stored `sampleError` on startup, updating in place or merging collisions via `mergeFailureClusters()` (`shared/handlers/failure-cluster-ops.ts`) so triage state survives. Re-run `npm run app:seed:demo` after any change. - **Spec health**: Endpoint `GET /api/projects/[id]/spec-health` groups by spec prefix. `SpecHealthTable.vue` renders a sortable `UTable` (pass rate, flaky rate, failures, tests, avg time per spec prefix; pass-rate dot + linked prefix). Add `spec-health` to `validTabs` and `tabItems` in project `index.vue` page. - **API docs live in the in-app Scalar UI, not in VitePress docs**: The auto-generated OpenAPI spec (`/_openapi.json`) and interactive reference (`/docs`) in the running app or demo are the single source of truth for API documentation. There is no `docs/api.md` file in the VitePress docs site. Instead, the VitePress nav/sidebar links to the demo's `/docs` page (`https://piwitests.github.io/demo/docs`). When documenting features in VitePress `.md` files, reference `[API docs](/docs)` (for self-hosted) or the live demo link rather than inline endpoint descriptions, and never create a static API reference page. -- **Locator healing**: Capture happens in the reporter fixtures (`reporter/src/internal/capture/capture-fixtures.ts`, mirrored by the dogfood `application/tests/fixtures.ts`): a `Proxy` wraps the page-level locator methods in `LOCATOR_METHODS` and, after each successful action, records the element's attributes plus ranked alternatives (`generateAlternatives` in `reporter/src/internal/capture/locator-healing.ts`) stamped with `captureCallerLocation()`. The snapshots ride the wire as the transient `locatorSnapshots` per-case field (`shared/types.ts`, never a column) → every ingest site (`submit`, `upload`, `[id]/events`, + the demo `app/demo/api/reporter.ts` mirror) passes it to `persistRunCases` → the **shared** `upsertLocatorSnapshots` (`server/utils/locator-healing.ts`, imported by both server and demo) upserts one row per `(test_case_id, location)` into `locator_snapshots`. **Two invariants live in that helper:** rows are deduped by `(caseId, location)` before the batch upsert (a repeated call site would otherwise make PostgreSQL reject the `ON CONFLICT DO UPDATE`), and the stale-location purge runs **only for cases whose run passed** (`purge` flag) so a run that failed before reaching later locators doesn't delete their valid prior-success rows. Lookup (`getLocatorHealing`) runs a ladder: exact call-site location → `file:line` → locator signature → element-match against the failing ARIA snapshot → ARIA fallback. The locator signature must hash identically on both sides — capture via `locatorSignature(method, args)` and lookup via `locatorSignatureFromExpression(expr)` (`shared/locator-healing.ts`); the error's leaf selector is extracted by `extractLeafSelector` (`shared/error-fingerprint.ts`). Pure helpers live in `shared/` (`locator-healing.ts`, `locator-fingerprint.ts`, `locator-healing.types.ts`); the reporter keeps a structural mirror (no `shared` import). Toggle with the `captureLocators` reporter option / `PIWI_CAPTURE_LOCATORS` env var (auto-off when `collectPerformanceMetrics` is false). Surfaced via `LocatorHealingPanel.vue` and the `locatorHealing` AI-diagnosis section. Re-run `npm run app:seed:demo` after changing the captured/stored shape. +- **Locator healing**: Capture happens in the reporter fixtures (`reporter/src/internal/capture/capture-fixtures.ts`, mirrored by the dogfood `application/tests/fixtures.ts`): a `Proxy` wraps the page-level locator methods in `LOCATOR_METHODS` and, after each successful action, records the element's attributes plus ranked alternatives (`generateAlternatives` in `reporter/src/internal/capture/locator-healing.ts`) stamped with `captureCallerLocation()`. The in-page probe also reports `hasLabel` (gates `getByLabel` — an accessible name approximated from placeholder/title must not produce one) and `selectorCounts` (querySelectorAll uniqueness counts; ambiguous testid/id/name/class alternatives are dropped). The live input `value` is deliberately never captured (secret leak). Snapshots are deduped by location (`dedupeSnapshotsByLocation`) before attaching. The snapshots ride the wire as the transient `locatorSnapshots` per-case field (`shared/types.ts`, never a column) → every ingest site (`submit`, `upload`, `[id]/events`, + the demo `app/demo/api/reporter.ts` mirror) passes it to `persistRunCases` → the **shared** `upsertLocatorSnapshots` (`server/utils/locator-healing.ts`, imported by both server and demo) upserts one row per `(test_case_id, location)` into `locator_snapshots`. **Two invariants live in that helper:** rows are deduped by `(caseId, location)` before the batch upsert (a repeated call site would otherwise make PostgreSQL reject the `ON CONFLICT DO UPDATE`), and the stale-location purge runs **only for cases whose run passed** (`purge` flag) so a run that failed before reaching later locators doesn't delete their valid prior-success rows. Lookup (`getLocatorHealing`) runs a ladder: exact call-site location → `file:line` (path-suffix tolerant) → locator signature → cross-test (same signature captured by another test in the project, freshest wins) → element-match against the failing ARIA snapshot → ARIA fallback. `recommendation.suggestAddTestId` is only meaningful for stability-scored sources (`prior-run`/`fingerprint`/`cross-test`) and is force-cleared otherwise. The locator signature must hash identically on both sides — capture via `locatorSignature(method, args)` and lookup via `locatorSignatureFromExpression(expr)` (`shared/locator-healing.ts`); the error's leaf selector is extracted by `extractLeafSelector` (`shared/error-fingerprint.ts`). Pure helpers live in `shared/` (`locator-healing.ts`, `locator-fingerprint.ts`, `locator-healing.types.ts`); the reporter keeps a structural mirror (no `shared` import). Toggle with the `captureLocators` reporter option / `PIWI_CAPTURE_LOCATORS` env var (auto-off when `collectPerformanceMetrics` is false). Surfaced via `LocatorHealingPanel.vue` and the `locatorHealing` AI-diagnosis section. Re-run `npm run app:seed:demo` after changing the captured/stored shape. - **Timed-out tests fold into `failedTests`**: There is no `timedOutTests` column on `test_runs`. Timed-out tests (per-case status `'timedOut'` from Playwright, camelCase; `'timedout'` lowercase in the declared `TestCaseStatus` union) are folded into `failedTests` so the run summary reconciles (`total = passed + failed + skipped + didNotRun`) and matches the UI (which already treats timed-out as failed in the status filter, color, and retry command). Every ingest site that writes `failedTests` MUST use `sumFailedAndTimedOut(body.failedTests, body.timedOutTests)` (for body-field sites: `finish.post.ts`, `submit.post.ts`, `upload.post.ts`, and the demo `app/demo/api/reporter.ts` mirror) or `countFailedFromTally(insertedStatusCounts)` (for per-status-tally sites: `events.post.ts` and the demo mirror). Helpers live in `shared/utils/test-counts.ts`. When adding a new ingest site, import and use these helpers — never write `failedTests: body.failedTests` directly. The reporter sends `timedOutTests` separately in the finish/submit body; `TestRunSubmitPayload`/`TestRunFinishPayload` declare it as optional. ## Environment diff --git a/application/app/components/shared/LocatorHealingPanel.vue b/application/app/components/shared/LocatorHealingPanel.vue index 1c8e424a..e9a55065 100644 --- a/application/app/components/shared/LocatorHealingPanel.vue +++ b/application/app/components/shared/LocatorHealingPanel.vue @@ -6,22 +6,13 @@ */ import { recommendLocatorFix } from '#shared/locator-healing'; -import type { RankedLocator, LocatorFixRecommendation } from '#shared/locator-healing.types'; +import type { RankedLocator, LocatorFixRecommendation, LocatorHealingResult } from '#shared/locator-healing.types'; const props = defineProps<{ runId: number; testRunsCaseId: number; }>(); -interface LocatorHealingResult { - failingLocator: { method: string; args: Record } | null; - fromPriorSuccess: RankedLocator[] | null; - fromElementMatch: RankedLocator[] | null; - fromAriaSnapshot: RankedLocator[] | null; - source: 'prior-run' | 'element-match' | 'fingerprint' | 'aria-snapshot' | 'none'; - recommendation: LocatorFixRecommendation | null; -} - const { data: healing, pending, @@ -64,18 +55,25 @@ const recommendationNote = computed(() => { }); const sourceNote = computed(() => { - switch (healing.value?.source) { - case 'prior-run': - return 'Pre-captured from the last passing run — highest confidence'; - case 'element-match': - return 'The element looks renamed or moved — these are fresh locators for its current identity on the failing page'; - case 'fingerprint': - return 'Matched by locator signature (line numbers shifted)'; - case 'aria-snapshot': - return 'Generated from the failure-time ARIA snapshot — limited, no HTML attributes'; - default: - return ''; - } + const note = (() => { + switch (healing.value?.source) { + case 'prior-run': + return 'Pre-captured from the last passing run — highest confidence'; + case 'element-match': + return 'The element looks renamed or moved — these are fresh locators for its current identity on the failing page'; + case 'fingerprint': + return 'Matched by locator signature (line numbers shifted)'; + case 'cross-test': + return 'Captured by another test in this project that uses the same locator'; + case 'aria-snapshot': + return 'Generated from the failure-time ARIA snapshot — limited, no HTML attributes'; + default: + return ''; + } + })(); + // Stored snapshots age — surface when the data was last captured. + const captured = healing.value?.capturedAt; + return captured ? `${note} · captured ${formatRelativeTime(captured)}` : note; }); // Subtitle color: green for pre-captured (high confidence), primary for a @@ -84,6 +82,7 @@ const sourceClass = computed(() => { switch (healing.value?.source) { case 'prior-run': case 'fingerprint': + case 'cross-test': return 'text-success-600 dark:text-success-400'; case 'element-match': return 'text-primary-600 dark:text-primary-400'; diff --git a/application/app/utils/help-content.ts b/application/app/utils/help-content.ts index a6f777ea..678511ca 100644 --- a/application/app/utils/help-content.ts +++ b/application/app/utils/help-content.ts @@ -518,7 +518,7 @@ export const HELP_TOPICS = { // ── Locator healing ──────────────────────────────────────────────────── 'locator-healing': { title: 'Alternative locators', - text: 'When a locator breaks after a UI change, Piwi suggests pre-captured alternatives from the last passing run. Each alternative is ranked by stability score — prefer data-testid (100) over CSS classes (10–40).', + text: 'When a locator breaks after a UI change, Piwi suggests pre-captured alternatives from the last passing run — or from another test in the project that uses the same locator. Each alternative is ranked by stability score — prefer data-testid (100) over CSS classes (10–40).', doc: 'reporter#locator-healing', }, } as const satisfies Record; diff --git a/application/server/api/test-runs/[id]/cases/[caseId]/locator-healing.get.ts b/application/server/api/test-runs/[id]/cases/[caseId]/locator-healing.get.ts index 3bea1474..02ac43a2 100644 --- a/application/server/api/test-runs/[id]/cases/[caseId]/locator-healing.get.ts +++ b/application/server/api/test-runs/[id]/cases/[caseId]/locator-healing.get.ts @@ -13,7 +13,7 @@ defineRouteMeta({ tags: ['Test Runs'], summary: 'Get locator healing suggestions for a failed test case', description: - 'Returns ranked alternative locator suggestions for a failing locator in a test run case. Uses pre-captured element snapshots from the last passing run, falling back to ARIA snapshot analysis.', + 'Returns ranked alternative locator suggestions for a failing locator in a test run case. Uses pre-captured element snapshots from the last passing run (including snapshots captured by other tests in the project that use the same locator), falling back to ARIA snapshot analysis.', parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'integer' }, description: 'Test run id' }, { name: 'caseId', in: 'path', required: true, schema: { type: 'integer' }, description: 'Test run case id' }, diff --git a/application/server/database/migrations-pg/0024_naive_black_widow.sql b/application/server/database/migrations-pg/0024_naive_black_widow.sql new file mode 100644 index 00000000..b4bbb416 --- /dev/null +++ b/application/server/database/migrations-pg/0024_naive_black_widow.sql @@ -0,0 +1 @@ +CREATE INDEX "idx_locator_snapshots_args_fp" ON "locator_snapshots" USING btree ("used_args_fp"); \ No newline at end of file diff --git a/application/server/database/migrations-pg/meta/0024_snapshot.json b/application/server/database/migrations-pg/meta/0024_snapshot.json new file mode 100644 index 00000000..706dfc67 --- /dev/null +++ b/application/server/database/migrations-pg/meta/0024_snapshot.json @@ -0,0 +1,3645 @@ +{ + "id": "359dc3fa-bbe1-418c-bf4e-57e8f902f27d", + "prevId": "cf4e15a7-32a4-45d2-9b67-e2326f7f28f0", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account_tokens": { + "name": "account_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_account_tokens_hash": { + "name": "idx_account_tokens_hash", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_tokens_user_id_users_id_fk": { + "name": "account_tokens_user_id_users_id_fk", + "tableFrom": "account_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_key_hash_unique": { + "name": "api_keys_key_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "key_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_settings": { + "name": "app_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cluster_merge_suggestions": { + "name": "cluster_merge_suggestions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cluster_a_id": { + "name": "cluster_a_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cluster_b_id": { + "name": "cluster_b_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_confidence": { + "name": "llm_confidence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "llm_reason": { + "name": "llm_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_cluster_merge_suggestions_pair": { + "name": "idx_cluster_merge_suggestions_pair", + "columns": [ + { + "expression": "cluster_a_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cluster_b_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cluster_merge_suggestions_project_status": { + "name": "idx_cluster_merge_suggestions_project_status", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cluster_merge_suggestions_project_id_projects_id_fk": { + "name": "cluster_merge_suggestions_project_id_projects_id_fk", + "tableFrom": "cluster_merge_suggestions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cluster_merge_suggestions_cluster_a_id_failure_clusters_id_fk": { + "name": "cluster_merge_suggestions_cluster_a_id_failure_clusters_id_fk", + "tableFrom": "cluster_merge_suggestions", + "tableTo": "failure_clusters", + "columnsFrom": [ + "cluster_a_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cluster_merge_suggestions_cluster_b_id_failure_clusters_id_fk": { + "name": "cluster_merge_suggestions_cluster_b_id_failure_clusters_id_fk", + "tableFrom": "cluster_merge_suggestions", + "tableTo": "failure_clusters", + "columnsFrom": [ + "cluster_b_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entity_links": { + "name": "entity_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "test_run_id": { + "name": "test_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "test_runs_case_id": { + "name": "test_runs_case_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "test_case_id": { + "name": "test_case_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'generic'" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_text": { + "name": "status_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_color": { + "name": "status_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "unfurled_at": { + "name": "unfurled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_entity_links_run": { + "name": "idx_entity_links_run", + "columns": [ + { + "expression": "test_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entity_links_case_run": { + "name": "idx_entity_links_case_run", + "columns": [ + { + "expression": "test_runs_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entity_links_case": { + "name": "idx_entity_links_case", + "columns": [ + { + "expression": "test_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entity_links_test_run_id_test_runs_id_fk": { + "name": "entity_links_test_run_id_test_runs_id_fk", + "tableFrom": "entity_links", + "tableTo": "test_runs", + "columnsFrom": [ + "test_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entity_links_test_runs_case_id_test_runs_cases_id_fk": { + "name": "entity_links_test_runs_case_id_test_runs_cases_id_fk", + "tableFrom": "entity_links", + "tableTo": "test_runs_cases", + "columnsFrom": [ + "test_runs_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entity_links_test_case_id_test_cases_id_fk": { + "name": "entity_links_test_case_id_test_cases_id_fk", + "tableFrom": "entity_links", + "tableTo": "test_cases", + "columnsFrom": [ + "test_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entity_links_created_by_users_id_fk": { + "name": "entity_links_created_by_users_id_fk", + "tableFrom": "entity_links", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.failure_cluster_aliases": { + "name": "failure_cluster_aliases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cluster_id": { + "name": "cluster_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_failure_cluster_aliases_project_fingerprint": { + "name": "idx_failure_cluster_aliases_project_fingerprint", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_failure_cluster_aliases_cluster": { + "name": "idx_failure_cluster_aliases_cluster", + "columns": [ + { + "expression": "cluster_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "failure_cluster_aliases_project_id_projects_id_fk": { + "name": "failure_cluster_aliases_project_id_projects_id_fk", + "tableFrom": "failure_cluster_aliases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "failure_cluster_aliases_cluster_id_failure_clusters_id_fk": { + "name": "failure_cluster_aliases_cluster_id_failure_clusters_id_fk", + "tableFrom": "failure_cluster_aliases", + "tableTo": "failure_clusters", + "columnsFrom": [ + "cluster_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.failure_clusters": { + "name": "failure_clusters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_type": { + "name": "error_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "selector": { + "name": "selector", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sample_error": { + "name": "sample_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_seen_run_id": { + "name": "first_seen_run_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "last_seen_run_id": { + "name": "last_seen_run_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "triage_note": { + "name": "triage_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manual_base_commit": { + "name": "manual_base_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurrences": { + "name": "occurrences", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_failure_clusters_project_fingerprint": { + "name": "idx_failure_clusters_project_fingerprint", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_failure_clusters_project_last_seen": { + "name": "idx_failure_clusters_project_last_seen", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_seen_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "failure_clusters_project_id_projects_id_fk": { + "name": "failure_clusters_project_id_projects_id_fk", + "tableFrom": "failure_clusters", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.failure_diagnoses": { + "name": "failure_diagnoses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "cluster_id": { + "name": "cluster_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'cluster'" + }, + "test_runs_case_id": { + "name": "test_runs_case_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "context_sha": { + "name": "context_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_cause": { + "name": "root_cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_note": { + "name": "feedback_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_failure_diagnoses_execution_scope": { + "name": "idx_failure_diagnoses_execution_scope", + "columns": [ + { + "expression": "test_runs_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "failure_diagnoses_cluster_id_failure_clusters_id_fk": { + "name": "failure_diagnoses_cluster_id_failure_clusters_id_fk", + "tableFrom": "failure_diagnoses", + "tableTo": "failure_clusters", + "columnsFrom": [ + "cluster_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "failure_diagnoses_test_runs_case_id_test_runs_cases_id_fk": { + "name": "failure_diagnoses_test_runs_case_id_test_runs_cases_id_fk", + "tableFrom": "failure_diagnoses", + "tableTo": "test_runs_cases", + "columnsFrom": [ + "test_runs_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.failure_diagnosis_versions": { + "name": "failure_diagnosis_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "diagnosis_id": { + "name": "diagnosis_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cluster_id": { + "name": "cluster_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'cluster'" + }, + "test_runs_case_id": { + "name": "test_runs_case_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_cause": { + "name": "root_cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "context_sha": { + "name": "context_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_fdv_diagnosis_id": { + "name": "idx_fdv_diagnosis_id", + "columns": [ + { + "expression": "diagnosis_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_fdv_cluster_id": { + "name": "idx_fdv_cluster_id", + "columns": [ + { + "expression": "cluster_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "failure_diagnosis_versions_diagnosis_id_failure_diagnoses_id_fk": { + "name": "failure_diagnosis_versions_diagnosis_id_failure_diagnoses_id_fk", + "tableFrom": "failure_diagnosis_versions", + "tableTo": "failure_diagnoses", + "columnsFrom": [ + "diagnosis_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "failure_diagnosis_versions_cluster_id_failure_clusters_id_fk": { + "name": "failure_diagnosis_versions_cluster_id_failure_clusters_id_fk", + "tableFrom": "failure_diagnosis_versions", + "tableTo": "failure_clusters", + "columnsFrom": [ + "cluster_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "failure_diagnosis_versions_test_runs_case_id_test_runs_cases_id_fk": { + "name": "failure_diagnosis_versions_test_runs_case_id_test_runs_cases_id_fk", + "tableFrom": "failure_diagnosis_versions", + "tableTo": "test_runs_cases", + "columnsFrom": [ + "test_runs_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files": { + "name": "files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "test_run_id": { + "name": "test_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "test_runs_case_id": { + "name": "test_runs_case_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subtype": { + "name": "subtype", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "blob_id": { + "name": "blob_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_files_test_run_id": { + "name": "idx_files_test_run_id", + "columns": [ + { + "expression": "test_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_files_test_runs_case_id": { + "name": "idx_files_test_runs_case_id", + "columns": [ + { + "expression": "test_runs_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "files_test_run_id_test_runs_id_fk": { + "name": "files_test_run_id_test_runs_id_fk", + "tableFrom": "files", + "tableTo": "test_runs", + "columnsFrom": [ + "test_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "files_test_runs_case_id_test_runs_cases_id_fk": { + "name": "files_test_runs_case_id_test_runs_cases_id_fk", + "tableFrom": "files", + "tableTo": "test_runs_cases", + "columnsFrom": [ + "test_runs_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "files_blob_id_trace_blobs_id_fk": { + "name": "files_blob_id_trace_blobs_id_fk", + "tableFrom": "files", + "tableTo": "trace_blobs", + "columnsFrom": [ + "blob_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.locator_snapshots": { + "name": "locator_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "test_case_id": { + "name": "test_case_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "used_method": { + "name": "used_method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "used_args": { + "name": "used_args", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "used_args_fp": { + "name": "used_args_fp", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "element_tag": { + "name": "element_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "element_attrs": { + "name": "element_attrs", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "element_text": { + "name": "element_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alternatives": { + "name": "alternatives", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen_run_id": { + "name": "last_seen_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_locator_snapshots_location": { + "name": "idx_locator_snapshots_location", + "columns": [ + { + "expression": "test_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "location", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_locator_snapshots_fp": { + "name": "idx_locator_snapshots_fp", + "columns": [ + { + "expression": "test_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "used_method", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "used_args_fp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_locator_snapshots_args_fp": { + "name": "idx_locator_snapshots_args_fp", + "columns": [ + { + "expression": "used_args_fp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "locator_snapshots_test_case_id_test_cases_id_fk": { + "name": "locator_snapshots_test_case_id_test_cases_id_fk", + "tableFrom": "locator_snapshots", + "tableTo": "test_cases", + "columnsFrom": [ + "test_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "locator_snapshots_last_seen_run_id_test_runs_id_fk": { + "name": "locator_snapshots_last_seen_run_id_test_runs_id_fk", + "tableFrom": "locator_snapshots", + "tableTo": "test_runs", + "columnsFrom": [ + "last_seen_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network_requests": { + "name": "network_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "test_runs_case_id": { + "name": "test_runs_case_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "test_run_id": { + "name": "test_run_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "normalized_url": { + "name": "normalized_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "server_logs": { + "name": "server_logs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_nr_run": { + "name": "idx_nr_run", + "columns": [ + { + "expression": "test_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_nr_case": { + "name": "idx_nr_case", + "columns": [ + { + "expression": "test_runs_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_nr_normalized_url": { + "name": "idx_nr_normalized_url", + "columns": [ + { + "expression": "normalized_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "network_requests_test_runs_case_id_test_runs_cases_id_fk": { + "name": "network_requests_test_runs_case_id_test_runs_cases_id_fk", + "tableFrom": "network_requests", + "tableTo": "test_runs_cases", + "columnsFrom": [ + "test_runs_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "network_requests_test_run_id_test_runs_id_fk": { + "name": "network_requests_test_run_id_test_runs_id_fk", + "tableFrom": "network_requests", + "tableTo": "test_runs", + "columnsFrom": [ + "test_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_notification_channels_user": { + "name": "idx_notification_channels_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_deliveries": { + "name": "notification_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_notification_deliveries_status": { + "name": "idx_notification_deliveries_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notification_deliveries_dedupe": { + "name": "idx_notification_deliveries_dedupe", + "columns": [ + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_deliveries_subscription_id_subscriptions_id_fk": { + "name": "notification_deliveries_subscription_id_subscriptions_id_fk", + "tableFrom": "notification_deliveries", + "tableTo": "subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_deliveries_channel_id_notification_channels_id_fk": { + "name": "notification_deliveries_channel_id_notification_channels_id_fk", + "tableFrom": "notification_deliveries", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_assignments": { + "name": "project_assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_project_assignments_user": { + "name": "idx_project_assignments_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_project_assignments_project": { + "name": "idx_project_assignments_project", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_project_assignments_user_project": { + "name": "idx_project_assignments_user_project", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_assignments_user_id_users_id_fk": { + "name": "project_assignments_user_id_users_id_fk", + "tableFrom": "project_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_assignments_project_id_projects_id_fk": { + "name": "project_assignments_project_id_projects_id_fk", + "tableFrom": "project_assignments", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_assignments_created_by_users_id_fk": { + "name": "project_assignments_created_by_users_id_fk", + "tableFrom": "project_assignments", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_tags": { + "name": "project_tags", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_project_tags_project_id": { + "name": "idx_project_tags_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_project_tags_tag_id": { + "name": "idx_project_tags_tag_id", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_tags_project_id_projects_id_fk": { + "name": "project_tags_project_id_projects_id_fk", + "tableFrom": "project_tags", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_tags_tag_id_tags_id_fk": { + "name": "project_tags_tag_id_tags_id_fk", + "tableFrom": "project_tags", + "tableTo": "tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_tags_project_id_tag_id_pk": { + "name": "project_tags_project_id_tag_id_pk", + "columns": [ + "project_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "diagnosis_instructions": { + "name": "diagnosis_instructions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scm_token": { + "name": "scm_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_projects_updated_at": { + "name": "idx_projects_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_name_unique": { + "name": "projects_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "events": { + "name": "events", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "filters": { + "name": "filters", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'realtime'" + }, + "digest_at": { + "name": "digest_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "muted_until": { + "name": "muted_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_subscriptions_project": { + "name": "idx_subscriptions_project", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscriptions_user": { + "name": "idx_subscriptions_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscriptions_channel": { + "name": "idx_subscriptions_channel", + "columns": [ + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscriptions_user_id_users_id_fk": { + "name": "subscriptions_user_id_users_id_fk", + "tableFrom": "subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "subscriptions_channel_id_notification_channels_id_fk": { + "name": "subscriptions_channel_id_notification_channels_id_fk", + "tableFrom": "subscriptions", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "subscriptions_project_id_projects_id_fk": { + "name": "subscriptions_project_id_projects_id_fk", + "tableFrom": "subscriptions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'neutral'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_tags_updated_at": { + "name": "idx_tags_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tags_text_unique": { + "name": "tags_text_unique", + "nullsNotDistinct": false, + "columns": [ + "text" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.test_cases": { + "name": "test_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suite_path": { + "name": "suite_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "suite_id": { + "name": "suite_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "flaky_root_cause": { + "name": "flaky_root_cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_test_cases_project_id": { + "name": "idx_test_cases_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_test_cases_file_path_title": { + "name": "idx_test_cases_file_path_title", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "suite_path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "test_cases_project_id_projects_id_fk": { + "name": "test_cases_project_id_projects_id_fk", + "tableFrom": "test_cases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "test_cases_suite_id_test_suites_id_fk": { + "name": "test_cases_suite_id_test_suites_id_fk", + "tableFrom": "test_cases", + "tableTo": "test_suites", + "columnsFrom": [ + "suite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.test_runs": { + "name": "test_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_time": { + "name": "start_time", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_tests": { + "name": "total_tests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "passed_tests": { + "name": "passed_tests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_tests": { + "name": "failed_tests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_tests": { + "name": "skipped_tests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "did_not_run_tests": { + "name": "did_not_run_tests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "flaky_tests": { + "name": "flaky_tests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "avg_test_duration": { + "name": "avg_test_duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "p90_test_duration": { + "name": "p90_test_duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "shard_total": { + "name": "shard_total", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "shards_finished": { + "name": "shards_finished", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_full_run": { + "name": "is_full_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "filter_details": { + "name": "filter_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "setup_steps": { + "name": "setup_steps", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_token": { + "name": "stream_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "playwright_version": { + "name": "playwright_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_test_runs_project_id": { + "name": "idx_test_runs_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_test_runs_project_start": { + "name": "idx_test_runs_project_start", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_time", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_test_runs_start_time": { + "name": "idx_test_runs_start_time", + "columns": [ + { + "expression": "start_time", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "test_runs_project_id_projects_id_fk": { + "name": "test_runs_project_id_projects_id_fk", + "tableFrom": "test_runs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.test_runs_cases": { + "name": "test_runs_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "test_run_id": { + "name": "test_run_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "test_case_id": { + "name": "test_case_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_cluster_id": { + "name": "failure_cluster_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "line": { + "name": "line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "column": { + "name": "column", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "steps": { + "name": "steps", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "step_events": { + "name": "step_events", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "slowest_step": { + "name": "slowest_step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slowest_step_duration": { + "name": "slowest_step_duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "wasted_time_ms": { + "name": "wasted_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "web_vitals": { + "name": "web_vitals", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "console_logs": { + "name": "console_logs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "aria_snapshot": { + "name": "aria_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "test_source": { + "name": "test_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "browser": { + "name": "browser", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "browser_name": { + "name": "browser_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "test_annotations": { + "name": "test_annotations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "worker_index": { + "name": "worker_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "shard_index": { + "name": "shard_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_new_regression": { + "name": "is_new_regression", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_new_flaky": { + "name": "is_new_flaky", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_test_runs_cases_test_run_id": { + "name": "idx_test_runs_cases_test_run_id", + "columns": [ + { + "expression": "test_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_test_runs_cases_test_case_id": { + "name": "idx_test_runs_cases_test_case_id", + "columns": [ + { + "expression": "test_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_test_runs_cases_failure_cluster_id": { + "name": "idx_test_runs_cases_failure_cluster_id", + "columns": [ + { + "expression": "failure_cluster_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_test_runs_cases_run_browser": { + "name": "idx_test_runs_cases_run_browser", + "columns": [ + { + "expression": "test_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "test_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "retries", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "browser_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "test_runs_cases_test_run_id_test_runs_id_fk": { + "name": "test_runs_cases_test_run_id_test_runs_id_fk", + "tableFrom": "test_runs_cases", + "tableTo": "test_runs", + "columnsFrom": [ + "test_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "test_runs_cases_test_case_id_test_cases_id_fk": { + "name": "test_runs_cases_test_case_id_test_cases_id_fk", + "tableFrom": "test_runs_cases", + "tableTo": "test_cases", + "columnsFrom": [ + "test_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "test_runs_cases_failure_cluster_id_failure_clusters_id_fk": { + "name": "test_runs_cases_failure_cluster_id_failure_clusters_id_fk", + "tableFrom": "test_runs_cases", + "tableTo": "failure_clusters", + "columnsFrom": [ + "failure_cluster_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.test_suites": { + "name": "test_suites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suite_path": { + "name": "suite_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "annotations": { + "name": "annotations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_test_suites_unique": { + "name": "idx_test_suites_unique", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "suite_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "test_suites_project_id_projects_id_fk": { + "name": "test_suites_project_id_projects_id_fk", + "tableFrom": "test_suites", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trace_blobs": { + "name": "trace_blobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_trace_blobs_project_hash": { + "name": "idx_trace_blobs_project_hash", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trace_blobs_project_id_projects_id_fk": { + "name": "trace_blobs_project_id_projects_id_fk", + "tableFrom": "trace_blobs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trace_resources": { + "name": "trace_resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_trace_resources_project_name": { + "name": "idx_trace_resources_project_name", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trace_resources_project_id_projects_id_fk": { + "name": "trace_resources_project_id_projects_id_fk", + "tableFrom": "trace_resources", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_provider": { + "name": "oauth_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_provider_id": { + "name": "oauth_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_users_oauth": { + "name": "idx_users_oauth", + "columns": [ + { + "expression": "oauth_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "oauth_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_email": { + "name": "idx_users_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/application/server/database/migrations-pg/meta/_journal.json b/application/server/database/migrations-pg/meta/_journal.json index 58d43849..0508c545 100644 --- a/application/server/database/migrations-pg/meta/_journal.json +++ b/application/server/database/migrations-pg/meta/_journal.json @@ -169,6 +169,13 @@ "when": 1782578655201, "tag": "0023_minor_loners", "breakpoints": true + }, + { + "idx": 24, + "version": "7", + "when": 1783094696344, + "tag": "0024_naive_black_widow", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/application/server/database/migrations/0026_careful_wendigo.sql b/application/server/database/migrations/0026_careful_wendigo.sql new file mode 100644 index 00000000..68c8229f --- /dev/null +++ b/application/server/database/migrations/0026_careful_wendigo.sql @@ -0,0 +1 @@ +CREATE INDEX `idx_locator_snapshots_args_fp` ON `locator_snapshots` (`used_args_fp`); \ No newline at end of file diff --git a/application/server/database/migrations/meta/0026_snapshot.json b/application/server/database/migrations/meta/0026_snapshot.json new file mode 100644 index 00000000..44c7da0b --- /dev/null +++ b/application/server/database/migrations/meta/0026_snapshot.json @@ -0,0 +1,3343 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "9e01b0c5-7e82-4b04-8509-612bf3e84471", + "prevId": "71335b21-d03d-474a-9b9d-cdcb6cc0014a", + "tables": { + "account_tokens": { + "name": "account_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_account_tokens_hash": { + "name": "idx_account_tokens_hash", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "account_tokens_user_id_users_id_fk": { + "name": "account_tokens_user_id_users_id_fk", + "tableFrom": "account_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "api_keys_key_hash_unique": { + "name": "api_keys_key_hash_unique", + "columns": [ + "key_hash" + ], + "isUnique": true + }, + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_settings": { + "name": "app_settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cluster_merge_suggestions": { + "name": "cluster_merge_suggestions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cluster_a_id": { + "name": "cluster_a_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cluster_b_id": { + "name": "cluster_b_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "score": { + "name": "score", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "llm_confidence": { + "name": "llm_confidence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "llm_reason": { + "name": "llm_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_cluster_merge_suggestions_pair": { + "name": "idx_cluster_merge_suggestions_pair", + "columns": [ + "cluster_a_id", + "cluster_b_id" + ], + "isUnique": true + }, + "idx_cluster_merge_suggestions_project_status": { + "name": "idx_cluster_merge_suggestions_project_status", + "columns": [ + "project_id", + "status" + ], + "isUnique": false + } + }, + "foreignKeys": { + "cluster_merge_suggestions_project_id_projects_id_fk": { + "name": "cluster_merge_suggestions_project_id_projects_id_fk", + "tableFrom": "cluster_merge_suggestions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cluster_merge_suggestions_cluster_a_id_failure_clusters_id_fk": { + "name": "cluster_merge_suggestions_cluster_a_id_failure_clusters_id_fk", + "tableFrom": "cluster_merge_suggestions", + "tableTo": "failure_clusters", + "columnsFrom": [ + "cluster_a_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cluster_merge_suggestions_cluster_b_id_failure_clusters_id_fk": { + "name": "cluster_merge_suggestions_cluster_b_id_failure_clusters_id_fk", + "tableFrom": "cluster_merge_suggestions", + "tableTo": "failure_clusters", + "columnsFrom": [ + "cluster_b_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "entity_links": { + "name": "entity_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "test_run_id": { + "name": "test_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "test_runs_case_id": { + "name": "test_runs_case_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "test_case_id": { + "name": "test_case_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'generic'" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_text": { + "name": "status_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color": { + "name": "status_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unfurled_at": { + "name": "unfurled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_entity_links_run": { + "name": "idx_entity_links_run", + "columns": [ + "test_run_id" + ], + "isUnique": false + }, + "idx_entity_links_case_run": { + "name": "idx_entity_links_case_run", + "columns": [ + "test_runs_case_id" + ], + "isUnique": false + }, + "idx_entity_links_case": { + "name": "idx_entity_links_case", + "columns": [ + "test_case_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "entity_links_test_run_id_test_runs_id_fk": { + "name": "entity_links_test_run_id_test_runs_id_fk", + "tableFrom": "entity_links", + "tableTo": "test_runs", + "columnsFrom": [ + "test_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entity_links_test_runs_case_id_test_runs_cases_id_fk": { + "name": "entity_links_test_runs_case_id_test_runs_cases_id_fk", + "tableFrom": "entity_links", + "tableTo": "test_runs_cases", + "columnsFrom": [ + "test_runs_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entity_links_test_case_id_test_cases_id_fk": { + "name": "entity_links_test_case_id_test_cases_id_fk", + "tableFrom": "entity_links", + "tableTo": "test_cases", + "columnsFrom": [ + "test_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entity_links_created_by_users_id_fk": { + "name": "entity_links_created_by_users_id_fk", + "tableFrom": "entity_links", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "failure_cluster_aliases": { + "name": "failure_cluster_aliases", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cluster_id": { + "name": "cluster_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_failure_cluster_aliases_project_fingerprint": { + "name": "idx_failure_cluster_aliases_project_fingerprint", + "columns": [ + "project_id", + "fingerprint" + ], + "isUnique": true + }, + "idx_failure_cluster_aliases_cluster": { + "name": "idx_failure_cluster_aliases_cluster", + "columns": [ + "cluster_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "failure_cluster_aliases_project_id_projects_id_fk": { + "name": "failure_cluster_aliases_project_id_projects_id_fk", + "tableFrom": "failure_cluster_aliases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "failure_cluster_aliases_cluster_id_failure_clusters_id_fk": { + "name": "failure_cluster_aliases_cluster_id_failure_clusters_id_fk", + "tableFrom": "failure_cluster_aliases", + "tableTo": "failure_clusters", + "columnsFrom": [ + "cluster_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "failure_clusters": { + "name": "failure_clusters", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_type": { + "name": "error_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selector": { + "name": "selector", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sample_error": { + "name": "sample_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_run_id": { + "name": "first_seen_run_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_run_id": { + "name": "last_seen_run_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'open'" + }, + "triage_note": { + "name": "triage_note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manual_base_commit": { + "name": "manual_base_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "occurrences": { + "name": "occurrences", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "embedding": { + "name": "embedding", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_failure_clusters_project_fingerprint": { + "name": "idx_failure_clusters_project_fingerprint", + "columns": [ + "project_id", + "fingerprint" + ], + "isUnique": true + }, + "idx_failure_clusters_project_last_seen": { + "name": "idx_failure_clusters_project_last_seen", + "columns": [ + "project_id", + "last_seen_run_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "failure_clusters_project_id_projects_id_fk": { + "name": "failure_clusters_project_id_projects_id_fk", + "tableFrom": "failure_clusters", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "failure_diagnoses": { + "name": "failure_diagnoses", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "cluster_id": { + "name": "cluster_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'cluster'" + }, + "test_runs_case_id": { + "name": "test_runs_case_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "context_sha": { + "name": "context_sha", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'running'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "root_cause": { + "name": "root_cause", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feedback_note": { + "name": "feedback_note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_failure_diagnoses_cluster_scope": { + "name": "idx_failure_diagnoses_cluster_scope", + "columns": [ + "cluster_id", + "scope" + ], + "isUnique": true + }, + "idx_failure_diagnoses_execution": { + "name": "idx_failure_diagnoses_execution", + "columns": [ + "test_runs_case_id", + "scope" + ], + "isUnique": true + } + }, + "foreignKeys": { + "failure_diagnoses_cluster_id_failure_clusters_id_fk": { + "name": "failure_diagnoses_cluster_id_failure_clusters_id_fk", + "tableFrom": "failure_diagnoses", + "tableTo": "failure_clusters", + "columnsFrom": [ + "cluster_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "failure_diagnoses_test_runs_case_id_test_runs_cases_id_fk": { + "name": "failure_diagnoses_test_runs_case_id_test_runs_cases_id_fk", + "tableFrom": "failure_diagnoses", + "tableTo": "test_runs_cases", + "columnsFrom": [ + "test_runs_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "failure_diagnosis_versions": { + "name": "failure_diagnosis_versions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "diagnosis_id": { + "name": "diagnosis_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cluster_id": { + "name": "cluster_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'cluster'" + }, + "test_runs_case_id": { + "name": "test_runs_case_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'running'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "root_cause": { + "name": "root_cause", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "context_sha": { + "name": "context_sha", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_fdv_diagnosis_id": { + "name": "idx_fdv_diagnosis_id", + "columns": [ + "diagnosis_id" + ], + "isUnique": false + }, + "idx_fdv_cluster_id": { + "name": "idx_fdv_cluster_id", + "columns": [ + "cluster_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "failure_diagnosis_versions_diagnosis_id_failure_diagnoses_id_fk": { + "name": "failure_diagnosis_versions_diagnosis_id_failure_diagnoses_id_fk", + "tableFrom": "failure_diagnosis_versions", + "tableTo": "failure_diagnoses", + "columnsFrom": [ + "diagnosis_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "failure_diagnosis_versions_cluster_id_failure_clusters_id_fk": { + "name": "failure_diagnosis_versions_cluster_id_failure_clusters_id_fk", + "tableFrom": "failure_diagnosis_versions", + "tableTo": "failure_clusters", + "columnsFrom": [ + "cluster_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "failure_diagnosis_versions_test_runs_case_id_test_runs_cases_id_fk": { + "name": "failure_diagnosis_versions_test_runs_case_id_test_runs_cases_id_fk", + "tableFrom": "failure_diagnosis_versions", + "tableTo": "test_runs_cases", + "columnsFrom": [ + "test_runs_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "files": { + "name": "files", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "test_run_id": { + "name": "test_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "test_runs_case_id": { + "name": "test_runs_case_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subtype": { + "name": "subtype", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "blob_id": { + "name": "blob_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_files_test_run_id": { + "name": "idx_files_test_run_id", + "columns": [ + "test_run_id" + ], + "isUnique": false + }, + "idx_files_test_runs_case_id": { + "name": "idx_files_test_runs_case_id", + "columns": [ + "test_runs_case_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "files_test_run_id_test_runs_id_fk": { + "name": "files_test_run_id_test_runs_id_fk", + "tableFrom": "files", + "tableTo": "test_runs", + "columnsFrom": [ + "test_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "files_test_runs_case_id_test_runs_cases_id_fk": { + "name": "files_test_runs_case_id_test_runs_cases_id_fk", + "tableFrom": "files", + "tableTo": "test_runs_cases", + "columnsFrom": [ + "test_runs_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "files_blob_id_trace_blobs_id_fk": { + "name": "files_blob_id_trace_blobs_id_fk", + "tableFrom": "files", + "tableTo": "trace_blobs", + "columnsFrom": [ + "blob_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "locator_snapshots": { + "name": "locator_snapshots", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "test_case_id": { + "name": "test_case_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_method": { + "name": "used_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_args": { + "name": "used_args", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_args_fp": { + "name": "used_args_fp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "element_tag": { + "name": "element_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "element_attrs": { + "name": "element_attrs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "element_text": { + "name": "element_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "alternatives": { + "name": "alternatives", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_run_id": { + "name": "last_seen_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_locator_snapshots_location": { + "name": "idx_locator_snapshots_location", + "columns": [ + "test_case_id", + "location" + ], + "isUnique": true + }, + "idx_locator_snapshots_fp": { + "name": "idx_locator_snapshots_fp", + "columns": [ + "test_case_id", + "used_method", + "used_args_fp" + ], + "isUnique": false + }, + "idx_locator_snapshots_args_fp": { + "name": "idx_locator_snapshots_args_fp", + "columns": [ + "used_args_fp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "locator_snapshots_test_case_id_test_cases_id_fk": { + "name": "locator_snapshots_test_case_id_test_cases_id_fk", + "tableFrom": "locator_snapshots", + "tableTo": "test_cases", + "columnsFrom": [ + "test_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "locator_snapshots_last_seen_run_id_test_runs_id_fk": { + "name": "locator_snapshots_last_seen_run_id_test_runs_id_fk", + "tableFrom": "locator_snapshots", + "tableTo": "test_runs", + "columnsFrom": [ + "last_seen_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "network_requests": { + "name": "network_requests", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "test_runs_case_id": { + "name": "test_runs_case_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "test_run_id": { + "name": "test_run_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "normalized_url": { + "name": "normalized_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_logs": { + "name": "server_logs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_nr_run": { + "name": "idx_nr_run", + "columns": [ + "test_run_id" + ], + "isUnique": false + }, + "idx_nr_case": { + "name": "idx_nr_case", + "columns": [ + "test_runs_case_id", + "status" + ], + "isUnique": false + }, + "idx_nr_normalized_url": { + "name": "idx_nr_normalized_url", + "columns": [ + "normalized_url" + ], + "isUnique": false + } + }, + "foreignKeys": { + "network_requests_test_runs_case_id_test_runs_cases_id_fk": { + "name": "network_requests_test_runs_case_id_test_runs_cases_id_fk", + "tableFrom": "network_requests", + "tableTo": "test_runs_cases", + "columnsFrom": [ + "test_runs_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "network_requests_test_run_id_test_runs_id_fk": { + "name": "network_requests_test_run_id_test_runs_id_fk", + "tableFrom": "network_requests", + "tableTo": "test_runs", + "columnsFrom": [ + "test_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "verified": { + "name": "verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_notification_channels_user": { + "name": "idx_notification_channels_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_deliveries": { + "name": "notification_deliveries", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scheduled_for": { + "name": "scheduled_for", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sent_at": { + "name": "sent_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_notification_deliveries_status": { + "name": "idx_notification_deliveries_status", + "columns": [ + "status", + "scheduled_for" + ], + "isUnique": false + }, + "idx_notification_deliveries_dedupe": { + "name": "idx_notification_deliveries_dedupe", + "columns": [ + "dedupe_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "notification_deliveries_subscription_id_subscriptions_id_fk": { + "name": "notification_deliveries_subscription_id_subscriptions_id_fk", + "tableFrom": "notification_deliveries", + "tableTo": "subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_deliveries_channel_id_notification_channels_id_fk": { + "name": "notification_deliveries_channel_id_notification_channels_id_fk", + "tableFrom": "notification_deliveries", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_assignments": { + "name": "project_assignments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_project_assignments_user": { + "name": "idx_project_assignments_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_project_assignments_project": { + "name": "idx_project_assignments_project", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "idx_project_assignments_user_project": { + "name": "idx_project_assignments_user_project", + "columns": [ + "user_id", + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_assignments_user_id_users_id_fk": { + "name": "project_assignments_user_id_users_id_fk", + "tableFrom": "project_assignments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_assignments_project_id_projects_id_fk": { + "name": "project_assignments_project_id_projects_id_fk", + "tableFrom": "project_assignments", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_assignments_created_by_users_id_fk": { + "name": "project_assignments_created_by_users_id_fk", + "tableFrom": "project_assignments", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_tags": { + "name": "project_tags", + "columns": { + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_project_tags_project_id": { + "name": "idx_project_tags_project_id", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "idx_project_tags_tag_id": { + "name": "idx_project_tags_tag_id", + "columns": [ + "tag_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "project_tags_project_id_projects_id_fk": { + "name": "project_tags_project_id_projects_id_fk", + "tableFrom": "project_tags", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_tags_tag_id_tags_id_fk": { + "name": "project_tags_tag_id_tags_id_fk", + "tableFrom": "project_tags", + "tableTo": "tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_tags_project_id_tag_id_pk": { + "columns": [ + "project_id", + "tag_id" + ], + "name": "project_tags_project_id_tag_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "diagnosis_instructions": { + "name": "diagnosis_instructions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scm_token": { + "name": "scm_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "projects_name_unique": { + "name": "projects_name_unique", + "columns": [ + "name" + ], + "isUnique": true + }, + "idx_projects_updated_at": { + "name": "idx_projects_updated_at", + "columns": [ + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "subscriptions": { + "name": "subscriptions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "events": { + "name": "events", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "filters": { + "name": "filters", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'realtime'" + }, + "digest_at": { + "name": "digest_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "muted_until": { + "name": "muted_until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_subscriptions_project": { + "name": "idx_subscriptions_project", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "idx_subscriptions_user": { + "name": "idx_subscriptions_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_subscriptions_channel": { + "name": "idx_subscriptions_channel", + "columns": [ + "channel_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "subscriptions_user_id_users_id_fk": { + "name": "subscriptions_user_id_users_id_fk", + "tableFrom": "subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "subscriptions_channel_id_notification_channels_id_fk": { + "name": "subscriptions_channel_id_notification_channels_id_fk", + "tableFrom": "subscriptions", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "subscriptions_project_id_projects_id_fk": { + "name": "subscriptions_project_id_projects_id_fk", + "tableFrom": "subscriptions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tags": { + "name": "tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'neutral'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tags_text_unique": { + "name": "tags_text_unique", + "columns": [ + "text" + ], + "isUnique": true + }, + "idx_tags_updated_at": { + "name": "idx_tags_updated_at", + "columns": [ + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "test_cases": { + "name": "test_cases", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "suite_path": { + "name": "suite_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "suite_id": { + "name": "suite_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "flaky_root_cause": { + "name": "flaky_root_cause", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_test_cases_project_id": { + "name": "idx_test_cases_project_id", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "idx_test_cases_file_path_title": { + "name": "idx_test_cases_file_path_title", + "columns": [ + "project_id", + "file_path", + "suite_path", + "title" + ], + "isUnique": false + } + }, + "foreignKeys": { + "test_cases_project_id_projects_id_fk": { + "name": "test_cases_project_id_projects_id_fk", + "tableFrom": "test_cases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "test_cases_suite_id_test_suites_id_fk": { + "name": "test_cases_suite_id_test_suites_id_fk", + "tableFrom": "test_cases", + "tableTo": "test_suites", + "columnsFrom": [ + "suite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "test_runs": { + "name": "test_runs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_time": { + "name": "start_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "total_tests": { + "name": "total_tests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "passed_tests": { + "name": "passed_tests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "failed_tests": { + "name": "failed_tests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "skipped_tests": { + "name": "skipped_tests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "did_not_run_tests": { + "name": "did_not_run_tests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "flaky_tests": { + "name": "flaky_tests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "avg_test_duration": { + "name": "avg_test_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "p90_test_duration": { + "name": "p90_test_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shard_total": { + "name": "shard_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shards_finished": { + "name": "shards_finished", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_full_run": { + "name": "is_full_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "filter_details": { + "name": "filter_details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "setup_steps": { + "name": "setup_steps", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stream_token": { + "name": "stream_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "playwright_version": { + "name": "playwright_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_test_runs_project_id": { + "name": "idx_test_runs_project_id", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "idx_test_runs_project_start": { + "name": "idx_test_runs_project_start", + "columns": [ + "project_id", + "start_time" + ], + "isUnique": false + }, + "idx_test_runs_start_time": { + "name": "idx_test_runs_start_time", + "columns": [ + "start_time" + ], + "isUnique": false + } + }, + "foreignKeys": { + "test_runs_project_id_projects_id_fk": { + "name": "test_runs_project_id_projects_id_fk", + "tableFrom": "test_runs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "test_runs_cases": { + "name": "test_runs_cases", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "test_run_id": { + "name": "test_run_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "test_case_id": { + "name": "test_case_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "failure_cluster_id": { + "name": "failure_cluster_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "line": { + "name": "line", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "column": { + "name": "column", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "steps": { + "name": "steps", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "step_events": { + "name": "step_events", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slowest_step": { + "name": "slowest_step", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slowest_step_duration": { + "name": "slowest_step_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wasted_time_ms": { + "name": "wasted_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "web_vitals": { + "name": "web_vitals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "console_logs": { + "name": "console_logs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aria_snapshot": { + "name": "aria_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "test_source": { + "name": "test_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "browser": { + "name": "browser", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "browser_name": { + "name": "browser_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "test_annotations": { + "name": "test_annotations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "worker_index": { + "name": "worker_index", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shard_index": { + "name": "shard_index", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_new_regression": { + "name": "is_new_regression", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_new_flaky": { + "name": "is_new_flaky", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_test_runs_cases_test_run_id": { + "name": "idx_test_runs_cases_test_run_id", + "columns": [ + "test_run_id" + ], + "isUnique": false + }, + "idx_test_runs_cases_test_case_id": { + "name": "idx_test_runs_cases_test_case_id", + "columns": [ + "test_case_id" + ], + "isUnique": false + }, + "idx_test_runs_cases_failure_cluster_id": { + "name": "idx_test_runs_cases_failure_cluster_id", + "columns": [ + "failure_cluster_id" + ], + "isUnique": false + }, + "idx_test_runs_cases_run_browser": { + "name": "idx_test_runs_cases_run_browser", + "columns": [ + "test_run_id", + "test_case_id", + "retries", + "browser_name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "test_runs_cases_test_run_id_test_runs_id_fk": { + "name": "test_runs_cases_test_run_id_test_runs_id_fk", + "tableFrom": "test_runs_cases", + "tableTo": "test_runs", + "columnsFrom": [ + "test_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "test_runs_cases_test_case_id_test_cases_id_fk": { + "name": "test_runs_cases_test_case_id_test_cases_id_fk", + "tableFrom": "test_runs_cases", + "tableTo": "test_cases", + "columnsFrom": [ + "test_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "test_runs_cases_failure_cluster_id_failure_clusters_id_fk": { + "name": "test_runs_cases_failure_cluster_id_failure_clusters_id_fk", + "tableFrom": "test_runs_cases", + "tableTo": "failure_clusters", + "columnsFrom": [ + "failure_cluster_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "test_suites": { + "name": "test_suites", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "suite_path": { + "name": "suite_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "annotations": { + "name": "annotations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_test_suites_unique": { + "name": "idx_test_suites_unique", + "columns": [ + "project_id", + "file_path", + "suite_path" + ], + "isUnique": true + } + }, + "foreignKeys": { + "test_suites_project_id_projects_id_fk": { + "name": "test_suites_project_id_projects_id_fk", + "tableFrom": "test_suites", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trace_blobs": { + "name": "trace_blobs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_trace_blobs_project_hash": { + "name": "idx_trace_blobs_project_hash", + "columns": [ + "project_id", + "hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "trace_blobs_project_id_projects_id_fk": { + "name": "trace_blobs_project_id_projects_id_fk", + "tableFrom": "trace_blobs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trace_resources": { + "name": "trace_resources", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_trace_resources_project_name": { + "name": "idx_trace_resources_project_name", + "columns": [ + "project_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "trace_resources_project_id_projects_id_fk": { + "name": "trace_resources_project_id_projects_id_fk", + "tableFrom": "trace_resources", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_provider": { + "name": "oauth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_provider_id": { + "name": "oauth_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + }, + "idx_users_oauth": { + "name": "idx_users_oauth", + "columns": [ + "oauth_provider", + "oauth_provider_id" + ], + "isUnique": true + }, + "idx_users_email": { + "name": "idx_users_email", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/application/server/database/migrations/meta/_journal.json b/application/server/database/migrations/meta/_journal.json index 4219fc9c..08e738ce 100644 --- a/application/server/database/migrations/meta/_journal.json +++ b/application/server/database/migrations/meta/_journal.json @@ -183,6 +183,13 @@ "when": 1782578647664, "tag": "0025_mute_wong", "breakpoints": true + }, + { + "idx": 26, + "version": "6", + "when": 1783094695497, + "tag": "0026_careful_wendigo", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/application/server/database/schema.pg.ts b/application/server/database/schema.pg.ts index be5d8eef..953e967f 100644 --- a/application/server/database/schema.pg.ts +++ b/application/server/database/schema.pg.ts @@ -386,6 +386,8 @@ export const locatorSnapshots = pgTable( (table) => ({ uniqueLocation: uniqueIndex('idx_locator_snapshots_location').on(table.testCaseId, table.location), fingerprintIdx: index('idx_locator_snapshots_fp').on(table.testCaseId, table.usedMethod, table.usedArgsFp), + // Cross-test healing looks a signature up across all of a project's cases. + argsFpIdx: index('idx_locator_snapshots_args_fp').on(table.usedArgsFp), }), ); diff --git a/application/server/database/schema.sqlite.ts b/application/server/database/schema.sqlite.ts index d4521512..0188a59d 100644 --- a/application/server/database/schema.sqlite.ts +++ b/application/server/database/schema.sqlite.ts @@ -384,6 +384,8 @@ export const locatorSnapshots = sqliteTable( (table) => ({ uniqueLocation: uniqueIndex('idx_locator_snapshots_location').on(table.testCaseId, table.location), fingerprintIdx: index('idx_locator_snapshots_fp').on(table.testCaseId, table.usedMethod, table.usedArgsFp), + // Cross-test healing looks a signature up across all of a project's cases. + argsFpIdx: index('idx_locator_snapshots_args_fp').on(table.usedArgsFp), }), ); diff --git a/application/server/utils/ai-context.ts b/application/server/utils/ai-context.ts index c8cfbabc..6730ccbc 100644 --- a/application/server/utils/ai-context.ts +++ b/application/server/utils/ai-context.ts @@ -31,6 +31,7 @@ import { resolveContextLimits } from './ai-context-limits'; import type { ContextLimits } from '#shared/ai-context-limits'; import { getTraceFailingActionSection } from './trace-parser'; import { getLocatorHealing } from './locator-healing'; +import { parseAriaCandidates, textSimilarity } from '#shared/locator-fingerprint'; import type { BuildContextOptions, DiagnosisScope, @@ -1168,37 +1169,17 @@ function parseLocatorFromError( // Match: getByText('some text') const textMatch = error.match(/getBy(Text|Label|Placeholder|Title|AltText)\(\s*['"]([^'"]+)['"]\s*\)/); if (textMatch) { - return { method: textMatch[1]!, name: textMatch[2], exact: false }; + return { method: `getBy${textMatch[1]!}`, name: textMatch[2], exact: false }; } return null; } -/** Parse ARIA snapshot YAML lines into {role, name} pairs. */ -function parseAriaEntries(snapshot: string): Array<{ role: string; name: string }> { - const entries: Array<{ role: string; name: string }> = []; - for (const line of snapshot.split('\n')) { - const roleMatch = line.match(/\[role=(\w+)\]/); - if (!roleMatch) continue; - const nameMatch = line.match(/"([^"]+)"/); - entries.push({ role: (roleMatch[1] ?? '').toLowerCase(), name: nameMatch ? (nameMatch[1] ?? '') : '' }); - } - return entries; -} - -/** Simple token-overlap similarity between two strings (0–1). */ -function tokenSimilarity(a: string, b: string): number { - const aTokens = new Set(a.toLowerCase().split(/\s+/)); - const bTokens = b.toLowerCase().split(/\s+/); - if (aTokens.size === 0 && bTokens.length === 0) return 1; - let overlap = 0; - for (const t of bTokens) if (aTokens.has(t)) overlap++; - return overlap / Math.max(aTokens.size, bTokens.length); -} - /** * Nearest accessible-name hint for locator failures. * When the failing error contains a role/name locator, parse the stored ARIA - * snapshot and surface the closest candidates. + * snapshot and surface the closest candidates. Parsing and similarity reuse + * the shared healing helpers (`parseAriaCandidates` / `textSimilarity`) so + * this section agrees with the locator-healing pipeline. */ function nearestAriaNamesSection(rep: RepresentativeRow): string | null { if (!rep.error && !rep.ariaSnapshot) return null; @@ -1209,7 +1190,7 @@ function nearestAriaNamesSection(rep: RepresentativeRow): string | null { const locator = parseLocatorFromError(error); if (!locator) return null; - const entries = parseAriaEntries(snapshot); + const entries = parseAriaCandidates(snapshot).filter((e): e is { role: string; name: string } => e.name != null); if (entries.length === 0) return null; // Filter entries matching the locator's role @@ -1220,7 +1201,7 @@ function nearestAriaNamesSection(rep: RepresentativeRow): string | null { // Score by token similarity const scored = sameRole.map((e) => ({ ...e, - score: tokenSimilarity(locator.name!, e.name), + score: textSimilarity(locator.name, e.name), })); scored.sort((a, b) => b.score - a.score); @@ -1228,9 +1209,11 @@ function nearestAriaNamesSection(rep: RepresentativeRow): string | null { if (top.length === 0) return null; const lines: string[] = ['### Nearest matching elements (from ARIA)']; - lines.push( - `Requested: ${locator.method}('${locator.role ?? ''}', name="${locator.name}"${locator.exact ? ', exact: true' : ''})`, - ); + const requested = + locator.method === 'getByRole' + ? `getByRole('${locator.role ?? ''}', { name: '${locator.name}'${locator.exact ? ', exact: true' : ''} })` + : `${locator.method}('${locator.name}')`; + lines.push(`Requested: ${requested}`); for (const e of top) { const note = e.score >= 1 @@ -1288,8 +1271,13 @@ async function locatorHealingSection( ? "the locator's element appears renamed/moved — these are fresh locators for its current identity on the failing page" : healing.source === 'fingerprint' ? 'matched by locator fingerprint from a prior passing run' - : 'derived from the current ARIA snapshot'; + : healing.source === 'cross-test' + ? 'the same locator was captured against the real DOM by another test in this project' + : 'derived from the current ARIA snapshot'; lines.push(`Source: ${healing.source} (${sourceLabel})`); + if (healing.capturedAt) { + lines.push(`Captured: ${healing.capturedAt}`); + } // Surface the single convention-preserving recommendation so the model picks // the same minimal, idiomatic fix the dashboard highlights — not just the @@ -2177,8 +2165,15 @@ export async function buildDiagnosisContext( push(section('passedPeers', 'Passed Peers', peersResult.section)); } + // Alternative locators from prior success / ARIA snapshot — computed here + // so the nearest-ARIA hint below can be skipped when healing already + // surfaces grounded alternatives (the two would duplicate evidence). + const healing = await locatorHealingSection(db, rep); + // Nearest accessible-name hint for locator failures - push(section('nearestAriaNames', 'Nearest Matching ARIA Names', nearestAriaNamesSection(rep))); + if (!healing.section) { + push(section('nearestAriaNames', 'Nearest Matching ARIA Names', nearestAriaNamesSection(rep))); + } // Compared to last pass (duration/vitals/console/steps deltas) + already-green check const baselineResult = await baselineComparisonSection(db, rep, cluster?.lastSeenRunId); @@ -2210,8 +2205,8 @@ export async function buildDiagnosisContext( // B1: Failing action from trace parsing push(section('failingAction', 'Failing Action (from Trace)', await failingActionSection(db, rep, limits))); - // Alternative locators from prior success / ARIA snapshot - const healing = await locatorHealingSection(db, rep); + // Alternative locators from prior success / ARIA snapshot (computed above, + // before the nearest-ARIA hint) push(section('locatorHealing', 'Alternative Locators (Locator Healing)', healing.section)); if (healing.coverage) { coverage = { ...coverage, locatorHealing: healing.coverage }; diff --git a/application/server/utils/locator-healing.ts b/application/server/utils/locator-healing.ts index e2814a48..7b424921 100644 --- a/application/server/utils/locator-healing.ts +++ b/application/server/utils/locator-healing.ts @@ -6,9 +6,9 @@ * "element changed" case), fuzzy matching by locator signature, and ARIA * snapshot generation when no prior snapshot exists. */ -import { and, eq, notInArray, sql, inArray } from 'drizzle-orm'; -import { locatorSnapshots, testRunsCases, type LocatorSnapshotRow } from '../database/schema'; -import { extractLeafSelector, extractTopFrameFile } from '#shared/error-fingerprint'; +import { and, eq, ne, notInArray, sql, inArray } from 'drizzle-orm'; +import { locatorSnapshots, testCases, testRunsCases, type LocatorSnapshotRow } from '../database/schema'; +import { extractLeafSelector } from '#shared/error-fingerprint'; import { locatorSignatureFromExpression, locatorExpressionMethod, @@ -21,25 +21,12 @@ import { TEXT_CONTENT_ROLES, type ElementFingerprint, } from '#shared/locator-fingerprint'; -import type { RankedLocator, LocatorSnapshot, LocatorFixRecommendation } from '#shared/locator-healing.types'; +import type { RankedLocator, LocatorSnapshot, LocatorHealingResult } from '#shared/locator-healing.types'; import type { DrizzleDB } from '#shared/handlers/db'; -export interface LocatorHealingResult { - failingLocator: { method: string; args: Record } | null; - fromPriorSuccess: RankedLocator[] | null; - /** - * Fresh locators generated from the element's *current* identity, when the - * pre-captured locator no longer matches the live page (renamed/moved element). - */ - fromElementMatch: RankedLocator[] | null; - fromAriaSnapshot: RankedLocator[] | null; - source: 'prior-run' | 'element-match' | 'fingerprint' | 'aria-snapshot' | 'none'; - /** - * The single recommended fix — convention-preserving where possible — chosen - * from the active alternative list. Null when no alternatives are available. - */ - recommendation: LocatorFixRecommendation | null; -} +// The payload shape lives in shared/ so the API handler, MCP tools, AI context +// and the dashboard panel all agree on it; re-exported for existing importers. +export type { LocatorHealingResult, LocatorHealingSource } from '#shared/locator-healing.types'; /** * Parse a Playwright locator expression into method + args. @@ -184,21 +171,20 @@ function normalizeParsedArgs(method: string, args: unknown[]): { method: string; } /** - * Extract the call-site location from a Playwright error's stack trace. - * Uses the first user-code frame (file not in node_modules). + * Extract the call-site location (`file:line:col`) from a Playwright error's + * stack frames — the first frame outside node_modules and Node internals. + * Handles both anonymous frames (`at tests/x.spec.ts:42:5`) and named ones + * (`at Object.foo (tests/x.spec.ts:42:5)`), mirroring `extractTopFrameFile`. */ function extractErrorLocation(error: string): string | null { - const topFrame = extractTopFrameFile(error); - if (!topFrame) return null; - - // The error message contains stack frames like: - // at tests/checkout.spec.ts:42:5 - const frameMatch = error.match(/\s+at\s+([^(:\s]+\.[a-z]+):(\d+):(\d+)/i); - if (frameMatch) { - return `${frameMatch[1]}:${frameMatch[2]}:${frameMatch[3]}`; + const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm; + let m: RegExpExecArray | null; + while ((m = frameRe.exec(error)) !== null) { + const file = m[1]!.replace(/\\/g, '/'); + if (file.includes('node_modules') || file.startsWith('node:')) continue; + return `${file}:${m[2]}:${m[3]}`; } - - return topFrame; // fallback: just the file + return null; } /** @@ -322,21 +308,34 @@ function tokenize(text: string): Set { * wins over prior-success (pre-captured), which wins over the ARIA fallback. * Kept in one place so every return path picks the recommendation the same way. */ +/** Sources whose alternative scores are real stability scores from a pre-captured DOM snapshot. */ +const STABILITY_SCORED_SOURCES = new Set(['prior-run', 'fingerprint', 'cross-test']); + function buildHealingResult( failingLocator: LocatorHealingResult['failingLocator'], fromPriorSuccess: RankedLocator[] | null, fromAriaSnapshot: RankedLocator[] | null, source: LocatorHealingResult['source'], fromElementMatch: RankedLocator[] | null = null, + capturedAt: Date | null = null, ): LocatorHealingResult { const alternatives = fromElementMatch ?? fromPriorSuccess ?? fromAriaSnapshot ?? []; + const recommendation = alternatives.length ? recommendLocatorFix(failingLocator?.method, alternatives) : null; + // "Everything is fragile — add a data-testid" only makes sense when the + // scores are stability scores. ARIA-fallback scores measure text-overlap + // relevance and element-match scores are a fixed band, so a low score there + // says nothing about the element lacking stable hooks. + if (recommendation && !STABILITY_SCORED_SOURCES.has(source)) { + recommendation.suggestAddTestId = false; + } return { failingLocator, fromPriorSuccess, fromElementMatch, fromAriaSnapshot, source, - recommendation: alternatives.length ? recommendLocatorFix(failingLocator?.method, alternatives) : null, + recommendation, + capturedAt: capturedAt ? capturedAt.toISOString() : null, }; } @@ -415,7 +414,7 @@ function resolveStoredHit( failingLocator: LocatorHealingResult['failingLocator'], hit: LocatorSnapshotRow, ariaSnapshot: string | null, - priorSource: 'prior-run' | 'fingerprint', + priorSource: 'prior-run' | 'fingerprint' | 'cross-test', ): LocatorHealingResult { const priorAlts = parseAlternativesColumn(hit); const fingerprint = fingerprintFromSnapshot(priorAlts, hit); @@ -423,7 +422,7 @@ function resolveStoredHit( if (fresh) { return buildHealingResult(failingLocator, null, null, 'element-match', fresh); } - return buildHealingResult(failingLocator, priorAlts, null, priorSource); + return buildHealingResult(failingLocator, priorAlts, null, priorSource, null, hit.lastSeenAt ?? null); } /** @@ -481,16 +480,28 @@ export async function getLocatorHealing(db: DrizzleDB, testRunsCaseId: number): } } - // Ladder 2: locator signature (method + ordered string literals) — survives - // line shifts. Cannot tell apart repeated identical locators; returns the - // first match. - if (selector && snaps.length > 0) { + if (selector) { const sig = await locatorSignatureFromExpression(selector); const method = locatorExpressionMethod(selector); + + // Ladder 2: locator signature (method + ordered string literals) — survives + // line shifts. Cannot tell apart repeated identical locators; returns the + // first match. const hit = snaps.find((s) => s.usedArgsFp === sig && (!method || s.usedMethod === method)); if (hit) { return resolveStoredHit(failingLocator, hit, row.ariaSnapshot ?? null, 'fingerprint'); } + + // Ladder 2.5: cross-test — the same locator captured by another test in + // the same project (assert-only locators are never captured by their own + // test, but an action elsewhere frequently touches the same element). + // Freshest capture wins. + if (testCaseId) { + const crossHit = await findCrossTestSnapshot(db, testCaseId, sig, method); + if (crossHit) { + return resolveStoredHit(failingLocator, crossHit, row.ariaSnapshot ?? null, 'cross-test'); + } + } } // Ladder 3: ARIA snapshot fallback @@ -502,6 +513,38 @@ export async function getLocatorHealing(db: DrizzleDB, testRunsCaseId: number): return buildHealingResult(failingLocator, null, null, 'none'); } +/** + * Find the freshest snapshot of the same locator signature captured by a + * *different* test case in the same project — the cross-test healing ladder. + * The project scope keeps suggestions on the same application; the signature + * (method + ordered string literals) guarantees it is the same locator text. + */ +async function findCrossTestSnapshot( + db: DrizzleDB, + testCaseId: number, + sig: string, + method: string | null, +): Promise { + const rows = await db + .select({ snap: locatorSnapshots }) + .from(locatorSnapshots) + .innerJoin(testCases, eq(locatorSnapshots.testCaseId, testCases.id)) + .where( + and( + eq( + testCases.projectId, + sql`(select project_id from test_cases where id = ${testCaseId})`, + ), + eq(locatorSnapshots.usedArgsFp, sig), + ne(locatorSnapshots.testCaseId, testCaseId), + ...(method ? [eq(locatorSnapshots.usedMethod, method)] : []), + ), + ) + .orderBy(sql`${locatorSnapshots.lastSeenAt} desc`) + .limit(1); + return rows[0]?.snap ?? null; +} + /** * Batch variant: fetch locator healing for multiple test-run-case IDs in * two queries instead of N * 2. Used by the MCP `get_cluster` tool to avoid @@ -562,8 +605,9 @@ export async function getLocatorHealingBatch( } } - // Ladder 2: signature - if (selector && snaps.length > 0) { + // Ladder 2: signature; ladder 2.5: cross-test (same locator captured by + // another test in the project) + if (selector) { const sig = await locatorSignatureFromExpression(selector); const method = locatorExpressionMethod(selector); const hit = snaps.find((s) => s.usedArgsFp === sig && (!method || s.usedMethod === method)); @@ -571,6 +615,13 @@ export async function getLocatorHealingBatch( results.set(row.id, resolveStoredHit(failingLocator, hit, row.ariaSnapshot ?? null, 'fingerprint')); continue; } + if (row.testCaseId) { + const crossHit = await findCrossTestSnapshot(db, row.testCaseId, sig, method); + if (crossHit) { + results.set(row.id, resolveStoredHit(failingLocator, crossHit, row.ariaSnapshot ?? null, 'cross-test')); + continue; + } + } } // Ladder 3: ARIA fallback @@ -586,14 +637,25 @@ export async function getLocatorHealingBatch( return results; } -/** Compare two `file:line:col` locations ignoring the trailing column. */ +/** + * Compare two `file:line:col` locations ignoring the trailing column. The file + * parts match on a path suffix at a `/` boundary, so the stored cwd-relative + * capture location (`tests/x.spec.ts`) still matches an absolute path from an + * error stack frame (`/repo/tests/x.spec.ts`). + */ function sameFileLine(a: string | null, b: string | null): boolean { if (!a || !b) return false; - return stripColumn(a) === stripColumn(b); + const [fileA, lineA] = splitFileLine(a); + const [fileB, lineB] = splitFileLine(b); + if (lineA !== lineB || !fileA || !fileB) return false; + if (fileA === fileB) return true; + return fileA.endsWith(`/${fileB}`) || fileB.endsWith(`/${fileA}`); } -function stripColumn(loc: string): string { - return loc.replace(/:\d+$/, ''); +/** Split `file:line:col` into `[file, line]`, dropping the column. */ +function splitFileLine(loc: string): [string, string] { + const m = loc.match(/^(.*):(\d+):\d+$/) ?? loc.match(/^(.*):(\d+)$/); + return m ? [m[1]!, m[2]!] : [loc, '']; } function parseAlternativesColumn(row: LocatorSnapshotRow): RankedLocator[] | null { @@ -628,7 +690,7 @@ export async function upsertLocatorSnapshots( perCase: Array<{ caseId: number; snapshots: LocatorSnapshot[] | null | undefined; purge?: boolean }>, runId: number, ): Promise { - // Keyed by `${caseId}${location}` so a repeated call site yields one row. + // Keyed by `${caseId}\x00${location}` so a repeated call site yields one row. const rowByKey = new Map(); const seenByCase = new Map>(); // Cases whose run completed (passed) — only these may purge stale locations. @@ -648,7 +710,7 @@ export async function upsertLocatorSnapshots( const captured = snapshots.filter((s) => s.element && s.location); const fps = await Promise.all(captured.map((s) => locatorSignature(s.used.method, s.used.args))); captured.forEach((snap, idx) => { - rowByKey.set(`${caseId}${snap.location}`, { + rowByKey.set(`${caseId}\x00${snap.location}`, { testCaseId: caseId, location: snap.location!, usedMethod: snap.used.method, diff --git a/application/server/utils/mcp/tools.ts b/application/server/utils/mcp/tools.ts index ea28a6b8..d515f942 100644 --- a/application/server/utils/mcp/tools.ts +++ b/application/server/utils/mcp/tools.ts @@ -1244,6 +1244,7 @@ const HANDLERS: Record = { return dropNulls({ testRunsCaseId: id, source: h.source, + capturedAt: h.capturedAt, failingLocator: h.failingLocator, recommendation: h.recommendation ? dropNulls({ diff --git a/application/shared/locator-healing.ts b/application/shared/locator-healing.ts index 0cb4e951..3bf034ef 100644 --- a/application/shared/locator-healing.ts +++ b/application/shared/locator-healing.ts @@ -85,38 +85,6 @@ export function recommendLocatorFix( }; } -/** - * Normalize locator args for deterministic hashing. - * - * Steps: - * 1. Deep-convert the args array to a canonical representation. - * 2. For object arguments, strip the `exact` key (it's a matching mode, - * not a locator identity signal), sort remaining keys alphabetically. - * 3. JSON.stringify with sorted keys. - * 4. SHA-256 hex digest of the resulting string. - * - * The same locator expressed with `{ exact: true }` vs without produces - * the same hash, so line-shift-resilient matching works across runs. - */ -export async function normalizeAndHashArgs(args: unknown[]): Promise { - const canonical = args.map(canonicalizeArg); - const payload = JSON.stringify(canonical); - return sha256Hex(payload); -} - -function canonicalizeArg(arg: unknown): unknown { - if (arg === null || typeof arg !== 'object') return arg; - if (Array.isArray(arg)) return arg.map(canonicalizeArg); - - const obj = arg as Record; - const normalized: Record = {}; - for (const key of Object.keys(obj).sort()) { - if (key === 'exact') continue; - normalized[key] = canonicalizeArg(obj[key]); - } - return normalized; -} - async function sha256Hex(input: string): Promise { // Use Web Crypto API — same API surface in Node 19+ and browsers. const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(input)); diff --git a/application/shared/locator-healing.types.ts b/application/shared/locator-healing.types.ts index 900adc00..8df8e110 100644 --- a/application/shared/locator-healing.types.ts +++ b/application/shared/locator-healing.types.ts @@ -35,6 +35,17 @@ export interface LocatorFixRecommendation { suggestAddTestId: boolean; } +/** + * Match counts for candidate selectors, probed against the live DOM at capture + * time. A count > 1 marks the selector ambiguous (strict-mode violation). + */ +export interface SelectorCounts { + testId?: number; + id?: number; + name?: number; + classes?: Record; +} + /** Raw element attributes captured after a successful action. */ export interface ElementAttributes { tagName: string; @@ -45,14 +56,16 @@ export interface ElementAttributes { accessibleName: string | null; /** Center point of the element's bounding box — spatial discriminator. */ center: { x: number; y: number } | null; + /** True when the element has an associated