diff --git a/gitnexus/src/core/ingestion/languages/r/package-config.ts b/gitnexus/src/core/ingestion/languages/r/package-config.ts index cfa306fc9..e1d0e4d94 100644 --- a/gitnexus/src/core/ingestion/languages/r/package-config.ts +++ b/gitnexus/src/core/ingestion/languages/r/package-config.ts @@ -383,7 +383,8 @@ export async function loadRPackageConfig(repoRoot: string): Promise { if (n.properties.language !== 'r') return; - if (typeof n.properties.ownerId === 'string') ownedNodes++; + if (n.properties.ownerId !== undefined) ownedNodes++; if ( (n.label === 'Method' || n.label === 'Property') && typeof n.properties.ownerNameHint === 'string' @@ -88,7 +88,7 @@ describe.skipIf(!hasDistWorker)('R worker never emits an owner id (before post-p for (const result of raw) { for (const symbol of result.symbols) { symbols++; - if (symbol.ownerId) ownedSymbols++; + if (symbol.ownerId !== undefined) ownedSymbols++; } } diff --git a/gitnexus/test/integration/resolvers/r-s4-anchors.test.ts b/gitnexus/test/integration/resolvers/r-s4-anchors.test.ts index 2d4a3810b..39e08dec7 100644 --- a/gitnexus/test/integration/resolvers/r-s4-anchors.test.ts +++ b/gitnexus/test/integration/resolvers/r-s4-anchors.test.ts @@ -58,8 +58,8 @@ describe('R S4/RefClass definitions are named by their first argument only', () it('defines the generics once, with no Function from valueClass = "numeric"', () => { const functions = getNodesByLabel(result, 'Function'); - expect(functions).toContain('describe'); - expect(functions).toContain('summarise'); + expect(functions.filter((n) => n === 'describe')).toHaveLength(1); + expect(functions.filter((n) => n === 'summarise')).toHaveLength(1); expect(functions).not.toContain('numeric'); }); diff --git a/gitnexus/test/unit/r-export-pattern.test.ts b/gitnexus/test/unit/r-export-pattern.test.ts index 65aaa5c33..3569777db 100644 --- a/gitnexus/test/unit/r-export-pattern.test.ts +++ b/gitnexus/test/unit/r-export-pattern.test.ts @@ -73,6 +73,12 @@ describe('compileRExportPattern', () => { }); it('does not treat an escaped bracket as a bracket expression', () => { + // `\[` is a literal bracket; the rest is an ordinary one-character class of `:alph`, + // so the pattern compiles (to itself) and matches `[` plus exactly one of those characters. + expect(compileRExportPattern('^\\[[:alpha:]$')?.source).toBe('^\\[[:alpha:]$'); + expect(matches('^\\[[:alpha:]$', '[a')).toBe(true); + expect(matches('^\\[[:alpha:]$', '[:')).toBe(true); + expect(matches('^\\[[:alpha:]$', '[b')).toBe(false); expect(matches('^\\[[:alpha:]$', '[:alpha:')).toBe(false); }); diff --git a/gitnexus/test/unit/scope-resolution/r/r-qualified-call.test.ts b/gitnexus/test/unit/scope-resolution/r/r-qualified-call.test.ts index 4488e4673..1ecf728b7 100644 --- a/gitnexus/test/unit/scope-resolution/r/r-qualified-call.test.ts +++ b/gitnexus/test/unit/scope-resolution/r/r-qualified-call.test.ts @@ -369,6 +369,30 @@ describe('loadRPackageConfig: truncation only when a skipped subtree hides an un if (locked) await fs.chmod(locked, 0o755).catch(() => undefined); } }); + + it('an unreadable DESCRIPTION is truncated, so its qualifier stays unknown', async () => { + if (typeof process.getuid === 'function' && process.getuid() === 0) return; // root reads anything + let locked = ''; + try { + const cfg = await withRepo(async (root) => { + locked = path.join(root, 'sub', 'DESCRIPTION'); + await writePkg(path.join(root, 'sub'), 'hiddenpkg'); + await fs.chmod(locked, 0o000); + // Guard: a platform that still reads a mode-000 file cannot exercise the catch. + await fs.readFile(locked).then( + () => { + throw new Error('DESCRIPTION is still readable; cannot simulate an I/O error'); + }, + () => undefined, + ); + }); + expect(cfg?.packages.has('hiddenpkg')).toBe(false); + expect(cfg?.truncated).toBe(true); + expect(rQualifierLocality('hiddenpkg', cfg as RPackageConfig, new Set())).toBe('unknown'); + } finally { + if (locked) await fs.chmod(locked, 0o644).catch(() => undefined); + } + }); }); describe('hidesUndiscoveredPackage', () => {