diff --git a/.gitignore b/.gitignore
index ea00b8f0..657bcd13 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ out
 coverage
 test/**/dist
 test/**/actual.js
+test/unit/cjs-querystring/who?what?idk!.js
diff --git a/test/unit.test.js b/test/unit.test.js
index ce269b38..e774439c 100644
--- a/test/unit.test.js
+++ b/test/unit.test.js
@@ -1,23 +1,39 @@
 const fs = require('fs');
-const { join, relative } = require('path');
+const { join, relative, sep } = require('path');
 const { nodeFileTrace } = require('../out/node-file-trace');
 
 global._unit = true;
 
-const skipOnWindows = ['yarn-workspaces', 'yarn-workspaces-base-root', 'yarn-workspace-esm', 'asset-symlink', 'require-symlink'];
+const skipOnWindows = ['yarn-workspaces', 'yarn-workspaces-base-root', 'yarn-workspace-esm', 'asset-symlink', 'require-symlink', 'cjs-querystring'];
 const unitTestDirs = fs.readdirSync(join(__dirname, 'unit'));
 const unitTests = [
-  ...unitTestDirs.map(testName => ({testName, isRoot: false})),
   ...unitTestDirs.map(testName => ({testName, isRoot: true})),
+  ...unitTestDirs.map(testName => ({testName, isRoot: false})),
 ];
 
 for (const { testName, isRoot } of unitTests) {
   const testSuffix = `${testName} from ${isRoot ? 'root' : 'cwd'}`;
-  if (process.platform === 'win32' && (isRoot || skipOnWindows.includes(testName))) {
-    console.log(`Skipping unit test on Windows: ${testSuffix}`);
-    continue;
-  };
   const unitPath = join(__dirname, 'unit', testName);
+
+  if (process.platform === 'win32') { 
+    if (isRoot || skipOnWindows.includes(testName)) {
+      console.log(`Skipping unit test on Windows: ${testSuffix}`);
+      continue;
+    }
+  } else {
+    if (testName === 'cjs-querystring') {
+      // Create (a git-ignored copy of) the file we need, since committing it
+      // breaks CI on Windows. See https://github.com/vercel/nft/pull/322.
+      const currentFilepath = join(unitPath, 'noPunctuation', 'whowhatidk.js');
+      const newFilepath = currentFilepath.replace(
+        'noPunctuation' + sep + 'whowhatidk.js',
+        'who?what?idk!.js'
+      );
+      if (!fs.existsSync(newFilepath)) {
+        fs.copyFileSync(currentFilepath, newFilepath);
+      }
+    }
+  }
   
   it(`should correctly trace ${testSuffix}`, async () => {
 
@@ -41,6 +57,25 @@ for (const { testName, isRoot } of unitTests) {
           return null
         }
       }
+
+      // mock an in-memory module store (such as webpack's) where the same filename with
+      // two different querystrings can correspond to two different modules, one importing
+      // the other
+      if (testName === 'querystring-self-import') {
+        if (id.endsWith('input.js') || id.endsWith('base.js') || id.endsWith('dep.js')) {
+          return fs.readFileSync(id).toString()
+        }
+
+        if (id.endsWith('base.js?__withQuery')) {
+          return `
+            import * as origBase from './base';
+            export const dogs = origBase.dogs.concat('Cory', 'Bodhi');
+            export const cats = origBase.cats.concat('Teaberry', 'Sassafras', 'Persephone');
+            export const rats = origBase.rats;
+          `;
+        }
+      }
+
       return this.constructor.prototype.readFile.apply(this, arguments);
     });
 
@@ -67,8 +102,8 @@ for (const { testName, isRoot } of unitTests) {
       if (testName === 'multi-input') {
         inputFileNames.push('input-2.js', 'input-3.js', 'input-4.js');
       }
-      
-      const { fileList, reasons } = await nodeFileTrace(
+
+      const { fileList, reasons, warnings } = await nodeFileTrace(
         inputFileNames.map(file => join(unitPath, file)), 
         {
           base: isRoot ? '/' : `${__dirname}/../`,
@@ -193,7 +228,7 @@ for (const { testName, isRoot } of unitTests) {
         expect(sortedFileList).toEqual(expected);
       }
       catch (e) {
-        console.warn(reasons);
+        console.warn({reasons, warnings});
         fs.writeFileSync(join(unitPath, 'actual.js'), JSON.stringify(sortedFileList, null, 2));
         throw e;
       }
diff --git a/test/unit/cjs-querystring/input.js b/test/unit/cjs-querystring/input.js
new file mode 100644
index 00000000..5a1baf7e
--- /dev/null
+++ b/test/unit/cjs-querystring/input.js
@@ -0,0 +1,7 @@
+// Test that CJS files treat question marks in filenames as any other character,
+// matching Node behavior
+
+// https://www.youtube.com/watch?v=2ve20PVNZ18
+
+const baseball = require('./who?what?idk!');
+console.log(baseball.players);
diff --git a/test/unit/cjs-querystring/noPunctuation/whowhatidk.js b/test/unit/cjs-querystring/noPunctuation/whowhatidk.js
new file mode 100644
index 00000000..4c4c4b72
--- /dev/null
+++ b/test/unit/cjs-querystring/noPunctuation/whowhatidk.js
@@ -0,0 +1,12 @@
+module.exports = {
+  players: {
+    first: 'Who',
+    second: 'What',
+    third: "I Don't Know",
+    left: 'Why',
+    center: 'Because',
+    pitcher: 'Tomorrow',
+    catcher: 'Today',
+    shortstop: "I Don't Give a Damn!",
+  },
+};
diff --git a/test/unit/cjs-querystring/output.js b/test/unit/cjs-querystring/output.js
new file mode 100644
index 00000000..296a204b
--- /dev/null
+++ b/test/unit/cjs-querystring/output.js
@@ -0,0 +1,5 @@
+[
+  "package.json",
+  "test/unit/cjs-querystring/input.js",
+  "test/unit/cjs-querystring/who?what?idk!.js"
+]
diff --git a/test/unit/esm-querystring-mjs/animalFacts/aardvark.mjs b/test/unit/esm-querystring-mjs/animalFacts/aardvark.mjs
new file mode 100644
index 00000000..a2efff8a
--- /dev/null
+++ b/test/unit/esm-querystring-mjs/animalFacts/aardvark.mjs
@@ -0,0 +1,4 @@
+import { numSpecies } from "./bear.mjs?beaver?bison";
+console.log(`There are ${numSpecies} species of bears.`);
+
+export const food = "termites";
diff --git a/test/unit/esm-querystring-mjs/animalFacts/bear.mjs b/test/unit/esm-querystring-mjs/animalFacts/bear.mjs
new file mode 100644
index 00000000..a8ee2dd0
--- /dev/null
+++ b/test/unit/esm-querystring-mjs/animalFacts/bear.mjs
@@ -0,0 +1,4 @@
+import * as cheetah from "./cheetah.mjs?cow=chipmunk";
+console.log(`Cheetahs can run ${cheetah.topSpeed} mph.`);
+
+export const numSpecies = 8;
diff --git a/test/unit/esm-querystring-mjs/animalFacts/cheetah.mjs b/test/unit/esm-querystring-mjs/animalFacts/cheetah.mjs
new file mode 100644
index 00000000..836b4ddf
--- /dev/null
+++ b/test/unit/esm-querystring-mjs/animalFacts/cheetah.mjs
@@ -0,0 +1 @@
+export const topSpeed = 65;
diff --git a/test/unit/esm-querystring-mjs/input.js b/test/unit/esm-querystring-mjs/input.js
new file mode 100644
index 00000000..a07ace77
--- /dev/null
+++ b/test/unit/esm-querystring-mjs/input.js
@@ -0,0 +1,6 @@
+// Test that querystrings of various forms get stripped from esm imports when those
+// imports contain the `.mjs` file extension
+
+import * as aardvark from "./animalFacts/aardvark.mjs?anteater";
+
+console.log(`Aardvarks eat ${aardvark.food}.`);
diff --git a/test/unit/esm-querystring-mjs/output.js b/test/unit/esm-querystring-mjs/output.js
new file mode 100644
index 00000000..95137ed7
--- /dev/null
+++ b/test/unit/esm-querystring-mjs/output.js
@@ -0,0 +1,7 @@
+[
+  "test/unit/esm-querystring-mjs/animalFacts/aardvark.mjs",
+  "test/unit/esm-querystring-mjs/animalFacts/bear.mjs",
+  "test/unit/esm-querystring-mjs/animalFacts/cheetah.mjs",
+  "test/unit/esm-querystring-mjs/input.js",
+  "test/unit/esm-querystring-mjs/package.json"
+]
\ No newline at end of file
diff --git a/test/unit/esm-querystring-mjs/package.json b/test/unit/esm-querystring-mjs/package.json
new file mode 100644
index 00000000..e986b24b
--- /dev/null
+++ b/test/unit/esm-querystring-mjs/package.json
@@ -0,0 +1,4 @@
+{
+  "private": true,
+  "type": "module"
+}
diff --git a/test/unit/esm-querystring/animalFacts/aardvark.js b/test/unit/esm-querystring/animalFacts/aardvark.js
new file mode 100644
index 00000000..4c497265
--- /dev/null
+++ b/test/unit/esm-querystring/animalFacts/aardvark.js
@@ -0,0 +1,4 @@
+import { numSpecies } from './bear?beaver?bison';
+console.log(`There are ${numSpecies} species of bears.`);
+
+export const food = 'termites';
diff --git a/test/unit/esm-querystring/animalFacts/bear.js b/test/unit/esm-querystring/animalFacts/bear.js
new file mode 100644
index 00000000..4578358b
--- /dev/null
+++ b/test/unit/esm-querystring/animalFacts/bear.js
@@ -0,0 +1,4 @@
+import * as cheetah from './cheetah?cow=chipmunk';
+console.log(`Cheetahs can run ${cheetah.topSpeed} mph.`);
+
+export const numSpecies = 8;
diff --git a/test/unit/esm-querystring/animalFacts/cheetah.js b/test/unit/esm-querystring/animalFacts/cheetah.js
new file mode 100644
index 00000000..836b4ddf
--- /dev/null
+++ b/test/unit/esm-querystring/animalFacts/cheetah.js
@@ -0,0 +1 @@
+export const topSpeed = 65;
diff --git a/test/unit/esm-querystring/input.js b/test/unit/esm-querystring/input.js
new file mode 100644
index 00000000..b0d51697
--- /dev/null
+++ b/test/unit/esm-querystring/input.js
@@ -0,0 +1,5 @@
+// Test that querystrings of various forms get stripped from esm imports
+
+import * as aardvark from './animalFacts/aardvark?anteater';
+
+console.log(`Aardvarks eat ${aardvark.food}.`);
diff --git a/test/unit/esm-querystring/output.js b/test/unit/esm-querystring/output.js
new file mode 100644
index 00000000..d03f7cb8
--- /dev/null
+++ b/test/unit/esm-querystring/output.js
@@ -0,0 +1,7 @@
+[
+  "test/unit/esm-querystring/animalFacts/aardvark.js",
+  "test/unit/esm-querystring/animalFacts/bear.js",
+  "test/unit/esm-querystring/animalFacts/cheetah.js",
+  "test/unit/esm-querystring/input.js",
+  "test/unit/esm-querystring/package.json"
+]
\ No newline at end of file
diff --git a/test/unit/esm-querystring/package.json b/test/unit/esm-querystring/package.json
new file mode 100644
index 00000000..e986b24b
--- /dev/null
+++ b/test/unit/esm-querystring/package.json
@@ -0,0 +1,4 @@
+{
+  "private": true,
+  "type": "module"
+}
diff --git a/test/unit/querystring-self-import/base.js b/test/unit/querystring-self-import/base.js
new file mode 100644
index 00000000..8f48a38a
--- /dev/null
+++ b/test/unit/querystring-self-import/base.js
@@ -0,0 +1,5 @@
+import * as dep from './dep';
+
+export const dogs = ['Charlie', 'Maisey'];
+export const cats = ['Piper'];
+export const rats = dep.rats;
diff --git a/test/unit/querystring-self-import/dep.js b/test/unit/querystring-self-import/dep.js
new file mode 100644
index 00000000..aaff193a
--- /dev/null
+++ b/test/unit/querystring-self-import/dep.js
@@ -0,0 +1 @@
+export const rats = ['Debra'];
diff --git a/test/unit/querystring-self-import/input.js b/test/unit/querystring-self-import/input.js
new file mode 100644
index 00000000..25f53ce9
--- /dev/null
+++ b/test/unit/querystring-self-import/input.js
@@ -0,0 +1,10 @@
+// Test that if a file and the same file with a querystring correspond to different
+// modules in memory, one can successfully import the other. The import chain
+// goes `input` (this file) -> `base?__withQuery` -> `base` -> `dep`, which means
+// that if `dep` shows up in `output`, we know that both `base?__withQuery` and
+// `base` have been loaded successfully.
+
+import * as baseWithQuery from './base?__withQuery';
+console.log('Dogs:', baseWithQuery.dogs);
+console.log('Cats:', baseWithQuery.cats);
+console.log('Rats:', baseWithQuery.rats);
diff --git a/test/unit/querystring-self-import/output.js b/test/unit/querystring-self-import/output.js
new file mode 100644
index 00000000..d6dc6b2c
--- /dev/null
+++ b/test/unit/querystring-self-import/output.js
@@ -0,0 +1,6 @@
+[
+  "package.json",
+  "test/unit/querystring-self-import/base.js",
+  "test/unit/querystring-self-import/dep.js",
+  "test/unit/querystring-self-import/input.js"
+]