kryptheon 0.1.10 → 0.1.12
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/bin/kryptheon.js +49 -11
- package/kryptheon-baselines.js +176 -0
- package/kryptheon-fixture.js +432 -388
- package/kryptheon-project.js +199 -0
- package/kryptheon-reporter.js +138 -5
- package/kryptheon-secrets.js +191 -191
- package/kryptheon-signature.js +11 -11
- package/package.json +74 -67
package/bin/kryptheon.js
CHANGED
|
@@ -17,6 +17,8 @@ const replay = require('../kryptheon-replay.js');
|
|
|
17
17
|
const selectors = require('../kryptheon-selectors.js');
|
|
18
18
|
const recordings = require('../kryptheon-recordings.js');
|
|
19
19
|
const cleanup = require('../kryptheon-cleanup.js');
|
|
20
|
+
const baselines = require('../kryptheon-baselines.js');
|
|
21
|
+
const project = require('../kryptheon-project.js');
|
|
20
22
|
|
|
21
23
|
const PACKAGE_DIR = path.join(__dirname, '..');
|
|
22
24
|
const USER_DIR = process.cwd();
|
|
@@ -1009,6 +1011,39 @@ async function offerToDropLogout(relativeFile) {
|
|
|
1009
1011
|
// corrected on the second attempt becomes a wrong login performed on every
|
|
1010
1012
|
// run. This takes those out and says what it took, because a tool that edits
|
|
1011
1013
|
// your recording without telling you is worse than one that leaves the mess.
|
|
1014
|
+
// A saved result whose recording is gone, or has been recorded again since,
|
|
1015
|
+
// describes a page nothing here visits any more. Left in place it is handed
|
|
1016
|
+
// to whichever new recording lands on the same name, which then fails on its
|
|
1017
|
+
// first run against a baseline it never created.
|
|
1018
|
+
function dropDeadBaselines() {
|
|
1019
|
+
const api = (function () {
|
|
1020
|
+
try {
|
|
1021
|
+
return require(path.join(PACKAGE_DIR, 'kryptheon-fixture.js'));
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
})();
|
|
1026
|
+
if (!api) return [];
|
|
1027
|
+
|
|
1028
|
+
let all;
|
|
1029
|
+
try {
|
|
1030
|
+
all = api.readBaselines(api.BASELINE_FILE);
|
|
1031
|
+
} catch (err) {
|
|
1032
|
+
return [];
|
|
1033
|
+
}
|
|
1034
|
+
if (!all || !Object.keys(all).length) return [];
|
|
1035
|
+
|
|
1036
|
+
const result = baselines.pruneBaselines(all, USER_DIR);
|
|
1037
|
+
if (!result.changed) return [];
|
|
1038
|
+
try {
|
|
1039
|
+
fs.writeFileSync(api.BASELINE_FILE, JSON.stringify(result.baselines, null, 2) + String.fromCharCode(10), "utf8");
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
return [];
|
|
1042
|
+
}
|
|
1043
|
+
for (const line of baselines.pruneLines(result.dropped)) console.log(line);
|
|
1044
|
+
return result.dropped;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1012
1047
|
function tidyRecording(relativeFile) {
|
|
1013
1048
|
const source = readSpec(relativeFile);
|
|
1014
1049
|
if (source === null) return [];
|
|
@@ -1490,22 +1525,23 @@ function triageSpecs() {
|
|
|
1490
1525
|
|
|
1491
1526
|
function check(options) {
|
|
1492
1527
|
const quiet = !!(options && options.quiet);
|
|
1528
|
+
|
|
1529
|
+
// Before anything else. Recordings and results are read out of whatever
|
|
1530
|
+
// folder this was run in, so being in the wrong one does not fail - it
|
|
1531
|
+
// succeeds, about the wrong project. That is the one outcome worth exiting
|
|
1532
|
+
// non-zero over even though nothing was tested.
|
|
1533
|
+
const here = project.inspectProject(USER_DIR);
|
|
1534
|
+
if (!here.ok) {
|
|
1535
|
+
for (const line of project.noProjectLines(here)) console.error(line);
|
|
1536
|
+
return 1;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1493
1539
|
// Deliberately not a failure. An assistant told to run this after every
|
|
1494
1540
|
// change will read a non-zero exit as "something broke" and start fixing
|
|
1495
1541
|
// code that is perfectly fine. Nothing has been recorded, so there is
|
|
1496
1542
|
// nothing to verify and nothing to repair.
|
|
1497
1543
|
if (!listSpecFiles().length) {
|
|
1498
|
-
console.log(
|
|
1499
|
-
console.log(' Nothing has been recorded yet, so there is nothing to check.');
|
|
1500
|
-
console.log('');
|
|
1501
|
-
console.log(' This is not a failure, and there is nothing in the code to fix.');
|
|
1502
|
-
console.log('');
|
|
1503
|
-
console.log(' To start, record a flow through the app:');
|
|
1504
|
-
console.log(' npx kryptheon record <url>');
|
|
1505
|
-
console.log('');
|
|
1506
|
-
console.log(' That opens a browser window, so it has to be run by a person in');
|
|
1507
|
-
console.log(' a normal terminal - it cannot be done from an AI assistant.');
|
|
1508
|
-
console.log('');
|
|
1544
|
+
for (const line of project.noRecordingsLines()) console.log(line);
|
|
1509
1545
|
return 0;
|
|
1510
1546
|
}
|
|
1511
1547
|
if (!fixtureResolvesForUser() && specsNeedThePackage()) {
|
|
@@ -1522,6 +1558,7 @@ function check(options) {
|
|
|
1522
1558
|
return 1;
|
|
1523
1559
|
}
|
|
1524
1560
|
keepArtefactsOutOfGit();
|
|
1561
|
+
dropDeadBaselines();
|
|
1525
1562
|
|
|
1526
1563
|
const triage = triageSpecs();
|
|
1527
1564
|
|
|
@@ -1933,6 +1970,7 @@ module.exports = {
|
|
|
1933
1970
|
codegenComplaints: codegenComplaints,
|
|
1934
1971
|
reportFragileSelectors: reportFragileSelectors,
|
|
1935
1972
|
tidyRecording: tidyRecording,
|
|
1973
|
+
dropDeadBaselines: dropDeadBaselines,
|
|
1936
1974
|
reportTidying: reportTidying,
|
|
1937
1975
|
remove: remove,
|
|
1938
1976
|
deleteRecording: deleteRecording,
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Keeping a saved result tied to the recording it came from.
|
|
2
|
+
//
|
|
3
|
+
// A baseline used to be identified by where the recording lived and what it was
|
|
4
|
+
// called. That is not enough. Delete the spec files with `del tests\*.spec.js` -
|
|
5
|
+
// which is what anyone does before the tool has taught them `kryptheon remove` -
|
|
6
|
+
// record the flow again, and the new recording is handed the same name. Same
|
|
7
|
+
// path, same title, same key. It inherits the old recording's saved page and
|
|
8
|
+
// fails on its very first run, against a baseline it never created and could
|
|
9
|
+
// never have matched.
|
|
10
|
+
//
|
|
11
|
+
// The report that came out of that said both of these at once:
|
|
12
|
+
//
|
|
13
|
+
// This has not passed before.
|
|
14
|
+
// Headings that are gone: "Transactions"
|
|
15
|
+
//
|
|
16
|
+
// Which cannot both be true. If it has never passed there is no baseline of its
|
|
17
|
+
// own to have changed, so there is nothing to show a difference against.
|
|
18
|
+
//
|
|
19
|
+
// So a saved result now also records a fingerprint of the recording that made
|
|
20
|
+
// it. When the recording is gone, or its steps are not the steps the baseline
|
|
21
|
+
// was taken from, the saved result describes something that no longer exists
|
|
22
|
+
// and is thrown away rather than compared against.
|
|
23
|
+
//
|
|
24
|
+
// Pure data work: no Playwright, no test runner.
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const replay = require('./kryptheon-replay.js');
|
|
29
|
+
|
|
30
|
+
/** The spec file half of a baseline key, in one consistent shape. */
|
|
31
|
+
function specOf(key) {
|
|
32
|
+
return String(key || '').split(' :: ')[0].split('\\').join('/');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function titleOf(key) {
|
|
36
|
+
const parts = String(key || '').split(' :: ');
|
|
37
|
+
return parts.length > 1 ? parts.slice(1).join(' :: ') : '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The fingerprint of the recording in a file, or null if it cannot be read. */
|
|
41
|
+
function fingerprintOf(userDir, specPath) {
|
|
42
|
+
let source;
|
|
43
|
+
try {
|
|
44
|
+
source = fs.readFileSync(path.join(userDir, specPath), 'utf8');
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return replay.recordingId(source);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What each saved result is: still current, orphaned, or stale.
|
|
53
|
+
*
|
|
54
|
+
* `orphan` - the recording it belongs to is not there any more.
|
|
55
|
+
* `stale` - a recording of that name exists, but it is not the one this was
|
|
56
|
+
* taken from. This is the case that cost twenty minutes: the file
|
|
57
|
+
* is present, so nothing looks wrong.
|
|
58
|
+
* `legacy` - written before fingerprints existed. There is no way to tell
|
|
59
|
+
* whether it matches, so it is kept, and a fingerprint is written
|
|
60
|
+
* in so the question can be answered next time. Guessing here would
|
|
61
|
+
* mean either throwing away a good baseline or, far worse, quietly
|
|
62
|
+
* accepting a real regression as the new normal.
|
|
63
|
+
*/
|
|
64
|
+
function classifyBaselines(baselines, userDir) {
|
|
65
|
+
const all = baselines || {};
|
|
66
|
+
const verdicts = [];
|
|
67
|
+
|
|
68
|
+
for (const key of Object.keys(all)) {
|
|
69
|
+
const entry = all[key];
|
|
70
|
+
const spec = specOf(key);
|
|
71
|
+
if (!entry || typeof entry !== 'object') {
|
|
72
|
+
verdicts.push({ key: key, state: 'orphan', spec: spec, title: titleOf(key) });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let exists = false;
|
|
77
|
+
try {
|
|
78
|
+
exists = fs.statSync(path.join(userDir, spec)).isFile();
|
|
79
|
+
} catch (e) {
|
|
80
|
+
exists = false;
|
|
81
|
+
}
|
|
82
|
+
if (!exists) {
|
|
83
|
+
verdicts.push({ key: key, state: 'orphan', spec: spec, title: titleOf(key) });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const now = fingerprintOf(userDir, spec);
|
|
88
|
+
if (!entry.recordingId) {
|
|
89
|
+
verdicts.push({ key: key, state: 'legacy', spec: spec, title: titleOf(key), fingerprint: now });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (now && entry.recordingId !== now) {
|
|
93
|
+
verdicts.push({ key: key, state: 'stale', spec: spec, title: titleOf(key), fingerprint: now });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
verdicts.push({ key: key, state: 'current', spec: spec, title: titleOf(key) });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return verdicts;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The saved results with the dead ones taken out.
|
|
104
|
+
*
|
|
105
|
+
* Returns a new object; nothing is written here. The caller decides whether the
|
|
106
|
+
* file is worth rewriting, so a check that changes nothing never touches it.
|
|
107
|
+
*/
|
|
108
|
+
function pruneBaselines(baselines, userDir) {
|
|
109
|
+
const all = baselines || {};
|
|
110
|
+
const verdicts = classifyBaselines(all, userDir);
|
|
111
|
+
const kept = {};
|
|
112
|
+
const dropped = [];
|
|
113
|
+
let stamped = 0;
|
|
114
|
+
|
|
115
|
+
for (const verdict of verdicts) {
|
|
116
|
+
if (verdict.state === 'orphan' || verdict.state === 'stale') {
|
|
117
|
+
dropped.push(verdict);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const entry = all[verdict.key];
|
|
121
|
+
if (verdict.state === 'legacy' && verdict.fingerprint) {
|
|
122
|
+
// Kept as it is, but tied to this recording from now on.
|
|
123
|
+
kept[verdict.key] = Object.assign({}, entry, { recordingId: verdict.fingerprint });
|
|
124
|
+
stamped++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
kept[verdict.key] = entry;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { baselines: kept, dropped: dropped, stamped: stamped, changed: dropped.length > 0 || stamped > 0 };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function plural(n, word) {
|
|
134
|
+
return n + ' ' + word + (n === 1 ? '' : 's');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* One line about what was thrown away, said as a fact rather than a warning.
|
|
139
|
+
*
|
|
140
|
+
* Nothing has gone wrong when this happens - a recording was deleted or redone,
|
|
141
|
+
* which is a normal thing to do. It is said out loud only so that a flow which
|
|
142
|
+
* suddenly starts from scratch is not a mystery.
|
|
143
|
+
*/
|
|
144
|
+
function pruneLines(dropped) {
|
|
145
|
+
const list = dropped || [];
|
|
146
|
+
if (!list.length) return [];
|
|
147
|
+
|
|
148
|
+
const orphans = list.filter((d) => d.state === 'orphan');
|
|
149
|
+
const stale = list.filter((d) => d.state === 'stale');
|
|
150
|
+
const lines = [];
|
|
151
|
+
|
|
152
|
+
if (orphans.length) {
|
|
153
|
+
lines.push(
|
|
154
|
+
' ' + plural(orphans.length, 'old baseline') + ' removed - ' +
|
|
155
|
+
(orphans.length === 1 ? 'its recording is' : 'their recordings are') + ' no longer here.',
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (stale.length) {
|
|
159
|
+
lines.push(
|
|
160
|
+
' ' + plural(stale.length, 'old baseline') + ' removed - ' +
|
|
161
|
+
(stale.length === 1 ? 'its recording has' : 'those recordings have') +
|
|
162
|
+
' been recorded again since.',
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
lines.push('');
|
|
166
|
+
return lines;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
specOf: specOf,
|
|
171
|
+
titleOf: titleOf,
|
|
172
|
+
fingerprintOf: fingerprintOf,
|
|
173
|
+
classifyBaselines: classifyBaselines,
|
|
174
|
+
pruneBaselines: pruneBaselines,
|
|
175
|
+
pruneLines: pruneLines,
|
|
176
|
+
};
|