Files
gstack/test/question-tuning-registry-path.test.ts
T
Garry TanandClaude Fable 5 96c22cbd21 fix(question-tuning): interpolate the absolute question-registry path (#2489)
The Question Tuning preamble pointed agents at a RELATIVE
scripts/question-registry.ts in the same sentence whose ${bin} path renders
absolute. Agents run with cwd in the USER'S project — the relative lookup
never resolves, silently fails, and the documented {skill}-{slug} fallback
fabricates a singleton question_id every time (one observed
/plan-eng-review session: 21/21 unregistered ids, so no per-question
preference can ever attach).

The resolver now interpolates ctx.paths.skillRoot the way sibling resolvers
interpolate bin paths: ~/.claude/skills/gstack/scripts/question-registry.ts
on Claude, $GSTACK_ROOT/scripts/question-registry.ts on env-var hosts.

test/question-tuning-registry-path.test.ts asserts the rendered path per
host, forbids the bare relative shape, and checks the target file exists in
the install tree.

Fixes #2489

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:59:34 -07:00

63 lines
2.5 KiB
TypeScript

/**
* Question Tuning registry path must be absolute (#2489).
*
* The preamble told agents to choose question_id from a RELATIVE
* `scripts/question-registry.ts` — which never resolves from a user's
* project cwd (the file lives only under the gstack install root). The
* lookup silently failed and agents fabricated ids via the {skill}-{slug}
* fallback (one observed /plan-eng-review session: 21/21 unregistered).
*
* The resolver now interpolates the installed path the same way ${bin}
* paths are interpolated: ctx.paths.skillRoot (a `~`-rooted path on Claude,
* $GSTACK_ROOT on env-var hosts).
*/
import { describe, test, expect } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
import { HOST_PATHS } from '../scripts/resolvers/types';
import type { TemplateContext } from '../scripts/resolvers/types';
import { generateQuestionTuning } from '../scripts/resolvers/question-tuning';
const ROOT = path.join(import.meta.dir, '..');
function makeCtx(host: 'claude' | 'codex'): TemplateContext {
return {
skillName: 'test-skill',
tmplPath: 'test.tmpl',
host,
paths: HOST_PATHS[host],
preambleTier: 2,
};
}
describe('question-tuning registry path is absolute (#2489)', () => {
test('claude host renders the installed registry path', () => {
const out = generateQuestionTuning(makeCtx('claude'));
expect(out).toContain('`~/.claude/skills/gstack/scripts/question-registry.ts`');
});
test('env-var host renders $GSTACK_ROOT-anchored registry path', () => {
const out = generateQuestionTuning(makeCtx('codex'));
expect(out).toContain('`$GSTACK_ROOT/scripts/question-registry.ts`');
});
test('no host renders a bare relative registry path', () => {
for (const host of ['claude', 'codex'] as const) {
const out = generateQuestionTuning(makeCtx(host));
// A backtick immediately before `scripts/` means the path renders
// relative — exactly the shape that never resolves from a project cwd.
expect(out).not.toContain('`scripts/question-registry.ts`');
}
});
test('the interpolated path points at a file that exists in the install tree', () => {
expect(fs.existsSync(path.join(ROOT, 'scripts', 'question-registry.ts'))).toBe(true);
});
test('rendered SKILL.md carries the absolute path', () => {
const rendered = fs.readFileSync(path.join(ROOT, 'plan-eng-review', 'SKILL.md'), 'utf-8');
expect(rendered).toContain('~/.claude/skills/gstack/scripts/question-registry.ts');
expect(rendered).not.toContain('`scripts/question-registry.ts`');
});
});