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.
@@ -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
- if (!existsSync(path)) return null;
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualty",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Qualty CLI for localhost and CI test runs",
5
5
  "bin": {
6
6
  "qualty": "bin/qualty.js"