kryptheon 0.1.11 → 0.1.13

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.
@@ -1,191 +1,191 @@
1
- // Which environment variables a recording needs, and whether they are there.
2
- //
3
- // Why this exists. When a password is typed during recording it is taken out
4
- // of the spec and replaced with `process.env.KRYPTHEON_PASSWORD || ''`. If
5
- // nobody ever creates the .env file, that expression quietly evaluates to an
6
- // empty string: the run submits a blank password, the login fails for the most
7
- // boring reason imaginable, and the report blames the app. Two hours can go
8
- // into that before anyone thinks to check the environment.
9
- //
10
- // So the names are read out of the spec before anything runs, and a recording
11
- // whose secret is missing is not run at all - a blank password is never sent.
12
-
13
- const fs = require('fs');
14
- const path = require('path');
15
-
16
- // The exact shape the scrubber writes. Anything else in the file that happens
17
- // to mention process.env is the author's own business and left alone.
18
- const SECRET_USE = /process\.env\.([A-Z][A-Z0-9_]*)\s*\|\|\s*(['"])\2/g;
19
-
20
- /** Every environment variable this recording falls back to an empty string for. */
21
- function requiredSecrets(source) {
22
- const out = [];
23
- const seen = Object.create(null);
24
- let match;
25
- SECRET_USE.lastIndex = 0;
26
- while ((match = SECRET_USE.exec(String(source || '')))) {
27
- const name = match[1];
28
- if (seen[name]) continue;
29
- seen[name] = true;
30
- out.push(name);
31
- }
32
- return out;
33
- }
34
-
35
- /**
36
- * Reads a .env file into a plain object. Never throws: a missing file simply
37
- * means nothing was defined there.
38
- *
39
- * Values are not expanded or unescaped beyond stripping one layer of quotes -
40
- * this only has to answer "is it set", and guessing at more would be a second
41
- * dialect of .env for people to trip over.
42
- */
43
- function readEnvFile(file) {
44
- const out = Object.create(null);
45
- let raw;
46
- try {
47
- raw = fs.readFileSync(file, 'utf8');
48
- } catch (e) {
49
- return out;
50
- }
51
- // A byte order mark would otherwise become part of the first name.
52
- if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
53
- for (const line of raw.split('\n')) {
54
- const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
55
- if (!match) continue;
56
- out[match[1]] = match[2].replace(/^(['"])([\s\S]*)\1$/, '$2');
57
- }
58
- return out;
59
- }
60
-
61
- /** A value that is present and not blank. An empty variable is not set. */
62
- function isSet(value) {
63
- return typeof value === 'string' && value.trim() !== '';
64
- }
65
-
66
- /**
67
- * Which of the names a recording needs are missing, looking at both the real
68
- * environment and the project's .env - the same two places the test itself
69
- * will look when it runs.
70
- */
71
- function missingSecrets(names, options) {
72
- const opts = options || {};
73
- const env = opts.env || process.env;
74
- const fromFile = opts.envFile ? readEnvFile(opts.envFile) : {};
75
- return (names || []).filter((name) => !isSet(env[name]) && !isSet(fromFile[name]));
76
- }
77
-
78
- /**
79
- * How to create the file without a byte order mark.
80
- *
81
- * PowerShell's own `Out-File -Encoding utf8` writes one, and so does Notepad,
82
- * which is why this spells out a command that does not rather than leaving it
83
- * to whatever the person reaches for first.
84
- */
85
- function howToWriteEnv(names, platform) {
86
- const line = names.map((name) => name + '=yourpassword').join('\\n');
87
- if ((platform || process.platform) === 'win32') {
88
- return [
89
- ' In PowerShell, this writes it without a byte order mark:',
90
- ' [IO.File]::WriteAllText("$PWD\\.env", "' + line + '`n")',
91
- '',
92
- ' Avoid Out-File and Notepad here: both add a mark that makes the first',
93
- ' line unreadable, and the password would silently come out blank.',
94
- ];
95
- }
96
- return [' For example:', " printf '" + line.replace(/\\n/g, "\\n") + "\\n' > .env"];
97
- }
98
-
99
- /** What to tell somebody who has not created the file yet. */
100
- function missingSecretLines(specName, names, options) {
101
- const opts = options || {};
102
- const many = names.length > 1;
103
- const lines = [
104
- ' Skipped ' + specName,
105
- ' This recording needs a password' + (many ? ' and other secrets' : '') + ', and there is none set.',
106
- '',
107
- ];
108
- if (opts.examplePath) {
109
- lines.push(' There is a ' + opts.examplePath + ' here already. Put the real value in it');
110
- lines.push(' and save it as .env, in the same folder.');
111
- } else {
112
- lines.push(' Create a .env file in this folder containing:');
113
- for (const name of names) lines.push(' ' + name + '=yourpassword');
114
- }
115
- lines.push('');
116
- for (const line of howToWriteEnv(names, opts.platform)) lines.push(line);
117
- lines.push('');
118
- lines.push(' Without it the recording would sign in with a blank password and');
119
- lines.push(' fail for a reason that has nothing to do with your app, so it was');
120
- lines.push(' not run. Keep .env out of version control.');
121
- return lines;
122
- }
123
-
124
- /**
125
- * Writes .env.example so nobody has to get the encoding right by hand.
126
- *
127
- * Plain UTF-8, no byte order mark. Never overwrites: the file may already hold
128
- * names from an earlier recording, and clobbering it would lose them.
129
- */
130
- function writeEnvExample(dir, names) {
131
- const file = path.join(dir, '.env.example');
132
- if (fs.existsSync(file)) return null;
133
- const body = (names || []).map((name) => name + '=yourpassword').join('\n') + '\n';
134
- try {
135
- fs.writeFileSync(file, body, { encoding: 'utf8' });
136
- return file;
137
- } catch (e) {
138
- return null; // a convenience, never a reason to fail a recording
139
- }
140
- }
141
-
142
- // Everything kryptheon leaves in a project that should not be committed: the
143
- // real password, and three files that are machine state rather than source.
144
- const GITIGNORE_ENTRIES = ['.env', 'kryptheon-baselines.json', 'kryptheon-history.jsonl', 'test-results/'];
145
-
146
- /**
147
- * Adds the missing entries to an existing .gitignore. Returns what it added.
148
- *
149
- * Only touches a file that is already there - creating one in a project that
150
- * has deliberately gone without is not this tool's decision to make.
151
- */
152
- function updateGitignore(dir, entries) {
153
- const file = path.join(dir, '.gitignore');
154
- let raw;
155
- try {
156
- raw = fs.readFileSync(file, { encoding: 'utf8' });
157
- } catch (e) {
158
- return [];
159
- }
160
- if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
161
- const present = new Set(
162
- raw
163
- .split('\n')
164
- .map((line) => line.trim().replace(/^\/+/, '').replace(/\/+$/, ''))
165
- .filter(Boolean),
166
- );
167
- const wanted = entries || GITIGNORE_ENTRIES;
168
- const missing = wanted.filter((entry) => !present.has(entry.replace(/\/+$/, '')));
169
- if (!missing.length) return [];
170
- const needsNewline = raw.length && !raw.endsWith('\n');
171
- const block = (needsNewline ? '\n' : '') + '\n# kryptheon\n' + missing.join('\n') + '\n';
172
- try {
173
- fs.appendFileSync(file, block, { encoding: 'utf8' });
174
- return missing;
175
- } catch (e) {
176
- return [];
177
- }
178
- }
179
-
180
- module.exports = {
181
- requiredSecrets: requiredSecrets,
182
- readEnvFile: readEnvFile,
183
- missingSecrets: missingSecrets,
184
- missingSecretLines: missingSecretLines,
185
- howToWriteEnv: howToWriteEnv,
186
- writeEnvExample: writeEnvExample,
187
- updateGitignore: updateGitignore,
188
- GITIGNORE_ENTRIES: GITIGNORE_ENTRIES,
189
- isSet: isSet,
190
- envFileFor: (dir) => path.join(dir, '.env'),
191
- };
1
+ // Which environment variables a recording needs, and whether they are there.
2
+ //
3
+ // Why this exists. When a password is typed during recording it is taken out
4
+ // of the spec and replaced with `process.env.KRYPTHEON_PASSWORD || ''`. If
5
+ // nobody ever creates the .env file, that expression quietly evaluates to an
6
+ // empty string: the run submits a blank password, the login fails for the most
7
+ // boring reason imaginable, and the report blames the app. Two hours can go
8
+ // into that before anyone thinks to check the environment.
9
+ //
10
+ // So the names are read out of the spec before anything runs, and a recording
11
+ // whose secret is missing is not run at all - a blank password is never sent.
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ // The exact shape the scrubber writes. Anything else in the file that happens
17
+ // to mention process.env is the author's own business and left alone.
18
+ const SECRET_USE = /process\.env\.([A-Z][A-Z0-9_]*)\s*\|\|\s*(['"])\2/g;
19
+
20
+ /** Every environment variable this recording falls back to an empty string for. */
21
+ function requiredSecrets(source) {
22
+ const out = [];
23
+ const seen = Object.create(null);
24
+ let match;
25
+ SECRET_USE.lastIndex = 0;
26
+ while ((match = SECRET_USE.exec(String(source || '')))) {
27
+ const name = match[1];
28
+ if (seen[name]) continue;
29
+ seen[name] = true;
30
+ out.push(name);
31
+ }
32
+ return out;
33
+ }
34
+
35
+ /**
36
+ * Reads a .env file into a plain object. Never throws: a missing file simply
37
+ * means nothing was defined there.
38
+ *
39
+ * Values are not expanded or unescaped beyond stripping one layer of quotes -
40
+ * this only has to answer "is it set", and guessing at more would be a second
41
+ * dialect of .env for people to trip over.
42
+ */
43
+ function readEnvFile(file) {
44
+ const out = Object.create(null);
45
+ let raw;
46
+ try {
47
+ raw = fs.readFileSync(file, 'utf8');
48
+ } catch (e) {
49
+ return out;
50
+ }
51
+ // A byte order mark would otherwise become part of the first name.
52
+ if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
53
+ for (const line of raw.split('\n')) {
54
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
55
+ if (!match) continue;
56
+ out[match[1]] = match[2].replace(/^(['"])([\s\S]*)\1$/, '$2');
57
+ }
58
+ return out;
59
+ }
60
+
61
+ /** A value that is present and not blank. An empty variable is not set. */
62
+ function isSet(value) {
63
+ return typeof value === 'string' && value.trim() !== '';
64
+ }
65
+
66
+ /**
67
+ * Which of the names a recording needs are missing, looking at both the real
68
+ * environment and the project's .env - the same two places the test itself
69
+ * will look when it runs.
70
+ */
71
+ function missingSecrets(names, options) {
72
+ const opts = options || {};
73
+ const env = opts.env || process.env;
74
+ const fromFile = opts.envFile ? readEnvFile(opts.envFile) : {};
75
+ return (names || []).filter((name) => !isSet(env[name]) && !isSet(fromFile[name]));
76
+ }
77
+
78
+ /**
79
+ * How to create the file without a byte order mark.
80
+ *
81
+ * PowerShell's own `Out-File -Encoding utf8` writes one, and so does Notepad,
82
+ * which is why this spells out a command that does not rather than leaving it
83
+ * to whatever the person reaches for first.
84
+ */
85
+ function howToWriteEnv(names, platform) {
86
+ const line = names.map((name) => name + '=yourpassword').join('\\n');
87
+ if ((platform || process.platform) === 'win32') {
88
+ return [
89
+ ' In PowerShell, this writes it without a byte order mark:',
90
+ ' [IO.File]::WriteAllText("$PWD\\.env", "' + line + '`n")',
91
+ '',
92
+ ' Avoid Out-File and Notepad here: both add a mark that makes the first',
93
+ ' line unreadable, and the password would silently come out blank.',
94
+ ];
95
+ }
96
+ return [' For example:', " printf '" + line.replace(/\\n/g, "\\n") + "\\n' > .env"];
97
+ }
98
+
99
+ /** What to tell somebody who has not created the file yet. */
100
+ function missingSecretLines(specName, names, options) {
101
+ const opts = options || {};
102
+ const many = names.length > 1;
103
+ const lines = [
104
+ ' Skipped ' + specName,
105
+ ' This recording needs a password' + (many ? ' and other secrets' : '') + ', and there is none set.',
106
+ '',
107
+ ];
108
+ if (opts.examplePath) {
109
+ lines.push(' There is a ' + opts.examplePath + ' here already. Put the real value in it');
110
+ lines.push(' and save it as .env, in the same folder.');
111
+ } else {
112
+ lines.push(' Create a .env file in this folder containing:');
113
+ for (const name of names) lines.push(' ' + name + '=yourpassword');
114
+ }
115
+ lines.push('');
116
+ for (const line of howToWriteEnv(names, opts.platform)) lines.push(line);
117
+ lines.push('');
118
+ lines.push(' Without it the recording would sign in with a blank password and');
119
+ lines.push(' fail for a reason that has nothing to do with your app, so it was');
120
+ lines.push(' not run. Keep .env out of version control.');
121
+ return lines;
122
+ }
123
+
124
+ /**
125
+ * Writes .env.example so nobody has to get the encoding right by hand.
126
+ *
127
+ * Plain UTF-8, no byte order mark. Never overwrites: the file may already hold
128
+ * names from an earlier recording, and clobbering it would lose them.
129
+ */
130
+ function writeEnvExample(dir, names) {
131
+ const file = path.join(dir, '.env.example');
132
+ if (fs.existsSync(file)) return null;
133
+ const body = (names || []).map((name) => name + '=yourpassword').join('\n') + '\n';
134
+ try {
135
+ fs.writeFileSync(file, body, { encoding: 'utf8' });
136
+ return file;
137
+ } catch (e) {
138
+ return null; // a convenience, never a reason to fail a recording
139
+ }
140
+ }
141
+
142
+ // Everything kryptheon leaves in a project that should not be committed: the
143
+ // real password, and three files that are machine state rather than source.
144
+ const GITIGNORE_ENTRIES = ['.env', 'kryptheon-baselines.json', 'kryptheon-history.jsonl', 'test-results/'];
145
+
146
+ /**
147
+ * Adds the missing entries to an existing .gitignore. Returns what it added.
148
+ *
149
+ * Only touches a file that is already there - creating one in a project that
150
+ * has deliberately gone without is not this tool's decision to make.
151
+ */
152
+ function updateGitignore(dir, entries) {
153
+ const file = path.join(dir, '.gitignore');
154
+ let raw;
155
+ try {
156
+ raw = fs.readFileSync(file, { encoding: 'utf8' });
157
+ } catch (e) {
158
+ return [];
159
+ }
160
+ if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
161
+ const present = new Set(
162
+ raw
163
+ .split('\n')
164
+ .map((line) => line.trim().replace(/^\/+/, '').replace(/\/+$/, ''))
165
+ .filter(Boolean),
166
+ );
167
+ const wanted = entries || GITIGNORE_ENTRIES;
168
+ const missing = wanted.filter((entry) => !present.has(entry.replace(/\/+$/, '')));
169
+ if (!missing.length) return [];
170
+ const needsNewline = raw.length && !raw.endsWith('\n');
171
+ const block = (needsNewline ? '\n' : '') + '\n# kryptheon\n' + missing.join('\n') + '\n';
172
+ try {
173
+ fs.appendFileSync(file, block, { encoding: 'utf8' });
174
+ return missing;
175
+ } catch (e) {
176
+ return [];
177
+ }
178
+ }
179
+
180
+ module.exports = {
181
+ requiredSecrets: requiredSecrets,
182
+ readEnvFile: readEnvFile,
183
+ missingSecrets: missingSecrets,
184
+ missingSecretLines: missingSecretLines,
185
+ howToWriteEnv: howToWriteEnv,
186
+ writeEnvExample: writeEnvExample,
187
+ updateGitignore: updateGitignore,
188
+ GITIGNORE_ENTRIES: GITIGNORE_ENTRIES,
189
+ isSet: isSet,
190
+ envFileFor: (dir) => path.join(dir, '.env'),
191
+ };
@@ -2,17 +2,17 @@
2
2
  // showing. Not a screenshot and not a pixel comparison - just the words a
3
3
  // person would use to say what is on screen.
4
4
  //
5
- // Why it exists. Automatic baselines currently remember the final address and
6
- // the page title. In a single-page app with a catch-all route those both stay
7
- // the same when a flow breaks: a login that no longer logs anyone in still
8
- // lands on the same URL with the same title, so the baseline sees no
9
- // difference and the run is reported as fine. The signature is the thing that
10
- // would have noticed - the headings, buttons, links and fields on the page
11
- // change even when the address does not.
5
+ // Why it exists. A baseline used to remember only the final address and the
6
+ // page title. In a single-page app with a catch-all route those both stay the
7
+ // same when a flow breaks: a login that no longer logs anyone in still lands on
8
+ // the same URL under the same title, so the baseline saw no difference and the
9
+ // run was reported as fine. The signature is the thing that notices - the
10
+ // headings, buttons, links and fields on a page change even when the address
11
+ // does not.
12
12
  //
13
- // STEP ONE ONLY. This module captures and prints. Nothing here is written to a
14
- // baseline, compared against anything, or allowed to affect whether a test
15
- // passes. Collection does not even run unless KRYPTHEON_DEBUG_SIGNATURE=1.
13
+ // A signature is part of the baseline: saved on a pass, compared on the next
14
+ // run, and a difference fails the test. KRYPTHEON_DEBUG_SIGNATURE=1 additionally
15
+ // prints what was captured, which is only ever a way to see the reasoning.
16
16
 
17
17
  const fs = require('fs');
18
18
 
@@ -495,7 +495,7 @@ function readRecorded(file) {
495
495
  }
496
496
 
497
497
  /**
498
- * Prints the signature. Step one does nothing else with it.
498
+ * Prints a signature, for when someone wants to see what was read.
499
499
  *
500
500
  * Goes to stderr so the reporter's own output, which is stdout, stays exactly
501
501
  * as it was - a person or a script reading the report sees no difference.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kryptheon",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Record what you do in your app and get plain-language reports when it breaks.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -38,21 +38,25 @@
38
38
  "@playwright/test": "^1.62.1"
39
39
  },
40
40
  "scripts": {
41
- "check": "node kryptheon-reporter.check.js && node kryptheon-signature.check.js && node kryptheon-signature.stream.check.js && node kryptheon-signature.baseline.check.js && node kryptheon-signature.settle.check.js && node kryptheon-signature.number.check.js && node kryptheon-step3.check.js && node kryptheon-watchdog.check.js && node kryptheon-record.check.js && node kryptheon-selectors.check.js && node kryptheon-recordings.check.js && node kryptheon-cleanup.check.js && node kryptheon-orphan.check.js && node kryptheon-project.check.js && node kryptheon-package.check.js",
41
+ "check": "node kryptheon-reporter.check.js && node kryptheon-signature.check.js && node kryptheon-signature.stream.check.js && node kryptheon-signature.baseline.check.js && node kryptheon-signature.settle.check.js && node kryptheon-signature.number.check.js && node kryptheon-env.check.js && node kryptheon-watchdog.check.js && node kryptheon-record.check.js && node kryptheon-selectors.check.js && node kryptheon-recordings.check.js && node kryptheon-cleanup.check.js && node kryptheon-orphan.check.js && node kryptheon-project.check.js && node kryptheon-setup.check.js && node kryptheon-load.check.js && node kryptheon-package.check.js",
42
+ "check:reporter": "node kryptheon-reporter.check.js",
43
+ "check:signature": "node kryptheon-signature.check.js",
42
44
  "check:stream": "node kryptheon-signature.stream.check.js",
43
45
  "check:baseline": "node kryptheon-signature.baseline.check.js",
44
46
  "check:settle": "node kryptheon-signature.settle.check.js",
45
- "check:step3": "node kryptheon-step3.check.js",
47
+ "check:number": "node kryptheon-signature.number.check.js",
48
+ "check:env": "node kryptheon-env.check.js",
46
49
  "check:watchdog": "node kryptheon-watchdog.check.js",
47
50
  "check:record": "node kryptheon-record.check.js",
48
- "check:package": "node kryptheon-package.check.js",
49
- "prepublishOnly": "node kryptheon-package.check.js",
50
51
  "check:selectors": "node kryptheon-selectors.check.js",
51
- "check:number": "node kryptheon-signature.number.check.js",
52
52
  "check:recordings": "node kryptheon-recordings.check.js",
53
53
  "check:cleanup": "node kryptheon-cleanup.check.js",
54
54
  "check:orphan": "node kryptheon-orphan.check.js",
55
- "check:project": "node kryptheon-project.check.js"
55
+ "check:project": "node kryptheon-project.check.js",
56
+ "check:setup": "node kryptheon-setup.check.js",
57
+ "check:load": "node kryptheon-load.check.js",
58
+ "check:package": "node kryptheon-package.check.js",
59
+ "prepublishOnly": "node kryptheon-package.check.js"
56
60
  },
57
61
  "keywords": [
58
62
  "playwright",