@hasna/testers 0.0.39 → 0.0.41

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/dist/cli/index.js CHANGED
@@ -16937,14 +16937,32 @@ function isCredentialReference(value) {
16937
16937
  var init_secrets_resolver = () => {};
16938
16938
 
16939
16939
  // src/lib/persona-auth.ts
16940
- function areCookiesFresh(persona) {
16940
+ function isSessionCookie(cookieName) {
16941
+ return !/(?:csrf|xsrf)/i.test(cookieName);
16942
+ }
16943
+ function getCookieExpires(cookie) {
16944
+ if (typeof cookie.expires === "number" && Number.isFinite(cookie.expires)) {
16945
+ return cookie.expires;
16946
+ }
16947
+ if (typeof cookie.expires === "string") {
16948
+ const parsed = Number(cookie.expires);
16949
+ return Number.isFinite(parsed) ? parsed : null;
16950
+ }
16951
+ return null;
16952
+ }
16953
+ function hasFreshAuthCookies(persona) {
16941
16954
  if (!persona.auth?.cookies?.length)
16942
16955
  return false;
16943
16956
  const cookies = persona.auth.cookies;
16957
+ const sessionCookies = cookies.filter((cookie) => ("name" in cookie) && isSessionCookie(String(cookie.name)));
16958
+ if (sessionCookies.length === 0) {
16959
+ return false;
16960
+ }
16944
16961
  const now2 = Date.now() / 1000;
16945
- const hasFutureExpiry = cookies.some((c) => c.expires && c.expires > now2 + 60);
16946
- if (hasFutureExpiry)
16947
- return true;
16962
+ const expiringSessionCookies = sessionCookies.map(getCookieExpires).filter((expires) => expires !== null && expires > 0);
16963
+ if (expiringSessionCookies.length > 0) {
16964
+ return expiringSessionCookies.some((expires) => expires > now2 + 60);
16965
+ }
16948
16966
  const updatedAt = new Date(persona.updatedAt).getTime();
16949
16967
  return Date.now() - updatedAt < COOKIE_MAX_AGE_MS;
16950
16968
  }
@@ -16982,6 +17000,7 @@ async function performLogin(page, persona, baseUrl) {
16982
17000
  const loginUrl = auth.loginPath.startsWith("http") ? auth.loginPath : `${baseUrl.replace(/\/$/, "")}${auth.loginPath}`;
16983
17001
  try {
16984
17002
  await page.goto(loginUrl, { timeout: 30000, waitUntil: "domcontentloaded" });
17003
+ await page.waitForLoadState("networkidle", { timeout: 1e4 }).catch(() => {});
16985
17004
  } catch (err) {
16986
17005
  return { success: false, method: "login", error: `Navigation to login page failed: ${err instanceof Error ? err.message : String(err)}` };
16987
17006
  }
@@ -17038,11 +17057,25 @@ async function performLogin(page, persona, baseUrl) {
17038
17057
  if (!passwordFilled) {
17039
17058
  return { success: false, method: "login", error: "Could not find password field on login page" };
17040
17059
  }
17060
+ await page.waitForFunction(() => {
17061
+ const submits = Array.from(document.querySelectorAll('button[type="submit"], input[type="submit"]'));
17062
+ return submits.length === 0 || submits.some((submit) => {
17063
+ const rect = submit.getBoundingClientRect();
17064
+ const visible = rect.width > 0 || rect.height > 0 || submit.getClientRects().length > 0;
17065
+ return visible && !submit.disabled;
17066
+ });
17067
+ }, null, { timeout: 5000 }).catch(() => {});
17041
17068
  let submitted = false;
17042
17069
  for (const sel of submitSelectors) {
17043
17070
  try {
17044
- const el = page.locator(sel).first();
17045
- if (await el.isVisible({ timeout: 2000 })) {
17071
+ const matches = page.locator(sel);
17072
+ const count = await matches.count().catch(() => 0);
17073
+ for (let i = 0;i < Math.min(count, 10); i++) {
17074
+ const el = matches.nth(i);
17075
+ if (!await el.isVisible({ timeout: 500 }).catch(() => false))
17076
+ continue;
17077
+ if (!await el.isEnabled({ timeout: 2000 }).catch(() => false))
17078
+ continue;
17046
17079
  await Promise.all([
17047
17080
  page.waitForNavigation({ timeout: 15000, waitUntil: "domcontentloaded" }).catch(() => {}),
17048
17081
  el.click({ timeout: 5000 })
@@ -17050,6 +17083,8 @@ async function performLogin(page, persona, baseUrl) {
17050
17083
  submitted = true;
17051
17084
  break;
17052
17085
  }
17086
+ if (submitted)
17087
+ break;
17053
17088
  } catch {}
17054
17089
  }
17055
17090
  if (!submitted) {
@@ -17066,6 +17101,11 @@ async function performLogin(page, persona, baseUrl) {
17066
17101
  const currentUrl = page.url();
17067
17102
  const isStillOnLogin = currentUrl.includes(auth.loginPath) || currentUrl.includes("/login") || currentUrl.includes("/signin") || currentUrl.includes("/auth");
17068
17103
  if (isStillOnLogin) {
17104
+ const cookies = await page.context().cookies().catch(() => []);
17105
+ const authCookies = cookies.filter((cookie) => isSessionCookie(cookie.name));
17106
+ if (authCookies.length > 0) {
17107
+ return { success: true, method: "login" };
17108
+ }
17069
17109
  let errorText = "";
17070
17110
  try {
17071
17111
  const errorEl = page.locator('[role="alert"], .error, .alert-error, [data-testid*="error"]').first();
@@ -17112,7 +17152,7 @@ async function ensurePersonaAuthenticated(page, persona, baseUrl) {
17112
17152
  if (!persona.auth) {
17113
17153
  return { success: true, method: "none" };
17114
17154
  }
17115
- if (areCookiesFresh(persona)) {
17155
+ if (hasFreshAuthCookies(persona)) {
17116
17156
  const restored = await restoreCookies(page, persona);
17117
17157
  if (restored) {
17118
17158
  return { success: true, method: "cookies" };
@@ -93997,7 +94037,7 @@ import chalk6 from "chalk";
93997
94037
  // package.json
93998
94038
  var package_default = {
93999
94039
  name: "@hasna/testers",
94000
- version: "0.0.39",
94040
+ version: "0.0.41",
94001
94041
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
94002
94042
  type: "module",
94003
94043
  main: "dist/index.js",
@@ -95724,6 +95764,40 @@ function formatProdDebugPlan(plan) {
95724
95764
  `);
95725
95765
  }
95726
95766
 
95767
+ // src/lib/persona-redaction.ts
95768
+ function stringKeys(value) {
95769
+ return value ? Object.keys(value).sort() : [];
95770
+ }
95771
+ function cookieNames(cookies) {
95772
+ if (!cookies)
95773
+ return [];
95774
+ return cookies.map((cookie) => cookie.name).filter((name) => typeof name === "string" && name.length > 0);
95775
+ }
95776
+ function redactPersona(persona) {
95777
+ if (!persona.auth)
95778
+ return { ...persona, auth: null };
95779
+ const names = cookieNames(persona.auth.cookies);
95780
+ const headerNames = stringKeys(persona.auth.headers);
95781
+ return {
95782
+ ...persona,
95783
+ auth: {
95784
+ emailConfigured: Boolean(persona.auth.email),
95785
+ passwordConfigured: Boolean(persona.auth.password),
95786
+ loginPath: persona.auth.loginPath,
95787
+ strategy: persona.auth.strategy,
95788
+ cookiesConfigured: names.length > 0,
95789
+ cookieCount: names.length,
95790
+ cookieNames: names,
95791
+ headersConfigured: headerNames.length > 0,
95792
+ headerNames,
95793
+ customScriptConfigured: Boolean(persona.auth.customScript)
95794
+ }
95795
+ };
95796
+ }
95797
+ function redactPersonas(personas) {
95798
+ return personas.map(redactPersona);
95799
+ }
95800
+
95727
95801
  // src/cli/index.tsx
95728
95802
  init_projects();
95729
95803
  init_personas();
@@ -97629,6 +97703,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
97629
97703
  process.exit(2);
97630
97704
  }, overallTimeoutMs).unref();
97631
97705
  }
97706
+ const personaIdList = opts.persona ? opts.persona.split(",").map((s2) => s2.trim()).filter(Boolean) : undefined;
97632
97707
  if (!opts.dryRun && !opts.background) {
97633
97708
  const budgetResult = checkBudget(0);
97634
97709
  if (budgetResult.warning) {
@@ -97712,7 +97787,9 @@ program2.command("run [url] [description]").alias("test").description("Run test
97712
97787
  timeout: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
97713
97788
  maxTurns: opts.maxTurns ? parseInt(opts.maxTurns, 10) : undefined,
97714
97789
  projectId,
97715
- engine: opts.browser
97790
+ engine: opts.browser,
97791
+ personaId: personaIdList?.[0],
97792
+ personaIds: personaIdList && personaIdList.length > 1 ? personaIdList : undefined
97716
97793
  });
97717
97794
  log(chalk6.green(`Run started in background: ${chalk6.bold(runId.slice(0, 8))}`));
97718
97795
  log(chalk6.dim(` Scenarios: ${scenarioCount}`));
@@ -97836,7 +97913,9 @@ program2.command("run [url] [description]").alias("test").description("Run test
97836
97913
  samples: parseInt(opts.samples ?? "1", 10),
97837
97914
  flakinessThreshold: parseFloat(opts.flakinessThreshold ?? "0.95"),
97838
97915
  a11y: opts.a11y ? typeof opts.a11y === "string" ? { level: opts.a11y } : true : undefined,
97839
- selfHeal: opts.selfHeal || undefined
97916
+ selfHeal: opts.selfHeal || undefined,
97917
+ personaId: personaIdList?.[0],
97918
+ personaIds: personaIdList && personaIdList.length > 1 ? personaIdList : undefined
97840
97919
  });
97841
97920
  if (opts.json || opts.output) {
97842
97921
  const jsonOutput = formatJSON(run3, results2);
@@ -97924,7 +98003,6 @@ program2.command("run [url] [description]").alias("test").description("Run test
97924
98003
  log(chalk6.yellow(" --diff: git diff failed. Running all scenarios."));
97925
98004
  }
97926
98005
  }
97927
- const personaIdList = opts.persona ? opts.persona.split(",").map((s2) => s2.trim()).filter(Boolean) : undefined;
97928
98006
  const { run: run2, results } = await runByFilter({
97929
98007
  url: url2,
97930
98008
  tags: opts.tag.length > 0 ? opts.tag : undefined,
@@ -100751,7 +100829,7 @@ personaCmd.command("list").description("List personas").option("--project <id>",
100751
100829
  globalOnly: opts.global ? true : undefined
100752
100830
  });
100753
100831
  if (opts.json) {
100754
- log(JSON.stringify(personas, null, 2));
100832
+ log(JSON.stringify(redactPersonas(personas), null, 2));
100755
100833
  return;
100756
100834
  }
100757
100835
  if (personas.length === 0) {
package/dist/index.js CHANGED
@@ -15496,14 +15496,32 @@ function isCredentialReference(value) {
15496
15496
 
15497
15497
  // src/lib/persona-auth.ts
15498
15498
  var COOKIE_MAX_AGE_MS = 60 * 60 * 1000;
15499
- function areCookiesFresh(persona) {
15499
+ function isSessionCookie(cookieName) {
15500
+ return !/(?:csrf|xsrf)/i.test(cookieName);
15501
+ }
15502
+ function getCookieExpires(cookie) {
15503
+ if (typeof cookie.expires === "number" && Number.isFinite(cookie.expires)) {
15504
+ return cookie.expires;
15505
+ }
15506
+ if (typeof cookie.expires === "string") {
15507
+ const parsed = Number(cookie.expires);
15508
+ return Number.isFinite(parsed) ? parsed : null;
15509
+ }
15510
+ return null;
15511
+ }
15512
+ function hasFreshAuthCookies(persona) {
15500
15513
  if (!persona.auth?.cookies?.length)
15501
15514
  return false;
15502
15515
  const cookies = persona.auth.cookies;
15516
+ const sessionCookies = cookies.filter((cookie) => ("name" in cookie) && isSessionCookie(String(cookie.name)));
15517
+ if (sessionCookies.length === 0) {
15518
+ return false;
15519
+ }
15503
15520
  const now2 = Date.now() / 1000;
15504
- const hasFutureExpiry = cookies.some((c) => c.expires && c.expires > now2 + 60);
15505
- if (hasFutureExpiry)
15506
- return true;
15521
+ const expiringSessionCookies = sessionCookies.map(getCookieExpires).filter((expires) => expires !== null && expires > 0);
15522
+ if (expiringSessionCookies.length > 0) {
15523
+ return expiringSessionCookies.some((expires) => expires > now2 + 60);
15524
+ }
15507
15525
  const updatedAt = new Date(persona.updatedAt).getTime();
15508
15526
  return Date.now() - updatedAt < COOKIE_MAX_AGE_MS;
15509
15527
  }
@@ -15541,6 +15559,7 @@ async function performLogin(page, persona, baseUrl) {
15541
15559
  const loginUrl = auth.loginPath.startsWith("http") ? auth.loginPath : `${baseUrl.replace(/\/$/, "")}${auth.loginPath}`;
15542
15560
  try {
15543
15561
  await page.goto(loginUrl, { timeout: 30000, waitUntil: "domcontentloaded" });
15562
+ await page.waitForLoadState("networkidle", { timeout: 1e4 }).catch(() => {});
15544
15563
  } catch (err) {
15545
15564
  return { success: false, method: "login", error: `Navigation to login page failed: ${err instanceof Error ? err.message : String(err)}` };
15546
15565
  }
@@ -15597,11 +15616,25 @@ async function performLogin(page, persona, baseUrl) {
15597
15616
  if (!passwordFilled) {
15598
15617
  return { success: false, method: "login", error: "Could not find password field on login page" };
15599
15618
  }
15619
+ await page.waitForFunction(() => {
15620
+ const submits = Array.from(document.querySelectorAll('button[type="submit"], input[type="submit"]'));
15621
+ return submits.length === 0 || submits.some((submit) => {
15622
+ const rect = submit.getBoundingClientRect();
15623
+ const visible = rect.width > 0 || rect.height > 0 || submit.getClientRects().length > 0;
15624
+ return visible && !submit.disabled;
15625
+ });
15626
+ }, null, { timeout: 5000 }).catch(() => {});
15600
15627
  let submitted = false;
15601
15628
  for (const sel of submitSelectors) {
15602
15629
  try {
15603
- const el = page.locator(sel).first();
15604
- if (await el.isVisible({ timeout: 2000 })) {
15630
+ const matches = page.locator(sel);
15631
+ const count = await matches.count().catch(() => 0);
15632
+ for (let i = 0;i < Math.min(count, 10); i++) {
15633
+ const el = matches.nth(i);
15634
+ if (!await el.isVisible({ timeout: 500 }).catch(() => false))
15635
+ continue;
15636
+ if (!await el.isEnabled({ timeout: 2000 }).catch(() => false))
15637
+ continue;
15605
15638
  await Promise.all([
15606
15639
  page.waitForNavigation({ timeout: 15000, waitUntil: "domcontentloaded" }).catch(() => {}),
15607
15640
  el.click({ timeout: 5000 })
@@ -15609,6 +15642,8 @@ async function performLogin(page, persona, baseUrl) {
15609
15642
  submitted = true;
15610
15643
  break;
15611
15644
  }
15645
+ if (submitted)
15646
+ break;
15612
15647
  } catch {}
15613
15648
  }
15614
15649
  if (!submitted) {
@@ -15625,6 +15660,11 @@ async function performLogin(page, persona, baseUrl) {
15625
15660
  const currentUrl = page.url();
15626
15661
  const isStillOnLogin = currentUrl.includes(auth.loginPath) || currentUrl.includes("/login") || currentUrl.includes("/signin") || currentUrl.includes("/auth");
15627
15662
  if (isStillOnLogin) {
15663
+ const cookies = await page.context().cookies().catch(() => []);
15664
+ const authCookies = cookies.filter((cookie) => isSessionCookie(cookie.name));
15665
+ if (authCookies.length > 0) {
15666
+ return { success: true, method: "login" };
15667
+ }
15628
15668
  let errorText = "";
15629
15669
  try {
15630
15670
  const errorEl = page.locator('[role="alert"], .error, .alert-error, [data-testid*="error"]').first();
@@ -15671,7 +15711,7 @@ async function ensurePersonaAuthenticated(page, persona, baseUrl) {
15671
15711
  if (!persona.auth) {
15672
15712
  return { success: true, method: "none" };
15673
15713
  }
15674
- if (areCookiesFresh(persona)) {
15714
+ if (hasFreshAuthCookies(persona)) {
15675
15715
  const restored = await restoreCookies(page, persona);
15676
15716
  if (restored) {
15677
15717
  return { success: true, method: "cookies" };
@@ -1,10 +1,12 @@
1
1
  import type { Page } from "playwright";
2
2
  import type { Persona } from "../types/index.js";
3
+ export declare function isSessionCookie(cookieName: string): boolean;
3
4
  export interface LoginResult {
4
5
  success: boolean;
5
6
  method: "cookies" | "login" | "none";
6
7
  error?: string;
7
8
  }
9
+ export declare function hasFreshAuthCookies(persona: Persona): boolean;
8
10
  /**
9
11
  * Perform login using a raw AuthConfig (e.g. from scenario.authConfig or an auth preset).
10
12
  * Resolves credentials via resolveCredential() supporting @secrets: and $ENV references.
@@ -1 +1 @@
1
- {"version":3,"file":"persona-auth.d.ts","sourceRoot":"","sources":["../../src/lib/persona-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAMjD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAoND;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,IAAI,EACV,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,EACvG,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,WAAW,CAAC,CAgCtB;AAED;;;;;;;;;GASG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,WAAW,CAAC,CAsBtB"}
1
+ {"version":3,"file":"persona-auth.d.ts","sourceRoot":"","sources":["../../src/lib/persona-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAMjD,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAkBD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAsB7D;AA2ND;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,IAAI,EACV,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,EACvG,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,WAAW,CAAC,CAgCtB;AAED;;;;;;;;;GASG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,WAAW,CAAC,CAsBtB"}
@@ -0,0 +1,19 @@
1
+ import type { Persona } from "../types/index.js";
2
+ export interface RedactedPersonaAuth {
3
+ emailConfigured: boolean;
4
+ passwordConfigured: boolean;
5
+ loginPath: string;
6
+ strategy: string;
7
+ cookiesConfigured: boolean;
8
+ cookieCount: number;
9
+ cookieNames: string[];
10
+ headersConfigured: boolean;
11
+ headerNames: string[];
12
+ customScriptConfigured: boolean;
13
+ }
14
+ export type RedactedPersona = Omit<Persona, "auth"> & {
15
+ auth: RedactedPersonaAuth | null;
16
+ };
17
+ export declare function redactPersona(persona: Persona): RedactedPersona;
18
+ export declare function redactPersonas(personas: Persona[]): RedactedPersona[];
19
+ //# sourceMappingURL=persona-redaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persona-redaction.d.ts","sourceRoot":"","sources":["../../src/lib/persona-redaction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,OAAO,CAAC;IACzB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,sBAAsB,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG;IACpD,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAAC;CAClC,CAAC;AAaF,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,CAoB/D;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,eAAe,EAAE,CAErE"}
package/dist/mcp/index.js CHANGED
@@ -52,7 +52,7 @@ var package_default;
52
52
  var init_package = __esm(() => {
53
53
  package_default = {
54
54
  name: "@hasna/testers",
55
- version: "0.0.39",
55
+ version: "0.0.41",
56
56
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
57
57
  type: "module",
58
58
  main: "dist/index.js",
@@ -20045,14 +20045,32 @@ function isCredentialReference(value) {
20045
20045
  var init_secrets_resolver = () => {};
20046
20046
 
20047
20047
  // src/lib/persona-auth.ts
20048
- function areCookiesFresh(persona) {
20048
+ function isSessionCookie(cookieName) {
20049
+ return !/(?:csrf|xsrf)/i.test(cookieName);
20050
+ }
20051
+ function getCookieExpires(cookie) {
20052
+ if (typeof cookie.expires === "number" && Number.isFinite(cookie.expires)) {
20053
+ return cookie.expires;
20054
+ }
20055
+ if (typeof cookie.expires === "string") {
20056
+ const parsed = Number(cookie.expires);
20057
+ return Number.isFinite(parsed) ? parsed : null;
20058
+ }
20059
+ return null;
20060
+ }
20061
+ function hasFreshAuthCookies(persona) {
20049
20062
  if (!persona.auth?.cookies?.length)
20050
20063
  return false;
20051
20064
  const cookies = persona.auth.cookies;
20065
+ const sessionCookies = cookies.filter((cookie) => ("name" in cookie) && isSessionCookie(String(cookie.name)));
20066
+ if (sessionCookies.length === 0) {
20067
+ return false;
20068
+ }
20052
20069
  const now2 = Date.now() / 1000;
20053
- const hasFutureExpiry = cookies.some((c) => c.expires && c.expires > now2 + 60);
20054
- if (hasFutureExpiry)
20055
- return true;
20070
+ const expiringSessionCookies = sessionCookies.map(getCookieExpires).filter((expires) => expires !== null && expires > 0);
20071
+ if (expiringSessionCookies.length > 0) {
20072
+ return expiringSessionCookies.some((expires) => expires > now2 + 60);
20073
+ }
20056
20074
  const updatedAt = new Date(persona.updatedAt).getTime();
20057
20075
  return Date.now() - updatedAt < COOKIE_MAX_AGE_MS;
20058
20076
  }
@@ -20090,6 +20108,7 @@ async function performLogin(page, persona, baseUrl) {
20090
20108
  const loginUrl = auth.loginPath.startsWith("http") ? auth.loginPath : `${baseUrl.replace(/\/$/, "")}${auth.loginPath}`;
20091
20109
  try {
20092
20110
  await page.goto(loginUrl, { timeout: 30000, waitUntil: "domcontentloaded" });
20111
+ await page.waitForLoadState("networkidle", { timeout: 1e4 }).catch(() => {});
20093
20112
  } catch (err) {
20094
20113
  return { success: false, method: "login", error: `Navigation to login page failed: ${err instanceof Error ? err.message : String(err)}` };
20095
20114
  }
@@ -20146,11 +20165,25 @@ async function performLogin(page, persona, baseUrl) {
20146
20165
  if (!passwordFilled) {
20147
20166
  return { success: false, method: "login", error: "Could not find password field on login page" };
20148
20167
  }
20168
+ await page.waitForFunction(() => {
20169
+ const submits = Array.from(document.querySelectorAll('button[type="submit"], input[type="submit"]'));
20170
+ return submits.length === 0 || submits.some((submit) => {
20171
+ const rect = submit.getBoundingClientRect();
20172
+ const visible = rect.width > 0 || rect.height > 0 || submit.getClientRects().length > 0;
20173
+ return visible && !submit.disabled;
20174
+ });
20175
+ }, null, { timeout: 5000 }).catch(() => {});
20149
20176
  let submitted = false;
20150
20177
  for (const sel of submitSelectors) {
20151
20178
  try {
20152
- const el = page.locator(sel).first();
20153
- if (await el.isVisible({ timeout: 2000 })) {
20179
+ const matches = page.locator(sel);
20180
+ const count = await matches.count().catch(() => 0);
20181
+ for (let i = 0;i < Math.min(count, 10); i++) {
20182
+ const el = matches.nth(i);
20183
+ if (!await el.isVisible({ timeout: 500 }).catch(() => false))
20184
+ continue;
20185
+ if (!await el.isEnabled({ timeout: 2000 }).catch(() => false))
20186
+ continue;
20154
20187
  await Promise.all([
20155
20188
  page.waitForNavigation({ timeout: 15000, waitUntil: "domcontentloaded" }).catch(() => {}),
20156
20189
  el.click({ timeout: 5000 })
@@ -20158,6 +20191,8 @@ async function performLogin(page, persona, baseUrl) {
20158
20191
  submitted = true;
20159
20192
  break;
20160
20193
  }
20194
+ if (submitted)
20195
+ break;
20161
20196
  } catch {}
20162
20197
  }
20163
20198
  if (!submitted) {
@@ -20174,6 +20209,11 @@ async function performLogin(page, persona, baseUrl) {
20174
20209
  const currentUrl = page.url();
20175
20210
  const isStillOnLogin = currentUrl.includes(auth.loginPath) || currentUrl.includes("/login") || currentUrl.includes("/signin") || currentUrl.includes("/auth");
20176
20211
  if (isStillOnLogin) {
20212
+ const cookies = await page.context().cookies().catch(() => []);
20213
+ const authCookies = cookies.filter((cookie) => isSessionCookie(cookie.name));
20214
+ if (authCookies.length > 0) {
20215
+ return { success: true, method: "login" };
20216
+ }
20177
20217
  let errorText = "";
20178
20218
  try {
20179
20219
  const errorEl = page.locator('[role="alert"], .error, .alert-error, [data-testid*="error"]').first();
@@ -20220,7 +20260,7 @@ async function ensurePersonaAuthenticated(page, persona, baseUrl) {
20220
20260
  if (!persona.auth) {
20221
20261
  return { success: true, method: "none" };
20222
20262
  }
20223
- if (areCookiesFresh(persona)) {
20263
+ if (hasFreshAuthCookies(persona)) {
20224
20264
  const restored = await restoreCookies(page, persona);
20225
20265
  if (restored) {
20226
20266
  return { success: true, method: "cookies" };
@@ -53435,6 +53475,40 @@ var init_workflow_agent = __esm(() => {
53435
53475
  init_runner();
53436
53476
  });
53437
53477
 
53478
+ // src/lib/persona-redaction.ts
53479
+ function stringKeys(value) {
53480
+ return value ? Object.keys(value).sort() : [];
53481
+ }
53482
+ function cookieNames(cookies) {
53483
+ if (!cookies)
53484
+ return [];
53485
+ return cookies.map((cookie) => cookie.name).filter((name21) => typeof name21 === "string" && name21.length > 0);
53486
+ }
53487
+ function redactPersona(persona) {
53488
+ if (!persona.auth)
53489
+ return { ...persona, auth: null };
53490
+ const names = cookieNames(persona.auth.cookies);
53491
+ const headerNames = stringKeys(persona.auth.headers);
53492
+ return {
53493
+ ...persona,
53494
+ auth: {
53495
+ emailConfigured: Boolean(persona.auth.email),
53496
+ passwordConfigured: Boolean(persona.auth.password),
53497
+ loginPath: persona.auth.loginPath,
53498
+ strategy: persona.auth.strategy,
53499
+ cookiesConfigured: names.length > 0,
53500
+ cookieCount: names.length,
53501
+ cookieNames: names,
53502
+ headersConfigured: headerNames.length > 0,
53503
+ headerNames,
53504
+ customScriptConfigured: Boolean(persona.auth.customScript)
53505
+ }
53506
+ };
53507
+ }
53508
+ function redactPersonas(personas) {
53509
+ return personas.map(redactPersona);
53510
+ }
53511
+
53438
53512
  // src/db/environments.ts
53439
53513
  var exports_environments = {};
53440
53514
  __export(exports_environments, {
@@ -86911,7 +86985,7 @@ function buildServer() {
86911
86985
  authPassword,
86912
86986
  authLoginPath
86913
86987
  });
86914
- return json3(persona);
86988
+ return json3(redactPersona(persona));
86915
86989
  } catch (error40) {
86916
86990
  return errorResponse(error40);
86917
86991
  }
@@ -86923,7 +86997,7 @@ function buildServer() {
86923
86997
  }, async ({ projectId, enabled, globalOnly }) => {
86924
86998
  try {
86925
86999
  const personas = listPersonas({ projectId, enabled, globalOnly });
86926
- return json3({ items: personas, total: personas.length });
87000
+ return json3({ items: redactPersonas(personas), total: personas.length });
86927
87001
  } catch (error40) {
86928
87002
  return errorResponse(error40);
86929
87003
  }
@@ -86938,7 +87012,7 @@ function buildServer() {
86938
87012
  const db2 = getDatabase();
86939
87013
  const scenarioRows = db2.query("SELECT id, short_id, name FROM scenarios WHERE persona_id = ?").all(persona.id);
86940
87014
  return json3({
86941
- ...persona,
87015
+ ...redactPersona(persona),
86942
87016
  usedByScenarios: scenarioRows.map((r2) => ({ id: r2.id, shortId: r2.short_id, name: r2.name }))
86943
87017
  });
86944
87018
  } catch (error40) {
@@ -86961,7 +87035,7 @@ function buildServer() {
86961
87035
  }, async ({ id, version: version2, ...updates }) => {
86962
87036
  try {
86963
87037
  const persona = updatePersona(id, updates, version2);
86964
- return json3(persona);
87038
+ return json3(redactPersona(persona));
86965
87039
  } catch (error40) {
86966
87040
  return errorResponse(error40, {
86967
87041
  fetchCurrent: () => getPersona(id)
@@ -86992,7 +87066,7 @@ function buildServer() {
86992
87066
  if (!scenario)
86993
87067
  return errorResponse(notFoundErr(scenarioId, "Scenario"));
86994
87068
  const updated = updateScenario(scenario.id, { personaId: persona.id }, scenario.version);
86995
- return json3({ ...updated, attachedPersona: persona });
87069
+ return json3({ ...updated, attachedPersona: redactPersona(persona) });
86996
87070
  } catch (error40) {
86997
87071
  return errorResponse(error40);
86998
87072
  }
@@ -87390,7 +87464,7 @@ function buildServer() {
87390
87464
  projectId: scenario.projectId ?? undefined,
87391
87465
  personaId: persona.id
87392
87466
  });
87393
- return json3({ ...clone2, attachedPersona: persona, clonedFrom: scenario.id });
87467
+ return json3({ ...clone2, attachedPersona: redactPersona(persona), clonedFrom: scenario.id });
87394
87468
  } catch (e2) {
87395
87469
  return errorResponse(e2);
87396
87470
  }
@@ -87655,7 +87729,7 @@ Context: ${context2}` : ""}`,
87655
87729
  const updated = syncPersonaFromContact2(persona.id);
87656
87730
  if (!updated)
87657
87731
  return json3({ synced: false, message: "No linked contact found or no changes needed" });
87658
- return json3({ synced: true, persona: updated });
87732
+ return json3({ synced: true, persona: redactPersona(updated) });
87659
87733
  } catch (e2) {
87660
87734
  return errorResponse(e2);
87661
87735
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":";AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAgHpE,wBAAgB,WAAW,IAAI,SAAS,CA2+EvC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":";AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAiHpE,wBAAgB,WAAW,IAAI,SAAS,CA2+EvC"}
@@ -46910,7 +46910,7 @@ import { join as join14 } from "path";
46910
46910
  // package.json
46911
46911
  var package_default = {
46912
46912
  name: "@hasna/testers",
46913
- version: "0.0.39",
46913
+ version: "0.0.41",
46914
46914
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
46915
46915
  type: "module",
46916
46916
  main: "dist/index.js",
@@ -48540,14 +48540,32 @@ function resolveCredential(value) {
48540
48540
 
48541
48541
  // src/lib/persona-auth.ts
48542
48542
  var COOKIE_MAX_AGE_MS = 60 * 60 * 1000;
48543
- function areCookiesFresh(persona) {
48543
+ function isSessionCookie(cookieName) {
48544
+ return !/(?:csrf|xsrf)/i.test(cookieName);
48545
+ }
48546
+ function getCookieExpires(cookie) {
48547
+ if (typeof cookie.expires === "number" && Number.isFinite(cookie.expires)) {
48548
+ return cookie.expires;
48549
+ }
48550
+ if (typeof cookie.expires === "string") {
48551
+ const parsed = Number(cookie.expires);
48552
+ return Number.isFinite(parsed) ? parsed : null;
48553
+ }
48554
+ return null;
48555
+ }
48556
+ function hasFreshAuthCookies(persona) {
48544
48557
  if (!persona.auth?.cookies?.length)
48545
48558
  return false;
48546
48559
  const cookies = persona.auth.cookies;
48560
+ const sessionCookies = cookies.filter((cookie) => ("name" in cookie) && isSessionCookie(String(cookie.name)));
48561
+ if (sessionCookies.length === 0) {
48562
+ return false;
48563
+ }
48547
48564
  const now2 = Date.now() / 1000;
48548
- const hasFutureExpiry = cookies.some((c) => c.expires && c.expires > now2 + 60);
48549
- if (hasFutureExpiry)
48550
- return true;
48565
+ const expiringSessionCookies = sessionCookies.map(getCookieExpires).filter((expires) => expires !== null && expires > 0);
48566
+ if (expiringSessionCookies.length > 0) {
48567
+ return expiringSessionCookies.some((expires) => expires > now2 + 60);
48568
+ }
48551
48569
  const updatedAt = new Date(persona.updatedAt).getTime();
48552
48570
  return Date.now() - updatedAt < COOKIE_MAX_AGE_MS;
48553
48571
  }
@@ -48585,6 +48603,7 @@ async function performLogin(page, persona, baseUrl) {
48585
48603
  const loginUrl = auth.loginPath.startsWith("http") ? auth.loginPath : `${baseUrl.replace(/\/$/, "")}${auth.loginPath}`;
48586
48604
  try {
48587
48605
  await page.goto(loginUrl, { timeout: 30000, waitUntil: "domcontentloaded" });
48606
+ await page.waitForLoadState("networkidle", { timeout: 1e4 }).catch(() => {});
48588
48607
  } catch (err) {
48589
48608
  return { success: false, method: "login", error: `Navigation to login page failed: ${err instanceof Error ? err.message : String(err)}` };
48590
48609
  }
@@ -48641,11 +48660,25 @@ async function performLogin(page, persona, baseUrl) {
48641
48660
  if (!passwordFilled) {
48642
48661
  return { success: false, method: "login", error: "Could not find password field on login page" };
48643
48662
  }
48663
+ await page.waitForFunction(() => {
48664
+ const submits = Array.from(document.querySelectorAll('button[type="submit"], input[type="submit"]'));
48665
+ return submits.length === 0 || submits.some((submit) => {
48666
+ const rect = submit.getBoundingClientRect();
48667
+ const visible = rect.width > 0 || rect.height > 0 || submit.getClientRects().length > 0;
48668
+ return visible && !submit.disabled;
48669
+ });
48670
+ }, null, { timeout: 5000 }).catch(() => {});
48644
48671
  let submitted = false;
48645
48672
  for (const sel of submitSelectors) {
48646
48673
  try {
48647
- const el = page.locator(sel).first();
48648
- if (await el.isVisible({ timeout: 2000 })) {
48674
+ const matches = page.locator(sel);
48675
+ const count = await matches.count().catch(() => 0);
48676
+ for (let i = 0;i < Math.min(count, 10); i++) {
48677
+ const el = matches.nth(i);
48678
+ if (!await el.isVisible({ timeout: 500 }).catch(() => false))
48679
+ continue;
48680
+ if (!await el.isEnabled({ timeout: 2000 }).catch(() => false))
48681
+ continue;
48649
48682
  await Promise.all([
48650
48683
  page.waitForNavigation({ timeout: 15000, waitUntil: "domcontentloaded" }).catch(() => {}),
48651
48684
  el.click({ timeout: 5000 })
@@ -48653,6 +48686,8 @@ async function performLogin(page, persona, baseUrl) {
48653
48686
  submitted = true;
48654
48687
  break;
48655
48688
  }
48689
+ if (submitted)
48690
+ break;
48656
48691
  } catch {}
48657
48692
  }
48658
48693
  if (!submitted) {
@@ -48669,6 +48704,11 @@ async function performLogin(page, persona, baseUrl) {
48669
48704
  const currentUrl = page.url();
48670
48705
  const isStillOnLogin = currentUrl.includes(auth.loginPath) || currentUrl.includes("/login") || currentUrl.includes("/signin") || currentUrl.includes("/auth");
48671
48706
  if (isStillOnLogin) {
48707
+ const cookies = await page.context().cookies().catch(() => []);
48708
+ const authCookies = cookies.filter((cookie) => isSessionCookie(cookie.name));
48709
+ if (authCookies.length > 0) {
48710
+ return { success: true, method: "login" };
48711
+ }
48672
48712
  let errorText = "";
48673
48713
  try {
48674
48714
  const errorEl = page.locator('[role="alert"], .error, .alert-error, [data-testid*="error"]').first();
@@ -48715,7 +48755,7 @@ async function ensurePersonaAuthenticated(page, persona, baseUrl) {
48715
48755
  if (!persona.auth) {
48716
48756
  return { success: true, method: "none" };
48717
48757
  }
48718
- if (areCookiesFresh(persona)) {
48758
+ if (hasFreshAuthCookies(persona)) {
48719
48759
  const restored = await restoreCookies(page, persona);
48720
48760
  if (restored) {
48721
48761
  return { success: true, method: "cookies" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/testers",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "description": "AI-powered QA testing CLI — spawns cheap AI agents to test web apps with headless browsers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",