|
15 | 15 | */ |
16 | 16 |
|
17 | 17 | import {strict as assert} from 'assert'; |
| 18 | +import {AsyncLocalStorage} from 'node:async_hooks'; |
18 | 19 | import {fork} from 'node:child_process'; |
19 | 20 | import {join} from 'node:path'; |
20 | 21 |
|
| 22 | +import {satisfies} from 'semver'; |
| 23 | + |
21 | 24 | import {isAsyncContextFrameActive} from '../src/async-context-frame'; |
22 | 25 |
|
| 26 | +const addon = require('node-gyp-build')(join(__dirname, '..', '..')) as { |
| 27 | + cpedMapContains(key?: unknown, value?: unknown): boolean; |
| 28 | +}; |
| 29 | + |
23 | 30 | const CHILD = join(__dirname, 'async-context-frame-child.js'); |
24 | 31 |
|
25 | 32 | const major = Number(process.versions.node.split('.')[0]); |
| 33 | +// ACF landed in 22.7.0, so the opt-in routes are gated on that, not on major 22. |
| 34 | +const hasAcfSupport = satisfies(process.versions.node, '>=22.7.0'); |
26 | 35 |
|
27 | 36 | interface ChildReport { |
28 | 37 | active: boolean; |
@@ -79,7 +88,7 @@ describe('isAsyncContextFrameActive', () => { |
79 | 88 | }); |
80 | 89 |
|
81 | 90 | it('reports it inactive when Node has no support for it', async function () { |
82 | | - if (major >= 22) return this.skip(); |
| 91 | + if (hasAcfSupport) return this.skip(); |
83 | 92 | const {active} = await probeChild(); |
84 | 93 | assert.equal(active, false); |
85 | 94 | }); |
@@ -107,15 +116,83 @@ describe('isAsyncContextFrameActive', () => { |
107 | 116 | }); |
108 | 117 |
|
109 | 118 | it('reports it active when NODE_OPTIONS turns it on', async function () { |
110 | | - // The mirror image, on the other Node line: 22 and 23 accept the flag in |
| 119 | + // The mirror image, on the other Node line: 22.7.0 through 23 accept the flag in |
111 | 120 | // NODE_OPTIONS (24 rejects it outright), again without it reaching execArgv, |
112 | 121 | // so inferring from execArgv concludes ACF is off when it is on — and the |
113 | 122 | // caller refuses to run in a process that would have worked. |
114 | | - if (major < 22 || major >= 24) return this.skip(); |
| 123 | + if (!hasAcfSupport || major >= 24) return this.skip(); |
115 | 124 | const {active, execArgv} = await probeChild({ |
116 | 125 | nodeOptions: '--experimental-async-context-frame', |
117 | 126 | }); |
118 | 127 | assert.deepEqual(execArgv, []); |
119 | 128 | assert.equal(active, true); |
120 | 129 | }); |
121 | 130 | }); |
| 131 | + |
| 132 | +// The detection asks whether the running storage is bound to its own store, |
| 133 | +// not merely whether the CPED slot holds a Map. These pin that difference: |
| 134 | +// without them, weakening the helper to a bare IsMap check would still pass |
| 135 | +// every test above. |
| 136 | +describe('cpedMapContains', () => { |
| 137 | + beforeEach(function () { |
| 138 | + // With ACF off nothing writes the slot, so every answer here is false for |
| 139 | + // an uninteresting reason. The routes that discriminate on/off are covered |
| 140 | + // by the child-process cases above. |
| 141 | + if (!isAsyncContextFrameActive()) this.skip(); |
| 142 | + }); |
| 143 | + |
| 144 | + it('finds the running storage bound to its store', () => { |
| 145 | + const als = new AsyncLocalStorage<object>(); |
| 146 | + const store = {}; |
| 147 | + let found = false; |
| 148 | + als.run(store, () => { |
| 149 | + found = addon.cpedMapContains(als, store); |
| 150 | + }); |
| 151 | + als.disable(); |
| 152 | + assert.equal(found, true); |
| 153 | + }); |
| 154 | + |
| 155 | + it('does not match a foreign key', () => { |
| 156 | + // CPED is a general embedder slot. Another native addon storing a Map there |
| 157 | + // must not be able to answer for us, which is the false positive an IsMap |
| 158 | + // check would admit. |
| 159 | + const als = new AsyncLocalStorage<object>(); |
| 160 | + const store = {}; |
| 161 | + let found = true; |
| 162 | + als.run(store, () => { |
| 163 | + found = addon.cpedMapContains(new AsyncLocalStorage<object>(), store); |
| 164 | + }); |
| 165 | + als.disable(); |
| 166 | + assert.equal(found, false); |
| 167 | + }); |
| 168 | + |
| 169 | + it('does not match a different value for the right key', () => { |
| 170 | + const als = new AsyncLocalStorage<object>(); |
| 171 | + let found = true; |
| 172 | + als.run({}, () => { |
| 173 | + found = addon.cpedMapContains(als, {}); |
| 174 | + }); |
| 175 | + als.disable(); |
| 176 | + assert.equal(found, false); |
| 177 | + }); |
| 178 | + |
| 179 | + it('is false outside any run', () => { |
| 180 | + const als = new AsyncLocalStorage<object>(); |
| 181 | + const store = {}; |
| 182 | + als.run(store, () => {}); |
| 183 | + als.disable(); |
| 184 | + assert.equal(addon.cpedMapContains(als, store), false); |
| 185 | + }); |
| 186 | + |
| 187 | + it('is false when called without a key and value', () => { |
| 188 | + // An absent key reads as undefined; so would a missing expected value, so |
| 189 | + // a malformed call must not compare the two and report success. |
| 190 | + const als = new AsyncLocalStorage<object>(); |
| 191 | + let found = true; |
| 192 | + als.run({}, () => { |
| 193 | + found = addon.cpedMapContains(); |
| 194 | + }); |
| 195 | + als.disable(); |
| 196 | + assert.equal(found, false); |
| 197 | + }); |
| 198 | +}); |
0 commit comments