flakewatch-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.
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +65 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/context.test.d.ts +2 -0
- package/dist/__tests__/context.test.d.ts.map +1 -0
- package/dist/__tests__/context.test.js +76 -0
- package/dist/__tests__/context.test.js.map +1 -0
- package/dist/__tests__/grouping.test.d.ts +2 -0
- package/dist/__tests__/grouping.test.d.ts.map +1 -0
- package/dist/__tests__/grouping.test.js +124 -0
- package/dist/__tests__/grouping.test.js.map +1 -0
- package/dist/__tests__/report.test.d.ts +2 -0
- package/dist/__tests__/report.test.d.ts.map +1 -0
- package/dist/__tests__/report.test.js +102 -0
- package/dist/__tests__/report.test.js.map +1 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +91 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +60 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +175 -0
- package/dist/context.js.map +1 -0
- package/dist/grouping.d.ts +23 -0
- package/dist/grouping.d.ts.map +1 -0
- package/dist/grouping.js +88 -0
- package/dist/grouping.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/llm.d.ts +12 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +188 -0
- package/dist/llm.js.map +1 -0
- package/dist/playwright-json.d.ts +46 -0
- package/dist/playwright-json.d.ts.map +1 -0
- package/dist/playwright-json.js +46 -0
- package/dist/playwright-json.js.map +1 -0
- package/dist/report.d.ts +7 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +121 -0
- package/dist/report.js.map +1 -0
- package/dist/triage.d.ts +25 -0
- package/dist/triage.d.ts.map +1 -0
- package/dist/triage.js +91 -0
- package/dist/triage.js.map +1 -0
- package/dist/verdicts.d.ts +56 -0
- package/dist/verdicts.d.ts.map +1 -0
- package/dist/verdicts.js +2 -0
- package/dist/verdicts.js.map +1 -0
- package/package.json +35 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/config.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateConfig } from '../config.js';
|
|
3
|
+
function makeConfig(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
llm: {
|
|
6
|
+
provider: 'anthropic',
|
|
7
|
+
model: 'claude-sonnet-4-20250514',
|
|
8
|
+
apiKey: 'test-key',
|
|
9
|
+
},
|
|
10
|
+
browser: {
|
|
11
|
+
method: 'mcp',
|
|
12
|
+
mcpServerCommand: 'npx @anthropic/chrome-devtools-mcp',
|
|
13
|
+
},
|
|
14
|
+
investigation: {
|
|
15
|
+
mode: 'screenshot',
|
|
16
|
+
maxConcurrent: 3,
|
|
17
|
+
maxTotal: 10,
|
|
18
|
+
massFailureThreshold: 0.5,
|
|
19
|
+
},
|
|
20
|
+
auth: {},
|
|
21
|
+
context: {
|
|
22
|
+
includeTrace: true,
|
|
23
|
+
includeConsole: true,
|
|
24
|
+
includeNetwork: true,
|
|
25
|
+
maxTestSourceTokens: 6000,
|
|
26
|
+
},
|
|
27
|
+
output: {
|
|
28
|
+
format: 'markdown',
|
|
29
|
+
dir: './flakewatch-reports',
|
|
30
|
+
},
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
describe('validateConfig', () => {
|
|
35
|
+
it('returns no errors for valid config', () => {
|
|
36
|
+
const errors = validateConfig(makeConfig());
|
|
37
|
+
expect(errors).toHaveLength(0);
|
|
38
|
+
});
|
|
39
|
+
it('errors on missing API key', () => {
|
|
40
|
+
const config = makeConfig();
|
|
41
|
+
config.llm.apiKey = undefined;
|
|
42
|
+
const errors = validateConfig(config);
|
|
43
|
+
expect(errors).toHaveLength(1);
|
|
44
|
+
expect(errors[0]).toContain('API key');
|
|
45
|
+
});
|
|
46
|
+
it('errors on invalid maxConcurrent', () => {
|
|
47
|
+
const config = makeConfig();
|
|
48
|
+
config.investigation.maxConcurrent = 0;
|
|
49
|
+
const errors = validateConfig(config);
|
|
50
|
+
expect(errors.some((e) => e.includes('maxConcurrent'))).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it('errors on invalid massFailureThreshold', () => {
|
|
53
|
+
const config = makeConfig();
|
|
54
|
+
config.investigation.massFailureThreshold = 1.5;
|
|
55
|
+
const errors = validateConfig(config);
|
|
56
|
+
expect(errors.some((e) => e.includes('massFailureThreshold'))).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
it('errors on zero massFailureThreshold', () => {
|
|
59
|
+
const config = makeConfig();
|
|
60
|
+
config.investigation.massFailureThreshold = 0;
|
|
61
|
+
const errors = validateConfig(config);
|
|
62
|
+
expect(errors.some((e) => e.includes('massFailureThreshold'))).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
//# sourceMappingURL=config.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../src/__tests__/config.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG9C,SAAS,UAAU,CAAC,YAAuC,EAAE;IAC3D,OAAO;QACL,GAAG,EAAE;YACH,QAAQ,EAAE,WAAW;YACrB,KAAK,EAAE,0BAA0B;YACjC,MAAM,EAAE,UAAU;SACnB;QACD,OAAO,EAAE;YACP,MAAM,EAAE,KAAK;YACb,gBAAgB,EAAE,oCAAoC;SACvD;QACD,aAAa,EAAE;YACb,IAAI,EAAE,YAAY;YAClB,aAAa,EAAE,CAAC;YAChB,QAAQ,EAAE,EAAE;YACZ,oBAAoB,EAAE,GAAG;SAC1B;QACD,IAAI,EAAE,EAAE;QACR,OAAO,EAAE;YACP,YAAY,EAAE,IAAI;YAClB,cAAc,EAAE,IAAI;YACpB,cAAc,EAAE,IAAI;YACpB,mBAAmB,EAAE,IAAI;SAC1B;QACD,MAAM,EAAE;YACN,MAAM,EAAE,UAAU;YAClB,GAAG,EAAE,sBAAsB;SAC5B;QACD,GAAG,SAAS;KACb,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,MAAM,GAAG,cAAc,CAAC,UAAU,EAAE,CAAC,CAAC;QAC5C,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,SAAS,CAAC;QAC9B,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,CAAC,aAAa,CAAC,aAAa,GAAG,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,CAAC,aAAa,CAAC,oBAAoB,GAAG,GAAG,CAAC;QAChD,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,CAAC,aAAa,CAAC,oBAAoB,GAAG,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/context.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { formatContextForLLM } from '../context.js';
|
|
3
|
+
function makeContext(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
testId: 'test-1',
|
|
6
|
+
testTitle: 'should display user profile',
|
|
7
|
+
testFile: 'tests/profile.spec.ts',
|
|
8
|
+
testSource: `test('should display user profile', async ({ page }) => {
|
|
9
|
+
await page.goto('/profile');
|
|
10
|
+
await expect(page.locator('.user-name')).toBeVisible();
|
|
11
|
+
});`,
|
|
12
|
+
pageObjectSources: new Map(),
|
|
13
|
+
errorMessage: "locator.click: Error: Element '.user-name' not found",
|
|
14
|
+
errorStack: 'at tests/profile.spec.ts:3:15',
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
describe('formatContextForLLM', () => {
|
|
19
|
+
it('includes test title and file', () => {
|
|
20
|
+
const output = formatContextForLLM(makeContext());
|
|
21
|
+
expect(output).toContain('should display user profile');
|
|
22
|
+
expect(output).toContain('tests/profile.spec.ts');
|
|
23
|
+
});
|
|
24
|
+
it('includes error message and stack', () => {
|
|
25
|
+
const output = formatContextForLLM(makeContext());
|
|
26
|
+
expect(output).toContain("Element '.user-name' not found");
|
|
27
|
+
expect(output).toContain('tests/profile.spec.ts:3:15');
|
|
28
|
+
});
|
|
29
|
+
it('includes test source code', () => {
|
|
30
|
+
const output = formatContextForLLM(makeContext());
|
|
31
|
+
expect(output).toContain("page.goto('/profile')");
|
|
32
|
+
expect(output).toContain('.user-name');
|
|
33
|
+
});
|
|
34
|
+
it('includes page object sources', () => {
|
|
35
|
+
const context = makeContext({
|
|
36
|
+
pageObjectSources: new Map([
|
|
37
|
+
['pages/profile.ts', 'export class ProfilePage { nameSelector = ".user-name"; }'],
|
|
38
|
+
]),
|
|
39
|
+
});
|
|
40
|
+
const output = formatContextForLLM(context);
|
|
41
|
+
expect(output).toContain('pages/profile.ts');
|
|
42
|
+
expect(output).toContain('ProfilePage');
|
|
43
|
+
});
|
|
44
|
+
it('includes intent annotation when present', () => {
|
|
45
|
+
const context = makeContext({
|
|
46
|
+
intentAnnotation: 'Verify the user name is visible on the profile page',
|
|
47
|
+
});
|
|
48
|
+
const output = formatContextForLLM(context);
|
|
49
|
+
expect(output).toContain('Verify the user name is visible');
|
|
50
|
+
});
|
|
51
|
+
it('includes screenshot path', () => {
|
|
52
|
+
const context = makeContext({
|
|
53
|
+
screenshotPath: './test-results/profile-chromium/screenshot.png',
|
|
54
|
+
});
|
|
55
|
+
const output = formatContextForLLM(context);
|
|
56
|
+
expect(output).toContain('screenshot.png');
|
|
57
|
+
});
|
|
58
|
+
it('includes network requests when present', () => {
|
|
59
|
+
const context = makeContext({
|
|
60
|
+
networkRequests: [
|
|
61
|
+
{ url: '/api/user', method: 'GET', status: 500, duration: 120 },
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
const output = formatContextForLLM(context);
|
|
65
|
+
expect(output).toContain('GET /api/user');
|
|
66
|
+
expect(output).toContain('500');
|
|
67
|
+
});
|
|
68
|
+
it('includes console messages when present', () => {
|
|
69
|
+
const context = makeContext({
|
|
70
|
+
consoleMessages: ['Error: Failed to fetch user data'],
|
|
71
|
+
});
|
|
72
|
+
const output = formatContextForLLM(context);
|
|
73
|
+
expect(output).toContain('Failed to fetch user data');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
//# sourceMappingURL=context.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.test.js","sourceRoot":"","sources":["../../src/__tests__/context.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grouping.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/grouping.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { groupFailures, selectGroupsForInvestigation } from '../grouping.js';
|
|
3
|
+
function makeFailure(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
testId: 'test-1',
|
|
6
|
+
testTitle: 'test one',
|
|
7
|
+
testFile: 'tests/example.spec.ts',
|
|
8
|
+
error: {
|
|
9
|
+
message: 'locator.click: Error: Element not found',
|
|
10
|
+
stack: 'at tests/example.spec.ts:10:5',
|
|
11
|
+
},
|
|
12
|
+
...overrides,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
describe('groupFailures', () => {
|
|
16
|
+
it('groups failures with identical error messages', () => {
|
|
17
|
+
const failures = [
|
|
18
|
+
makeFailure({ testId: '1', testFile: 'tests/a.spec.ts' }),
|
|
19
|
+
makeFailure({ testId: '2', testFile: 'tests/b.spec.ts' }),
|
|
20
|
+
makeFailure({
|
|
21
|
+
testId: '3',
|
|
22
|
+
error: { message: 'Different error', stack: 'at tests/c.spec.ts:5:1' },
|
|
23
|
+
}),
|
|
24
|
+
];
|
|
25
|
+
const groups = groupFailures(failures);
|
|
26
|
+
expect(groups).toHaveLength(2);
|
|
27
|
+
const largeGroup = groups.find((g) => g.failures.length === 2);
|
|
28
|
+
expect(largeGroup).toBeDefined();
|
|
29
|
+
expect(largeGroup.failures.map((f) => f.testId)).toEqual(['1', '2']);
|
|
30
|
+
});
|
|
31
|
+
it('picks the simplest test (shortest path) as sample', () => {
|
|
32
|
+
const failures = [
|
|
33
|
+
makeFailure({ testId: '1', testFile: 'tests/very/deep/nested/test.spec.ts' }),
|
|
34
|
+
makeFailure({ testId: '2', testFile: 'tests/a.spec.ts' }),
|
|
35
|
+
];
|
|
36
|
+
const groups = groupFailures(failures);
|
|
37
|
+
expect(groups).toHaveLength(1);
|
|
38
|
+
expect(groups[0].sample.testId).toBe('2');
|
|
39
|
+
});
|
|
40
|
+
it('normalizes dynamic values in error messages', () => {
|
|
41
|
+
const failures = [
|
|
42
|
+
makeFailure({
|
|
43
|
+
testId: '1',
|
|
44
|
+
error: {
|
|
45
|
+
message: 'Timeout 30000ms waiting for element #user-123',
|
|
46
|
+
stack: 'at tests/a.spec.ts:10:5',
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
makeFailure({
|
|
50
|
+
testId: '2',
|
|
51
|
+
error: {
|
|
52
|
+
message: 'Timeout 5000ms waiting for element #user-456',
|
|
53
|
+
stack: 'at tests/a.spec.ts:10:5',
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
];
|
|
57
|
+
const groups = groupFailures(failures);
|
|
58
|
+
expect(groups).toHaveLength(1);
|
|
59
|
+
});
|
|
60
|
+
it('separates errors from different stack locations', () => {
|
|
61
|
+
const failures = [
|
|
62
|
+
makeFailure({
|
|
63
|
+
testId: '1',
|
|
64
|
+
error: {
|
|
65
|
+
message: 'Element not found',
|
|
66
|
+
stack: 'at tests/login.spec.ts:10:5',
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
makeFailure({
|
|
70
|
+
testId: '2',
|
|
71
|
+
error: {
|
|
72
|
+
message: 'Element not found',
|
|
73
|
+
stack: 'at tests/dashboard.spec.ts:25:3',
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
];
|
|
77
|
+
const groups = groupFailures(failures);
|
|
78
|
+
expect(groups).toHaveLength(2);
|
|
79
|
+
});
|
|
80
|
+
it('handles failures with no stack trace', () => {
|
|
81
|
+
const failures = [
|
|
82
|
+
makeFailure({ testId: '1', error: { message: 'Error' } }),
|
|
83
|
+
makeFailure({ testId: '2', error: { message: 'Error' } }),
|
|
84
|
+
];
|
|
85
|
+
const groups = groupFailures(failures);
|
|
86
|
+
expect(groups).toHaveLength(1);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe('selectGroupsForInvestigation', () => {
|
|
90
|
+
it('returns all groups when under limits', () => {
|
|
91
|
+
const groups = [
|
|
92
|
+
{ id: 'g1', signature: 'err1', failures: [makeFailure()], sample: makeFailure() },
|
|
93
|
+
{ id: 'g2', signature: 'err2', failures: [makeFailure()], sample: makeFailure() },
|
|
94
|
+
];
|
|
95
|
+
const { selected, circuitBroken } = selectGroupsForInvestigation(groups, 100, { maxTotal: 10, massFailureThreshold: 0.5 });
|
|
96
|
+
expect(selected).toHaveLength(2);
|
|
97
|
+
expect(circuitBroken).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
it('triggers circuit breaker when failure rate exceeds threshold', () => {
|
|
100
|
+
const bigGroup = {
|
|
101
|
+
id: 'g1',
|
|
102
|
+
signature: 'err1',
|
|
103
|
+
failures: Array.from({ length: 60 }, (_, i) => makeFailure({ testId: `test-${i}` })),
|
|
104
|
+
sample: makeFailure(),
|
|
105
|
+
};
|
|
106
|
+
const { selected, circuitBroken } = selectGroupsForInvestigation([bigGroup], 100, { maxTotal: 10, massFailureThreshold: 0.5 });
|
|
107
|
+
expect(circuitBroken).toBe(true);
|
|
108
|
+
expect(selected).toHaveLength(1);
|
|
109
|
+
});
|
|
110
|
+
it('respects maxTotal limit', () => {
|
|
111
|
+
const groups = Array.from({ length: 20 }, (_, i) => ({
|
|
112
|
+
id: `g${i}`,
|
|
113
|
+
signature: `err${i}`,
|
|
114
|
+
failures: [makeFailure({ testId: `test-${i}` })],
|
|
115
|
+
sample: makeFailure({ testId: `test-${i}` }),
|
|
116
|
+
}));
|
|
117
|
+
const { selected } = selectGroupsForInvestigation(groups, 1000, {
|
|
118
|
+
maxTotal: 5,
|
|
119
|
+
massFailureThreshold: 0.5,
|
|
120
|
+
});
|
|
121
|
+
expect(selected).toHaveLength(5);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
//# sourceMappingURL=grouping.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grouping.test.js","sourceRoot":"","sources":["../../src/__tests__/grouping.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/report.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { formatMarkdownReport, formatGitHubComment } from '../report.js';
|
|
3
|
+
function makeReport(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
timestamp: '2026-03-13T12:00:00Z',
|
|
6
|
+
totalFailures: 3,
|
|
7
|
+
investigated: 2,
|
|
8
|
+
skippedByCircuitBreaker: false,
|
|
9
|
+
groups: [
|
|
10
|
+
{
|
|
11
|
+
groupId: 'group-0',
|
|
12
|
+
errorSignature: 'Element not found',
|
|
13
|
+
count: 2,
|
|
14
|
+
sampleTestId: 'test-1',
|
|
15
|
+
verdict: 'stale_test',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
groupId: 'group-1',
|
|
19
|
+
errorSignature: 'API returned 500',
|
|
20
|
+
count: 1,
|
|
21
|
+
sampleTestId: 'test-3',
|
|
22
|
+
verdict: 'likely_bug',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
results: [
|
|
26
|
+
{
|
|
27
|
+
testId: 'test-1',
|
|
28
|
+
testTitle: 'should show user name',
|
|
29
|
+
testFile: 'tests/profile.spec.ts',
|
|
30
|
+
groupId: 'group-0',
|
|
31
|
+
verdict: {
|
|
32
|
+
type: 'stale_test',
|
|
33
|
+
confidence: 0.9,
|
|
34
|
+
summary: 'The selector .user-name was renamed to .profile-name',
|
|
35
|
+
evidence: {
|
|
36
|
+
reasoning: 'The page loads correctly but uses .profile-name instead of .user-name',
|
|
37
|
+
},
|
|
38
|
+
proposedFix: {
|
|
39
|
+
filePath: 'tests/profile.spec.ts',
|
|
40
|
+
originalCode: "page.locator('.user-name')",
|
|
41
|
+
fixedCode: "page.locator('.profile-name')",
|
|
42
|
+
explanation: 'Update selector to match current DOM',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
investigationDurationMs: 2500,
|
|
46
|
+
tokensUsed: 1200,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
testId: 'test-3',
|
|
50
|
+
testTitle: 'should load dashboard data',
|
|
51
|
+
testFile: 'tests/dashboard.spec.ts',
|
|
52
|
+
groupId: 'group-1',
|
|
53
|
+
verdict: {
|
|
54
|
+
type: 'likely_bug',
|
|
55
|
+
confidence: 0.85,
|
|
56
|
+
summary: 'The /api/dashboard endpoint returns 500',
|
|
57
|
+
evidence: {
|
|
58
|
+
reasoning: 'Network request to /api/dashboard fails with Internal Server Error',
|
|
59
|
+
networkErrors: [{ url: '/api/dashboard', status: 500, method: 'GET' }],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
investigationDurationMs: 3200,
|
|
63
|
+
tokensUsed: 1500,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
...overrides,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
describe('formatMarkdownReport', () => {
|
|
70
|
+
it('includes the header with counts', () => {
|
|
71
|
+
const md = formatMarkdownReport(makeReport());
|
|
72
|
+
expect(md).toContain('Flakewatch Analysis');
|
|
73
|
+
expect(md).toContain('3');
|
|
74
|
+
expect(md).toContain('2');
|
|
75
|
+
});
|
|
76
|
+
it('groups results by verdict type', () => {
|
|
77
|
+
const md = formatMarkdownReport(makeReport());
|
|
78
|
+
expect(md).toContain('Likely Bugs (1)');
|
|
79
|
+
expect(md).toContain('Stale Tests (fix proposed) (1)');
|
|
80
|
+
});
|
|
81
|
+
it('includes proposed fix as diff', () => {
|
|
82
|
+
const md = formatMarkdownReport(makeReport());
|
|
83
|
+
expect(md).toContain('```diff');
|
|
84
|
+
expect(md).toContain("- page.locator('.user-name')");
|
|
85
|
+
expect(md).toContain("+ page.locator('.profile-name')");
|
|
86
|
+
});
|
|
87
|
+
it('shows circuit breaker warning when triggered', () => {
|
|
88
|
+
const md = formatMarkdownReport(makeReport({ skippedByCircuitBreaker: true }));
|
|
89
|
+
expect(md).toContain('Circuit breaker');
|
|
90
|
+
});
|
|
91
|
+
it('includes token and duration stats', () => {
|
|
92
|
+
const md = formatMarkdownReport(makeReport());
|
|
93
|
+
expect(md).toContain('tokens used');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('formatGitHubComment', () => {
|
|
97
|
+
it('includes the flakewatch footer', () => {
|
|
98
|
+
const comment = formatGitHubComment(makeReport());
|
|
99
|
+
expect(comment).toContain('flakewatch');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
//# sourceMappingURL=report.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.test.js","sourceRoot":"","sources":["../../src/__tests__/report.test.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface LLMConfig {
|
|
2
|
+
provider: 'anthropic';
|
|
3
|
+
model: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface BrowserConfig {
|
|
7
|
+
method: 'mcp';
|
|
8
|
+
mcpServerCommand: string;
|
|
9
|
+
}
|
|
10
|
+
export type InvestigationMode = 'screenshot' | 'full';
|
|
11
|
+
export interface InvestigationConfig {
|
|
12
|
+
mode: InvestigationMode;
|
|
13
|
+
maxConcurrent: number;
|
|
14
|
+
maxTotal: number;
|
|
15
|
+
massFailureThreshold: number;
|
|
16
|
+
baseURL?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface AuthConfig {
|
|
19
|
+
storageStatePath?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ContextConfig {
|
|
22
|
+
includeTrace: boolean;
|
|
23
|
+
includeConsole: boolean;
|
|
24
|
+
includeNetwork: boolean;
|
|
25
|
+
maxTestSourceTokens: number;
|
|
26
|
+
}
|
|
27
|
+
export interface OutputConfig {
|
|
28
|
+
format: 'markdown' | 'json' | 'github-comment';
|
|
29
|
+
dir: string;
|
|
30
|
+
}
|
|
31
|
+
export interface SmartRetryConfig {
|
|
32
|
+
llm: LLMConfig;
|
|
33
|
+
browser: BrowserConfig;
|
|
34
|
+
investigation: InvestigationConfig;
|
|
35
|
+
auth: AuthConfig;
|
|
36
|
+
context: ContextConfig;
|
|
37
|
+
output: OutputConfig;
|
|
38
|
+
}
|
|
39
|
+
export declare function loadConfig(configPath?: string, overrides?: Partial<SmartRetryConfig>): Promise<SmartRetryConfig>;
|
|
40
|
+
export declare function validateConfig(config: SmartRetryConfig): string[];
|
|
41
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,WAAW,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,KAAK,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,MAAM,CAAC;AAEtD,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,iBAAiB,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,OAAO,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,EAAE,OAAO,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,gBAAgB,CAAC;IAC/C,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,SAAS,CAAC;IACf,OAAO,EAAE,aAAa,CAAC;IACvB,aAAa,EAAE,mBAAmB,CAAC;IACnC,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AA+BD,wBAAsB,UAAU,CAC9B,UAAU,CAAC,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,GACpC,OAAO,CAAC,gBAAgB,CAAC,CA6B3B;AA0BD,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,EAAE,CAyBjE"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
const DEFAULT_CONFIG = {
|
|
3
|
+
llm: {
|
|
4
|
+
provider: 'anthropic',
|
|
5
|
+
model: 'claude-sonnet-4-20250514',
|
|
6
|
+
apiKey: process.env['ANTHROPIC_API_KEY'],
|
|
7
|
+
},
|
|
8
|
+
browser: {
|
|
9
|
+
method: 'mcp',
|
|
10
|
+
mcpServerCommand: 'npx chrome-devtools-mcp --headless --isolated',
|
|
11
|
+
},
|
|
12
|
+
investigation: {
|
|
13
|
+
mode: 'screenshot',
|
|
14
|
+
maxConcurrent: 3,
|
|
15
|
+
maxTotal: 10,
|
|
16
|
+
massFailureThreshold: 0.5,
|
|
17
|
+
},
|
|
18
|
+
auth: {},
|
|
19
|
+
context: {
|
|
20
|
+
includeTrace: true,
|
|
21
|
+
includeConsole: true,
|
|
22
|
+
includeNetwork: true,
|
|
23
|
+
maxTestSourceTokens: 6000,
|
|
24
|
+
},
|
|
25
|
+
output: {
|
|
26
|
+
format: 'markdown',
|
|
27
|
+
dir: './flakewatch-reports',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
export async function loadConfig(configPath, overrides) {
|
|
31
|
+
let fileConfig = {};
|
|
32
|
+
if (configPath) {
|
|
33
|
+
const absolutePath = resolve(configPath);
|
|
34
|
+
try {
|
|
35
|
+
const module = await import(absolutePath);
|
|
36
|
+
fileConfig = module.default ?? module;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error(`Failed to load config from ${absolutePath}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// Try default config locations
|
|
44
|
+
for (const name of ['flakewatch.config.ts', 'flakewatch.config.js']) {
|
|
45
|
+
try {
|
|
46
|
+
const module = await import(resolve(name));
|
|
47
|
+
fileConfig = module.default ?? module;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Try next
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return deepMerge(DEFAULT_CONFIG, fileConfig, (overrides ?? {}));
|
|
56
|
+
}
|
|
57
|
+
function deepMerge(...sources) {
|
|
58
|
+
const result = {};
|
|
59
|
+
for (const source of sources) {
|
|
60
|
+
for (const [key, value] of Object.entries(source)) {
|
|
61
|
+
if (value !== undefined &&
|
|
62
|
+
typeof value === 'object' &&
|
|
63
|
+
value !== null &&
|
|
64
|
+
!Array.isArray(value)) {
|
|
65
|
+
result[key] = deepMerge(result[key] ?? {}, value);
|
|
66
|
+
}
|
|
67
|
+
else if (value !== undefined) {
|
|
68
|
+
result[key] = value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
export function validateConfig(config) {
|
|
75
|
+
const errors = [];
|
|
76
|
+
if (!config.llm.apiKey) {
|
|
77
|
+
errors.push('Missing LLM API key. Set ANTHROPIC_API_KEY env var or configure llm.apiKey.');
|
|
78
|
+
}
|
|
79
|
+
if (config.investigation.maxConcurrent < 1) {
|
|
80
|
+
errors.push('investigation.maxConcurrent must be >= 1');
|
|
81
|
+
}
|
|
82
|
+
if (config.investigation.maxTotal < 1) {
|
|
83
|
+
errors.push('investigation.maxTotal must be >= 1');
|
|
84
|
+
}
|
|
85
|
+
if (config.investigation.massFailureThreshold <= 0 ||
|
|
86
|
+
config.investigation.massFailureThreshold > 1) {
|
|
87
|
+
errors.push('investigation.massFailureThreshold must be between 0 and 1');
|
|
88
|
+
}
|
|
89
|
+
return errors;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ContextConfig } from './config.js';
|
|
2
|
+
export interface TestFailure {
|
|
3
|
+
testId: string;
|
|
4
|
+
testTitle: string;
|
|
5
|
+
testFile: string;
|
|
6
|
+
line?: number;
|
|
7
|
+
error: {
|
|
8
|
+
message: string;
|
|
9
|
+
stack?: string;
|
|
10
|
+
};
|
|
11
|
+
duration?: number;
|
|
12
|
+
retry?: number;
|
|
13
|
+
screenshotPath?: string;
|
|
14
|
+
tracePath?: string;
|
|
15
|
+
annotations?: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface FailureContext {
|
|
18
|
+
testId: string;
|
|
19
|
+
testTitle: string;
|
|
20
|
+
testFile: string;
|
|
21
|
+
testSource: string;
|
|
22
|
+
pageObjectSources: Map<string, string>;
|
|
23
|
+
intentAnnotation?: string;
|
|
24
|
+
errorMessage: string;
|
|
25
|
+
errorStack?: string;
|
|
26
|
+
failingLine?: FailingLine;
|
|
27
|
+
screenshotPath?: string;
|
|
28
|
+
traceActions?: TraceAction[];
|
|
29
|
+
consoleMessages?: string[];
|
|
30
|
+
networkRequests?: NetworkRequest[];
|
|
31
|
+
}
|
|
32
|
+
export interface FailingLine {
|
|
33
|
+
file: string;
|
|
34
|
+
line: number;
|
|
35
|
+
column?: number;
|
|
36
|
+
code: string;
|
|
37
|
+
context: string;
|
|
38
|
+
}
|
|
39
|
+
export interface TraceAction {
|
|
40
|
+
action: string;
|
|
41
|
+
selector?: string;
|
|
42
|
+
timestamp: number;
|
|
43
|
+
duration: number;
|
|
44
|
+
error?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface NetworkRequest {
|
|
47
|
+
url: string;
|
|
48
|
+
method: string;
|
|
49
|
+
status?: number;
|
|
50
|
+
duration?: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Package a test failure with all relevant context for LLM analysis.
|
|
54
|
+
*/
|
|
55
|
+
export declare function packageFailureContext(failure: TestFailure, config: ContextConfig): Promise<FailureContext>;
|
|
56
|
+
/**
|
|
57
|
+
* Format failure context as a prompt for LLM analysis.
|
|
58
|
+
*/
|
|
59
|
+
export declare function formatContextForLLM(context: FailureContext): string;
|
|
60
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE;QACL,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IAGjB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAG1B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,WAAW,CAAC;IAG1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,cAAc,CAAC,CA2BzB;AAwFD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CA+EnE"}
|