@ia-qa/self-healing 1.16.0 → 1.19.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/README.md +98 -1
- package/TUTORIAL.md +58 -0
- package/dist/ai/models.d.ts +12 -0
- package/dist/ai/models.js +33 -1
- package/dist/ai/models.js.map +1 -1
- package/dist/ai/resolver.d.ts +29 -6
- package/dist/ai/resolver.js +42 -7
- package/dist/ai/resolver.js.map +1 -1
- package/dist/cli/args.js +2 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/fix.js +9 -2
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/history.js +8 -1
- package/dist/cli/history.js.map +1 -1
- package/dist/cli/index.js +47 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/map.d.ts +15 -4
- package/dist/cli/map.js +66 -13
- package/dist/cli/map.js.map +1 -1
- package/dist/cli/secret.d.ts +1 -0
- package/dist/cli/secret.js +172 -0
- package/dist/cli/secret.js.map +1 -0
- package/dist/cli/session.d.ts +24 -0
- package/dist/cli/session.js +119 -0
- package/dist/cli/session.js.map +1 -0
- package/dist/cli/skill.d.ts +37 -14
- package/dist/cli/skill.js +36 -30
- package/dist/cli/skill.js.map +1 -1
- package/dist/config.d.ts +23 -2
- package/dist/config.js +50 -0
- package/dist/config.js.map +1 -1
- package/dist/history.d.ts +39 -0
- package/dist/history.js +58 -14
- package/dist/history.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +27 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +9 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/secretStore.d.ts +49 -0
- package/dist/secretStore.js +244 -0
- package/dist/secretStore.js.map +1 -0
- package/dist/sessionCheck.d.ts +96 -0
- package/dist/sessionCheck.js +182 -0
- package/dist/sessionCheck.js.map +1 -0
- package/dist/unrepaired.d.ts +73 -0
- package/dist/unrepaired.js +0 -0
- package/dist/unrepaired.js.map +1 -0
- package/dist/volatile.d.ts +8 -0
- package/dist/volatile.js +24 -7
- package/dist/volatile.js.map +1 -1
- package/dist/volatileBlindSpot.d.ts +67 -0
- package/dist/volatileBlindSpot.js +0 -0
- package/dist/volatileBlindSpot.js.map +1 -0
- package/package.json +1 -1
- package/skills/ia-qa-heal/SKILL.md +29 -3
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a session is not opening the app — told apart, not listed.
|
|
3
|
+
*
|
|
4
|
+
* `assertLanded` names three causes: "expired, rotated, or never covering this domain". All
|
|
5
|
+
* three are true statements about the world and the message offers no way to choose between
|
|
6
|
+
* them, so the reader is left to guess — and one of them means "run `login` again" while
|
|
7
|
+
* another means "your `baseUrl` and your session are about different hosts", which no amount
|
|
8
|
+
* of logging in will fix. A field report ended with someone writing their own
|
|
9
|
+
* `check-session.mjs` to answer it.
|
|
10
|
+
*
|
|
11
|
+
* **Two of the three are answerable from the file alone**, which is the finding that makes
|
|
12
|
+
* this verb worth having rather than a second wrapper around a browser:
|
|
13
|
+
*
|
|
14
|
+
* - **expired** — a cookie carries its own `expires`. A file whose session cookies are all
|
|
15
|
+
* in the past cannot authenticate anything, and no request has to be made to know it.
|
|
16
|
+
* - **wrong-host** — a cookie carries its `domain`. If nothing in the file is scoped to the
|
|
17
|
+
* host `baseUrl` points at, the session was taken somewhere else. This is the one a
|
|
18
|
+
* person can stare at for an hour: the session works, the app works, and they are not the
|
|
19
|
+
* same app. `www.` and an apex fold together, as everywhere else in this package.
|
|
20
|
+
* - **rotated** — cookies present, unexpired, correctly scoped, and the app still answers
|
|
21
|
+
* with a login. Only the server knows why (a deploy, a secret rotation, a forced logout),
|
|
22
|
+
* and only a request can observe it.
|
|
23
|
+
*
|
|
24
|
+
* So the offline analysis runs first and the browser is used only for the question it is the
|
|
25
|
+
* only witness to. Everything here is pure; the I/O lives in `cli/session.ts`.
|
|
26
|
+
*/
|
|
27
|
+
/** Playwright's storageState, as much of it as this file needs. */
|
|
28
|
+
export interface StoredSession {
|
|
29
|
+
cookies?: Array<{
|
|
30
|
+
name: string;
|
|
31
|
+
domain?: string;
|
|
32
|
+
path?: string;
|
|
33
|
+
expires?: number;
|
|
34
|
+
}>;
|
|
35
|
+
origins?: Array<{
|
|
36
|
+
origin: string;
|
|
37
|
+
localStorage?: Array<{
|
|
38
|
+
name: string;
|
|
39
|
+
value: string;
|
|
40
|
+
}>;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
export type SessionCause =
|
|
44
|
+
/** Every cookie's `expires` is in the past. */
|
|
45
|
+
'expired'
|
|
46
|
+
/** Nothing in the file is scoped to the host `baseUrl` names. */
|
|
47
|
+
| 'wrong-host'
|
|
48
|
+
/** The file looks usable and the app still shows a login — the server dropped it. */
|
|
49
|
+
| 'rotated'
|
|
50
|
+
/** A storageState with nothing in it: this app keeps its token where the file cannot reach. */
|
|
51
|
+
| 'empty'
|
|
52
|
+
/** It opens the app. */
|
|
53
|
+
| 'holds'
|
|
54
|
+
/** The check could not run — a navigation error is not a verdict about the session. */
|
|
55
|
+
| 'unknown';
|
|
56
|
+
export interface SessionFacts {
|
|
57
|
+
cookies: number;
|
|
58
|
+
origins: number;
|
|
59
|
+
/** Cookies whose `expires` is in the past, and the soonest one still valid. */
|
|
60
|
+
expired: number;
|
|
61
|
+
/** Seconds until the first cookie expires, when any says. Negative: all gone. */
|
|
62
|
+
expiresInSeconds: number | null;
|
|
63
|
+
/** Cookie domains, as written in the file. */
|
|
64
|
+
domains: string[];
|
|
65
|
+
/** True when at least one cookie or origin is scoped to the app's host. */
|
|
66
|
+
coversHost: boolean;
|
|
67
|
+
host: string;
|
|
68
|
+
}
|
|
69
|
+
/** `www.` and an apex are one site, the same fold `discover --sitemap` applies. */
|
|
70
|
+
export declare function foldHost(host: string): string;
|
|
71
|
+
/** A cookie domain (`.example.com`) covers a host when the host ends with it. */
|
|
72
|
+
export declare function domainCovers(domain: string, host: string): boolean;
|
|
73
|
+
export declare function readFacts(state: StoredSession, baseUrl: string, now?: number): SessionFacts;
|
|
74
|
+
export interface SessionVerdict {
|
|
75
|
+
cause: SessionCause;
|
|
76
|
+
/** True only when the app was actually opened and did not show a login. */
|
|
77
|
+
holds: boolean;
|
|
78
|
+
headline: string;
|
|
79
|
+
/** What to do, or nothing when it holds — a remedy printed on a good run is noise. */
|
|
80
|
+
remedy: string[];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The verdict, from the file's own facts plus — when one was possible — what the app
|
|
84
|
+
* answered.
|
|
85
|
+
*
|
|
86
|
+
* `landed` is undefined when no request was made, which is a legitimate state: the offline
|
|
87
|
+
* causes are conclusive on their own, and opening a browser to re-confirm an expiry date is
|
|
88
|
+
* a request nobody needs to make.
|
|
89
|
+
*/
|
|
90
|
+
export declare function judgeSession(facts: SessionFacts, replay?: {
|
|
91
|
+
landed: string;
|
|
92
|
+
wall: boolean;
|
|
93
|
+
error?: string;
|
|
94
|
+
}, loginCommand?: string): SessionVerdict;
|
|
95
|
+
/** "3 days", "4 hours", "12 minutes" — enough precision to act, never a timestamp to parse. */
|
|
96
|
+
export declare function describeAge(seconds: number): string;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Why a session is not opening the app — told apart, not listed.
|
|
4
|
+
*
|
|
5
|
+
* `assertLanded` names three causes: "expired, rotated, or never covering this domain". All
|
|
6
|
+
* three are true statements about the world and the message offers no way to choose between
|
|
7
|
+
* them, so the reader is left to guess — and one of them means "run `login` again" while
|
|
8
|
+
* another means "your `baseUrl` and your session are about different hosts", which no amount
|
|
9
|
+
* of logging in will fix. A field report ended with someone writing their own
|
|
10
|
+
* `check-session.mjs` to answer it.
|
|
11
|
+
*
|
|
12
|
+
* **Two of the three are answerable from the file alone**, which is the finding that makes
|
|
13
|
+
* this verb worth having rather than a second wrapper around a browser:
|
|
14
|
+
*
|
|
15
|
+
* - **expired** — a cookie carries its own `expires`. A file whose session cookies are all
|
|
16
|
+
* in the past cannot authenticate anything, and no request has to be made to know it.
|
|
17
|
+
* - **wrong-host** — a cookie carries its `domain`. If nothing in the file is scoped to the
|
|
18
|
+
* host `baseUrl` points at, the session was taken somewhere else. This is the one a
|
|
19
|
+
* person can stare at for an hour: the session works, the app works, and they are not the
|
|
20
|
+
* same app. `www.` and an apex fold together, as everywhere else in this package.
|
|
21
|
+
* - **rotated** — cookies present, unexpired, correctly scoped, and the app still answers
|
|
22
|
+
* with a login. Only the server knows why (a deploy, a secret rotation, a forced logout),
|
|
23
|
+
* and only a request can observe it.
|
|
24
|
+
*
|
|
25
|
+
* So the offline analysis runs first and the browser is used only for the question it is the
|
|
26
|
+
* only witness to. Everything here is pure; the I/O lives in `cli/session.ts`.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.foldHost = foldHost;
|
|
30
|
+
exports.domainCovers = domainCovers;
|
|
31
|
+
exports.readFacts = readFacts;
|
|
32
|
+
exports.judgeSession = judgeSession;
|
|
33
|
+
exports.describeAge = describeAge;
|
|
34
|
+
/** `www.` and an apex are one site, the same fold `discover --sitemap` applies. */
|
|
35
|
+
function foldHost(host) {
|
|
36
|
+
return host.replace(/^www\./i, '').toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
/** A cookie domain (`.example.com`) covers a host when the host ends with it. */
|
|
39
|
+
function domainCovers(domain, host) {
|
|
40
|
+
const d = foldHost(domain.replace(/^\./, ''));
|
|
41
|
+
const h = foldHost(host);
|
|
42
|
+
return h === d || h.endsWith(`.${d}`);
|
|
43
|
+
}
|
|
44
|
+
function readFacts(state, baseUrl, now = Date.now()) {
|
|
45
|
+
let host = '';
|
|
46
|
+
try {
|
|
47
|
+
// `hostname`, never `host`: a cookie is not scoped by port (RFC 6265 ignores it
|
|
48
|
+
// entirely), so comparing `127.0.0.1` against `127.0.0.1:49331` reports a session on
|
|
49
|
+
// the right machine as belonging to another application. Caught by the live test on the
|
|
50
|
+
// first run, and it would have hit every `localhost:3000` in existence — a confidently
|
|
51
|
+
// wrong diagnosis, in the one verb written to stop those.
|
|
52
|
+
host = new URL(baseUrl).hostname;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* an unusable baseUrl is the caller's problem to report */
|
|
56
|
+
}
|
|
57
|
+
const cookies = state.cookies ?? [];
|
|
58
|
+
const origins = state.origins ?? [];
|
|
59
|
+
// `expires` is seconds since epoch, and -1 means a session cookie: no stated expiry, which
|
|
60
|
+
// is not the same as expired. Counting those as gone would report a live session as dead.
|
|
61
|
+
const dated = cookies.filter((c) => typeof c.expires === 'number' && c.expires > 0);
|
|
62
|
+
const nowSeconds = now / 1000;
|
|
63
|
+
const expired = dated.filter((c) => c.expires < nowSeconds).length;
|
|
64
|
+
const future = dated.filter((c) => c.expires >= nowSeconds).map((c) => c.expires);
|
|
65
|
+
const expiresInSeconds = future.length > 0
|
|
66
|
+
? Math.round(Math.min(...future) - nowSeconds)
|
|
67
|
+
: dated.length > 0
|
|
68
|
+
? Math.round(Math.max(...dated.map((c) => c.expires)) - nowSeconds)
|
|
69
|
+
: null;
|
|
70
|
+
const domains = [...new Set(cookies.map((c) => c.domain ?? '').filter(Boolean))];
|
|
71
|
+
const coversHost = host === '' ||
|
|
72
|
+
domains.some((d) => domainCovers(d, host)) ||
|
|
73
|
+
origins.some((o) => {
|
|
74
|
+
try {
|
|
75
|
+
return foldHost(new URL(o.origin).hostname) === foldHost(host);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return { cookies: cookies.length, origins: origins.length, expired, expiresInSeconds, domains, coversHost, host };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The verdict, from the file's own facts plus — when one was possible — what the app
|
|
85
|
+
* answered.
|
|
86
|
+
*
|
|
87
|
+
* `landed` is undefined when no request was made, which is a legitimate state: the offline
|
|
88
|
+
* causes are conclusive on their own, and opening a browser to re-confirm an expiry date is
|
|
89
|
+
* a request nobody needs to make.
|
|
90
|
+
*/
|
|
91
|
+
function judgeSession(facts, replay, loginCommand = 'ia-qa-heal login') {
|
|
92
|
+
if (facts.cookies === 0 && facts.origins === 0) {
|
|
93
|
+
return {
|
|
94
|
+
cause: 'empty',
|
|
95
|
+
holds: false,
|
|
96
|
+
headline: 'This session file holds nothing — no cookies, no local storage.',
|
|
97
|
+
remedy: [
|
|
98
|
+
'A storageState carries cookies and local storage, and nothing else. An app that keeps',
|
|
99
|
+
'its token in IndexedDB, in memory or in a service worker leaves nothing for it to hold,',
|
|
100
|
+
'however logged in the browser was.',
|
|
101
|
+
`For those, capture during your own suite's run instead: IAQA_CAPTURE=1 with "testCommand".`,
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (facts.cookies > 0 && facts.expired === facts.cookies) {
|
|
106
|
+
const ago = facts.expiresInSeconds === null ? '' : ` (the last one ${describeAge(-facts.expiresInSeconds)} ago)`;
|
|
107
|
+
return {
|
|
108
|
+
cause: 'expired',
|
|
109
|
+
holds: false,
|
|
110
|
+
headline: `Every cookie in this session has expired${ago}.`,
|
|
111
|
+
remedy: [`A person logs in again: ${loginCommand}`, 'Nothing about your app or your config is wrong.'],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (!facts.coversHost) {
|
|
115
|
+
return {
|
|
116
|
+
cause: 'wrong-host',
|
|
117
|
+
holds: false,
|
|
118
|
+
headline: `Nothing in this session is scoped to ${facts.host} — its cookies are for ${facts.domains.length > 0 ? facts.domains.join(', ') : '(no domain stated)'}.`,
|
|
119
|
+
remedy: [
|
|
120
|
+
'The session and the baseUrl are about different applications, so logging in again will',
|
|
121
|
+
'produce the same file. Either point baseUrl at the host this session was taken on, or',
|
|
122
|
+
`take one on this host: ${loginCommand} --url ${facts.host ? `https://${facts.host}` : '<your app>'}`,
|
|
123
|
+
'(www. and the apex are treated as one host, so that difference is not the cause here.)',
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (replay?.error) {
|
|
128
|
+
return {
|
|
129
|
+
cause: 'unknown',
|
|
130
|
+
holds: false,
|
|
131
|
+
headline: `The session looks usable, but the app could not be opened to confirm it: ${replay.error}`,
|
|
132
|
+
remedy: [
|
|
133
|
+
'This is not a verdict about the session — nothing was proven either way.',
|
|
134
|
+
'Check that the app is reachable from this machine, then run this again.',
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (replay && !replay.wall) {
|
|
139
|
+
return {
|
|
140
|
+
cause: 'holds',
|
|
141
|
+
holds: true,
|
|
142
|
+
headline: `This session opens the app — landed on ${replay.landed}, no login form.`,
|
|
143
|
+
remedy: [],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (replay?.wall) {
|
|
147
|
+
return {
|
|
148
|
+
cause: 'rotated',
|
|
149
|
+
holds: false,
|
|
150
|
+
headline: `The cookies are valid and scoped to ${facts.host}, but the app still answered with a login (landed on ${replay.landed}).`,
|
|
151
|
+
remedy: [
|
|
152
|
+
'The server dropped this session: a deploy, a secret rotation, a forced logout, or a',
|
|
153
|
+
'second factor it now asks for. Nothing in the file can say which.',
|
|
154
|
+
`A person logs in again: ${loginCommand}`,
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
// No replay was attempted and no offline cause fired: the file is plausible, and plausible
|
|
159
|
+
// is not the same as working. Saying "holds" here is the exact false green this package
|
|
160
|
+
// exists to refuse.
|
|
161
|
+
return {
|
|
162
|
+
cause: 'unknown',
|
|
163
|
+
holds: false,
|
|
164
|
+
headline: 'This session file looks usable, but nothing opened the app to prove it.',
|
|
165
|
+
remedy: ['Run the check without --offline to find out.'],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** "3 days", "4 hours", "12 minutes" — enough precision to act, never a timestamp to parse. */
|
|
169
|
+
function describeAge(seconds) {
|
|
170
|
+
const s = Math.abs(Math.round(seconds));
|
|
171
|
+
if (s < 90)
|
|
172
|
+
return `${s} second${s === 1 ? '' : 's'}`;
|
|
173
|
+
const m = Math.round(s / 60);
|
|
174
|
+
if (m < 90)
|
|
175
|
+
return `${m} minute${m === 1 ? '' : 's'}`;
|
|
176
|
+
const h = Math.round(m / 60);
|
|
177
|
+
if (h < 48)
|
|
178
|
+
return `${h} hour${h === 1 ? '' : 's'}`;
|
|
179
|
+
const d = Math.round(h / 24);
|
|
180
|
+
return `${d} day${d === 1 ? '' : 's'}`;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=sessionCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionCheck.js","sourceRoot":"","sources":["../src/sessionCheck.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Usage } from './ingest';
|
|
2
|
+
/**
|
|
3
|
+
* "No test file referenced any of the drifted selectors" — the false negative.
|
|
4
|
+
*
|
|
5
|
+
* Field report: that line was printed over a project whose Page Object contains, verbatim,
|
|
6
|
+
* `a[href="/index.php/cart"]` — one of the drifted selectors. The statement was simply
|
|
7
|
+
* untrue, and the reader could only conclude the inventory was broken.
|
|
8
|
+
*
|
|
9
|
+
* The line was measuring the wrong set. `rewrites` holds the rows the tool decided it could
|
|
10
|
+
* **repair**; a row that drifted and could not be repaired is in neither `rewrites` nor the
|
|
11
|
+
* resulting edits, so "nothing matched a file" got reported as "your tests do not use any of
|
|
12
|
+
* this". Two very different sentences: one is about the suite, the other about the tool's
|
|
13
|
+
* own reach.
|
|
14
|
+
*
|
|
15
|
+
* What the reader needed was the third sentence, which nobody was writing: **your tests do
|
|
16
|
+
* reference it, and here is why I cannot repair it.** Saying "no" where the honest answer is
|
|
17
|
+
* "I don't know, for this reason" is the single failure mode that costs the most trust,
|
|
18
|
+
* because it is indistinguishable from a working check.
|
|
19
|
+
*
|
|
20
|
+
* Everything here is pure, and it reads the two files that were already on disk the whole
|
|
21
|
+
* time: the drift rows, and `usage.json`.
|
|
22
|
+
*/
|
|
23
|
+
export interface UnrepairedRow {
|
|
24
|
+
status: string;
|
|
25
|
+
role?: string;
|
|
26
|
+
name?: string;
|
|
27
|
+
selector?: string;
|
|
28
|
+
newName?: string;
|
|
29
|
+
candidateCount?: number;
|
|
30
|
+
nameMasked?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface UnrepairedFinding {
|
|
33
|
+
selector?: string;
|
|
34
|
+
role: string;
|
|
35
|
+
name: string;
|
|
36
|
+
status: string;
|
|
37
|
+
/** Why this one could not be repaired, in a sentence about the element. */
|
|
38
|
+
why: string;
|
|
39
|
+
/** How the suite reaches it. */
|
|
40
|
+
via: Array<'selector' | 'name'>;
|
|
41
|
+
sites: Array<{
|
|
42
|
+
file: string;
|
|
43
|
+
line: number;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Why a drifted row the suite uses was left alone.
|
|
48
|
+
*
|
|
49
|
+
* Each sentence names the property of the ELEMENT that makes the repair unprovable, not the
|
|
50
|
+
* internal status word — a reader who is told `ambiguous` has learned a vocabulary, and a
|
|
51
|
+
* reader told "two elements now share that role and name, so a repair would be a coin flip"
|
|
52
|
+
* has learned what to go and look at.
|
|
53
|
+
*/
|
|
54
|
+
export declare function reasonFor(row: UnrepairedRow): string;
|
|
55
|
+
/**
|
|
56
|
+
* Rows that drifted, were not repaired, and that the suite demonstrably uses.
|
|
57
|
+
*
|
|
58
|
+
* `repaired` is the set of selector literals the fix is about to rewrite — anything in it is
|
|
59
|
+
* already reported as an edit and must not be listed again as a gap.
|
|
60
|
+
*
|
|
61
|
+
* No inventory ⇒ no findings, and the caller must then say "not measured" rather than
|
|
62
|
+
* anything resembling "no". That distinction is the whole point of this module.
|
|
63
|
+
*/
|
|
64
|
+
export declare function referencedButUnrepaired(rows: UnrepairedRow[], repaired: Set<string>, usage: Usage | null): UnrepairedFinding[];
|
|
65
|
+
/**
|
|
66
|
+
* The block that replaces "no test file referenced any of the drifted selectors".
|
|
67
|
+
*
|
|
68
|
+
* Three states, and they must stay three: repaired nothing *and* your tests use none of it;
|
|
69
|
+
* repaired nothing *but* your tests use these, here is why; and repaired nothing *and*
|
|
70
|
+
* nobody has told me what your tests use. The middle one did not exist, and the third was
|
|
71
|
+
* being printed as the first.
|
|
72
|
+
*/
|
|
73
|
+
export declare function unrepairedLines(findings: UnrepairedFinding[], hasInventory: boolean): string[];
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unrepaired.js","sourceRoot":"","sources":["../src/unrepaired.ts"],"names":[],"mappings":"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"}
|
package/dist/volatile.d.ts
CHANGED
|
@@ -29,8 +29,16 @@ export interface VolatilePattern {
|
|
|
29
29
|
export declare function parseVolatilePatterns(patterns: string[] | undefined): VolatilePattern[];
|
|
30
30
|
/** Shared with `nameMask.ts`, so both declarations speak one pattern dialect. */
|
|
31
31
|
export declare function globToRegex(glob: string): RegExp;
|
|
32
|
+
/** The pattern that excludes this element, or null. Named, because the message needs it. */
|
|
33
|
+
export declare function matchVolatile(el: MappedElement, patterns: VolatilePattern[]): VolatilePattern | null;
|
|
32
34
|
export declare function isVolatile(el: MappedElement, patterns: VolatilePattern[]): boolean;
|
|
35
|
+
/** An element that left the contract, with the pattern that removed it. */
|
|
36
|
+
export interface VolatileExclusion {
|
|
37
|
+
element: MappedElement;
|
|
38
|
+
pattern: string;
|
|
39
|
+
}
|
|
33
40
|
export declare function filterVolatile(elements: MappedElement[], patterns: VolatilePattern[]): {
|
|
34
41
|
kept: MappedElement[];
|
|
35
42
|
excluded: number;
|
|
43
|
+
exclusions: VolatileExclusion[];
|
|
36
44
|
};
|
package/dist/volatile.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseVolatilePatterns = parseVolatilePatterns;
|
|
4
4
|
exports.globToRegex = globToRegex;
|
|
5
|
+
exports.matchVolatile = matchVolatile;
|
|
5
6
|
exports.isVolatile = isVolatile;
|
|
6
7
|
exports.filterVolatile = filterVolatile;
|
|
7
8
|
const FIELDS = ['href', 'selector', 'name', 'role'];
|
|
@@ -22,27 +23,43 @@ function globToRegex(glob) {
|
|
|
22
23
|
const escaped = glob.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
23
24
|
return new RegExp(`^${escaped}$`, 'i');
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
/** The pattern that excludes this element, or null. Named, because the message needs it. */
|
|
27
|
+
function matchVolatile(el, patterns) {
|
|
26
28
|
for (const p of patterns) {
|
|
27
29
|
if (p.field === 'any') {
|
|
28
30
|
if ((el.href !== undefined && p.regex.test(el.href)) ||
|
|
29
31
|
p.regex.test(el.selector) ||
|
|
30
32
|
p.regex.test(el.name)) {
|
|
31
|
-
return
|
|
33
|
+
return p;
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
else {
|
|
35
37
|
const value = el[p.field];
|
|
36
38
|
if (value !== undefined && p.regex.test(value))
|
|
37
|
-
return
|
|
39
|
+
return p;
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
|
-
return
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
function isVolatile(el, patterns) {
|
|
45
|
+
return matchVolatile(el, patterns) !== null;
|
|
41
46
|
}
|
|
42
47
|
function filterVolatile(elements, patterns) {
|
|
43
48
|
if (patterns.length === 0)
|
|
44
|
-
return { kept: elements, excluded: 0 };
|
|
45
|
-
const kept =
|
|
46
|
-
|
|
49
|
+
return { kept: elements, excluded: 0, exclusions: [] };
|
|
50
|
+
const kept = [];
|
|
51
|
+
const exclusions = [];
|
|
52
|
+
for (const el of elements) {
|
|
53
|
+
const hit = matchVolatile(el, patterns);
|
|
54
|
+
// The excluded elements are kept, not just counted. They are the only evidence that
|
|
55
|
+
// can answer "is anything my tests use being silently dropped?" — see
|
|
56
|
+
// `volatileBlindSpots`, and they exist nowhere else: by construction they never
|
|
57
|
+
// reach a contract.
|
|
58
|
+
if (hit)
|
|
59
|
+
exclusions.push({ element: el, pattern: hit.raw });
|
|
60
|
+
else
|
|
61
|
+
kept.push(el);
|
|
62
|
+
}
|
|
63
|
+
return { kept, excluded: exclusions.length, exclusions };
|
|
47
64
|
}
|
|
48
65
|
//# sourceMappingURL=volatile.js.map
|
package/dist/volatile.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"volatile.js","sourceRoot":"","sources":["../src/volatile.ts"],"names":[],"mappings":";;AAiCA,sDAWC;AAGD,kCAGC;
|
|
1
|
+
{"version":3,"file":"volatile.js","sourceRoot":"","sources":["../src/volatile.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Usage } from './ingest';
|
|
2
|
+
import type { VolatileExclusion } from './volatile';
|
|
3
|
+
/**
|
|
4
|
+
* The declaration that removes an element, checked against the suite that uses it.
|
|
5
|
+
*
|
|
6
|
+
* `volatile` is the most destructive declaration this tool takes: a matching element never
|
|
7
|
+
* enters a contract at all — not its selector, not its name, not its href, not its
|
|
8
|
+
* coverage. That is correct for a rotating feed item, and it is a **permanent blind spot**
|
|
9
|
+
* for anything else. Nothing ever reports drift on an element that is not in the contract,
|
|
10
|
+
* because from the gate's point of view it does not exist.
|
|
11
|
+
*
|
|
12
|
+
* Until now `map` printed only a count — `N volatile excluded` — and a count cannot
|
|
13
|
+
* distinguish "40 feed cards" from "40 feed cards and the cart button your suite clicks".
|
|
14
|
+
* Field report: an element excluded by a pattern was referenced by a Page Object, and
|
|
15
|
+
* nobody was told for fifteen days.
|
|
16
|
+
*
|
|
17
|
+
* **Its conservative sibling already does this.** `nameMask.ts` refuses a mask that would
|
|
18
|
+
* swallow a label `usage.names` says the tests locate by — the exact same cross-check,
|
|
19
|
+
* between the exact same two files. `volatile` is the one that removes more and checked
|
|
20
|
+
* less.
|
|
21
|
+
*
|
|
22
|
+
* Three restraints, and they are what makes this a warning worth reading:
|
|
23
|
+
*
|
|
24
|
+
* **It warns, it never refuses.** A mask is a comparison-time policy and can be declined
|
|
25
|
+
* without losing anything; `volatile` is applied at capture, and refusing it would either
|
|
26
|
+
* write a contract the user did not ask for or abandon the page. The user declared this on
|
|
27
|
+
* purpose. What they did not choose is being uninformed about it.
|
|
28
|
+
*
|
|
29
|
+
* **No inventory ⇒ no message.** A project that has never run `ingest` has no evidence
|
|
30
|
+
* either way, and "no test uses it" is exactly the false negative this file exists to stop
|
|
31
|
+
* being printed. Silence here means *not measured*, and the caller says so rather than
|
|
32
|
+
* implying the exclusions are safe.
|
|
33
|
+
*
|
|
34
|
+
* **Silent on the good case.** An exclusion nothing references produces no line at all. A
|
|
35
|
+
* warning that fires on the ordinary run is one people learn to scroll past, and then it is
|
|
36
|
+
* not there on the day it matters.
|
|
37
|
+
*/
|
|
38
|
+
export interface VolatileBlindSpot {
|
|
39
|
+
/** What was excluded, as the contract would have addressed it. */
|
|
40
|
+
selector: string;
|
|
41
|
+
name: string;
|
|
42
|
+
role: string;
|
|
43
|
+
/** The `volatile` pattern that removed it — the line the user would edit. */
|
|
44
|
+
pattern: string;
|
|
45
|
+
/** How the suite reaches it: by selector, by accessible name, or both. */
|
|
46
|
+
via: Array<'selector' | 'name'>;
|
|
47
|
+
/** Where, in the user's own files. */
|
|
48
|
+
sites: Array<{
|
|
49
|
+
file: string;
|
|
50
|
+
line: number;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Which excluded elements the suite actually uses.
|
|
55
|
+
*
|
|
56
|
+
* Deduplicated by selector + name: a navbar element excluded on 142 pages is one blind
|
|
57
|
+
* spot and one fix, not 142 findings — the same reasoning `check` applies to dead links.
|
|
58
|
+
*/
|
|
59
|
+
export declare function volatileBlindSpots(exclusions: VolatileExclusion[], usage: Usage | null): VolatileBlindSpot[];
|
|
60
|
+
/**
|
|
61
|
+
* The warning, as one block.
|
|
62
|
+
*
|
|
63
|
+
* Rendered here rather than in the CLI so the message cannot be worded one way by `map` and
|
|
64
|
+
* another by whatever reads this next — the `checkHeadline` rule. Returns null when there
|
|
65
|
+
* is nothing to say, which is the ordinary case.
|
|
66
|
+
*/
|
|
67
|
+
export declare function volatileBlindSpotLines(spots: VolatileBlindSpot[]): string[] | null;
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"volatileBlindSpot.js","sourceRoot":"","sources":["../src/volatileBlindSpot.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ia-qa/self-healing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "Your Playwright, Cypress or Selenium tests break when a selector moves — this finds the element again and rewrites the test. Deterministic: no LLM decides whether your build passes. Runs entirely on your machine, with a local MCP server for agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"self-healing",
|
|
@@ -64,6 +64,7 @@ exists only in a state nothing captured (behind a tab, a modal, a mode toggle).
|
|
|
64
64
|
| "is this getting better or worse?" | `history` (`--json`) — the trend no single run can reconstruct |
|
|
65
65
|
| "gate my pipeline on it" | `diff --junit <file>` — every major CI charts the trend natively |
|
|
66
66
|
| `map` says "login wall" / the app is behind SSO | if a storageState already exists (most authenticated Playwright suites write one in `globalSetup`): `--session <file>`, `IAQA_SESSION`, or `"session"` in config.json — no second login. Otherwise tell the human to run `ia-qa-heal login` — **you cannot do it** (§4.12) |
|
|
67
|
+
| "is the session still good?" / before blaming the app for a wall | `session --check`. It tells the three causes **apart**: `expired` (every cookie's date is past) and `wrong-host` (nothing is scoped to `baseUrl`) are decided **from the file, no browser**; `rotated` (the file is fine, the server still answers a login) needs one request. Exit 0 holds · 1 does not · 2 could not tell. `--offline` skips the browser entirely. Run it **before** a long `map`, and when a tour or a map reports a wall — a `wrong-host` is the one no amount of logging in will fix |
|
|
67
68
|
| "let me look at it / decide myself" | tell the human to run `ia-qa-heal ui` — **do not run it yourself** (§4.8) |
|
|
68
69
|
| first-time setup | `ia-qa-heal init --yes --base-url <url> --discover [--tests <paths>] [--bootstrap]` — asks nothing, so it cannot hang. `--discover` fills `pages` from their sitemap (plain fetch, no browser); `--bootstrap` also chains ingest → map → baseline. Writing `.ia-qa/config.json` by hand still works |
|
|
69
70
|
| "288 lost, red everywhere, on an app that looks fine" | read the **systemic delta** block `diff` prints — the same element gone from ~every page is one fact about the *capture*, not N drifts. Usually a consent banner, a tour or a flag: the fix is `prepare` in config.json (§4.16), not a rewrite |
|
|
@@ -125,6 +126,9 @@ clicking the wrong thing), or a name-drift finding that is not attributable.
|
|
|
125
126
|
included, so a real break on that button would never be caught again. And never propose a mask
|
|
126
127
|
over a label the tests locate by name: the tool refuses it out loud, and talking the human past
|
|
127
128
|
that refusal is putting a green gate over a red suite.
|
|
129
|
+
**`map` now names the elements a `volatile` pattern removed that the suite still uses**, with
|
|
130
|
+
`file:line` and the pattern to narrow — relay that warning in full. It fires only on the bad
|
|
131
|
+
case, so if you see it, something the tests click will never be gated again.
|
|
128
132
|
10. **Never read a `map` that exited 1 as "nothing was mapped".** A page that fails is now isolated:
|
|
129
133
|
the rest are still written, its own previous contract is left untouched, and the summary says
|
|
130
134
|
`N of M pages mapped`. Read that line — the pages it did not reach are `stale` in the next diff,
|
|
@@ -139,7 +143,13 @@ clicking the wrong thing), or a name-drift finding that is not attributable.
|
|
|
139
143
|
12. **Never try to log a suite in yourself.** When `map` reports a login wall it prints the whole
|
|
140
144
|
remedy: a filled-in `auth` block when the form is readable, and the fact that a form fill
|
|
141
145
|
cannot pass SSO with MFA, a consent screen or a magic link. Read past the first line before
|
|
142
|
-
concluding anything.
|
|
146
|
+
concluding anything. **Run `session --check` first**: it says *which* of expired / wrong-host /
|
|
147
|
+
rotated it is, and two of those need no browser. `wrong-host` means the session and the
|
|
148
|
+
`baseUrl` are about different applications — telling the human to log in again there wastes
|
|
149
|
+
their time and produces the identical file. Note that with a session in use, `map` now stops
|
|
150
|
+
at the first wall instead of walking every remaining page into it: `N of M pages mapped` with
|
|
151
|
+
the rest **not attempted** (they keep their previous contracts and read `stale`), so do not
|
|
152
|
+
report those as pages that failed. For everything a form cannot answer there is exactly one step, and it
|
|
143
153
|
belongs to the human: `ia-qa-heal login` opens a visible browser, they log in once, and the
|
|
144
154
|
session is reused by `map`. It refuses without a TTY and under CI — that refusal is aimed at
|
|
145
155
|
you. Never ask for their password, never put a credential in `config.json` (it holds `secrets`
|
|
@@ -197,11 +207,19 @@ clicking the wrong thing), or a name-drift finding that is not attributable.
|
|
|
197
207
|
`run`'s live coverage and `map`'s "N of M pages": a surface left unmeasured is never a pass.
|
|
198
208
|
The remedy is `--deep-budget <n>` or a narrower `--depth 1`, never ignoring the line.
|
|
199
209
|
|
|
200
|
-
19. **Never
|
|
210
|
+
19. **Never read "nothing could be rewritten" as "their tests do not use it".** `fix` prints
|
|
211
|
+
three different sentences and they mean three different things: *nothing to repair and your
|
|
212
|
+
tests reference none of it*; *nothing to repair **but** your tests DO reference these — here
|
|
213
|
+
is each one with `file:line` and why it could not be repaired*; and *not measured, because no
|
|
214
|
+
`usage.json` exists*. The middle one is the interesting case and the last one is not a "no".
|
|
215
|
+
Relay the reason per element — "two elements now answer to that role and name, so repairing
|
|
216
|
+
would be a coin flip" is what the human acts on, not the status word.
|
|
217
|
+
|
|
218
|
+
20. **Never present `--deep` as free.** It clicks its way through the page's states, so it costs
|
|
201
219
|
~1 min per page where a plain `map` costs seconds. Recommend it for building or refreshing a
|
|
202
220
|
**baseline**, not for a per-commit gate, and say so when you propose it.
|
|
203
221
|
|
|
204
|
-
|
|
222
|
+
21. **Never raise `--depth` to "get better coverage".** The default is 1 because it *finishes*.
|
|
205
223
|
Depth 2 was measured at +30% elements for 3× the time and **never completed at any budget** —
|
|
206
224
|
80, 150 and 250 clicks all returned the same elements, so the extra clicks re-walk states that
|
|
207
225
|
hold nothing new. If a user asks for deeper, tell them what it costs and that the contract
|
|
@@ -227,6 +245,14 @@ Nothing, except: requests to the user's **own** `baseUrl` (`check`'s link half,
|
|
|
227
245
|
— GET only, same origin, never a `/logout` or `/delete` URL), and `ia-qa-heal-ai` calling the
|
|
228
246
|
user's own LLM if they opted in. Say that plainly rather than implying more or less.
|
|
229
247
|
|
|
248
|
+
**Where the key lives.** `ia-qa-heal-ai` needs a provider key, and it is never a file value.
|
|
249
|
+
Prefer the OS store — hand the human `ia-qa-heal secret set <NAME>` to run themselves (it asks
|
|
250
|
+
for the value on a terminal and refuses a pipe or an argument), then reference it as
|
|
251
|
+
`{ "source": "keychain", "key": "<NAME>" }`. You can confirm it resolves with
|
|
252
|
+
`secret check <NAME>`; **there is no verb that prints a stored value and you must not look for
|
|
253
|
+
one**. In CI there is no store and no person: keep `{ "source": "env" }` and let the runner
|
|
254
|
+
hold the secret. Never ask the human to paste a key to you; if they do, tell them to rotate it.
|
|
255
|
+
|
|
230
256
|
`ui` does not change that: it binds `127.0.0.1` only, is gated by a per-process token, and serves a
|
|
231
257
|
page with no external asset. If asked whether it "hosts" anything, the answer is no — it is a local
|
|
232
258
|
process that dies with the browser tab.
|