Skip to content

Commit 94fa4f9

Browse files
ndemiancclaude
andcommitted
fix(ai): address PR #21 review — folder-name fallback + real-fs smoke test
- projectRules: a folder entry with a root but no name produced "undefined/AGENTS.md" in multi-root labels; fall back to path.basename(f.root). - test: add the real-filesystem smoke test the doc referenced (writes an on-disk AGENTS.md and reads it back) — it was only run inline before — plus a case for the name fallback. 12 cases now. - docs: describe the test coverage accurately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7880859 commit 94fa4f9

3 files changed

Lines changed: 30 additions & 2 deletions

File tree

docs/PROJECT-RULES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ model as Cursor and Copilot rules files. Two things bound the risk:
8383
|------|------|
8484
| `extensions/levelcode-ai/projectRules.js` | Pure loader: `loadProjectRules(folders, readFile)``{ text, sources }`. File reading is injected as a callback, so it's testable without a filesystem. |
8585
| `extensions/levelcode-ai/agent.js` | Reads with `fs`, folds `rules.text` into the system prompt, posts the timeline chip, and `dbg('projectRules.loaded', …)`. |
86-
| `extensions/levelcode-ai/test/projectRules.test.js` | 10 unit cases (discovery, alias fallback, first-present-wins, multi-root, empty-skip, truncation, throwing reader) plus a real-filesystem smoke test. |
86+
| `extensions/levelcode-ai/test/projectRules.test.js` | Unit cases with an injected reader (discovery, alias fallback, first-present-wins, multi-root labelling incl. a name fallback, empty-skip, truncation, throwing reader) plus a real-filesystem smoke test that writes an on-disk `AGENTS.md` and reads it back. |
8787

8888
## Not yet (planned)
8989

extensions/levelcode-ai/projectRules.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function loadProjectRules(folders, readFile) {
3636
let content = null;
3737
try { content = readFile(path.join(f.root, name)); } catch { content = null; }
3838
if (content && content.trim()) {
39-
const label = multi ? f.name + '/' + name : name;
39+
const label = multi ? (f.name || path.basename(f.root)) + '/' + name : name; // never "undefined/AGENTS.md"
4040
let body = content.trim();
4141
if (body.length > PER_FILE_CAP) { body = body.slice(0, PER_FILE_CAP) + '\n\n…[' + label + ' truncated at ' + PER_FILE_CAP + ' chars]'; }
4242
blocks.push('### ' + label + '\n' + body);

extensions/levelcode-ai/test/projectRules.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,32 @@ test('a malformed folder entry is skipped', () => {
9393
assert.deepStrictEqual(r.sources, ['AGENTS.md']);
9494
});
9595

96+
test('multi-root: a folder with no name falls back to its basename (never "undefined/…")', () => {
97+
const r = loadProjectRules([{ root: '/ws/app' }, F2], reader({
98+
[at('/ws/app', 'AGENTS.md')]: 'a',
99+
[at('/ws/api', 'AGENTS.md')]: 'b',
100+
}));
101+
assert.deepStrictEqual(r.sources, ['app/AGENTS.md', 'api/AGENTS.md']);
102+
assert.ok(!r.text.includes('undefined/'), 'label leaked an undefined folder name');
103+
});
104+
105+
// The cases above inject a fake reader; this one uses the real fs to prove the on-disk path works.
106+
test('real filesystem: discovers and reads an on-disk rules file (smoke)', () => {
107+
const fs = require('fs');
108+
const os = require('os');
109+
const real = (abs) => { try { return fs.readFileSync(abs, 'utf8'); } catch { return null; } };
110+
const withRules = fs.mkdtempSync(path.join(os.tmpdir(), 'lc-rules-'));
111+
const noRules = fs.mkdtempSync(path.join(os.tmpdir(), 'lc-none-'));
112+
try {
113+
fs.writeFileSync(path.join(withRules, 'AGENTS.md'), '# Rules\n- Use 2-space indent');
114+
const hit = loadProjectRules([{ name: 'app', root: withRules }], real);
115+
assert.deepStrictEqual(hit.sources, ['AGENTS.md']);
116+
assert.ok(hit.text.includes('Use 2-space indent'), 'on-disk rules content was not folded in');
117+
assert.deepStrictEqual(loadProjectRules([{ name: 'empty', root: noRules }], real), { text: '', sources: [] });
118+
} finally {
119+
fs.rmSync(withRules, { recursive: true, force: true });
120+
fs.rmSync(noRules, { recursive: true, force: true });
121+
}
122+
});
123+
96124
console.log(n + ' passing');

0 commit comments

Comments
 (0)