qualty 0.1.18 → 0.1.20
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/local-runner.js +193 -3
- package/bin/qualty.js +7 -6
- package/package.json +1 -1
package/bin/local-runner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync, mkdtempSync, existsSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, writeFileSync, mkdtempSync, existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
@@ -78,6 +78,161 @@ function localAuthStatePath({ orgId, projectId, profile }) {
|
|
|
78
78
|
return join(homedir(), ".qualty", "local-contexts", orgSeg, projectSeg, `${profileSeg}.json`);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
export function summarizeStorageStateFile(statePath) {
|
|
82
|
+
const path = String(statePath || "").trim();
|
|
83
|
+
if (!path || !existsSync(path)) {
|
|
84
|
+
return { exists: false, path };
|
|
85
|
+
}
|
|
86
|
+
const stat = statSync(path);
|
|
87
|
+
let parsed = null;
|
|
88
|
+
try {
|
|
89
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return {
|
|
92
|
+
exists: true,
|
|
93
|
+
path,
|
|
94
|
+
bytes: stat.size,
|
|
95
|
+
parseError: String(err?.message || err),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const cookies = Array.isArray(parsed.cookies) ? parsed.cookies : [];
|
|
99
|
+
const origins = Array.isArray(parsed.origins) ? parsed.origins : [];
|
|
100
|
+
return {
|
|
101
|
+
exists: true,
|
|
102
|
+
path,
|
|
103
|
+
bytes: stat.size,
|
|
104
|
+
cookieCount: cookies.length,
|
|
105
|
+
cookieDomains: [...new Set(cookies.map((c) => c.domain || c.url || "?"))],
|
|
106
|
+
storageOrigins: origins.map((entry) => ({
|
|
107
|
+
origin: entry.origin || "?",
|
|
108
|
+
localStorageKeys: Array.isArray(entry.localStorage)
|
|
109
|
+
? entry.localStorage.map((item) => String(item?.name || "").trim()).filter(Boolean)
|
|
110
|
+
: [],
|
|
111
|
+
})),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hostFromUrl(rawUrl) {
|
|
116
|
+
try {
|
|
117
|
+
return new URL(String(rawUrl || "").trim()).host;
|
|
118
|
+
} catch {
|
|
119
|
+
return "";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function logAuthStateDiagnostics(prefix, summary, details = {}) {
|
|
124
|
+
const tag = prefix || "[qualty][auth]";
|
|
125
|
+
const {
|
|
126
|
+
profile = "",
|
|
127
|
+
source = "",
|
|
128
|
+
claimProfileName = "",
|
|
129
|
+
claimLocalProfileName = "",
|
|
130
|
+
testUrl = "",
|
|
131
|
+
noAuthState = false,
|
|
132
|
+
} = details;
|
|
133
|
+
|
|
134
|
+
if (noAuthState) {
|
|
135
|
+
// eslint-disable-next-line no-console
|
|
136
|
+
console.log(`${tag} skipped (--no-auth-state)`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (source) {
|
|
141
|
+
// eslint-disable-next-line no-console
|
|
142
|
+
console.log(`${tag} resolution: ${source}`);
|
|
143
|
+
}
|
|
144
|
+
if (profile) {
|
|
145
|
+
// eslint-disable-next-line no-console
|
|
146
|
+
console.log(`${tag} profile: "${profile}"`);
|
|
147
|
+
}
|
|
148
|
+
if (claimProfileName) {
|
|
149
|
+
// eslint-disable-next-line no-console
|
|
150
|
+
console.log(`${tag} dashboard saved login: "${claimProfileName}"`);
|
|
151
|
+
}
|
|
152
|
+
if (claimLocalProfileName && claimLocalProfileName !== profile) {
|
|
153
|
+
// eslint-disable-next-line no-console
|
|
154
|
+
console.log(`${tag} dashboard local_profile_name: "${claimLocalProfileName}"`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!summary?.path) {
|
|
158
|
+
// eslint-disable-next-line no-console
|
|
159
|
+
console.log(`${tag} no auth state path resolved`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!summary.exists) {
|
|
164
|
+
// eslint-disable-next-line no-console
|
|
165
|
+
console.log(`${tag} auth state file missing: ${summary.path}`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (summary.parseError) {
|
|
170
|
+
// eslint-disable-next-line no-console
|
|
171
|
+
console.log(
|
|
172
|
+
`${tag} auth state file is not valid JSON (${summary.bytes} bytes): ${summary.parseError}`
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// eslint-disable-next-line no-console
|
|
178
|
+
console.log(`${tag} auth state file found (${summary.bytes} bytes): ${summary.path}`);
|
|
179
|
+
// eslint-disable-next-line no-console
|
|
180
|
+
console.log(
|
|
181
|
+
`${tag} contents: ${summary.cookieCount} cookie(s)` +
|
|
182
|
+
(summary.cookieDomains.length ? ` [${summary.cookieDomains.join(", ")}]` : "")
|
|
183
|
+
);
|
|
184
|
+
for (const originEntry of summary.storageOrigins || []) {
|
|
185
|
+
const keys = originEntry.localStorageKeys.length
|
|
186
|
+
? originEntry.localStorageKeys.join(", ")
|
|
187
|
+
: "none";
|
|
188
|
+
// eslint-disable-next-line no-console
|
|
189
|
+
console.log(`${tag} origin ${originEntry.origin}: localStorage keys [${keys}]`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (testUrl) {
|
|
193
|
+
const testHost = hostFromUrl(testUrl);
|
|
194
|
+
const storageHosts = new Set(
|
|
195
|
+
(summary.storageOrigins || [])
|
|
196
|
+
.map((entry) => hostFromUrl(entry.origin))
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
);
|
|
199
|
+
for (const cookieDomain of summary.cookieDomains || []) {
|
|
200
|
+
if (cookieDomain && cookieDomain !== "?") storageHosts.add(String(cookieDomain).replace(/^\./, ""));
|
|
201
|
+
}
|
|
202
|
+
// eslint-disable-next-line no-console
|
|
203
|
+
console.log(`${tag} test URL: ${testUrl}`);
|
|
204
|
+
if (testHost && storageHosts.size > 0 && !storageHosts.has(testHost)) {
|
|
205
|
+
// eslint-disable-next-line no-console
|
|
206
|
+
console.warn(
|
|
207
|
+
`${tag} warning: test host "${testHost}" does not match saved auth origins/cookies (${[...storageHosts].join(", ")}). localStorage will not apply until origins match.`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const emptyStorage =
|
|
213
|
+
summary.cookieCount === 0 &&
|
|
214
|
+
(summary.storageOrigins || []).every((entry) => entry.localStorageKeys.length === 0);
|
|
215
|
+
if (emptyStorage) {
|
|
216
|
+
// eslint-disable-next-line no-console
|
|
217
|
+
console.warn(`${tag} warning: auth state file appears empty (no cookies or localStorage)`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function logAuthStateAfterInjection(tag, context, { testUrl = "" } = {}) {
|
|
222
|
+
try {
|
|
223
|
+
const cookies = await context.cookies();
|
|
224
|
+
// eslint-disable-next-line no-console
|
|
225
|
+
console.log(`${tag} injected into browser context: ${cookies.length} cookie(s) loaded`);
|
|
226
|
+
if (testUrl) {
|
|
227
|
+
// eslint-disable-next-line no-console
|
|
228
|
+
console.log(`${tag} localStorage applies after navigation to a matching origin (${testUrl})`);
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
// eslint-disable-next-line no-console
|
|
232
|
+
console.warn(`${tag} could not verify injected auth state: ${err?.message || err}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
81
236
|
const localBindingSetupPromises = new Map();
|
|
82
237
|
|
|
83
238
|
async function promptYesNo(question, defaultYes = true) {
|
|
@@ -1131,6 +1286,37 @@ export async function runLocalExecution({
|
|
|
1131
1286
|
? resolvedStoragePath
|
|
1132
1287
|
: null;
|
|
1133
1288
|
|
|
1289
|
+
let authSource = "none";
|
|
1290
|
+
if (!noAuthState && finalStoragePath) {
|
|
1291
|
+
if (setupFromClaim?.path && finalStoragePath === setupFromClaim.path) {
|
|
1292
|
+
authSource = "interactive setup during run";
|
|
1293
|
+
} else if (storageStatePath && finalStoragePath === storageStatePath) {
|
|
1294
|
+
authSource = "--auth-profile flag";
|
|
1295
|
+
} else if (resolvedStoragePath && finalStoragePath === resolvedStoragePath) {
|
|
1296
|
+
authSource = claimProfile ? "dashboard saved login profile" : "resolved profile path";
|
|
1297
|
+
} else {
|
|
1298
|
+
authSource = "resolved profile path";
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
logAuthStateDiagnostics("[qualty][auth]", summarizeStorageStateFile(finalStoragePath), {
|
|
1303
|
+
profile: effectiveProfile || "",
|
|
1304
|
+
source: authSource,
|
|
1305
|
+
claimProfileName: String(claimSavedLogin?.name || "").trim(),
|
|
1306
|
+
claimLocalProfileName: claimProfile,
|
|
1307
|
+
testUrl: url,
|
|
1308
|
+
noAuthState: Boolean(noAuthState),
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
if (!noAuthState && effectiveProfile && !finalStoragePath) {
|
|
1312
|
+
const candidates = [storageStatePath, resolvedStoragePath].filter(Boolean);
|
|
1313
|
+
// eslint-disable-next-line no-console
|
|
1314
|
+
console.warn(
|
|
1315
|
+
`[qualty][auth] profile "${effectiveProfile}" was requested but no auth state file exists` +
|
|
1316
|
+
(candidates.length ? ` (checked: ${candidates.join(", ")})` : "")
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1134
1320
|
const browserServer = await chromium.launchServer({
|
|
1135
1321
|
headless: headless !== false,
|
|
1136
1322
|
host: "127.0.0.1",
|
|
@@ -1142,10 +1328,14 @@ export async function runLocalExecution({
|
|
|
1142
1328
|
};
|
|
1143
1329
|
if (finalStoragePath) {
|
|
1144
1330
|
contextOptions.storageState = finalStoragePath;
|
|
1145
|
-
const logProfile = effectiveProfile || "default";
|
|
1146
|
-
console.log(`[qualty][auth] using saved context "${logProfile}": ${finalStoragePath}`);
|
|
1147
1331
|
}
|
|
1148
1332
|
const context = await browser.newContext(contextOptions);
|
|
1333
|
+
if (finalStoragePath) {
|
|
1334
|
+
await logAuthStateAfterInjection("[qualty][auth]", context, { testUrl: url });
|
|
1335
|
+
} else if (!noAuthState && (effectiveProfile || claimSavedLogin?.name)) {
|
|
1336
|
+
// eslint-disable-next-line no-console
|
|
1337
|
+
console.log("[qualty][auth] running without saved login state injection");
|
|
1338
|
+
}
|
|
1149
1339
|
const page = await context.newPage();
|
|
1150
1340
|
const networkCollector = attachNetworkCollector(page);
|
|
1151
1341
|
const attachmentSpecs = Array.isArray(claim.file_attachments) ? claim.file_attachments : [];
|
package/bin/qualty.js
CHANGED
|
@@ -15,7 +15,7 @@ import { homedir, hostname } from "node:os";
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { createInterface } from "node:readline/promises";
|
|
17
17
|
import process from "node:process";
|
|
18
|
-
import { rewriteUrlForLocalRun, runLocalCi } from "./local-runner.js";
|
|
18
|
+
import { rewriteUrlForLocalRun, runLocalCi, summarizeStorageStateFile, logAuthStateDiagnostics } from "./local-runner.js";
|
|
19
19
|
|
|
20
20
|
/** CLI backend URL is fixed by Qualty distribution. */
|
|
21
21
|
const DEFAULT_QUALTY_API_URL = "https://qualty-api-production.up.railway.app";
|
|
@@ -1630,7 +1630,12 @@ function resolveLocalAuthStateForRun({ args, orgId, projectId }) {
|
|
|
1630
1630
|
const profile = requestedProfile;
|
|
1631
1631
|
if (!profile) return null;
|
|
1632
1632
|
const path = localAuthStatePath({ orgId, projectId, profile });
|
|
1633
|
-
|
|
1633
|
+
const summary = summarizeStorageStateFile(path);
|
|
1634
|
+
logAuthStateDiagnostics("[qualty][auth]", summary, {
|
|
1635
|
+
profile,
|
|
1636
|
+
source: "pre-run --auth-profile check",
|
|
1637
|
+
});
|
|
1638
|
+
if (!summary.exists) return null;
|
|
1634
1639
|
return { profile, path };
|
|
1635
1640
|
}
|
|
1636
1641
|
|
|
@@ -1949,10 +1954,6 @@ async function main() {
|
|
|
1949
1954
|
...(authChoice.noAuthState ? { "no-auth-state": true } : {}),
|
|
1950
1955
|
};
|
|
1951
1956
|
const localAuth = resolveLocalAuthStateForRun({ args: localAuthArgs, orgId, projectId });
|
|
1952
|
-
if (localAuth?.path) {
|
|
1953
|
-
// eslint-disable-next-line no-console
|
|
1954
|
-
console.log(`[qualty][auth] Reusing profile "${localAuth.profile}"`);
|
|
1955
|
-
}
|
|
1956
1957
|
const localRunArgs = {
|
|
1957
1958
|
apiUrl,
|
|
1958
1959
|
token,
|