@mjasnikovs/pi-task 0.40.38 → 0.40.40

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.
@@ -65,6 +65,9 @@ export declare function pinnedLocalPort(vars: Record<string, string>): number |
65
65
  export interface SessionRequest {
66
66
  method: string;
67
67
  path: string;
68
+ /** Where the chain ENDED — the path `status` and `mimeType` actually describe.
69
+ * Absent in a pre-existing recorded session, where it falls back to `path`. */
70
+ finalPath?: string;
68
71
  status: number | null;
69
72
  mimeType: string | null;
70
73
  failed: boolean;
@@ -96,6 +99,8 @@ export interface DeepSessionFacts {
96
99
  path: string;
97
100
  status: number | null;
98
101
  failed: boolean;
102
+ /** The sign-in chain redirected, so `status` is the hop it LANDED on. */
103
+ redirected?: boolean;
99
104
  } | null;
100
105
  /** Same-origin XHR/fetch requests issued at or after the sign-in request,
101
106
  * excluding that request itself. Derived: `sessionRequests` in phase 'post'. */
@@ -207,7 +207,13 @@ export function deriveLegacyFacts(log) {
207
207
  const auth = log.find(r => r.phase === 'auth') ?? null;
208
208
  const data = log.filter(r => r.phase === 'post' && r.initiator === 'xhr');
209
209
  return {
210
- authRequest: auth === null ? null : ({ method: auth.method, path: auth.path, status: auth.status, failed: auth.failed }),
210
+ authRequest: auth === null ? null : ({
211
+ method: auth.method,
212
+ path: auth.path,
213
+ status: auth.status,
214
+ failed: auth.failed,
215
+ redirected: auth.redirected
216
+ }),
211
217
  postAuthDataAttempted: data.length,
212
218
  postAuthData2xx: data.filter(r => r.status !== null && r.status >= 200 && r.status < 300)
213
219
  .length
@@ -217,6 +223,11 @@ export function deriveLegacyFacts(log) {
217
223
  * answered No. 501 is included because a server that routes but implements
218
224
  * nothing is the same dead call from the client's side. */
219
225
  const MISSING_ROUTE_STATUS = new Set([404, 405, 501]);
226
+ /** A redirected entry's `status` and `mimeType` belong to its LAST hop, so the two
227
+ * rules below may read them only where that hop is still the client's own business.
228
+ * Before and during sign-in, a 302 to the login page is the normal unauthenticated
229
+ * flow; after sign-in, that same redirect IS the defect. */
230
+ const lastHopJudgesTheClient = (r) => !r.redirected || r.phase === 'post';
220
231
  /**
221
232
  * Judge a recorded session. The ONE thing that may FAIL is a session the SERVER
222
233
  * authenticated (2xx on the sign-in request) whose client then could not use it:
@@ -283,13 +294,15 @@ export function judgeDeepSession(f) {
283
294
  // password, a missing permission — the app working), 5xx is the server failing,
284
295
  // and both keep their existing behaviour. Only "no such route" is here.
285
296
  const missingRoute = (f.sessionRequests ?? []).find(r => r.initiator === 'xhr'
286
- && !r.redirected
297
+ && lastHopJudgesTheClient(r)
287
298
  && r.status !== null
288
299
  && MISSING_ROUTE_STATUS.has(r.status));
289
300
  if (missingRoute) {
301
+ const landed = missingRoute.finalPath ?? missingRoute.path;
302
+ const hop = landed === missingRoute.path ? '' : ` → \`${landed}\``;
290
303
  return {
291
304
  outcome: 'fail',
292
- detail: `\`${missingRoute.method} ${missingRoute.path}\` → ${String(missingRoute.status)}: `
305
+ detail: `\`${missingRoute.method} ${missingRoute.path}\`${hop} → ${String(missingRoute.status)}: `
293
306
  + 'the client sent this to a path the server does not route. This is a dead client '
294
307
  + 'call — a base URL joined twice, a renamed route, a wrong method. No type or mock '
295
308
  + 'can produce a route that is not mounted.'
@@ -299,20 +312,27 @@ export function judgeDeepSession(f) {
299
312
  // `GET /*` → index.html fallback returns 200 for a route it does not have, so
300
313
  // the status is healthy and the body is the app shell. A document navigation
301
314
  // answered with HTML is normal; an XHR asking for data and getting HTML is a
302
- // call that reached nothing. A redirected request is exempt because the mime
303
- // came from the hop it ended on: a fetch login that 302s to a page reads as
304
- // HTML while being a sign-in that worked.
305
- const swallowed = (f.sessionRequests ?? []).find(r => r.initiator === 'xhr' && !r.redirected && (r.mimeType ?? '').startsWith('text/html'));
315
+ // call that reached nothing. A fetch login that 302s to a page reads as HTML
316
+ // while being a sign-in that worked, hence the last-hop guard.
317
+ const swallowed = (f.sessionRequests ?? []).find(r => r.initiator === 'xhr'
318
+ && lastHopJudgesTheClient(r)
319
+ && (r.mimeType ?? '').startsWith('text/html'));
306
320
  if (swallowed) {
321
+ const landed = swallowed.finalPath ?? swallowed.path;
322
+ const hop = landed === swallowed.path ? '' : ` → \`${landed}\``;
323
+ const cause = swallowed.redirected ?
324
+ 'The call was bounced to a page, so the session the client carried is not one this '
325
+ + 'server accepts and the answer is the SPA shell.'
326
+ : 'The route is not mounted and the catch-all answered instead, so the client sees a '
327
+ + '200 it cannot parse.';
307
328
  return {
308
329
  outcome: 'fail',
309
- detail: `\`${swallowed.method} ${swallowed.path}\` → ${String(swallowed.status)} `
330
+ detail: `\`${swallowed.method} ${swallowed.path}\`${hop} → ${String(swallowed.status)} `
310
331
  + `${swallowed.mimeType ?? ''}: an XHR asked this server for data and got the SPA `
311
- + 'shell. The route is not mounted and the catch-all answered instead, so the client '
312
- + 'sees a 200 it cannot parse. No status check can see this.'
332
+ + `shell. ${cause} No status check can see this.`
313
333
  };
314
334
  }
315
- const { method, path: p, status, failed } = f.authRequest;
335
+ const { method, path: p, status, failed, redirected: authRedirected } = f.authRequest;
316
336
  if (failed || status === null || status < 200 || status >= 300) {
317
337
  return {
318
338
  outcome: 'skip',
@@ -323,6 +343,18 @@ export function judgeDeepSession(f) {
323
343
  }
324
344
  const signedIn = `signed in (\`${method} ${p}\` → ${status})`;
325
345
  if (!f.leftAuthWall) {
346
+ // A form sign-in that redirects lands its status on the hop it reached, so a
347
+ // 200 here is the landing page's, not a verdict on the credentials. Landing
348
+ // back on the wall is what a REJECTED password looks like, and the gate's one
349
+ // FAIL needs the server to have said yes.
350
+ if (authRedirected === true) {
351
+ return {
352
+ outcome: 'skip',
353
+ note: `the sign-in request (\`${method} ${p}\`) redirected, so its ${String(status)} `
354
+ + 'describes the page it landed on and not the credentials, and the client is '
355
+ + 'still on the wall — the authenticated half of the app was NOT observed'
356
+ };
357
+ }
326
358
  return {
327
359
  outcome: 'fail',
328
360
  detail: `${signedIn} but the client NEVER LEFT THE SIGN-IN WALL: the page is still `
@@ -669,6 +701,7 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
669
701
  const existing = requests.get(id);
670
702
  if (p.redirectResponse !== undefined && existing) {
671
703
  existing.finalUrl = String(req?.url ?? existing.finalUrl);
704
+ existing.redirected = true;
672
705
  }
673
706
  else {
674
707
  requests.set(id, {
@@ -679,6 +712,7 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
679
712
  status: null,
680
713
  mimeType: null,
681
714
  failed: false,
715
+ redirected: false,
682
716
  seq: nextSeq++
683
717
  });
684
718
  }
@@ -748,11 +782,12 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
748
782
  out.push({
749
783
  method: r.method,
750
784
  path: pathOf(r.url),
785
+ finalPath: pathOf(r.finalUrl),
751
786
  status: r.status,
752
787
  mimeType: r.mimeType,
753
788
  failed: r.failed,
754
789
  initiator: initiatorOf(r),
755
- redirected: r.finalUrl !== r.url,
790
+ redirected: r.redirected,
756
791
  phase: id === authId ? 'auth'
757
792
  : r.seq >= postSeq ? 'post'
758
793
  : 'pre'
@@ -813,8 +848,10 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
813
848
  ?? firstRequest(r => r.seq >= fillSeq && sameOrigin(r) && r.method !== 'GET' && r.type === 'Document');
814
849
  // An SSO sign-in addresses this app and ends on the provider, so it is absent
815
850
  // from the same-origin log above and "no request to our own origin" would
816
- // misname it.
817
- const offsiteSignIn = authId !== null ? null : (firstRequest(r => r.seq >= fillSeq
851
+ // misname it. The two windows are authId's, for the same reasons: anything the
852
+ // submit issued counts, and only a NAVIGATION counts back to the fill, where the
853
+ // background traffic it races would otherwise name a bogus identity provider.
854
+ const offsiteSignIn = authId !== null ? null : (firstRequest(r => (r.seq >= submitSeq || (r.seq >= fillSeq && r.type === 'Document'))
818
855
  && r.method !== 'GET'
819
856
  && addressedOrigin(r)
820
857
  && !sameOrigin(r)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.40.38",
3
+ "version": "0.40.40",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",