@mjasnikovs/pi-task 0.40.38 → 0.40.39

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,10 +312,11 @@ 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) {
307
321
  return {
308
322
  outcome: 'fail',
@@ -312,7 +326,7 @@ export function judgeDeepSession(f) {
312
326
  + 'sees a 200 it cannot parse. No status check can see this.'
313
327
  };
314
328
  }
315
- const { method, path: p, status, failed } = f.authRequest;
329
+ const { method, path: p, status, failed, redirected: authRedirected } = f.authRequest;
316
330
  if (failed || status === null || status < 200 || status >= 300) {
317
331
  return {
318
332
  outcome: 'skip',
@@ -323,6 +337,18 @@ export function judgeDeepSession(f) {
323
337
  }
324
338
  const signedIn = `signed in (\`${method} ${p}\` → ${status})`;
325
339
  if (!f.leftAuthWall) {
340
+ // A form sign-in that redirects lands its status on the hop it reached, so a
341
+ // 200 here is the landing page's, not a verdict on the credentials. Landing
342
+ // back on the wall is what a REJECTED password looks like, and the gate's one
343
+ // FAIL needs the server to have said yes.
344
+ if (authRedirected === true) {
345
+ return {
346
+ outcome: 'skip',
347
+ note: `the sign-in request (\`${method} ${p}\`) redirected, so its ${String(status)} `
348
+ + 'describes the page it landed on and not the credentials, and the client is '
349
+ + 'still on the wall — the authenticated half of the app was NOT observed'
350
+ };
351
+ }
326
352
  return {
327
353
  outcome: 'fail',
328
354
  detail: `${signedIn} but the client NEVER LEFT THE SIGN-IN WALL: the page is still `
@@ -748,6 +774,7 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
748
774
  out.push({
749
775
  method: r.method,
750
776
  path: pathOf(r.url),
777
+ finalPath: pathOf(r.finalUrl),
751
778
  status: r.status,
752
779
  mimeType: r.mimeType,
753
780
  failed: r.failed,
@@ -813,9 +840,12 @@ export async function driveSession(cdp, { url, credentials, judge, quietMs }) {
813
840
  ?? firstRequest(r => r.seq >= fillSeq && sameOrigin(r) && r.method !== 'GET' && r.type === 'Document');
814
841
  // An SSO sign-in addresses this app and ends on the provider, so it is absent
815
842
  // from the same-origin log above and "no request to our own origin" would
816
- // misname it.
843
+ // misname it. Document-only for the same reason the authId fallback is: a
844
+ // telemetry beacon the fill races that happens to end off-origin would otherwise
845
+ // name a bogus identity provider.
817
846
  const offsiteSignIn = authId !== null ? null : (firstRequest(r => r.seq >= fillSeq
818
847
  && r.method !== 'GET'
848
+ && r.type === 'Document'
819
849
  && addressedOrigin(r)
820
850
  && !sameOrigin(r)
821
851
  && r.finalUrl.startsWith('http')));
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.39",
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",