fix(doctor): distinguish vector capability from repository index state (#3228)

Co-authored-by: Eva <eva@100yen.org>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
EVA 2026-09-09 19:06:41 +07:00 • committed by GitHub
parent e2da8d90ce
commit 25974caf05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 89 additions and 10 deletions

View file

@ -291,13 +291,15 @@ export const doctorCommand = async () => {
console.log(` ${padDisplayEnd('', 18)}${remedy}`);
}
}
// Semantic mode follows the probe, not the platform: without a loadable
// VECTOR extension the index can be neither built nor queried, so search is
// really on exact scan no matter what the platform would allow.
// Doctor has no repository target, so this probe can report only whether the
// runtime can load VECTOR. It cannot claim that a repository has built the
// named HNSW index. Keep the capability and repository state distinct.
console.log(
` ${label('doctor.labels.semanticMode', 18)}${
vectorProbe.loaded ? capabilities.semanticMode : 'exact-scan'
}`,
` ${label('doctor.labels.semanticMode', 18)}${t(
vectorProbe.loaded
? 'doctor.vectorCapability.indexUnverified'
: 'doctor.vectorCapability.exactScanOnly',
)}`,
);
// Surface the optional-extension install policy so offline users can see
// whether analyze/query will reach the network (extension.ladybugdb.com).

View file

@ -123,8 +123,10 @@ export const en = {
'doctor.labels.onnx': 'ONNX:',
'doctor.labels.graphStore': 'Graph store:',
'doctor.labels.fullTextSearch': 'Full-text search:',
'doctor.labels.vectorIndex': 'VECTOR index:',
'doctor.labels.semanticMode': 'Semantic mode:',
'doctor.labels.vectorIndex': 'VECTOR extension:',
'doctor.labels.semanticMode': 'Semantic support:',
'doctor.vectorCapability.indexUnverified': 'vector-index capable (repository index not checked)',
'doctor.vectorCapability.exactScanOnly': 'exact-scan only (VECTOR extension unavailable)',
'doctor.labels.exactScanLimit': 'Exact scan limit:',
'doctor.labels.note': 'Note:',
'doctor.labels.backend': 'Backend:',

View file

@ -119,8 +119,10 @@ export const zhCN = {
'doctor.labels.onnx': 'ONNX:',
'doctor.labels.graphStore': '图存储:',
'doctor.labels.fullTextSearch': '全文搜索:',
'doctor.labels.vectorIndex': '向量索引:',
'doctor.labels.semanticMode': '语义模式:',
'doctor.labels.vectorIndex': 'VECTOR 扩展:',
'doctor.labels.semanticMode': '语义支持:',
'doctor.vectorCapability.indexUnverified': '支持向量索引(未检查仓库索引)',
'doctor.vectorCapability.exactScanOnly': '仅精确扫描(VECTOR 扩展不可用)',
'doctor.labels.exactScanLimit': '精确扫描上限:',
'doctor.labels.note': '说明:',
'doctor.labels.backend': '后端:',

View file

@ -8,8 +8,20 @@ import {
pageSizeDoctorLines,
poolSizeDoctorLine,
} from '../../src/cli/doctor.js';
import { setCliLanguage, type SupportedCliLanguage } from '../../src/cli/i18n/index.js';
import type { NativeCheckResult } from '../../src/core/lbug/native-check.js';
const nativeProbeState = vi.hoisted(() => ({ vectorLoaded: true }));
vi.mock('../../src/core/lbug/native-check.js', () => ({
checkLbugNative: () => ({ ok: true, binaryPath: '/synthetic/lbugjs.node' }),
probeFtsExtensionLoad: async () => ({ loaded: true }),
probeVectorExtensionLoad: async () =>
nativeProbeState.vectorLoaded
? { loaded: true }
: { loaded: false, reason: 'synthetic VECTOR load failure' },
}));
describe('doctor output formatting', () => {
it('keeps ASCII padding equivalent to String.padEnd', () => {
expect(displayWidth('OS:')).toBe(3);
@ -29,6 +41,67 @@ describe('doctor output formatting', () => {
});
});
describe('doctor VECTOR capability claims', () => {
const ENV_KEYS = [
'GITNEXUS_EMBEDDING_URL',
'GITNEXUS_EMBEDDING_MODEL',
'GITNEXUS_EMBEDDING_DIMS',
] as const;
const savedEnv = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]]));
const renderDoctor = async (
vectorLoaded: boolean,
language: SupportedCliLanguage = 'en',
): Promise<string> => {
nativeProbeState.vectorLoaded = vectorLoaded;
setCliLanguage(language);
process.env.GITNEXUS_EMBEDDING_URL = 'http://127.0.0.1:9/v1';
process.env.GITNEXUS_EMBEDDING_MODEL = 'synthetic-doctor';
process.env.GITNEXUS_EMBEDDING_DIMS = '384';
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
await doctorCommand();
return log.mock.calls.map((args) => args.map(String).join(' ')).join('\n');
};
afterEach(() => {
nativeProbeState.vectorLoaded = true;
setCliLanguage(null);
vi.restoreAllMocks();
for (const key of ENV_KEYS) {
if (savedEnv[key] === undefined) delete process.env[key];
else process.env[key] = savedEnv[key];
}
});
it('reports a loaded extension without claiming a repository index exists', async () => {
const output = await renderDoctor(true);
expect(output).toContain('VECTOR extension: available');
expect(output).toContain(
'Semantic support: vector-index capable (repository index not checked)',
);
expect(output).not.toContain('VECTOR index:');
expect(output).not.toContain('Semantic mode:');
});
it('reports exact-scan capability when the extension cannot load', async () => {
const output = await renderDoctor(false);
expect(output).toContain('VECTOR extension: unavailable');
expect(output).toContain('Semantic support: exact-scan only (VECTOR extension unavailable)');
});
it('keeps the corrected capability claims localized', async () => {
const output = await renderDoctor(true, 'zh-CN');
expect(output).toContain('VECTOR 扩展:');
expect(output).toContain('语义支持:');
expect(output).toContain('支持向量索引(未检查仓库索引)');
});
});
describe('doctor embedding-runtime support status', () => {
it('flags local embeddings as unavailable on macOS Intel (darwin/x64)', () => {
const { status, detail } = localEmbeddingDoctorStatus({