@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 : ({
|
|
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
|
-
&&
|
|
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}
|
|
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
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
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}
|
|
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
|
-
+
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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",
|