fix: strip comments before scanning for missing imports (#676)

* fix: strip comments before scanning for missing imports

The scanForMissingImports regex matched require() and import() patterns
inside JSDoc comments, causing false-positive missing module detection.
A documented path like `require('./commands/proactive.js')` in a comment
was resolved from the wrong directory, marked as missing, then the global
onResolve handler intercepted ALL imports of that specifier — including
valid ones — replacing them with truthy noop stubs that broke runtime.

Strip block (/* */) and line (//) comments from source before scanning.

* fix: repair 10 pre-existing test failures

- promptIdentity.test.ts: define MACRO global (ISSUES_EXPLAINER etc.)
  for test mode where Bun.define build-time replacements aren't active
- context.test.ts: clear OPENAI_MODEL env var in each test — the user's
  environment (e.g. OPENAI_MODEL=github_copilot/gpt-5.4) polluted the
  provider-qualified lookup, returning wrong context windows
- openclaudePaths.test.ts: set CLAUDE_CONFIG_DIR to force .openclaude
  path when ~/.openclaude doesn't exist on the test machine
This commit is contained in:
Nourrisse Florian
2026-04-15 13:42:26 +02:00
committed by GitHub
parent 12dd3755c6
commit a00b7928de
4 changed files with 37 additions and 2 deletions

View File

@@ -367,9 +367,17 @@ export const SeverityNumber = {};
const full = pathMod.join(dir, ent.name) const full = pathMod.join(dir, ent.name)
if (ent.isDirectory()) { walk(full); continue } if (ent.isDirectory()) { walk(full); continue }
if (!/\.(ts|tsx)$/.test(ent.name)) continue if (!/\.(ts|tsx)$/.test(ent.name)) continue
const code: string = fs.readFileSync(full, 'utf-8') const rawCode: string = fs.readFileSync(full, 'utf-8')
const fileDir = pathMod.dirname(full) const fileDir = pathMod.dirname(full)
// Strip comments before scanning for imports/requires.
// The regex scanner matches require()/import() patterns
// inside JSDoc comments, causing false-positive missing
// module detection that breaks the build with noop stubs.
const code = rawCode
.replace(/\/\*[\s\S]*?\*\//g, '') // block comments
.replace(/\/\/.*$/gm, '') // line comments
// Collect static imports: import { X } from '...' // Collect static imports: import { X } from '...'
for (const m of code.matchAll(/import\s+(?:\{([^}]*)\}|(\w+))?\s*(?:,\s*\{([^}]*)\})?\s*from\s+['"](.*?)['"]/g)) { for (const m of code.matchAll(/import\s+(?:\{([^}]*)\}|(\w+))?\s*(?:,\s*\{([^}]*)\})?\s*from\s+['"](.*?)['"]/g)) {
checkAndRegister(m[4], fileDir, m[1] || m[3] || '') checkAndRegister(m[4], fileDir, m[1] || m[3] || '')

View File

@@ -1,5 +1,16 @@
import { afterEach, expect, test } from 'bun:test' import { afterEach, expect, test } from 'bun:test'
// MACRO is replaced at build time by Bun.define but not in test mode.
// Define it globally so tests that import modules using MACRO don't crash.
;(globalThis as Record<string, unknown>).MACRO = {
VERSION: '99.0.0',
DISPLAY_VERSION: '0.0.0-test',
BUILD_TIME: new Date().toISOString(),
ISSUES_EXPLAINER: 'report the issue at https://github.com/anthropics/claude-code/issues',
PACKAGE_URL: '@gitlawb/openclaude',
NATIVE_PACKAGE_URL: undefined,
}
import { getSystemPrompt, DEFAULT_AGENT_PROMPT } from './prompts.js' import { getSystemPrompt, DEFAULT_AGENT_PROMPT } from './prompts.js'
import { CLI_SYSPROMPT_PREFIXES, getCLISyspromptPrefix } from './system.js' import { CLI_SYSPROMPT_PREFIXES, getCLISyspromptPrefix } from './system.js'
import { CLAUDE_CODE_GUIDE_AGENT } from '../tools/AgentTool/built-in/claudeCodeGuideAgent.js' import { CLAUDE_CODE_GUIDE_AGENT } from '../tools/AgentTool/built-in/claudeCodeGuideAgent.js'

View File

@@ -9,6 +9,7 @@ import {
const originalEnv = { const originalEnv = {
CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI,
CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS, CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS,
OPENAI_MODEL: process.env.OPENAI_MODEL,
} }
afterEach(() => { afterEach(() => {
@@ -23,11 +24,17 @@ afterEach(() => {
process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS =
originalEnv.CLAUDE_CODE_MAX_OUTPUT_TOKENS originalEnv.CLAUDE_CODE_MAX_OUTPUT_TOKENS
} }
if (originalEnv.OPENAI_MODEL === undefined) {
delete process.env.OPENAI_MODEL
} else {
process.env.OPENAI_MODEL = originalEnv.OPENAI_MODEL
}
}) })
test('deepseek-chat uses provider-specific context and output caps', () => { test('deepseek-chat uses provider-specific context and output caps', () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.CLAUDE_CODE_USE_OPENAI = '1'
delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
delete process.env.OPENAI_MODEL
expect(getContextWindowForModel('deepseek-chat')).toBe(128_000) expect(getContextWindowForModel('deepseek-chat')).toBe(128_000)
expect(getModelMaxOutputTokens('deepseek-chat')).toEqual({ expect(getModelMaxOutputTokens('deepseek-chat')).toEqual({
@@ -40,6 +47,7 @@ test('deepseek-chat uses provider-specific context and output caps', () => {
test('deepseek-chat clamps oversized max output overrides to the provider limit', () => { test('deepseek-chat clamps oversized max output overrides to the provider limit', () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '32000' process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '32000'
delete process.env.OPENAI_MODEL
expect(getMaxOutputTokensForModel('deepseek-chat')).toBe(8_192) expect(getMaxOutputTokensForModel('deepseek-chat')).toBe(8_192)
}) })
@@ -47,6 +55,7 @@ test('deepseek-chat clamps oversized max output overrides to the provider limit'
test('gpt-4o uses provider-specific context and output caps', () => { test('gpt-4o uses provider-specific context and output caps', () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.CLAUDE_CODE_USE_OPENAI = '1'
delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
delete process.env.OPENAI_MODEL
expect(getContextWindowForModel('gpt-4o')).toBe(128_000) expect(getContextWindowForModel('gpt-4o')).toBe(128_000)
expect(getModelMaxOutputTokens('gpt-4o')).toEqual({ expect(getModelMaxOutputTokens('gpt-4o')).toEqual({
@@ -59,6 +68,7 @@ test('gpt-4o uses provider-specific context and output caps', () => {
test('gpt-4o clamps oversized max output overrides to the provider limit', () => { test('gpt-4o clamps oversized max output overrides to the provider limit', () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '32000' process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '32000'
delete process.env.OPENAI_MODEL
expect(getMaxOutputTokensForModel('gpt-4o')).toBe(16_384) expect(getMaxOutputTokensForModel('gpt-4o')).toBe(16_384)
}) })
@@ -66,6 +76,7 @@ test('gpt-4o clamps oversized max output overrides to the provider limit', () =>
test('gpt-5.4 family uses provider-specific context and output caps', () => { test('gpt-5.4 family uses provider-specific context and output caps', () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.CLAUDE_CODE_USE_OPENAI = '1'
delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
delete process.env.OPENAI_MODEL
expect(getContextWindowForModel('gpt-5.4')).toBe(1_050_000) expect(getContextWindowForModel('gpt-5.4')).toBe(1_050_000)
expect(getModelMaxOutputTokens('gpt-5.4')).toEqual({ expect(getModelMaxOutputTokens('gpt-5.4')).toEqual({
@@ -98,6 +109,7 @@ test('gpt-5.4 family keeps large max output overrides within provider limits', (
test('MiniMax-M2.7 uses explicit provider-specific context and output caps', () => { test('MiniMax-M2.7 uses explicit provider-specific context and output caps', () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.CLAUDE_CODE_USE_OPENAI = '1'
delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
delete process.env.OPENAI_MODEL
expect(getContextWindowForModel('MiniMax-M2.7')).toBe(204_800) expect(getContextWindowForModel('MiniMax-M2.7')).toBe(204_800)
expect(getModelMaxOutputTokens('MiniMax-M2.7')).toEqual({ expect(getModelMaxOutputTokens('MiniMax-M2.7')).toEqual({
@@ -110,6 +122,7 @@ test('MiniMax-M2.7 uses explicit provider-specific context and output caps', ()
test('unknown openai-compatible models use the 128k fallback window (not 8k, see #635)', () => { test('unknown openai-compatible models use the 128k fallback window (not 8k, see #635)', () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.CLAUDE_CODE_USE_OPENAI = '1'
delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
delete process.env.OPENAI_MODEL
expect(getContextWindowForModel('some-unknown-3p-model')).toBe(128_000) expect(getContextWindowForModel('some-unknown-3p-model')).toBe(128_000)
}) })
@@ -117,6 +130,7 @@ test('unknown openai-compatible models use the 128k fallback window (not 8k, see
test('MiniMax-M2.5 and M2.1 use explicit provider-specific context and output caps', () => { test('MiniMax-M2.5 and M2.1 use explicit provider-specific context and output caps', () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.CLAUDE_CODE_USE_OPENAI = '1'
delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
delete process.env.OPENAI_MODEL
expect(getContextWindowForModel('MiniMax-M2.5')).toBe(204_800) expect(getContextWindowForModel('MiniMax-M2.5')).toBe(204_800)
expect(getContextWindowForModel('MiniMax-M2.5-highspeed')).toBe(204_800) expect(getContextWindowForModel('MiniMax-M2.5-highspeed')).toBe(204_800)

View File

@@ -76,7 +76,9 @@ describe('OpenClaude paths', () => {
}) })
test('local installer uses openclaude wrapper path', async () => { test('local installer uses openclaude wrapper path', async () => {
delete process.env.CLAUDE_CONFIG_DIR // Force .openclaude config home so the test doesn't fall back to
// ~/.claude when ~/.openclaude doesn't exist on this machine.
process.env.CLAUDE_CONFIG_DIR = join(homedir(), '.openclaude')
const { getLocalClaudePath } = await importFreshLocalInstaller() const { getLocalClaudePath } = await importFreshLocalInstaller()
expect(getLocalClaudePath()).toBe( expect(getLocalClaudePath()).toBe(