@resolveio/server-lib 22.3.108 → 22.3.110
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/http/auth.js +20 -1
- package/http/auth.js.map +1 -1
- package/package.json +1 -1
- package/services/codex-client.d.ts +1 -0
- package/services/codex-client.js +6 -0
- package/services/codex-client.js.map +1 -1
- package/util/ai-qa-policy.js +9 -1
- package/util/ai-qa-policy.js.map +1 -1
- package/util/ai-runner-qa-auth.js +52 -4
- package/util/ai-runner-qa-auth.js.map +1 -1
- package/util/ai-runner-qa-tools.js +283 -17
- package/util/ai-runner-qa-tools.js.map +1 -1
- package/util/support-runner-v5.js +30 -3
- package/util/support-runner-v5.js.map +1 -1
|
@@ -138,6 +138,10 @@ function buildResolveIORunnerQaEnvScript(options) {
|
|
|
138
138
|
'if [ "${RESOLVEIO_SUPPORT_CODEX_LANE:-}" = "build" ] || [ "${RESOLVEIO_SUPPORT_CODEX_LANE:-}" = "support_build" ] || [ "${RESOLVEIO_SUPPORT_CODEX_LANE:-}" = "support-build" ]; then',
|
|
139
139
|
' if [ "${1:-}" = "run" ]; then',
|
|
140
140
|
' case "${2:-}" in',
|
|
141
|
+
' build-dev)',
|
|
142
|
+
' echo "ResolveIO support lane guard: refusing npm run build-dev from the 5.3 build lane. Use a finite build-prod/tsc/unit self-gate or return a blocked self-gate." >&2',
|
|
143
|
+
' exit 86',
|
|
144
|
+
' ;;',
|
|
141
145
|
' server|client|start|dev|serve|local-qa|qa|qa:*|test:browser|test:e2e|e2e)',
|
|
142
146
|
' echo "ResolveIO support lane guard: refusing npm run ${2:-} from the 5.3 build lane. Browser/server/client QA belongs to the 5.4 mini QA lane." >&2',
|
|
143
147
|
' exit 86',
|
|
@@ -219,6 +223,63 @@ function buildResolveIORunnerQaEnvScript(options) {
|
|
|
219
223
|
' export PATH="$RESOLVEIO_QA_COMMAND_GUARD_BIN:$PATH"',
|
|
220
224
|
' hash -r 2>/dev/null || true',
|
|
221
225
|
'fi',
|
|
226
|
+
'export RESOLVEIO_QA_REAL_NODE="${RESOLVEIO_QA_REAL_NODE:-$(resolveio_qa_find_real_command node || true)}"',
|
|
227
|
+
'if [ -n "$RESOLVEIO_QA_REAL_NODE" ]; then',
|
|
228
|
+
' cat > "$RESOLVEIO_QA_COMMAND_GUARD_BIN/node" <<\'RESOLVEIO_QA_NODE_GUARD\'',
|
|
229
|
+
'#!/usr/bin/env bash',
|
|
230
|
+
'set -u',
|
|
231
|
+
'REAL_NODE="${RESOLVEIO_QA_REAL_NODE:-}"',
|
|
232
|
+
'GUARD_BIN="${RESOLVEIO_QA_COMMAND_GUARD_BIN:-}"',
|
|
233
|
+
'if [ -z "$REAL_NODE" ] || [ "$REAL_NODE" = "$0" ]; then',
|
|
234
|
+
' OLD_IFS="$IFS"',
|
|
235
|
+
' IFS=":"',
|
|
236
|
+
' for dir in $PATH; do',
|
|
237
|
+
' [ -n "$dir" ] || continue',
|
|
238
|
+
' [ -n "$GUARD_BIN" ] && [ "$dir" = "$GUARD_BIN" ] && continue',
|
|
239
|
+
' if [ -x "$dir/node" ]; then REAL_NODE="$dir/node"; break; fi',
|
|
240
|
+
' done',
|
|
241
|
+
' IFS="$OLD_IFS"',
|
|
242
|
+
'fi',
|
|
243
|
+
'if [ -z "$REAL_NODE" ] || [ ! -x "$REAL_NODE" ]; then',
|
|
244
|
+
' echo "ResolveIO QA command guard: unable to locate real node." >&2',
|
|
245
|
+
' exit 127',
|
|
246
|
+
'fi',
|
|
247
|
+
'resolveio_qa_node_guard_active() {',
|
|
248
|
+
' case "${RESOLVEIO_SUPPORT_CODEX_LANE:-}" in qa|support_qa|support-qa) return 0 ;; *) return 1 ;; esac',
|
|
249
|
+
'}',
|
|
250
|
+
'resolveio_qa_scan_inline_node_script() {',
|
|
251
|
+
' local script_file="$1"',
|
|
252
|
+
' [ -f "$script_file" ] || return 0',
|
|
253
|
+
" if grep -Eiq \"networkidle0|networkidle2|page\\.waitForTimeout|page\\.waitForEvent|page\\.\\$x|require\\([\\\"']puppeteer[\\\"']\\)|puppeteer browsers install|npx puppeteer|playwright install\" \"$script_file\"; then",
|
|
254
|
+
' echo "ResolveIO QA command guard: refusing fragile/forbidden browser QA script. Use the shared QA helpers, domcontentloaded plus concrete DOM assertions, puppeteer-core from staged NODE_PATH, and delay(ms) instead of networkidle*/waitForTimeout/page.\\$x/browser installs." >&2',
|
|
255
|
+
' exit 86',
|
|
256
|
+
' fi',
|
|
257
|
+
'}',
|
|
258
|
+
'if resolveio_qa_node_guard_active; then',
|
|
259
|
+
' case "${1:-}" in',
|
|
260
|
+
' -)',
|
|
261
|
+
' TMP_SCRIPT="${RESOLVEIO_QA_TMP:-/tmp/resolveio-support-qa}/inline-node-guard-$$.js"',
|
|
262
|
+
' mkdir -p "$(dirname "$TMP_SCRIPT")"',
|
|
263
|
+
' cat > "$TMP_SCRIPT"',
|
|
264
|
+
' resolveio_qa_scan_inline_node_script "$TMP_SCRIPT"',
|
|
265
|
+
' shift',
|
|
266
|
+
' exec "$REAL_NODE" "$TMP_SCRIPT" "$@"',
|
|
267
|
+
' ;;',
|
|
268
|
+
' -e|--eval)',
|
|
269
|
+
' TMP_SCRIPT="${RESOLVEIO_QA_TMP:-/tmp/resolveio-support-qa}/inline-node-guard-$$.js"',
|
|
270
|
+
' mkdir -p "$(dirname "$TMP_SCRIPT")"',
|
|
271
|
+
' printf "%s\\n" "${2:-}" > "$TMP_SCRIPT"',
|
|
272
|
+
' resolveio_qa_scan_inline_node_script "$TMP_SCRIPT"',
|
|
273
|
+
' exec "$REAL_NODE" "$@"',
|
|
274
|
+
' ;;',
|
|
275
|
+
' esac',
|
|
276
|
+
'fi',
|
|
277
|
+
'exec "$REAL_NODE" "$@"',
|
|
278
|
+
'RESOLVEIO_QA_NODE_GUARD',
|
|
279
|
+
' chmod +x "$RESOLVEIO_QA_COMMAND_GUARD_BIN/node"',
|
|
280
|
+
' export PATH="$RESOLVEIO_QA_COMMAND_GUARD_BIN:$PATH"',
|
|
281
|
+
' hash -r 2>/dev/null || true',
|
|
282
|
+
'fi',
|
|
222
283
|
toolsBinPath ? "export PATH=\"".concat(toolsBinPath, ":$PATH\"") : '',
|
|
223
284
|
'export NODE_ENV=development',
|
|
224
285
|
'# Local QA must start the application HTTP server even when launched from a dedicated support-manager worker.',
|
|
@@ -250,9 +311,10 @@ function buildResolveIORunnerQaEnvScript(options) {
|
|
|
250
311
|
"export ".concat(altKeepaliveVar, "=\"").concat('${' + altKeepaliveVar + ':-${' + keepaliveVar + '}}', "\""),
|
|
251
312
|
"export ".concat(mongoPortVar, "=\"").concat('${' + mongoPortVar + ':-${' + altMongoPortVar + ':-3001}}', "\""),
|
|
252
313
|
"export ".concat(altMongoPortVar, "=\"").concat('${' + altMongoPortVar + ':-${' + mongoPortVar + '}}', "\""),
|
|
253
|
-
|
|
254
|
-
"export ".concat(
|
|
255
|
-
"export
|
|
314
|
+
'export RESOLVEIO_QA_INHERITED_MONGO_URL="${RESOLVEIO_QA_INHERITED_MONGO_URL:-${MONGO_URL:-}}"',
|
|
315
|
+
"export ".concat(mongoUrlVar, "=\"mongodb://127.0.0.1:").concat('${' + mongoPortVar + '}', "/resolveio?directConnection=true\""),
|
|
316
|
+
"export ".concat(altMongoUrlVar, "=\"").concat('${' + mongoUrlVar + '}', "\""),
|
|
317
|
+
"export MONGO_URL=\"".concat('${' + mongoUrlVar + '}', "\""),
|
|
256
318
|
"export ".concat(usernameVar, "=\"").concat('${' + usernameVar + ':-${' + altUsernameVar + ':-' + username + '}}', "\""),
|
|
257
319
|
"export ".concat(altUsernameVar, "=\"").concat('${' + altUsernameVar + ':-${' + usernameVar + '}}', "\""),
|
|
258
320
|
"export ".concat(passwordVar, "=\"").concat('${' + passwordVar + ':-${' + altPasswordVar + ':-' + password + '}}', "\""),
|
|
@@ -273,18 +335,26 @@ function buildResolveIORunnerQaEnvScript(options) {
|
|
|
273
335
|
"export ".concat(envVar(mode, 'LIVE_DATA_REQUIRED'), "=\"").concat('${' + envVar(mode, 'LIVE_DATA_REQUIRED') + ':-${' + envVar(altMode, 'LIVE_DATA_REQUIRED') + ':-' + qaLiveDataRequired + '}}', "\""),
|
|
274
336
|
"export ".concat(envVar(altMode, 'LIVE_DATA_REQUIRED'), "=\"").concat('${' + envVar(altMode, 'LIVE_DATA_REQUIRED') + ':-${' + envVar(mode, 'LIVE_DATA_REQUIRED') + '}}', "\""),
|
|
275
337
|
"export RESOLVEIO_QA_LIVE_DATA_REQUIRED=\"".concat('${RESOLVEIO_QA_LIVE_DATA_REQUIRED:-$' + envVar(mode, 'LIVE_DATA_REQUIRED') + '}', "\""),
|
|
276
|
-
'export PUPPETEER_CACHE_DIR="
|
|
277
|
-
'
|
|
278
|
-
'
|
|
279
|
-
'
|
|
338
|
+
'export PUPPETEER_CACHE_DIR="/var/lib/resolveio/puppeteer"',
|
|
339
|
+
'RESOLVEIO_QA_NODE_PATH_ENTRIES=""',
|
|
340
|
+
'for RESOLVEIO_QA_NODE_MODULES in "$PWD/node_modules" "$PWD/server/node_modules" "$(cd "$PWD/.." 2>/dev/null && pwd)/node_modules" "$(cd "$PWD/.." 2>/dev/null && pwd)/server/node_modules" /var/app/current/node_modules; do',
|
|
341
|
+
' if [ -d "$RESOLVEIO_QA_NODE_MODULES" ]; then',
|
|
342
|
+
' if [ -z "$RESOLVEIO_QA_NODE_PATH_ENTRIES" ]; then RESOLVEIO_QA_NODE_PATH_ENTRIES="$RESOLVEIO_QA_NODE_MODULES"; else RESOLVEIO_QA_NODE_PATH_ENTRIES="$RESOLVEIO_QA_NODE_PATH_ENTRIES:$RESOLVEIO_QA_NODE_MODULES"; fi',
|
|
343
|
+
' fi',
|
|
344
|
+
'done',
|
|
345
|
+
'if [ -n "$RESOLVEIO_QA_NODE_PATH_ENTRIES" ]; then',
|
|
346
|
+
' export NODE_PATH="${NODE_PATH:+$NODE_PATH:}$RESOLVEIO_QA_NODE_PATH_ENTRIES"',
|
|
280
347
|
'fi',
|
|
348
|
+
'unset RESOLVEIO_QA_NODE_MODULES RESOLVEIO_QA_NODE_PATH_ENTRIES',
|
|
281
349
|
'export PLAYWRIGHT_BROWSERS_PATH="${PLAYWRIGHT_BROWSERS_PATH:-$RESOLVEIO_QA_HOME/.cache/ms-playwright}"',
|
|
350
|
+
'# QA rows should launch their own scoped browser unless the runner explicitly provides a live endpoint later.',
|
|
351
|
+
'unset RESOLVEIO_RUNNER_QA_BROWSER_URL RESOLVEIO_SUPPORT_QA_BROWSER_URL',
|
|
282
352
|
'if [ -n "${PUPPETEER_EXECUTABLE_PATH:-}" ] && [ -x "$PUPPETEER_EXECUTABLE_PATH" ]; then',
|
|
283
353
|
' export CHROME_BIN="${CHROME_BIN:-$PUPPETEER_EXECUTABLE_PATH}"',
|
|
284
354
|
'elif [ -n "${CHROME_BIN:-}" ] && [ -x "$CHROME_BIN" ]; then',
|
|
285
355
|
' export PUPPETEER_EXECUTABLE_PATH="$CHROME_BIN"',
|
|
286
356
|
'else',
|
|
287
|
-
" for ".concat(browserLoopVar, " in
|
|
357
|
+
" for ".concat(browserLoopVar, " in /var/lib/resolveio/puppeteer/chrome-headless-shell/linux-*/chrome-headless-shell-linux64/chrome-headless-shell /var/lib/resolveio/puppeteer/chrome/linux-*/chrome-linux64/chrome /usr/bin/google-chrome-stable /usr/bin/google-chrome /usr/bin/chromium /usr/bin/chromium-browser; do"),
|
|
288
358
|
" if [ -x \"$".concat(browserLoopVar, "\" ]; then"),
|
|
289
359
|
" export PUPPETEER_EXECUTABLE_PATH=\"$".concat(browserLoopVar, "\""),
|
|
290
360
|
" export CHROME_BIN=\"$".concat(browserLoopVar, "\""),
|
|
@@ -797,7 +867,7 @@ function buildResolveIORunnerLocalQaScript() {
|
|
|
797
867
|
' SERVER_REQUIRED=1',
|
|
798
868
|
' if [ "${RESOLVEIO_QA_PRESTART_MONGO_READY:-}" = "1" ] && [ -x "$PROJECT_ROOT/server/node_modules/gulp/bin/gulp.js" ] && [ -f "$PROJECT_ROOT/server/gulpfile.js" ] && grep -q "mongo-start" "$PROJECT_ROOT/server/gulpfile.js"; then',
|
|
799
869
|
' echo "ResolveIO AI runner QA using pre-seeded local Mongo; starting server without mongo-start." | tee -a "$ARTIFACT_DIR/runner.log"',
|
|
800
|
-
' (cd "$PROJECT_ROOT/server" && source "$TOOLS_DIR/env.sh" && if [ -f "$HOME/.nvm/nvm.sh" ]; then source "$HOME/.nvm/nvm.sh"; nvm use 22 >/dev/null 2>&1 || true; fi; node_modules/gulp/bin/gulp.js 2>&1 | tee "$ARTIFACT_DIR/server.log") &',
|
|
870
|
+
' (cd "$PROJECT_ROOT/server" && source "$TOOLS_DIR/env.sh" && if [ -f "$HOME/.nvm/nvm.sh" ]; then source "$HOME/.nvm/nvm.sh"; nvm use 22 >/dev/null 2>&1 || true; fi; { node_modules/gulp/bin/gulp.js compile dotenv methodAndPublicationListGenerator && echo "ResolveIO AI runner QA starting node tmp/index.js" && node --inspect=127.0.0.1:"$INSPECT_PORT" --max_old_space_size=3000 tmp/index.js; } 2>&1 | tee "$ARTIFACT_DIR/server.log") &',
|
|
801
871
|
' elif node -e "const p=require(process.argv[1]); process.exit(p.scripts&&p.scripts.server?0:1)" "$PROJECT_ROOT/server/package.json" >/dev/null 2>&1; then',
|
|
802
872
|
' (cd "$PROJECT_ROOT/server" && source "$TOOLS_DIR/env.sh" && npm run server 2>&1 | tee "$ARTIFACT_DIR/server.log") &',
|
|
803
873
|
' elif [ -x "$PROJECT_ROOT/server/start_server.sh" ]; then',
|
|
@@ -814,7 +884,8 @@ function buildResolveIORunnerLocalQaScript() {
|
|
|
814
884
|
'CLIENT_HOST="${RESOLVEIO_RUNNER_QA_CLIENT_HOST:-${RESOLVEIO_SUPPORT_QA_CLIENT_HOST:-0.0.0.0}}"',
|
|
815
885
|
'if [ -x "$PROJECT_ROOT/node_modules/.bin/ng" ]; then',
|
|
816
886
|
' prepare_angular_cache_dirs',
|
|
817
|
-
'
|
|
887
|
+
' ANGULAR_OLD_SPACE="${RESOLVEIO_RUNNER_QA_ANGULAR_OLD_SPACE_MB:-${RESOLVEIO_SUPPORT_QA_ANGULAR_OLD_SPACE_MB:-3072}}"',
|
|
888
|
+
' (cd "$PROJECT_ROOT" && source "$TOOLS_DIR/env.sh" && node --max_old_space_size="$ANGULAR_OLD_SPACE" ./node_modules/.bin/ng serve --watch --configuration local --host "$CLIENT_HOST" --port "$CLIENT_PORT" "${ANGULAR_PREBUNDLE_ARGS[@]}" 2>&1 | tee "$ARTIFACT_DIR/client.log") &',
|
|
818
889
|
'elif [ -x "$PROJECT_ROOT/start_client.sh" ]; then',
|
|
819
890
|
' (cd "$PROJECT_ROOT" && source "$TOOLS_DIR/env.sh" && ./start_client.sh 2>&1 | tee "$ARTIFACT_DIR/client.log") &',
|
|
820
891
|
'elif node -e "const p=require(process.argv[1]); process.exit(p.scripts&&p.scripts.client?0:1)" "$PROJECT_ROOT/package.json" >/dev/null 2>&1; then',
|
|
@@ -1014,6 +1085,7 @@ function buildResolveIORunnerQaLiveDataSeederScript() {
|
|
|
1014
1085
|
'#!/usr/bin/env node',
|
|
1015
1086
|
'const fs = require("fs");',
|
|
1016
1087
|
'const path = require("path");',
|
|
1088
|
+
'const crypto = require("crypto");',
|
|
1017
1089
|
'',
|
|
1018
1090
|
'if (isBuildLane()) {',
|
|
1019
1091
|
' console.error("ResolveIO support lane guard: live-data seeding is owned by the QA lane, not the 5.3 build lane.");',
|
|
@@ -1217,16 +1289,153 @@ function buildResolveIORunnerQaLiveDataSeederScript() {
|
|
|
1217
1289
|
'',
|
|
1218
1290
|
'function unique(values) { return Array.from(new Set((values || []).filter(Boolean).map(String))); }',
|
|
1219
1291
|
'',
|
|
1292
|
+
'function collectEmailLikeValues(value, out = new Set(), depth = 0) {',
|
|
1293
|
+
' if (depth > 6 || value == null) return out;',
|
|
1294
|
+
' if (typeof value === "string") {',
|
|
1295
|
+
' const matches = value.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/gi) || [];',
|
|
1296
|
+
' matches.forEach((email) => out.add(String(email || "").trim().toLowerCase()));',
|
|
1297
|
+
' return out;',
|
|
1298
|
+
' }',
|
|
1299
|
+
' if (Array.isArray(value)) { value.forEach((entry) => collectEmailLikeValues(entry, out, depth + 1)); return out; }',
|
|
1300
|
+
' if (typeof value === "object") { Object.values(value).forEach((entry) => collectEmailLikeValues(entry, out, depth + 1)); }',
|
|
1301
|
+
' return out;',
|
|
1302
|
+
'}',
|
|
1303
|
+
'',
|
|
1304
|
+
'function readContextJsonFiles() {',
|
|
1305
|
+
' const files = [',
|
|
1306
|
+
' path.join(artifactDir, "manual-ticket.metadata.json"),',
|
|
1307
|
+
' path.join(artifactDir, "email.metadata.json"),',
|
|
1308
|
+
' path.join(repoRoot, ".resolveio-support-context", "manual-ticket.metadata.json"),',
|
|
1309
|
+
' path.join(projectRoot, ".resolveio-support-context", "manual-ticket.metadata.json"),',
|
|
1310
|
+
' path.join(repoRoot, ".resolveio-support-context", "email.metadata.json"),',
|
|
1311
|
+
' path.join(projectRoot, ".resolveio-support-context", "email.metadata.json"),',
|
|
1312
|
+
' path.join(repoRoot, ".resolveio-context", "manual-ticket.metadata.json"),',
|
|
1313
|
+
' path.join(projectRoot, ".resolveio-context", "manual-ticket.metadata.json"),',
|
|
1314
|
+
' path.join(repoRoot, ".resolveio-context", "email.metadata.json"),',
|
|
1315
|
+
' path.join(projectRoot, ".resolveio-context", "email.metadata.json")',
|
|
1316
|
+
' ];',
|
|
1317
|
+
' return files.map((file) => readJsonIfExists(file)).filter(Boolean);',
|
|
1318
|
+
'}',
|
|
1319
|
+
'',
|
|
1320
|
+
'function extractQaUserHints() {',
|
|
1321
|
+
' const affected = unique([',
|
|
1322
|
+
' process.env.RESOLVEIO_QA_AFFECTED_USER_EMAIL,',
|
|
1323
|
+
' process.env.RESOLVEIO_SUPPORT_QA_AFFECTED_USER_EMAIL,',
|
|
1324
|
+
' process.env.RESOLVEIO_RUNNER_QA_AFFECTED_USER_EMAIL',
|
|
1325
|
+
' ].map((value) => String(value || "").trim().toLowerCase()).filter(Boolean));',
|
|
1326
|
+
' const reporter = unique([',
|
|
1327
|
+
' process.env.RESOLVEIO_QA_TICKET_REPORTER_EMAIL,',
|
|
1328
|
+
' process.env.RESOLVEIO_SUPPORT_QA_TICKET_REPORTER_EMAIL,',
|
|
1329
|
+
' process.env.RESOLVEIO_RUNNER_QA_TICKET_REPORTER_EMAIL',
|
|
1330
|
+
' ].map((value) => String(value || "").trim().toLowerCase()).filter(Boolean));',
|
|
1331
|
+
' const explicitQa = unique([',
|
|
1332
|
+
' process.env.RESOLVEIO_QA_USERNAME,',
|
|
1333
|
+
' process.env.RESOLVEIO_SUPPORT_QA_USERNAME,',
|
|
1334
|
+
' process.env.RESOLVEIO_RUNNER_QA_USERNAME',
|
|
1335
|
+
' ].map((value) => String(value || "").trim().toLowerCase()).filter((value) => value && !/^(admin|dev@resolveio\\.com)$/i.test(value)));',
|
|
1336
|
+
' const contextEmails = new Set();',
|
|
1337
|
+
' readContextJsonFiles().forEach((json) => collectEmailLikeValues(json, contextEmails));',
|
|
1338
|
+
' collectEmailLikeValues(readSeedHintText(), contextEmails);',
|
|
1339
|
+
' const candidates = unique([...affected, ...reporter, ...explicitQa, ...Array.from(contextEmails)]);',
|
|
1340
|
+
' return { affected, reporter, explicitQa, candidates: candidates.slice(0, 12) };',
|
|
1341
|
+
'}',
|
|
1342
|
+
'',
|
|
1343
|
+
'function qaUserLookupQuery(candidates) {',
|
|
1344
|
+
' const ors = [];',
|
|
1345
|
+
' for (const candidate of candidates || []) {',
|
|
1346
|
+
' const value = String(candidate || "").trim();',
|
|
1347
|
+
' if (!value) continue;',
|
|
1348
|
+
' ors.push({ username: value }, { email: value }, { "emails.address": value });',
|
|
1349
|
+
' }',
|
|
1350
|
+
' return ors.length ? { $or: ors } : null;',
|
|
1351
|
+
'}',
|
|
1352
|
+
'',
|
|
1353
|
+
'function normalizeSeededQaUser(doc, password) {',
|
|
1354
|
+
' const now = new Date();',
|
|
1355
|
+
' const next = { ...(doc || {}) };',
|
|
1356
|
+
' const other = next.other && typeof next.other === "object" ? next.other : {};',
|
|
1357
|
+
' const settings = next.settings && typeof next.settings === "object" ? next.settings : {};',
|
|
1358
|
+
' next.active = true;',
|
|
1359
|
+
' next.readonly = false;',
|
|
1360
|
+
' next.other = { ...other, tour_completed: true, took_tour: true, core_tour_completed: true, welcome_tour_completed: true, top_navigation_tour_completed: true, user_settings_tour_completed: true };',
|
|
1361
|
+
' next.settings = { ...settings, collapsable_menu: false };',
|
|
1362
|
+
' next.updatedAt = now;',
|
|
1363
|
+
' if (password) {',
|
|
1364
|
+
' const salt = crypto.randomBytes(32).toString("hex");',
|
|
1365
|
+
' next.salt = salt;',
|
|
1366
|
+
' next.hash = crypto.pbkdf2Sync(password, salt, 25000, 512, "sha256").toString("hex");',
|
|
1367
|
+
' if (next.services && typeof next.services === "object" && next.services.password) delete next.services.password;',
|
|
1368
|
+
' next.attempts = 0;',
|
|
1369
|
+
' }',
|
|
1370
|
+
' return next;',
|
|
1371
|
+
'}',
|
|
1372
|
+
'',
|
|
1373
|
+
'async function copyQaIdentityUsers(sourceDb, targetDb, summary) {',
|
|
1374
|
+
' const hints = extractQaUserHints();',
|
|
1375
|
+
' summary.selected.qa_user_hints = hints;',
|
|
1376
|
+
' const query = qaUserLookupQuery(hints.candidates);',
|
|
1377
|
+
' if (!query) return [];',
|
|
1378
|
+
' const users = await sourceDb.collection("users").find(query).limit(8).toArray();',
|
|
1379
|
+
' if (!users.length) {',
|
|
1380
|
+
' summary.notes.push(`No live user matched QA identity hints: ${hints.candidates.join(", ")}`);',
|
|
1381
|
+
' return [];',
|
|
1382
|
+
' }',
|
|
1383
|
+
' const password = process.env.RESOLVEIO_RUNNER_QA_PASSWORD || process.env.RESOLVEIO_SUPPORT_QA_PASSWORD || "";',
|
|
1384
|
+
' for (const user of users) {',
|
|
1385
|
+
' const normalized = normalizeSeededQaUser(user, password);',
|
|
1386
|
+
' await targetDb.collection("users").replaceOne({ _id: normalized._id }, normalized, { upsert: true });',
|
|
1387
|
+
' }',
|
|
1388
|
+
' summary.collections.users = (summary.collections.users || 0) + users.length;',
|
|
1389
|
+
' const preferred = users.find((user) => hints.affected.includes(String(user.email || user.username || "").toLowerCase()))',
|
|
1390
|
+
' || users.find((user) => hints.reporter.includes(String(user.email || user.username || "").toLowerCase()))',
|
|
1391
|
+
' || users[0];',
|
|
1392
|
+
' summary.selected.qa_user_context = {',
|
|
1393
|
+
' preferred_username: String(preferred && (preferred.email || preferred.username) || "").trim(),',
|
|
1394
|
+
' preferred_email: String(preferred && preferred.email || "").trim(),',
|
|
1395
|
+
' preferred_user_id: String(preferred && preferred._id || "").trim(),',
|
|
1396
|
+
' affected_user_email: hints.affected[0] || "",',
|
|
1397
|
+
' reporter_email: hints.reporter[0] || "",',
|
|
1398
|
+
' copied_user_ids: users.map((user) => String(user && user._id || "")).filter(Boolean),',
|
|
1399
|
+
' reason: hints.affected.length ? "affected_user" : (hints.reporter.length ? "ticket_reporter" : "context_email")',
|
|
1400
|
+
' };',
|
|
1401
|
+
' summary.notes.push(`Seeded ${users.length} live QA identity user(s); preferred browser QA user is ${summary.selected.qa_user_context.preferred_username || summary.selected.qa_user_context.preferred_user_id}.`);',
|
|
1402
|
+
' return users;',
|
|
1403
|
+
'}',
|
|
1404
|
+
'',
|
|
1220
1405
|
'function readSeedHintText() {',
|
|
1221
|
-
' const chunks = [
|
|
1406
|
+
' const chunks = [',
|
|
1407
|
+
' process.env.RESOLVEIO_SUPPORT_QA_ROW_FILTER || "",',
|
|
1408
|
+
' process.env.RESOLVEIO_SUPPORT_QA_CURRENT_ROW_WORKFLOW || "",',
|
|
1409
|
+
' process.env.RESOLVEIO_SUPPORT_QA_CURRENT_ROW_ASSERTION || "",',
|
|
1410
|
+
' process.env.RESOLVEIO_RUNNER_QA_ROW_FILTER || "",',
|
|
1411
|
+
' process.env.RESOLVEIO_RUNNER_QA_CURRENT_ROW_WORKFLOW || "",',
|
|
1412
|
+
' process.env.RESOLVEIO_RUNNER_QA_CURRENT_ROW_ASSERTION || "",',
|
|
1413
|
+
' process.env.RESOLVEIO_QA_SEED_HINTS || "",',
|
|
1414
|
+
' process.env.RESOLVEIO_SUPPORT_QA_SEED_HINTS || "",',
|
|
1415
|
+
' process.env.RESOLVEIO_RUNNER_QA_SEED_HINTS || ""',
|
|
1416
|
+
' ];',
|
|
1222
1417
|
' const candidates = [',
|
|
1418
|
+
' path.join(artifactDir, "qa-coverage-matrix.json"),',
|
|
1419
|
+
' path.join(artifactDir, "qa-row-lock.json"),',
|
|
1223
1420
|
' path.join(repoRoot, ".resolveio-support-context", "manual-ticket.request.txt"),',
|
|
1224
1421
|
' path.join(projectRoot, ".resolveio-support-context", "manual-ticket.request.txt"),',
|
|
1225
1422
|
' path.join(repoRoot, ".resolveio-context", "manual-ticket.request.txt"),',
|
|
1226
1423
|
' path.join(projectRoot, ".resolveio-context", "manual-ticket.request.txt")',
|
|
1227
1424
|
' ];',
|
|
1228
1425
|
' for (const candidate of candidates) {',
|
|
1229
|
-
' try {
|
|
1426
|
+
' try {',
|
|
1427
|
+
' const raw = fs.readFileSync(candidate, "utf8");',
|
|
1428
|
+
' chunks.push(raw);',
|
|
1429
|
+
' if (/qa-coverage-matrix\\.json$/.test(candidate)) {',
|
|
1430
|
+
' const matrix = JSON.parse(raw);',
|
|
1431
|
+
' const rows = Array.isArray(matrix && matrix.rows) ? matrix.rows : [];',
|
|
1432
|
+
' for (const row of rows) chunks.push([row.workflow, row.route, row.assertion, row.required_proof, row.data_id, row.data_name, row.blocker, row.evidence, row.caption].filter(Boolean).join("\\n"));',
|
|
1433
|
+
' }',
|
|
1434
|
+
' if (/qa-row-lock\\.json$/.test(candidate)) {',
|
|
1435
|
+
' const lock = JSON.parse(raw);',
|
|
1436
|
+
' chunks.push([lock.workflow, lock.route, lock.assertion, lock.required_proof, lock.rowFilter, lock.blocker].filter(Boolean).join("\\n"));',
|
|
1437
|
+
' }',
|
|
1438
|
+
' } catch (error) {}',
|
|
1230
1439
|
' }',
|
|
1231
1440
|
' return chunks.filter(Boolean).join("\\n");',
|
|
1232
1441
|
'}',
|
|
@@ -1275,6 +1484,11 @@ function buildResolveIORunnerQaLiveDataSeederScript() {
|
|
|
1275
1484
|
' const value = String(match[1] || "").trim();',
|
|
1276
1485
|
' if (isUsefulSeedIdentifier(value)) identifiers.add(value);',
|
|
1277
1486
|
' }',
|
|
1487
|
+
' const typedUnit = /\\b(?:truck|trailer|unit|asset)\\s+([A-Z0-9][A-Z0-9._-]{1,})\\b/gi;',
|
|
1488
|
+
' while ((match = typedUnit.exec(text)) !== null) {',
|
|
1489
|
+
' const value = String(match[1] || "").trim();',
|
|
1490
|
+
' if (isUsefulSeedIdentifier(value)) identifiers.add(value);',
|
|
1491
|
+
' }',
|
|
1278
1492
|
' if (shouldAutoDiscoverAssetContext()) extractSeedIdentifiers().forEach((value) => identifiers.add(value));',
|
|
1279
1493
|
' return Array.from(identifiers).slice(0, 20);',
|
|
1280
1494
|
'}',
|
|
@@ -1297,7 +1511,9 @@ function buildResolveIORunnerQaLiveDataSeederScript() {
|
|
|
1297
1511
|
' const explicit = /\\b(?:invoice|inv|bol|order|pso|ticket|wo|work\\s*order|delivery|treatment)\\s*(?:#|no\\.?|number|num)?\\s*[:#-]?\\s*([A-Z0-9][A-Z0-9._-]{2,})\\b/gi;',
|
|
1298
1512
|
' let match;',
|
|
1299
1513
|
' while ((match = explicit.exec(text)) !== null) identifiers.add(match[1]);',
|
|
1300
|
-
' const
|
|
1514
|
+
' const typedUnit = /\\b(?:truck|trailer|asset|unit)\\s+([A-Z0-9][A-Z0-9._-]{1,})\\b/gi;',
|
|
1515
|
+
' while ((match = typedUnit.exec(text)) !== null) identifiers.add(match[1]);',
|
|
1516
|
+
' const numeric = /\\b\\d{3,}\\b/g;',
|
|
1301
1517
|
' while ((match = numeric.exec(text)) !== null) identifiers.add(match[0]);',
|
|
1302
1518
|
' return Array.from(identifiers)',
|
|
1303
1519
|
' .map((value) => String(value || "").trim())',
|
|
@@ -1772,6 +1988,7 @@ function buildResolveIORunnerQaLiveDataSeederScript() {
|
|
|
1772
1988
|
' const summary = { profile: includeBillingFixtures ? "billing_inventory" : "live_context", collections: {}, selected: {}, notes: [] };',
|
|
1773
1989
|
' const identifiers = extractSeedIdentifiers();',
|
|
1774
1990
|
' summary.selected.seed_identifiers = identifiers;',
|
|
1991
|
+
' const qaIdentityUsers = await copyQaIdentityUsers(sourceDb, targetDb, summary);',
|
|
1775
1992
|
' const truckTreatingBolContextDocs = await copyTruckTreatingBolContext(sourceDb, targetDb, summary);',
|
|
1776
1993
|
' const ticketAssetContextDocs = await copyTicketAssetContext(sourceDb, targetDb, summary);',
|
|
1777
1994
|
' const productionInterchangeablesContextDocs = await copyProductionInterchangeablesContext(sourceDb, targetDb, summary);',
|
|
@@ -1813,7 +2030,7 @@ function buildResolveIORunnerQaLiveDataSeederScript() {
|
|
|
1813
2030
|
' summary.notes.push("No live billable delivery or truck-treatment records matched the Billing Dashboard awaiting-invoice filters.");',
|
|
1814
2031
|
' }',
|
|
1815
2032
|
'',
|
|
1816
|
-
' const sourceDocs = [...truckTreatingBolContextDocs, ...ticketAssetContextDocs, ...productionInterchangeablesContextDocs, ...productionDeliveries, ...truckTreatingDeliveries];',
|
|
2033
|
+
' const sourceDocs = [...qaIdentityUsers, ...truckTreatingBolContextDocs, ...ticketAssetContextDocs, ...productionInterchangeablesContextDocs, ...productionDeliveries, ...truckTreatingDeliveries];',
|
|
1817
2034
|
' const customerIds = idValues(sourceDocs, ["id_customer"]);',
|
|
1818
2035
|
' const locationIds = idValues(sourceDocs, ["id_location"]);',
|
|
1819
2036
|
' const wellGroupIds = idValues(sourceDocs, ["id_well_group"]);',
|
|
@@ -2234,9 +2451,52 @@ function buildResolveIORunnerQaWorkflowProbeScript() {
|
|
|
2234
2451
|
'async function pageSummary(page) {',
|
|
2235
2452
|
' return page.evaluate(() => {',
|
|
2236
2453
|
' const bodyText = (document.body && document.body.innerText || "").replace(/\\s+/g, " ").trim();',
|
|
2237
|
-
'
|
|
2454
|
+
' const rows = Array.from(document.querySelectorAll("tr")).map(row => (row.innerText || "").replace(/\\s+/g, " ").trim()).filter(Boolean);',
|
|
2455
|
+
' return { url: location.href, title: document.title, bodyTextSnippet: bodyText.slice(0, 1200), rowCount: rows.length, rows: rows.slice(0, 8), hasLoginText: /Employee\\/Customer Login|Employee Sign In|Customer Access|Unable to sign in/i.test(bodyText), hasOfflineModeText: bodyText.includes("*** OFFLINE MODE ***"), storageState: { hasRefreshToken: !!localStorage.getItem("refreshToken"), hasAccessToken: !!localStorage.getItem("accessToken"), hasUser: !!localStorage.getItem("user") } };',
|
|
2238
2456
|
' });',
|
|
2239
2457
|
'}',
|
|
2458
|
+
'function pathMatchesRoute(currentUrl, expectedRoute) {',
|
|
2459
|
+
' try {',
|
|
2460
|
+
' const current = new URL(currentUrl);',
|
|
2461
|
+
' const expected = new URL(expectedRoute, clientUrl);',
|
|
2462
|
+
' return current.pathname.replace(/\\/+$/, "") === expected.pathname.replace(/\\/+$/, "");',
|
|
2463
|
+
' } catch (error) {',
|
|
2464
|
+
' return false;',
|
|
2465
|
+
' }',
|
|
2466
|
+
'}',
|
|
2467
|
+
'async function waitForAuthenticatedApp(page) {',
|
|
2468
|
+
' await page.goto(`${clientUrl}/home`, { waitUntil: "domcontentloaded", timeout: 60000 });',
|
|
2469
|
+
' const deadline = Date.now() + Number(process.env.RESOLVEIO_RUNNER_QA_AUTH_WARMUP_TIMEOUT_MS || process.env.RESOLVEIO_SUPPORT_QA_AUTH_WARMUP_TIMEOUT_MS || 45000);',
|
|
2470
|
+
' let summary = await pageSummary(page);',
|
|
2471
|
+
' while (Date.now() < deadline) {',
|
|
2472
|
+
' summary = await pageSummary(page);',
|
|
2473
|
+
' if (!summary.hasLoginText && !summary.hasOfflineModeText && summary.storageState.hasRefreshToken && summary.storageState.hasAccessToken && summary.storageState.hasUser) {',
|
|
2474
|
+
' await delay(Number(process.env.RESOLVEIO_RUNNER_QA_POST_AUTH_SETTLE_MS || process.env.RESOLVEIO_SUPPORT_QA_POST_AUTH_SETTLE_MS || 2500));',
|
|
2475
|
+
' return await pageSummary(page);',
|
|
2476
|
+
' }',
|
|
2477
|
+
' await delay(1000);',
|
|
2478
|
+
' }',
|
|
2479
|
+
' throw new Error(`QA auth warmup failed before target route. Page summary: ${JSON.stringify(summary).slice(0, 1000)}`);',
|
|
2480
|
+
'}',
|
|
2481
|
+
'async function waitForHydratedTargetRoute(page) {',
|
|
2482
|
+
' const deadline = Date.now() + Number(process.env.RESOLVEIO_RUNNER_QA_ROUTE_HYDRATION_TIMEOUT_MS || process.env.RESOLVEIO_SUPPORT_QA_ROUTE_HYDRATION_TIMEOUT_MS || 45000);',
|
|
2483
|
+
' let summary = await pageSummary(page);',
|
|
2484
|
+
' while (Date.now() < deadline) {',
|
|
2485
|
+
' summary = await pageSummary(page);',
|
|
2486
|
+
' const routeOk = pathMatchesRoute(summary.url, targetRoute);',
|
|
2487
|
+
' const shellOnly = !summary.bodyTextSnippet || /^(Home|MENU|Logout|\\u00A9|The All-in-One Software)\\b/i.test(summary.bodyTextSnippet);',
|
|
2488
|
+
' if (routeOk && !summary.hasLoginText && !summary.hasOfflineModeText && !shellOnly) {',
|
|
2489
|
+
' return summary;',
|
|
2490
|
+
' }',
|
|
2491
|
+
' if (summary.hasLoginText || summary.hasOfflineModeText || !routeOk || shellOnly) {',
|
|
2492
|
+
' await delay(1500);',
|
|
2493
|
+
' await page.goto(new URL(targetRoute, clientUrl).href, { waitUntil: "domcontentloaded", timeout: 60000 }).catch(() => null);',
|
|
2494
|
+
' } else {',
|
|
2495
|
+
' await delay(1500);',
|
|
2496
|
+
' }',
|
|
2497
|
+
' }',
|
|
2498
|
+
' throw new Error(`QA route hydration failed for ${targetRoute}. Page summary: ${JSON.stringify(summary).slice(0, 1000)}`);',
|
|
2499
|
+
'}',
|
|
2240
2500
|
'function billingDashboardHasVisibleWork(text) {',
|
|
2241
2501
|
' const value = String(text || "");',
|
|
2242
2502
|
' const patterns = [',
|
|
@@ -2292,10 +2552,10 @@ function buildResolveIORunnerQaWorkflowProbeScript() {
|
|
|
2292
2552
|
' await page.setViewport({ width: viewportWidth, height: viewportHeight });',
|
|
2293
2553
|
' const auth = await login();',
|
|
2294
2554
|
' await seedAuth(page, auth);',
|
|
2555
|
+
' await waitForAuthenticatedApp(page);',
|
|
2295
2556
|
' await page.goto(`${clientUrl}${targetRoute}`, { waitUntil: "domcontentloaded", timeout: 60000 });',
|
|
2296
2557
|
' await page.waitForSelector("body", { timeout: 30000 });',
|
|
2297
|
-
' await
|
|
2298
|
-
' let summary = await pageSummary(page);',
|
|
2558
|
+
' let summary = await waitForHydratedTargetRoute(page);',
|
|
2299
2559
|
' if (summary.hasLoginText || summary.hasOfflineModeText || !summary.bodyTextSnippet) throw new Error(`Workflow route did not reach authenticated app: ${JSON.stringify(summary).slice(0, 1000)}`);',
|
|
2300
2560
|
' summary = await waitForBillingDashboardWork(page) || summary;',
|
|
2301
2561
|
' const caption = `Workflow route ready: ${targetRoute} loaded in authenticated local QA with live seeded data available.`;',
|
|
@@ -2353,8 +2613,14 @@ function buildResolveIORunnerQaToolsReadme(options) {
|
|
|
2353
2613
|
'Do not use version-fragile browser helpers such as `page.$x` or `page.waitForTimeout`; use DOM traversal/locator-compatible helpers and a local `delay(ms)` promise helper, then rerun the same matrix row if the QA script itself fails.',
|
|
2354
2614
|
'Do not run `npm run build-dev`, `ng build`, or another Angular compile while keepalive `ng serve` is running. If a full Angular build is required after browser QA, first run the staged `stop-local-qa.sh`, then build, then restart `run-local-qa.sh` for final browser proof.',
|
|
2355
2615
|
'Use desktop screenshots at 1920x1080 by default unless the task is explicitly mobile/responsive. Every screenshot must have a customer-facing caption.',
|
|
2616
|
+
'For From/To, source/target, dropdown, combobox, rio-select, filter, item, customer, yard, chemical, or treatment-plan selection workflows, capture pass screenshots only after the selected values are visible in the controls/chips/labels. The QA matrix data/assertion/caption must name the selected values; empty controls, placeholder text, or route-load screenshots are blocker evidence, not pass evidence.',
|
|
2356
2617
|
'For import/export/form-submit/data workflows, prove before/action/after with representative data and a concrete row/count/value assertion.',
|
|
2618
|
+
'For pricing import/upload bugs, drive the actual Manage Pricing browser import for the attached/template variants and assert localhost Mongo pricing row/count/value changes. Validator-only proof must fail.',
|
|
2619
|
+
'For asset location/current-yard bugs, prove the exact asset/unit on list, detail, and edit/save screens, with a joined/canonical yard assertion and stale yard name absence proof.',
|
|
2620
|
+
'For Update Interchangeables/Mass Update chemical bugs, select the real source and target chemicals, run the update, and assert before/after treatment-plan/tank document counts or ids in localhost Mongo. Route-load or dropdown-only proof must fail.',
|
|
2357
2621
|
'For data-backed workflows, run `qa-live-data-seed.js` after the local app is ready and before browser clickthrough. It reads a bounded live-data slice from `RESOLVEIO_QA_LIVE_MONGO_URL` or staged mongo context, writes only to localhost QA Mongo, prefers ticket-mentioned invoice/BOL/order/ticket identifiers, and records `qa-artifacts/qa-live-data-seed-result.json`.',
|
|
2622
|
+
'For support-ticket QA, use the ticket reporter or named affected user when the live seed can identify/copy that user. `qa-live-data-seed-result.json.selected.qa_user_context` and `auth-bootstrap-result.json.user` must agree, or the row is not ready to pass.',
|
|
2623
|
+
'For customer-reported data bugs, QA must seed and prove the exact named production records in local QA. If the exact unit/BOL/invoice/chemical/customer/yard/user is missing, fail with a seed/query blocker instead of switching to a representative record.',
|
|
2358
2624
|
'For bug fixes, use `bugfix-comparison-qa.sh` so baseline/master runs the exact same repro before the candidate/PR run. A passing candidate is not enough unless the comparison result shows baseline failed or the report explicitly explains why the baseline failure could not be reproduced.',
|
|
2359
2625
|
"Use `$".concat(usernameVar, "` and `$").concat(passwordVar, "` for the local fixture admin account unless ticket/app-specific credentials are provided."),
|
|
2360
2626
|
'The env file reuses `/var/lib/resolveio/puppeteer`, npm cache, worker-safe Browserslist settings, and Angular cache prep so QA should not download a browser or rebuild cold caches unnecessarily.',
|