@odavl/guardian 0.1.0-rc1 → 0.2.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/CHANGELOG.md +62 -0
- package/README.md +3 -3
- package/bin/guardian.js +212 -8
- package/package.json +6 -1
- package/src/guardian/attempt-engine.js +19 -5
- package/src/guardian/attempt.js +61 -39
- package/src/guardian/attempts-filter.js +63 -0
- package/src/guardian/baseline.js +44 -10
- package/src/guardian/browser-pool.js +131 -0
- package/src/guardian/browser.js +28 -1
- package/src/guardian/ci-mode.js +15 -0
- package/src/guardian/ci-output.js +37 -0
- package/src/guardian/cli-summary.js +117 -4
- package/src/guardian/data-guardian-detector.js +189 -0
- package/src/guardian/detection-layers.js +271 -0
- package/src/guardian/first-run.js +49 -0
- package/src/guardian/flag-validator.js +97 -0
- package/src/guardian/flow-executor.js +309 -44
- package/src/guardian/language-detection.js +99 -0
- package/src/guardian/market-reporter.js +16 -1
- package/src/guardian/parallel-executor.js +116 -0
- package/src/guardian/prerequisite-checker.js +101 -0
- package/src/guardian/preset-loader.js +18 -12
- package/src/guardian/profile-loader.js +96 -0
- package/src/guardian/reality.js +382 -46
- package/src/guardian/run-summary.js +20 -0
- package/src/guardian/semantic-contact-detection.js +255 -0
- package/src/guardian/semantic-contact-finder.js +200 -0
- package/src/guardian/semantic-targets.js +234 -0
- package/src/guardian/smoke.js +258 -0
- package/src/guardian/snapshot.js +23 -1
- package/src/guardian/success-evaluator.js +214 -0
- package/src/guardian/timeout-profiles.js +57 -0
- package/src/guardian/wait-for-outcome.js +120 -0
- package/src/guardian/watch-runner.js +185 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { isCiMode } = require('./ci-mode');
|
|
4
|
+
const { formatRunSummary } = require('./run-summary');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_DEBOUNCE_MS = 400;
|
|
7
|
+
const DEFAULT_MAX_FILES = 5;
|
|
8
|
+
|
|
9
|
+
function isIgnored(filePath, artifactsDir = './artifacts') {
|
|
10
|
+
const normalized = path.normalize(filePath);
|
|
11
|
+
const parts = normalized.split(path.sep).filter(Boolean);
|
|
12
|
+
const ignorePrefixes = [
|
|
13
|
+
'node_modules',
|
|
14
|
+
'.git',
|
|
15
|
+
'.odavl-guardian',
|
|
16
|
+
'dist',
|
|
17
|
+
'build'
|
|
18
|
+
];
|
|
19
|
+
if (artifactsDir) {
|
|
20
|
+
ignorePrefixes.push(path.normalize(artifactsDir));
|
|
21
|
+
}
|
|
22
|
+
return parts.some(p => ignorePrefixes.includes(p)) || normalized.includes('market-run');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function collectWatchPaths(config) {
|
|
26
|
+
const roots = [
|
|
27
|
+
'guardian.config.json',
|
|
28
|
+
'guardian.policy.json',
|
|
29
|
+
'guardian.profile.docs.yaml',
|
|
30
|
+
'guardian.profile.ecommerce.yaml',
|
|
31
|
+
'guardian.profile.marketing.yaml',
|
|
32
|
+
'guardian.profile.saas.yaml',
|
|
33
|
+
'flows',
|
|
34
|
+
'policies',
|
|
35
|
+
'data',
|
|
36
|
+
'scripts',
|
|
37
|
+
'test'
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const paths = [];
|
|
42
|
+
roots.forEach(p => {
|
|
43
|
+
const full = path.resolve(p);
|
|
44
|
+
if (seen.has(full)) return;
|
|
45
|
+
if (fs.existsSync(full)) {
|
|
46
|
+
seen.add(full);
|
|
47
|
+
paths.push(full);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return paths;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatChangedFiles(changed) {
|
|
55
|
+
if (!changed.length) return 'unknown';
|
|
56
|
+
const unique = Array.from(new Set(changed));
|
|
57
|
+
const shown = unique.slice(0, DEFAULT_MAX_FILES).map(f => path.relative(process.cwd(), f));
|
|
58
|
+
const more = unique.length - shown.length;
|
|
59
|
+
if (more > 0) {
|
|
60
|
+
shown.push(`… ${more} more`);
|
|
61
|
+
}
|
|
62
|
+
return shown.join(', ');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function startWatchMode(config, deps = {}) {
|
|
66
|
+
const runReality = deps.runReality || require('./reality').executeReality;
|
|
67
|
+
const watchFactory = deps.watchFactory || fs.watch;
|
|
68
|
+
const debounceMs = deps.debounceMs || config.watchDebounceMs || DEFAULT_DEBOUNCE_MS;
|
|
69
|
+
const log = deps.log || console.log;
|
|
70
|
+
const warn = deps.warn || console.warn;
|
|
71
|
+
const exitFn = deps.exit || process.exit.bind(process);
|
|
72
|
+
const isCi = deps.isCi != null ? deps.isCi : isCiMode();
|
|
73
|
+
const onRunComplete = deps.onRunComplete || null;
|
|
74
|
+
const onWatcher = deps.onWatcher || null;
|
|
75
|
+
const bindSignal = deps.bindSignal || process.on.bind(process);
|
|
76
|
+
|
|
77
|
+
const quietConfig = {
|
|
78
|
+
...config,
|
|
79
|
+
quiet: true,
|
|
80
|
+
flowOptions: { ...(config.flowOptions || {}), quiet: true }
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (isCi) {
|
|
84
|
+
warn('CI detected; --watch ignored');
|
|
85
|
+
const once = await runReality({ ...config, watch: false });
|
|
86
|
+
return { watchStarted: false, exitCode: once.exitCode };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let running = false;
|
|
90
|
+
let pending = false;
|
|
91
|
+
let stopped = false;
|
|
92
|
+
let debounceTimer = null;
|
|
93
|
+
let changedFiles = [];
|
|
94
|
+
let lastExitCode = 0;
|
|
95
|
+
const watchers = [];
|
|
96
|
+
|
|
97
|
+
const watchPaths = collectWatchPaths(config);
|
|
98
|
+
|
|
99
|
+
const runOnce = async (label) => {
|
|
100
|
+
if (stopped) return { exitCode: lastExitCode };
|
|
101
|
+
running = true;
|
|
102
|
+
const result = await runReality(quietConfig);
|
|
103
|
+
lastExitCode = result.exitCode || 0;
|
|
104
|
+
log(formatRunSummary({
|
|
105
|
+
flowResults: result.flowResults || [],
|
|
106
|
+
diffResult: result.diffResult || null,
|
|
107
|
+
baselineCreated: result.baselineCreated || false,
|
|
108
|
+
exitCode: lastExitCode
|
|
109
|
+
}, { label: 'Summary' }));
|
|
110
|
+
running = false;
|
|
111
|
+
if (onRunComplete) {
|
|
112
|
+
onRunComplete(result);
|
|
113
|
+
}
|
|
114
|
+
if (pending && !stopped) {
|
|
115
|
+
pending = false;
|
|
116
|
+
scheduleRun();
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const scheduleRun = () => {
|
|
122
|
+
if (running) {
|
|
123
|
+
pending = true;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (debounceTimer) {
|
|
127
|
+
clearTimeout(debounceTimer);
|
|
128
|
+
}
|
|
129
|
+
debounceTimer = setTimeout(() => {
|
|
130
|
+
const filesLabel = formatChangedFiles(changedFiles);
|
|
131
|
+
changedFiles = [];
|
|
132
|
+
log(`Change detected: ${filesLabel}`);
|
|
133
|
+
runOnce('change');
|
|
134
|
+
}, debounceMs);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const handleFsEvent = (basePath) => (event, filename) => {
|
|
138
|
+
if (stopped) return;
|
|
139
|
+
const candidate = filename ? path.join(basePath, filename) : basePath;
|
|
140
|
+
if (isIgnored(candidate, config.artifactsDir)) return;
|
|
141
|
+
changedFiles.push(candidate);
|
|
142
|
+
scheduleRun();
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
for (const p of watchPaths) {
|
|
146
|
+
try {
|
|
147
|
+
const watcher = watchFactory(p, { recursive: true }, handleFsEvent(p));
|
|
148
|
+
watchers.push(watcher);
|
|
149
|
+
if (onWatcher) onWatcher(watcher);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
warn(`Watch attach failed for ${p}: ${err.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
log('WATCH MODE: ON');
|
|
156
|
+
runOnce('initial').catch(err => {
|
|
157
|
+
warn(`Watch run failed: ${err.message}`);
|
|
158
|
+
running = false;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const stop = () => {
|
|
162
|
+
stopped = true;
|
|
163
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
164
|
+
watchers.forEach(w => {
|
|
165
|
+
try { if (w && typeof w.close === 'function') w.close(); } catch {}
|
|
166
|
+
});
|
|
167
|
+
return lastExitCode;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
bindSignal('SIGINT', () => {
|
|
171
|
+
const code = stop();
|
|
172
|
+
exitFn(code);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
watchStarted: true,
|
|
177
|
+
stop,
|
|
178
|
+
getLastExitCode: () => lastExitCode,
|
|
179
|
+
simulateChange: (filePath) => handleFsEvent(path.dirname(filePath) || '.')("change", path.basename(filePath)),
|
|
180
|
+
isRunning: () => running,
|
|
181
|
+
hasPending: () => pending
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { startWatchMode, collectWatchPaths, isIgnored };
|