@aplaytest/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/config/schema.d.ts +247 -0
  3. package/dist/config/schema.d.ts.map +1 -0
  4. package/dist/config/schema.js +279 -0
  5. package/dist/config/schema.js.map +1 -0
  6. package/dist/evidence/id.d.ts +17 -0
  7. package/dist/evidence/id.d.ts.map +1 -0
  8. package/dist/evidence/id.js +20 -0
  9. package/dist/evidence/id.js.map +1 -0
  10. package/dist/evidence/redact.d.ts +22 -0
  11. package/dist/evidence/redact.d.ts.map +1 -0
  12. package/dist/evidence/redact.js +97 -0
  13. package/dist/evidence/redact.js.map +1 -0
  14. package/dist/evidence/store.d.ts +51 -0
  15. package/dist/evidence/store.d.ts.map +1 -0
  16. package/dist/evidence/store.js +148 -0
  17. package/dist/evidence/store.js.map +1 -0
  18. package/dist/evidence/types.d.ts +183 -0
  19. package/dist/evidence/types.d.ts.map +1 -0
  20. package/dist/evidence/types.js +21 -0
  21. package/dist/evidence/types.js.map +1 -0
  22. package/dist/history/ingest-playwright.d.ts +26 -0
  23. package/dist/history/ingest-playwright.d.ts.map +1 -0
  24. package/dist/history/ingest-playwright.js +73 -0
  25. package/dist/history/ingest-playwright.js.map +1 -0
  26. package/dist/history/ingest.d.ts +29 -0
  27. package/dist/history/ingest.d.ts.map +1 -0
  28. package/dist/history/ingest.js +86 -0
  29. package/dist/history/ingest.js.map +1 -0
  30. package/dist/history/memory-index.d.ts +88 -0
  31. package/dist/history/memory-index.d.ts.map +1 -0
  32. package/dist/history/memory-index.js +236 -0
  33. package/dist/history/memory-index.js.map +1 -0
  34. package/dist/history/playwright-report.d.ts +102 -0
  35. package/dist/history/playwright-report.d.ts.map +1 -0
  36. package/dist/history/playwright-report.js +274 -0
  37. package/dist/history/playwright-report.js.map +1 -0
  38. package/dist/history/store.d.ts +73 -0
  39. package/dist/history/store.d.ts.map +1 -0
  40. package/dist/history/store.js +243 -0
  41. package/dist/history/store.js.map +1 -0
  42. package/dist/history/types.d.ts +69 -0
  43. package/dist/history/types.d.ts.map +1 -0
  44. package/dist/history/types.js +18 -0
  45. package/dist/history/types.js.map +1 -0
  46. package/dist/history/url.d.ts +79 -0
  47. package/dist/history/url.d.ts.map +1 -0
  48. package/dist/history/url.js +121 -0
  49. package/dist/history/url.js.map +1 -0
  50. package/dist/index.d.ts +39 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +31 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/locator/stability.d.ts +63 -0
  55. package/dist/locator/stability.d.ts.map +1 -0
  56. package/dist/locator/stability.js +127 -0
  57. package/dist/locator/stability.js.map +1 -0
  58. package/dist/taxonomy/classify.d.ts +63 -0
  59. package/dist/taxonomy/classify.d.ts.map +1 -0
  60. package/dist/taxonomy/classify.js +344 -0
  61. package/dist/taxonomy/classify.js.map +1 -0
  62. package/dist/taxonomy/kinds.d.ts +49 -0
  63. package/dist/taxonomy/kinds.d.ts.map +1 -0
  64. package/dist/taxonomy/kinds.js +139 -0
  65. package/dist/taxonomy/kinds.js.map +1 -0
  66. package/dist/taxonomy/parse-error.d.ts +85 -0
  67. package/dist/taxonomy/parse-error.d.ts.map +1 -0
  68. package/dist/taxonomy/parse-error.js +179 -0
  69. package/dist/taxonomy/parse-error.js.map +1 -0
  70. package/dist/version.d.ts +10 -0
  71. package/dist/version.d.ts.map +1 -0
  72. package/dist/version.js +10 -0
  73. package/dist/version.js.map +1 -0
  74. package/dist-cjs/.tsbuildinfo +1 -0
  75. package/dist-cjs/config/schema.js +283 -0
  76. package/dist-cjs/config/schema.js.map +1 -0
  77. package/dist-cjs/evidence/id.js +24 -0
  78. package/dist-cjs/evidence/id.js.map +1 -0
  79. package/dist-cjs/evidence/redact.js +103 -0
  80. package/dist-cjs/evidence/redact.js.map +1 -0
  81. package/dist-cjs/evidence/store.js +156 -0
  82. package/dist-cjs/evidence/store.js.map +1 -0
  83. package/dist-cjs/evidence/types.js +25 -0
  84. package/dist-cjs/evidence/types.js.map +1 -0
  85. package/dist-cjs/history/ingest-playwright.js +76 -0
  86. package/dist-cjs/history/ingest-playwright.js.map +1 -0
  87. package/dist-cjs/history/ingest.js +89 -0
  88. package/dist-cjs/history/ingest.js.map +1 -0
  89. package/dist-cjs/history/memory-index.js +242 -0
  90. package/dist-cjs/history/memory-index.js.map +1 -0
  91. package/dist-cjs/history/playwright-report.js +277 -0
  92. package/dist-cjs/history/playwright-report.js.map +1 -0
  93. package/dist-cjs/history/store.js +247 -0
  94. package/dist-cjs/history/store.js.map +1 -0
  95. package/dist-cjs/history/types.js +23 -0
  96. package/dist-cjs/history/types.js.map +1 -0
  97. package/dist-cjs/history/url.js +127 -0
  98. package/dist-cjs/history/url.js.map +1 -0
  99. package/dist-cjs/index.js +88 -0
  100. package/dist-cjs/index.js.map +1 -0
  101. package/dist-cjs/locator/stability.js +134 -0
  102. package/dist-cjs/locator/stability.js.map +1 -0
  103. package/dist-cjs/package.json +3 -0
  104. package/dist-cjs/taxonomy/classify.js +348 -0
  105. package/dist-cjs/taxonomy/classify.js.map +1 -0
  106. package/dist-cjs/taxonomy/kinds.js +145 -0
  107. package/dist-cjs/taxonomy/kinds.js.map +1 -0
  108. package/dist-cjs/taxonomy/parse-error.js +186 -0
  109. package/dist-cjs/taxonomy/parse-error.js.map +1 -0
  110. package/dist-cjs/version.js +13 -0
  111. package/dist-cjs/version.js.map +1 -0
  112. package/package.json +39 -0
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Map a merged Playwright JSON report onto history records.
3
+ *
4
+ * This is a runner adapter for suites that cannot install the atest reporter —
5
+ * most importantly, a suite whose test job is owned by a shared CI template
6
+ * that only uploads `blob-report/` and the merged JSON. Playwright's own
7
+ * `--reporter=json` output is then the ONLY channel through which run data
8
+ * escapes, and it carries more than it first appears to.
9
+ *
10
+ * Pure and synchronous, over structural interfaces: the whole mapping is
11
+ * unit-testable against a plain object, with no store and no filesystem.
12
+ * `ingestPlaywrightJson` does the I/O and hands the result in.
13
+ *
14
+ * ── What this path gives up, and what it does not ───────────────────────────
15
+ * No evidence bundle: no ARIA snapshot, no locator candidates, no network
16
+ * ledger, no console capture. Healing therefore still requires the reporter.
17
+ *
18
+ * What it does NOT give up is identity. Every field below is read from the
19
+ * report rather than invented, because a history row that cannot be joined to
20
+ * the rows around it is worse than no row at all — it looks like data and
21
+ * scores like noise.
22
+ */
23
+ import { createHash } from 'node:crypto';
24
+ import { classify } from '../taxonomy/classify.js';
25
+ import { joinErrors, parsePlaywrightError } from '../taxonomy/parse-error.js';
26
+ import { ATEST_VERSION } from '../version.js';
27
+ import { RUN_SCHEMA_VERSION } from './types.js';
28
+ const RUN_ID_PREFIX = 'pwjson_';
29
+ const RUN_ID_LENGTH = 12;
30
+ function toOutcome(status) {
31
+ if (status === 'passed')
32
+ return 'passed';
33
+ if (status === 'timedOut')
34
+ return 'timedOut';
35
+ if (status === 'skipped')
36
+ return 'skipped';
37
+ if (status === 'interrupted')
38
+ return 'interrupted';
39
+ return 'failed';
40
+ }
41
+ /**
42
+ * Playwright's stable test id, which the JSON reporter emits as `spec.id` and
43
+ * which is exactly what `AttemptRecord.testId` is documented to hold. It is
44
+ * unique per (test, project) and survives a title rename.
45
+ *
46
+ * Taking it from the report rather than composing one is the single most
47
+ * important line in this file. A composed `file::title` id is a DIFFERENT ID
48
+ * SPACE from the reporter's, so history gathered on this path would never join
49
+ * to history gathered after the reporter is installed: every test would look
50
+ * new, every score would reset to "insufficient data", and the old rows would
51
+ * linger as phantom tests forever. The adoption sequence that starts on JSON
52
+ * and later adds the reporter is the normal one, so that migration cliff has to
53
+ * not exist.
54
+ *
55
+ * The fallback exists only for a report old enough to lack `spec.id`. It
56
+ * includes the title path, so two same-titled tests in different `describe`
57
+ * blocks stay distinct — but it is still a separate id space, and the caller is
58
+ * told about it.
59
+ */
60
+ function attemptTestId(spec, file, titlePath) {
61
+ if (typeof spec.id === 'string' && spec.id !== '')
62
+ return spec.id;
63
+ return [file, ...titlePath, spec.title ?? ''].join('::');
64
+ }
65
+ /**
66
+ * Classify a failing attempt from its error text.
67
+ *
68
+ * The classifier reads `message + stack` for nearly every rule, so this path
69
+ * recovers most of the taxonomy without an evidence bundle — including `infra`,
70
+ * which matters more than it looks: doc 06 requires infra attempts to be
71
+ * excluded from flake statistics, and a hard-coded `'unknown'` would have let
72
+ * every browser crash count as evidence of flakiness.
73
+ *
74
+ * One honest gap. `contract.app-error` needs captured console output, which
75
+ * only the reporter has. Without it, an application exception whose sole trace
76
+ * was a console message classifies as whatever assertion failed downstream of
77
+ * it — typically `assertion_visibility`. That over-weights a real bug as flake
78
+ * rather than hiding it, which is the safer direction to be wrong in, but it is
79
+ * a reason to prefer the reporter once evidence egress exists.
80
+ */
81
+ function classifyAttempt(result, timeoutMs) {
82
+ const { message, stack } = joinErrors(result.errors);
83
+ if (message === '' && stack === '')
84
+ return 'unknown';
85
+ const parsed = parsePlaywrightError(message, stack);
86
+ const duration = result.duration ?? 0;
87
+ return classify({
88
+ message,
89
+ stack,
90
+ matcher: parsed.matcher,
91
+ timedOut: parsed.timedOut || result.status === 'timedOut',
92
+ // The reporter's sidecars. Absent here by construction, not by accident.
93
+ consoleErrors: [],
94
+ failedRequests: [],
95
+ budgetUsedRatio: timeoutMs > 0 ? duration / timeoutMs : 0,
96
+ }).kind;
97
+ }
98
+ function collectAttempts(suites, file, titlePath) {
99
+ const out = [];
100
+ for (const suite of suites ?? []) {
101
+ const suiteFile = suite.file ?? file;
102
+ const path = suite.title !== undefined && suite.title !== '' ? [...titlePath, suite.title] : titlePath;
103
+ for (const spec of suite.specs ?? []) {
104
+ const specFile = spec.file ?? suiteFile;
105
+ for (const test of spec.tests ?? []) {
106
+ for (const result of test.results ?? []) {
107
+ const outcome = toOutcome(result.status);
108
+ const failing = outcome === 'failed' || outcome === 'timedOut';
109
+ out.push({
110
+ testId: attemptTestId(spec, specFile, path),
111
+ title: spec.title ?? '',
112
+ titlePath: path,
113
+ file: specFile,
114
+ line: spec.line ?? 0,
115
+ project: test.projectName ?? 'default',
116
+ tags: spec.tags ?? [],
117
+ retry: result.retry ?? 0,
118
+ outcome,
119
+ failureKind: failing ? classifyAttempt(result, test.timeout ?? 0) : null,
120
+ durationMs: result.duration ?? 0,
121
+ workerIndex: result.workerIndex ?? 0,
122
+ // A merged report has already folded its shards together and does
123
+ // not record which one an attempt came from.
124
+ shard: null,
125
+ traceId: null,
126
+ evidenceId: null,
127
+ coScheduled: [],
128
+ routes: [],
129
+ });
130
+ }
131
+ }
132
+ }
133
+ out.push(...collectAttempts(suite.suites, suiteFile, path));
134
+ }
135
+ return out;
136
+ }
137
+ /** Ordered so a duplicate can never mask the worse outcome. */
138
+ const SEVERITY = {
139
+ skipped: 0,
140
+ passed: 1,
141
+ interrupted: 2,
142
+ timedOut: 3,
143
+ failed: 4,
144
+ };
145
+ /**
146
+ * Collapse attempts that share the store's primary key
147
+ * `(run_id, test_id, project, retry)`.
148
+ *
149
+ * With a real `spec.id` this should never fire — it did before, only because
150
+ * the composed id mapped two genuinely distinct setup tests onto one key. It
151
+ * stays as a safety net because the failure mode it guards is severe out of all
152
+ * proportion to its cause: `store.ingest` is a single transaction, so ONE
153
+ * duplicate row used to roll back the entire run. On a real 8-shard acceptance
154
+ * report that cost all 120 attempts, and the only symptom was a single
155
+ * "could not be stored" warning — history silently never accumulated while
156
+ * every CI job reported success.
157
+ *
158
+ * Collisions are returned, not swallowed. A silent dedupe here would be the
159
+ * same class of bug one level down: quietly discarding evidence and looking
160
+ * healthy while doing it.
161
+ */
162
+ function collapseDuplicates(attempts) {
163
+ const byKey = new Map();
164
+ const collapsed = [];
165
+ for (const attempt of attempts) {
166
+ const key = `${attempt.testId}\x00${attempt.project}\x00${attempt.retry}`;
167
+ const existing = byKey.get(key);
168
+ if (existing === undefined) {
169
+ byKey.set(key, attempt);
170
+ continue;
171
+ }
172
+ // Keep the more severe outcome. A duplicate that passed must never mask
173
+ // one that failed — that is precisely the instability scoring exists for.
174
+ const keep = SEVERITY[attempt.outcome] > SEVERITY[existing.outcome] ? attempt : existing;
175
+ byKey.set(key, keep);
176
+ collapsed.push(`${attempt.title} [${attempt.project}] retry ${attempt.retry}`);
177
+ }
178
+ return { kept: [...byKey.values()], collapsed };
179
+ }
180
+ /**
181
+ * A run id derived from the report, never from the clock.
182
+ *
183
+ * `store.ingest` promises idempotency — "CI re-runs, shard merges, and repeated
184
+ * artifact ingestion all replay the same run id" — and enforces it by deleting
185
+ * a run's attempts before reinserting them. A `Date.now()` id opts out of that
186
+ * guarantee silently: re-ingesting one artifact twice (a re-run of the analyze
187
+ * job, a replay of an archived report) writes both copies and double-counts
188
+ * every attempt, which corrupts every score derived from them.
189
+ *
190
+ * The CI build URL is the natural key where the report carries one. Otherwise
191
+ * digest the attempt tuples, which is stable for a given report and distinct
192
+ * across runs because attempt start times are millisecond-precision.
193
+ */
194
+ function deriveRunId(report, attempts) {
195
+ const buildHref = report.config?.metadata?.ci?.buildHref;
196
+ const seed = buildHref !== undefined && buildHref !== ''
197
+ ? buildHref
198
+ : attempts
199
+ .map(a => `${a.testId} ${a.project} ${a.retry} ${a.durationMs}`)
200
+ .sort()
201
+ .join('\n');
202
+ const digest = createHash('sha256').update(seed).digest('hex');
203
+ return `${RUN_ID_PREFIX}${digest.slice(0, RUN_ID_LENGTH)}`;
204
+ }
205
+ /**
206
+ * The wall-clock window the run actually occupied.
207
+ *
208
+ * Flake scoring weights attempts by recency (`halfLifeDays`), so the timestamp
209
+ * has to be when the test RAN, not when somebody ingested the file. Using
210
+ * ingest time would make a replayed six-month-old archive weigh as heavily as
211
+ * this morning's run — and back-filling history from archived artifacts is one
212
+ * of the reasons this adapter exists.
213
+ */
214
+ function runWindow(report, now) {
215
+ const spans = [];
216
+ const walk = (suites) => {
217
+ for (const suite of suites ?? []) {
218
+ for (const spec of suite.specs ?? []) {
219
+ for (const test of spec.tests ?? []) {
220
+ for (const result of test.results ?? []) {
221
+ if (result.startTime === undefined)
222
+ continue;
223
+ const start = Date.parse(result.startTime);
224
+ if (Number.isNaN(start))
225
+ continue;
226
+ spans.push({ start, end: start + (result.duration ?? 0) });
227
+ }
228
+ }
229
+ }
230
+ walk(suite.suites);
231
+ }
232
+ };
233
+ walk(report.suites);
234
+ if (spans.length === 0) {
235
+ const fallback = new Date(now()).toISOString();
236
+ return { startedAt: fallback, finishedAt: fallback };
237
+ }
238
+ return {
239
+ startedAt: new Date(Math.min(...spans.map(s => s.start))).toISOString(),
240
+ finishedAt: new Date(Math.max(...spans.map(s => s.end))).toISOString(),
241
+ };
242
+ }
243
+ export function mapPlaywrightReport(report, options) {
244
+ const { identity } = options;
245
+ const now = options.now ?? Date.now;
246
+ const { kept, collapsed } = collapseDuplicates(collectAttempts(report.suites, '', []));
247
+ const { startedAt, finishedAt } = runWindow(report, now);
248
+ const ci = report.config?.metadata?.ci;
249
+ const shard = report.config?.shard;
250
+ const run = {
251
+ schemaVersion: RUN_SCHEMA_VERSION,
252
+ runId: deriveRunId(report, kept),
253
+ startedAt,
254
+ finishedAt,
255
+ // Report first, environment second. Same-commit variance is a documented
256
+ // flake signal and it needs the commit the tests actually ran against, not
257
+ // whatever is checked out in the job doing the ingest.
258
+ commit: ci?.commitHash ?? identity.commit,
259
+ branch: ci?.branch ?? identity.branch,
260
+ appEnv: identity.appEnv,
261
+ ci: ci !== undefined || identity.ci,
262
+ // `actualWorkers` is what the run really used; `workers` is what was asked
263
+ // for, and the two differ under sharding.
264
+ workers: report.config?.metadata?.actualWorkers ?? report.config?.workers ?? 0,
265
+ shard: shard !== null && shard !== undefined && shard.current !== undefined && shard.total !== undefined
266
+ ? { current: shard.current, total: shard.total }
267
+ : null,
268
+ atestVersion: ATEST_VERSION,
269
+ playwrightVersion: report.config?.version ?? null,
270
+ attempts: kept,
271
+ };
272
+ return { run, collapsed };
273
+ }
274
+ //# sourceMappingURL=playwright-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright-report.js","sourceRoot":"","sources":["../../src/history/playwright-report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Test history — the substrate the flaky engine reasons over.
3
+ *
4
+ * Backed by `node:sqlite`, which ships with Node. That is a deliberate choice
5
+ * over `better-sqlite3`: a native module would need a compiler on every
6
+ * developer machine and every CI image, and the first time it failed to build
7
+ * people would disable the tool rather than debug it. Zero native
8
+ * dependencies is what makes this adoptable.
9
+ *
10
+ * The interface is async so a Postgres driver can slot in unchanged at scale;
11
+ * the SQLite driver simply resolves immediately.
12
+ */
13
+ import type { AttemptRecord, RunRecord } from './types.js';
14
+ /** An attempt joined to the metadata of the run that produced it. */
15
+ export interface HistoricalAttempt extends AttemptRecord {
16
+ readonly runId: string;
17
+ /** Run start, ISO 8601 — the clock recency weighting uses. */
18
+ readonly startedAt: string;
19
+ readonly commit: string | null;
20
+ readonly branch: string | null;
21
+ readonly ci: boolean;
22
+ readonly workers: number;
23
+ }
24
+ export interface HistoryQuery {
25
+ readonly testId?: string;
26
+ readonly project?: string;
27
+ /** Inclusive lower bound on the run's start time, ISO 8601. */
28
+ readonly since?: string;
29
+ /** Most recent N attempts. Applied after ordering by run time, descending. */
30
+ readonly limit?: number;
31
+ }
32
+ export interface TestKey {
33
+ readonly testId: string;
34
+ readonly project: string;
35
+ readonly title: string;
36
+ readonly file: string;
37
+ }
38
+ export interface HistoryStore {
39
+ ingest(run: RunRecord): Promise<void>;
40
+ attempts(query?: HistoryQuery): Promise<HistoricalAttempt[]>;
41
+ testKeys(): Promise<TestKey[]>;
42
+ runCount(): Promise<number>;
43
+ prune(olderThanIso: string): Promise<number>;
44
+ close(): Promise<void>;
45
+ /**
46
+ * Records the driver listed but could not read.
47
+ *
48
+ * Present on the Azure blob driver (truncated uploads, schema-skewed
49
+ * objects). Absent on SQLite and memory, which either throw or never skip.
50
+ * Surfaced rather than thrown so one unreadable object among thousands
51
+ * cannot cost the rest of the store.
52
+ */
53
+ readonly skipped?: readonly {
54
+ readonly name: string;
55
+ readonly reason: string;
56
+ }[];
57
+ }
58
+ export declare class SqliteHistoryStore implements HistoryStore {
59
+ private readonly db;
60
+ constructor(url: string);
61
+ /**
62
+ * Idempotent by construction. CI re-runs, shard merges, and repeated
63
+ * artifact ingestion all replay the same run id — double-counting an
64
+ * attempt would silently corrupt every score derived from it.
65
+ */
66
+ ingest(run: RunRecord): Promise<void>;
67
+ attempts(query?: HistoryQuery): Promise<HistoricalAttempt[]>;
68
+ testKeys(): Promise<TestKey[]>;
69
+ runCount(): Promise<number>;
70
+ prune(olderThanIso: string): Promise<number>;
71
+ close(): Promise<void>;
72
+ }
73
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/history/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAW,SAAS,EAAE,MAAM,YAAY,CAAC;AAIpE,qEAAqE;AACrE,MAAM,WAAW,iBAAkB,SAAQ,aAAa;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;IAC7D,QAAQ,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAC/B,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5B,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAClF;AAiID,qBAAa,kBAAmB,YAAW,YAAY;IACrD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAe;gBAEtB,GAAG,EAAE,MAAM;IAOvB;;;;OAIG;IACG,MAAM,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAqGrC,QAAQ,CAAC,KAAK,GAAE,YAAiB,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAiChE,QAAQ,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAiB9B,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;IAK3B,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAM5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Test history — the substrate the flaky engine reasons over.
3
+ *
4
+ * Backed by `node:sqlite`, which ships with Node. That is a deliberate choice
5
+ * over `better-sqlite3`: a native module would need a compiler on every
6
+ * developer machine and every CI image, and the first time it failed to build
7
+ * people would disable the tool rather than debug it. Zero native
8
+ * dependencies is what makes this adoptable.
9
+ *
10
+ * The interface is async so a Postgres driver can slot in unchanged at scale;
11
+ * the SQLite driver simply resolves immediately.
12
+ */
13
+ import { DatabaseSync } from 'node:sqlite';
14
+ import { mkdirSync } from 'node:fs';
15
+ import { dirname } from 'node:path';
16
+ const SCHEMA = `
17
+ CREATE TABLE IF NOT EXISTS runs (
18
+ run_id TEXT PRIMARY KEY,
19
+ started_at TEXT NOT NULL,
20
+ finished_at TEXT,
21
+ commit_sha TEXT,
22
+ branch TEXT,
23
+ app_env TEXT,
24
+ ci INTEGER NOT NULL,
25
+ workers INTEGER NOT NULL,
26
+ shard_current INTEGER,
27
+ shard_total INTEGER,
28
+ atest_version TEXT NOT NULL,
29
+ playwright_version TEXT
30
+ );
31
+
32
+ CREATE TABLE IF NOT EXISTS attempts (
33
+ run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
34
+ test_id TEXT NOT NULL,
35
+ project TEXT NOT NULL,
36
+ retry INTEGER NOT NULL,
37
+ title TEXT NOT NULL,
38
+ title_path TEXT NOT NULL,
39
+ file TEXT NOT NULL,
40
+ line INTEGER NOT NULL,
41
+ tags TEXT NOT NULL,
42
+ outcome TEXT NOT NULL,
43
+ failure_kind TEXT,
44
+ duration_ms INTEGER NOT NULL,
45
+ worker_index INTEGER NOT NULL,
46
+ shard_current INTEGER,
47
+ shard_total INTEGER,
48
+ trace_id TEXT,
49
+ evidence_id TEXT,
50
+ co_scheduled TEXT NOT NULL,
51
+ routes TEXT NOT NULL DEFAULT '[]',
52
+ PRIMARY KEY (run_id, test_id, project, retry)
53
+ );
54
+
55
+ CREATE INDEX IF NOT EXISTS idx_attempts_test ON attempts(test_id, project);
56
+ CREATE INDEX IF NOT EXISTS idx_attempts_outcome ON attempts(outcome, failure_kind);
57
+ CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at);
58
+ `;
59
+ function parseJsonArray(raw) {
60
+ try {
61
+ const parsed = JSON.parse(raw);
62
+ return Array.isArray(parsed) ? parsed.filter((v) => typeof v === 'string') : [];
63
+ }
64
+ catch {
65
+ return [];
66
+ }
67
+ }
68
+ function toHistoricalAttempt(row) {
69
+ return {
70
+ runId: row.run_id,
71
+ testId: row.test_id,
72
+ project: row.project,
73
+ retry: row.retry,
74
+ title: row.title,
75
+ titlePath: parseJsonArray(row.title_path),
76
+ file: row.file,
77
+ line: row.line,
78
+ tags: parseJsonArray(row.tags),
79
+ outcome: row.outcome,
80
+ failureKind: row.failure_kind,
81
+ durationMs: row.duration_ms,
82
+ workerIndex: row.worker_index,
83
+ shard: row.shard_current === null || row.shard_total === null
84
+ ? null
85
+ : { current: row.shard_current, total: row.shard_total },
86
+ traceId: row.trace_id,
87
+ evidenceId: row.evidence_id,
88
+ coScheduled: parseJsonArray(row.co_scheduled),
89
+ routes: parseJsonArray(row.routes ?? '[]'),
90
+ startedAt: row.started_at,
91
+ commit: row.commit_sha,
92
+ branch: row.branch,
93
+ ci: row.ci === 1,
94
+ workers: row.workers,
95
+ };
96
+ }
97
+ /**
98
+ * `undefined` is not bindable by node:sqlite — only `null` is.
99
+ *
100
+ * The distinction never shows up in a hand-written fixture where every field is
101
+ * present, and it is invisible in the type system because `string | null` and a
102
+ * MISSING key both satisfy a `Partial<RunRecord>` shape check. What it produces
103
+ * at runtime is `Provided value cannot be bound to SQLite parameter 6`, which
104
+ * names neither the field nor the file — and because the insert is inside a
105
+ * transaction, the whole batch rolls back. One run record written by a
106
+ * different atest version therefore silently costs you every other record in
107
+ * the directory.
108
+ */
109
+ function nullable(value) {
110
+ return value ?? null;
111
+ }
112
+ export class SqliteHistoryStore {
113
+ db;
114
+ constructor(url) {
115
+ if (url !== ':memory:')
116
+ mkdirSync(dirname(url), { recursive: true });
117
+ this.db = new DatabaseSync(url);
118
+ this.db.exec('PRAGMA foreign_keys = ON');
119
+ this.db.exec(SCHEMA);
120
+ }
121
+ /**
122
+ * Idempotent by construction. CI re-runs, shard merges, and repeated
123
+ * artifact ingestion all replay the same run id — double-counting an
124
+ * attempt would silently corrupt every score derived from it.
125
+ */
126
+ async ingest(run) {
127
+ const insertRun = this.db.prepare(`
128
+ -- UPSERT, never INSERT OR REPLACE.
129
+ --
130
+ -- SQLite implements OR REPLACE as DELETE-then-INSERT, so replacing a run
131
+ -- row fires the attempts table's ON DELETE CASCADE. Under sharding every
132
+ -- shard shares one run id, so ingesting shard 2 deleted shard 1's
133
+ -- attempts before writing its own — the shard-scoped delete below cannot
134
+ -- help, because the rows are already gone by the time it runs. Measured:
135
+ -- three shard files carrying four attempts ingested as zero.
136
+ --
137
+ -- DO UPDATE mutates the row in place. No delete, no cascade.
138
+ INSERT INTO runs
139
+ (run_id, started_at, finished_at, commit_sha, branch, app_env, ci, workers,
140
+ shard_current, shard_total, atest_version, playwright_version)
141
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
142
+ ON CONFLICT(run_id) DO UPDATE SET
143
+ finished_at = excluded.finished_at,
144
+ commit_sha = excluded.commit_sha,
145
+ branch = excluded.branch,
146
+ app_env = excluded.app_env,
147
+ ci = excluded.ci,
148
+ workers = excluded.workers,
149
+ atest_version = excluded.atest_version,
150
+ playwright_version = excluded.playwright_version
151
+ -- started_at keeps the EARLIEST shard's value, and shard_* are left
152
+ -- alone: a run spanning shards has no single shard number, and letting
153
+ -- the last writer stamp one would misreport the run.
154
+ `);
155
+ // Scoped to THIS shard, not the whole run.
156
+ //
157
+ // `DELETE FROM attempts WHERE run_id = ?` is correct for an unsharded run
158
+ // and catastrophic for a sharded one: every shard shares the run id, so
159
+ // ingesting six shard files left only the sixth shard's attempts — one
160
+ // sixth of the data, with no error anywhere. `IS` rather than `=` because
161
+ // an unsharded record carries NULL, and NULL = NULL is never true.
162
+ const deleteAttempts = this.db.prepare('DELETE FROM attempts WHERE run_id = ? AND shard_current IS ? AND shard_total IS ?');
163
+ const insertAttempt = this.db.prepare(`
164
+ INSERT INTO attempts
165
+ (run_id, test_id, project, retry, title, title_path, file, line, tags, outcome,
166
+ failure_kind, duration_ms, worker_index, shard_current, shard_total,
167
+ trace_id, evidence_id, co_scheduled, routes)
168
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
169
+ `);
170
+ this.db.exec('BEGIN');
171
+ try {
172
+ insertRun.run(run.runId, run.startedAt, nullable(run.finishedAt), nullable(run.commit), nullable(run.branch), nullable(run.appEnv), run.ci ? 1 : 0, run.workers ?? 0, run.shard?.current ?? null, run.shard?.total ?? null, run.atestVersion ?? '', nullable(run.playwrightVersion));
173
+ deleteAttempts.run(run.runId, run.shard?.current ?? null, run.shard?.total ?? null);
174
+ for (const attempt of run.attempts) {
175
+ insertAttempt.run(run.runId, attempt.testId, attempt.project, attempt.retry, attempt.title, JSON.stringify(attempt.titlePath ?? []), attempt.file, attempt.line, JSON.stringify(attempt.tags ?? []), attempt.outcome, nullable(attempt.failureKind), attempt.durationMs ?? 0, attempt.workerIndex ?? 0,
176
+ // Fall back to the RUN's shard. The scoped delete above matches on
177
+ // these columns, so an attempt that omits its shard while the run
178
+ // declares one would never be cleaned up — a re-run of that shard
179
+ // would leave the previous attempt rows behind and inflate every
180
+ // statistic derived from them. An attempt executed during shard 1
181
+ // of 3 belongs to shard 1 of 3; recording anything else is wrong.
182
+ attempt.shard?.current ?? run.shard?.current ?? null, attempt.shard?.total ?? run.shard?.total ?? null, nullable(attempt.traceId), nullable(attempt.evidenceId), JSON.stringify(attempt.coScheduled ?? []), JSON.stringify(attempt.routes ?? []));
183
+ }
184
+ this.db.exec('COMMIT');
185
+ }
186
+ catch (error) {
187
+ this.db.exec('ROLLBACK');
188
+ throw error;
189
+ }
190
+ }
191
+ async attempts(query = {}) {
192
+ const where = [];
193
+ const params = [];
194
+ if (query.testId !== undefined) {
195
+ where.push('a.test_id = ?');
196
+ params.push(query.testId);
197
+ }
198
+ if (query.project !== undefined) {
199
+ where.push('a.project = ?');
200
+ params.push(query.project);
201
+ }
202
+ if (query.since !== undefined) {
203
+ where.push('r.started_at >= ?');
204
+ params.push(query.since);
205
+ }
206
+ const clause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
207
+ const limit = query.limit === undefined ? '' : `LIMIT ${Number(query.limit)}`;
208
+ const rows = this.db
209
+ .prepare(`SELECT a.*, r.started_at, r.commit_sha, r.branch, r.ci, r.workers
210
+ FROM attempts a JOIN runs r ON r.run_id = a.run_id
211
+ ${clause}
212
+ ORDER BY r.started_at DESC, a.retry ASC
213
+ ${limit}`)
214
+ .all(...params);
215
+ return rows.map(toHistoricalAttempt);
216
+ }
217
+ async testKeys() {
218
+ const rows = this.db
219
+ .prepare(`SELECT test_id, project, title, file FROM attempts
220
+ GROUP BY test_id, project
221
+ ORDER BY file, title`)
222
+ .all();
223
+ return rows.map(r => ({
224
+ testId: r.test_id,
225
+ project: r.project,
226
+ title: r.title,
227
+ file: r.file,
228
+ }));
229
+ }
230
+ async runCount() {
231
+ const row = this.db.prepare('SELECT COUNT(*) AS n FROM runs').get();
232
+ return row.n;
233
+ }
234
+ async prune(olderThanIso) {
235
+ const before = await this.runCount();
236
+ this.db.prepare('DELETE FROM runs WHERE started_at < ?').run(olderThanIso);
237
+ return before - (await this.runCount());
238
+ }
239
+ async close() {
240
+ this.db.close();
241
+ }
242
+ }
243
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/history/store.ts"],"names":[],"mappings":"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"}