test(r): pin the non-indexed R file types and the experimental classification

- Replace the vacuous ".Rprofile/.Renviron are not indexed" test (no fixture
  contained such files) with an r-non-indexed fixture holding .Rprofile,
  .Renviron, .Rmd and .qmd that define functions next to a plain .R positive
  control; the Rmd/qmd bodies are valid R so routing them to the R parser would
  fail the test.
- Add getLanguageFromFilename negatives for those names.
- Add R to the query-compilation smoke list.
- Assert R's LanguageClassifications value ('experimental'), which the docs
  claim but nothing checked.
This commit is contained in:
Mick Cooney 2026-10-12 00:00:47 +01:00
parent 75eaac750c
commit 7ad2ac2bc0
9 changed files with 74 additions and 12 deletions

View file

@ -0,0 +1 @@
R_LIBS_USER=~/R/library

View file

@ -0,0 +1 @@
rprofile_fn <- function() 1

View file

@ -0,0 +1,3 @@
# Valid R on purpose: if this extension were ever routed to the R parser the
# function below would be indexed, so the negative test can actually fail.
rmd_fn <- function() 1

View file

@ -0,0 +1,2 @@
# Positive control: a plain .R file is indexed.
indexed_control <- function() 1

View file

@ -0,0 +1,2 @@
# Valid R on purpose (see analysis.Rmd).
qmd_fn <- function() 1

View file

@ -34,6 +34,7 @@ describe('Query compilation smoke tests', () => {
[SupportedLanguages.Kotlin]: 'Test.kt',
[SupportedLanguages.Swift]: 'test.swift',
[SupportedLanguages.Dart]: 'test.dart',
[SupportedLanguages.R]: 'test.R',
};
// Known query compilation failures — remove from this set as PRs fix them

View file

@ -374,20 +374,44 @@ describe('R function definitions and calls', () => {
expect(itemsField).toBeDefined();
expect(itemsField!.properties.visibility).toBe('public');
});
});
// --- Negative tests ---
/**
* Files the R provider must not index. The `r-non-indexed` fixture holds a
* `.Rprofile`, `.Renviron`, `.Rmd` and `.qmd`, each defining a function, next to a plain
* `real.R` positive control (docs/languages/r-provider.md, "Limits").
*/
describe('R files that are not indexed', () => {
let result: PipelineResult;
it('does not index .Rprofile or .Renviron files', () => {
const allNodes: string[] = [];
result.graph.forEachNode((n) => {
if (
n.properties.filePath?.includes('.Rprofile') ||
n.properties.filePath?.includes('.Renviron')
) {
allNodes.push(n.properties.name);
}
});
expect(allNodes).toHaveLength(0);
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'r-non-indexed'), () => {});
}, 60000);
it('indexes the plain .R control file (so the negatives below are not vacuous)', () => {
expect(getNodesByLabel(result, 'Function')).toEqual(['indexed_control']);
});
// The scanner may still record a bare File node for these; what must not exist is any
// parsed symbol (Function, Variable, ...) from them.
it.each(['.Rprofile', '.Renviron', 'analysis.Rmd', 'report.qmd'])(
'creates no symbol node for %s',
(name) => {
const hits: string[] = [];
result.graph.forEachNode((n) => {
if (n.label !== 'File' && n.properties.filePath?.endsWith(name)) {
hits.push(`${n.label}:${n.properties.name}`);
}
});
expect(hits).toEqual([]);
},
);
it('defines no function from the non-indexed files', () => {
const functions = getNodesByLabel(result, 'Function');
expect(functions).not.toContain('rprofile_fn');
expect(functions).not.toContain('rmd_fn');
expect(functions).not.toContain('qmd_fn');
});
});

View file

@ -159,6 +159,18 @@ describe('getLanguageFromFilename', () => {
it.each(['.r', '.R'])('detects %s files', (ext) => {
expect(getLanguageFromFilename(`analysis${ext}`)).toBe(SupportedLanguages.R);
});
// docs/languages/r-provider.md: only `.r` / `.R` are handled.
it.each([
'analysis.Rmd',
'report.qmd',
'.Rprofile',
'.Renviron',
'Rprofile.site',
'x.Rhistory',
])('returns null for %s', (name) => {
expect(getLanguageFromFilename(name)).toBeNull();
});
});
describe('unsupported', () => {

View file

@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest';
import { LanguageClassifications, isProductionLanguage } from 'gitnexus-shared';
import { SupportedLanguages } from '../../src/config/supported-languages.js';
// docs/languages/r-provider.md describes R as an experimental provider. The classification
// map is type-forced to contain R but nothing else asserts its value, so a silent
// promotion to 'production' (a governance change) would otherwise go unnoticed.
describe('R language classification', () => {
it('classifies R as experimental', () => {
expect(LanguageClassifications[SupportedLanguages.R]).toBe('experimental');
});
it('does not treat R as a production language', () => {
expect(isProductionLanguage(SupportedLanguages.R)).toBe(false);
});
});