Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ci-deflake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"myst-cli": patch
---

DOI resolver tests run against recorded doi.org fixtures instead of the live network (set TEST_LIVE_DOI=1 to also run the live variants), and the end-to-end runner supports per-case vitest retries for the known-environmental Jupyter execution tests.
134 changes: 95 additions & 39 deletions packages/myst-cli/src/transforms/doi.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, expect, it } from 'vitest';
import type { ISession } from '../session/types';
import { Session } from '../session';
import { resolveDOIAsBibTeX, resolveDOIAsCSLJSON } from './dois';
import fixtures from './fixtures/doi.json';

const PRIESTLEY_1972_CSL_JSON = [
{
Expand Down Expand Up @@ -48,45 +50,99 @@ const BARTELS_1997_CSL_JSON = [
volume: '18',
},
];
describe.each([
{ resolver: resolveDOIAsBibTeX, name: 'BibTeX' },
{ resolver: resolveDOIAsCSLJSON, name: 'CSL-JSON' },
])('DOI Resolvers for $name', ({ resolver, name }) => {
it('short DOI resolves', async () => {
const data = await resolver(new Session(), 'https://doi.org/cr3qwn');
expect(data).toMatchObject(PRIESTLEY_1972_CSL_JSON);
});
it('url encoded DOI resolves', async () => {
const data = await resolver(
new Session(),
'https://doi.org/10.1175%2F1520-0493%281972%29100%3C0081%3AOTAOSH%3E2.3.CO%3B2',
);
expect(data).toMatchObject(PRIESTLEY_1972_CSL_JSON);
});
it('markdown link with strange characters resolves', async () => {
const data = await resolver(
new Session(),
'https://doi.org/10.1175/1520-0493(1972)100<0081:OTAOSH>2.3.CO;2',
);
expect(data).toMatchObject(PRIESTLEY_1972_CSL_JSON);
});
it('markdown link with strange characters resolves', async () => {
const data = await resolver(
new Session(),
'https://doi.org/10.1002/(SICI)1096-987X(199709)18:12%3C1450::AID-JCC3%3E3.0.CO;2-I',
);
// Both of these are different depending on the resolver
// The URL is encoded, the ISSN is actually different?!
delete data?.[0].URL;
delete data?.[0].ISSN;
const dateParts = data?.[0].issued?.['date-parts']?.[0];
if (dateParts && dateParts.length > 1) {
// Remove the date-parts for the month.
// The month `sept` is sometimes returned by crossref but only `sep` is parsed by citation-js.
// This started showing up in April 2026.
// For this test, just ensure the year is parsed correctly, which is what is shown in our UI and citation renderers.
dateParts.pop();

/**
* A session whose fetch serves recorded doi.org responses (see fixtures/doi.json),
* so these tests do not depend on live doi.org availability — the parsing
* pipeline (content negotiation, BibTeX/CSL-JSON parsing) is still exercised.
*
* The fixture is selected by DOI substring; the URL must still be consumable
* by fetch (i.e. a valid URL after WHATWG normalization), which preserves the
* "strange characters" coverage of the original live tests.
*/
function mockDOISession(): ISession {
const mockFetch = async (input: URL | RequestInfo, init?: RequestInit) => {
// Throws on malformed URLs, like real fetch would
const url = new URL(typeof input === 'string' ? input : ((input as Request).url ?? input));
const doiPath = decodeURIComponent(url.pathname).toLowerCase();
const fixture =
doiPath.includes('10.1175') || doiPath.includes('cr3qwn')
? fixtures.priestley
: doiPath.includes('10.1002')
? fixtures.bartels
: undefined;
if (!fixture) return { ok: false } as Response;
const accept = new Headers(init?.headers as HeadersInit).get('Accept') ?? '';
if (accept.includes('csl+json')) {
return {
ok: true,
json: async () => fixture.csl,
text: async () => JSON.stringify(fixture.csl),
} as Response;
}
expect(data).toMatchObject(BARTELS_1997_CSL_JSON);
return {
ok: true,
text: async () => fixture.bibtex,
json: async () => JSON.parse(fixture.bibtex),
} as Response;
};
// A real Session keeps the stub aligned with the full ISession surface;
// only fetch is overridden
const session = new Session();
session.fetch = mockFetch as ISession['fetch'];
return session;
}

// Set TEST_LIVE_DOI=1 to run the same cases against live doi.org (e.g. to
// refresh fixtures or check for upstream data drift); CI uses the mock only.
const sessions: { name: string; makeSession: () => ISession }[] = [
{ name: 'recorded', makeSession: mockDOISession },
];
if (process.env.TEST_LIVE_DOI === '1') {
sessions.push({ name: 'live doi.org', makeSession: () => new Session() });
}

sessions.forEach(({ name: sessionName, makeSession }) => {
describe.each([
{ resolver: resolveDOIAsBibTeX, name: 'BibTeX' },
{ resolver: resolveDOIAsCSLJSON, name: 'CSL-JSON' },
])(`DOI Resolvers for $name (${sessionName})`, ({ resolver }) => {
it('short DOI resolves', async () => {
const data = await resolver(makeSession(), 'https://doi.org/cr3qwn');
expect(data).toMatchObject(PRIESTLEY_1972_CSL_JSON);
});
it('url encoded DOI resolves', async () => {
const data = await resolver(
makeSession(),
'https://doi.org/10.1175%2F1520-0493%281972%29100%3C0081%3AOTAOSH%3E2.3.CO%3B2',
);
expect(data).toMatchObject(PRIESTLEY_1972_CSL_JSON);
});
it('markdown link with strange characters resolves', async () => {
const data = await resolver(
makeSession(),
'https://doi.org/10.1175/1520-0493(1972)100<0081:OTAOSH>2.3.CO;2',
);
expect(data).toMatchObject(PRIESTLEY_1972_CSL_JSON);
});
it('markdown link with strange characters resolves (sici DOI)', async () => {
const data = await resolver(
makeSession(),
'https://doi.org/10.1002/(SICI)1096-987X(199709)18:12%3C1450::AID-JCC3%3E3.0.CO;2-I',
);
// Both of these are different depending on the resolver
// The URL is encoded, the ISSN is actually different?!
delete data?.[0].URL;
delete data?.[0].ISSN;
const dateParts = data?.[0].issued?.['date-parts']?.[0];
if (dateParts && dateParts.length > 1) {
// Remove the date-parts for the month.
// The month `sept` is sometimes returned by crossref but only `sep` is parsed by citation-js.
// This started showing up in April 2026.
// For this test, just ensure the year is parsed correctly, which is what is shown in our UI and citation renderers.
dateParts.pop();
}
expect(data).toMatchObject(BARTELS_1997_CSL_JSON);
});
});
});
Loading