Skip to content

Commit 215316b

Browse files
authored
Merge pull request #428 from playfulart/fix-docket-upload-case-id-misattribution
fix(district): stop attributing docket uploads to a stale cached case id
2 parents 8329d69 + 9d02f4d commit 215316b

4 files changed

Lines changed: 226 additions & 7 deletions

File tree

CHANGES.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ Changes:
1111
- None yet
1212

1313
Fixes:
14-
- None yet
14+
- Stop attributing district docket report uploads to the per-tab cached
15+
case id when the page context yields none: derive the id from the
16+
sheet's own goDLS document links first. The cached id can belong to a
17+
different case viewed earlier in the same tab, which uploaded one
18+
case's entire docket sheet under another case's `pacer_case_id` and
19+
merged it into the wrong archive docket.
1520

1621
For developers:
1722
- Nothing yet

spec/ContentDelegateSpec.js

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,27 @@ describe('The ContentDelegate class', function () {
508508
await cd.handleDocketDisplayPage();
509509
expect(dispatchBackgroundFetch).not.toHaveBeenCalled();
510510
});
511+
512+
it('when a fresh tab has no storage entry at all', async function () {
513+
const cd = new ContentDelegate(
514+
tabId,
515+
docketDisplayUrl,
516+
undefined,
517+
'canb',
518+
undefined,
519+
undefined,
520+
[]
521+
);
522+
// A tab that never stored anything yields undefined, not {}.
523+
window.chrome.storage.local.get = jasmine
524+
.createSpy()
525+
.and.callFake((_, cb) => {
526+
cb({ options: { recap_enabled: true } });
527+
});
528+
dispatchBackgroundFetch = jasmine.createSpy();
529+
await cd.handleDocketDisplayPage();
530+
expect(dispatchBackgroundFetch).not.toHaveBeenCalled();
531+
});
511532
});
512533

513534
describe('when the history state is already set', function () {
@@ -596,6 +617,16 @@ describe('The ContentDelegate class', function () {
596617
});
597618

598619
describe('when the docket page is not an interstitial page', function () {
620+
const makeLink = (caseId, docId) => {
621+
const a = document.createElement('a');
622+
a.href = `https://ecf.canb.uscourts.gov/doc1/${docId}`;
623+
a.setAttribute(
624+
'onclick',
625+
`goDLS('/doc1/${docId}','${caseId}','5','','','1','','','');` +
626+
'return(false);'
627+
);
628+
return a;
629+
};
599630
beforeEach(function () {
600631
clearDocumentBody();
601632
table = document.createElement('table');
@@ -670,6 +701,133 @@ describe('The ContentDelegate class', function () {
670701
expect(button.length).toBe(1);
671702
});
672703

704+
it('prefers the goDLS case id over a stale tab id', async function () {
705+
// The tab's cached caseId ('531591' in the storage mock)
706+
// belongs to another case; the page's goDLS links must win.
707+
const link = makeLink('177277', '034031424909');
708+
const cd = new ContentDelegate(
709+
tabId,
710+
docketDisplayUrl,
711+
docketDisplayPath,
712+
'canb',
713+
undefined, // no case id derived from url/referrer/inputs
714+
undefined,
715+
[link]
716+
);
717+
dispatchBackgroundNotifier = jasmine.createSpy();
718+
dispatchBackgroundFetch = jasmine
719+
.createSpy()
720+
.and.callFake(fakeBackgroundFetch);
721+
spyOn(history, 'replaceState');
722+
await cd.handleDocketDisplayPage();
723+
expect(dispatchBackgroundFetch).toHaveBeenCalledWith(
724+
jasmine.objectContaining({
725+
action: 'upload',
726+
data: jasmine.objectContaining({ pacer_case_id: '177277' }),
727+
})
728+
);
729+
expect(dispatchBackgroundFetch).not.toHaveBeenCalledWith(
730+
jasmine.objectContaining({
731+
data: jasmine.objectContaining({ pacer_case_id: '531591' }),
732+
})
733+
);
734+
// the stale cached id is corrected for later pages in this tab
735+
expect(window.chrome.storage.local.set).toHaveBeenCalledWith(
736+
jasmine.objectContaining({
737+
1234: jasmine.objectContaining({ caseId: '177277' }),
738+
}),
739+
jasmine.any(Function)
740+
);
741+
});
742+
743+
it('never overrides a context-derived case id', async function () {
744+
// On a consolidated MEMBER docket the document links point at
745+
// the LEAD case, so an id the page context provided (url,
746+
// inputs, referrer) always beats the goDLS majority.
747+
const link = makeLink('177277', '034031424909'); // lead case
748+
const cd = new ContentDelegate(
749+
tabId,
750+
docketDisplayUrl,
751+
docketDisplayPath,
752+
'canb',
753+
'531591', // the member case, from the page context
754+
undefined,
755+
[link]
756+
);
757+
dispatchBackgroundNotifier = jasmine.createSpy();
758+
dispatchBackgroundFetch = jasmine
759+
.createSpy()
760+
.and.callFake(fakeBackgroundFetch);
761+
spyOn(history, 'replaceState');
762+
await cd.handleDocketDisplayPage();
763+
expect(dispatchBackgroundFetch).toHaveBeenCalledWith(
764+
jasmine.objectContaining({
765+
action: 'upload',
766+
data: jasmine.objectContaining({ pacer_case_id: '531591' }),
767+
})
768+
);
769+
});
770+
771+
it('uses the majority goDLS id on merged sheets', async function () {
772+
// Consolidated/MDL dockets legitimately link member cases'
773+
// documents — a stray link must not outvote the sheet.
774+
const cd = new ContentDelegate(
775+
tabId,
776+
docketDisplayUrl,
777+
docketDisplayPath,
778+
'canb',
779+
undefined,
780+
undefined,
781+
[
782+
makeLink('177277', '034031424909'),
783+
makeLink('177277', '034031424910'),
784+
makeLink('177277', '034031424911'),
785+
makeLink('999999', '034031424912'), // stray member-case link
786+
]
787+
);
788+
dispatchBackgroundNotifier = jasmine.createSpy();
789+
dispatchBackgroundFetch = jasmine
790+
.createSpy()
791+
.and.callFake(fakeBackgroundFetch);
792+
spyOn(history, 'replaceState');
793+
await cd.handleDocketDisplayPage();
794+
expect(dispatchBackgroundFetch).toHaveBeenCalledWith(
795+
jasmine.objectContaining({
796+
action: 'upload',
797+
data: jasmine.objectContaining({ pacer_case_id: '177277' }),
798+
})
799+
);
800+
});
801+
802+
it('falls back when the goDLS ids are tied', async function () {
803+
// Equal votes for two ids: don't guess; use the cached id
804+
// ('531591' in the storage mock) as before.
805+
const cd = new ContentDelegate(
806+
tabId,
807+
docketDisplayUrl,
808+
docketDisplayPath,
809+
'canb',
810+
undefined,
811+
undefined,
812+
[
813+
makeLink('177277', '034031424909'),
814+
makeLink('888888', '034031424910'),
815+
]
816+
);
817+
dispatchBackgroundNotifier = jasmine.createSpy();
818+
dispatchBackgroundFetch = jasmine
819+
.createSpy()
820+
.and.callFake(fakeBackgroundFetch);
821+
spyOn(history, 'replaceState');
822+
await cd.handleDocketDisplayPage();
823+
expect(dispatchBackgroundFetch).toHaveBeenCalledWith(
824+
jasmine.objectContaining({
825+
action: 'upload',
826+
data: jasmine.objectContaining({ pacer_case_id: '531591' }),
827+
})
828+
);
829+
});
830+
673831
it('calls uploadDocket and responds to a negative result', async function () {
674832
const cd = docketDisplayContentDelegate;
675833
dispatchBackgroundNotifier = jasmine.createSpy();

src/content_delegate.js

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -269,12 +269,30 @@ ContentDelegate.prototype.handleDocketDisplayPage = async function () {
269269
// check if appellate
270270
// let isAppellate = PACER.isAppellateCourt(this.court);
271271

272-
// if the content_delegate didn't pull the case Id on initialization,
273-
// check the page for a lead case dktrpt url.
274-
const tabStorage = await getItemsFromStorage(this.tabId);
275-
this.pacer_case_id = this.pacer_case_id
276-
? this.pacer_case_id
277-
: tabStorage.caseId;
272+
// If initialization yielded no case id, derive it from the page itself:
273+
// the sheet's goDLS document links carry it. The per-tab cached id is
274+
// the LAST resort — it can belong to a different case viewed earlier in
275+
// this tab. An id the page context already provided is never overridden:
276+
// consolidated member dockets legitimately link the lead case's
277+
// documents, so the goDLS plurality only speaks when nothing else does.
278+
if (!this.pacer_case_id) {
279+
const pageCaseId = PACER.getCaseIdFromDocketDisplayLinks(this.links);
280+
const tabStorage = (await getItemsFromStorage(this.tabId)) || {};
281+
if (pageCaseId) {
282+
if (tabStorage.caseId && tabStorage.caseId !== pageCaseId) {
283+
console.warn(
284+
`RECAP: Ignoring cached case id ${tabStorage.caseId}: the ` +
285+
`docket's own links say it belongs to case ${pageCaseId}.`
286+
);
287+
}
288+
this.pacer_case_id = pageCaseId;
289+
// Seed the tab's cached case ID so a later docket-report load in this
290+
// same tab starts with the correct case ID instead of a stale one.
291+
await saveCaseIdinTabStorage({ tabId: this.tabId }, pageCaseId);
292+
} else {
293+
this.pacer_case_id = tabStorage.caseId;
294+
}
295+
}
278296

279297
// If we don't have this.pacer_case_id at this point, punt.
280298
if (!this.pacer_case_id) return;

src/pacer.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,44 @@ let PACER = {
462462
}
463463
},
464464

465+
// Returns the most common (plurality) `de_caseid` across the page's
466+
// document links' `goDLS()` handlers. Plurality is intentional because
467+
// consolidated/MDL docket sheets can legitimately link documents from
468+
// multiple member cases. Returns `undefined` when there is no `goDLS`
469+
// evidence or when the most common case IDs are tied.
470+
getCaseIdFromDocketDisplayLinks: function (links) {
471+
// Count how many document links point to each case ID. The case ID
472+
// appearing most often is likely the case represented by the docket page.
473+
const caseIdCounts = {};
474+
for (const link of Array.from(links)) {
475+
if (!PACER.isDoc1Url(link.href)) continue;
476+
477+
const goDLS = PACER.parseGoDLSFunction(link.getAttribute('onclick'));
478+
if (goDLS && goDLS.de_caseid && goDLS.de_caseid !== '0') {
479+
caseIdCounts[goDLS.de_caseid] =
480+
(caseIdCounts[goDLS.de_caseid] ?? 0) + 1;
481+
}
482+
}
483+
484+
// Convert the counts object into an array of [caseId, count] pairs
485+
// and sort descending so the most frequently linked case is first.
486+
const sortedCaseIds = Object.entries(caseIdCounts).sort(
487+
(a, b) => b[1] - a[1]
488+
);
489+
490+
// No valid case IDs were found.
491+
if (sortedCaseIds.length === 0) return undefined;
492+
493+
const [mostLinkedCaseId, mostLinkedCount] = sortedCaseIds[0];
494+
const [, secondMostLinkedCount] = sortedCaseIds[1] ?? [];
495+
496+
// If multiple case IDs have the same highest count, we cannot determine
497+
// which case is the correct one, so avoid returning an arbitrary result.
498+
const hasTieForFirst = mostLinkedCount === secondMostLinkedCount;
499+
500+
return hasTieForFirst ? undefined : mostLinkedCaseId;
501+
},
502+
465503
// Given a URL that satisfies isDocketQueryUrl, gets its case number.
466504
getCaseNumberFromUrls: function (urls) {
467505
// Iterate over an array of URLs and get the case number from the

0 commit comments

Comments
 (0)