Skip to content

Commit 97eec82

Browse files
committed
fix: baseResolver cache issue
1 parent cf30dae commit 97eec82

2 files changed

Lines changed: 82 additions & 0 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { BaseResolver, createConfig, logger } from '@redocly/openapi-core';
2+
3+
import * as flowRunner from '../modules/flow-runner/index.js';
4+
import type * as loggerOutput from '../modules/logger-output/index.js';
5+
import { run } from '../run.js';
6+
7+
vi.mock('../modules/flow-runner/index.js', () => ({
8+
runTestFile: vi.fn(),
9+
}));
10+
11+
vi.mock('../modules/logger-output/index.js', async () => {
12+
const actual = await vi.importActual<typeof loggerOutput>('../modules/logger-output/index.js');
13+
return {
14+
...actual,
15+
displayErrors: vi.fn(),
16+
displaySummary: vi.fn(),
17+
};
18+
});
19+
20+
describe('run', () => {
21+
const defaultRunResult = {
22+
executedWorkflows: [],
23+
ctx: { noSecretsMasking: false, secretsSet: new Set() },
24+
};
25+
26+
beforeEach(() => {
27+
vi.clearAllMocks();
28+
vi.mocked(flowRunner.runTestFile).mockResolvedValue(defaultRunResult as any);
29+
});
30+
31+
it('clears the externalRefResolver cache between file runs so mutated parsed docs do not leak into the next file', async () => {
32+
const config = await createConfig({});
33+
const externalRefResolver = new BaseResolver(config.resolve);
34+
35+
externalRefResolver.cache.set('/seeded/before/run.yaml', Promise.resolve({} as any));
36+
37+
const seenCacheSizes: number[] = [];
38+
vi.mocked(flowRunner.runTestFile).mockImplementation(async ({ options }) => {
39+
seenCacheSizes.push(options.externalRefResolver?.cache.size ?? -1);
40+
options.externalRefResolver?.cache.set(`/leaked/${options.file}`, Promise.resolve({} as any));
41+
return defaultRunResult as any;
42+
});
43+
44+
await run({
45+
files: ['a.arazzo.yaml', 'b.arazzo.yaml', 'c.arazzo.yaml'],
46+
config,
47+
maxSteps: 2000,
48+
maxFetchTimeout: 40_000,
49+
requestFileLoader: { getFileBody: async () => new Blob() },
50+
logger,
51+
fetch,
52+
externalRefResolver,
53+
});
54+
55+
expect(seenCacheSizes).toEqual([0, 0, 0]);
56+
});
57+
58+
it('does not throw when externalRefResolver is not provided', async () => {
59+
const config = await createConfig({});
60+
61+
await expect(
62+
run({
63+
files: ['a.arazzo.yaml'],
64+
config,
65+
maxSteps: 2000,
66+
maxFetchTimeout: 40_000,
67+
requestFileLoader: { getFileBody: async () => new Blob() },
68+
logger,
69+
fetch,
70+
})
71+
).resolves.toBeDefined();
72+
});
73+
});

packages/respect-core/src/run.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ export async function run(options: RespectOptions): Promise<RunFileResult[]> {
4747
const runAllFilesResult = [];
4848

4949
for (const path of files) {
50+
// The runner mutates the parsed Arazzo document in place (adds `workflow.time`
51+
// and `step.checks`). When `externalRefResolver` is shared across files, its
52+
// cache would return those mutated documents to the lint pass of subsequent
53+
// files (e.g. ones previously executed via `$sourceDescriptions.*` workflow
54+
// references), producing false "property is not expected here" struct errors.
55+
// Clear the cache between files so each file starts from freshly parsed
56+
// documents while preserving the resolver's transport config (proxy, mTLS, etc).
57+
options.externalRefResolver?.cache.clear();
58+
5059
const result = await runFile({
5160
options: { ...options, file: path },
5261
startedAt: performance.now(),

0 commit comments

Comments
 (0)