Enforce flat wiki keys

This commit is contained in:
Andrey Avtomonov 2026-05-12 16:53:12 +02:00
parent 45e5530a92
commit f138ead5be
21 changed files with 323 additions and 73 deletions

View file

@ -185,8 +185,8 @@ describe('runKtxAgent', () => {
search: vi.fn(async () => ({
results: [
{
key: 'metrics/revenue',
path: 'knowledge/global/metrics/revenue.md',
key: 'metrics-revenue',
path: 'knowledge/global/metrics-revenue.md',
scope: 'GLOBAL' as const,
summary: 'Revenue metric definition',
score: 0.02459016393442623,
@ -207,8 +207,8 @@ describe('runKtxAgent', () => {
expect(JSON.parse(io.stdout())).toEqual({
results: [
expect.objectContaining({
key: 'metrics/revenue',
path: 'knowledge/global/metrics/revenue.md',
key: 'metrics-revenue',
path: 'knowledge/global/metrics-revenue.md',
matchReasons: ['lexical', 'token'],
}),
],

View file

@ -1139,7 +1139,7 @@ describe('runKtxCli', () => {
projectDir: tempDir,
inputMode: 'disabled',
cliVersion: '0.0.0-private',
anthropicApiKeyEnv: 'ANTHROPIC_API_KEY',
anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
anthropicModel: 'claude-sonnet-4-6',
skipLlm: false,
}),
@ -1200,7 +1200,7 @@ describe('runKtxCli', () => {
inputMode: 'disabled',
skipLlm: true,
embeddingBackend: 'openai',
embeddingApiKeyEnv: 'OPENAI_API_KEY',
embeddingApiKeyEnv: 'OPENAI_API_KEY', // pragma: allowlist secret
skipEmbeddings: false,
}),
setupIo.io,
@ -1301,7 +1301,7 @@ describe('runKtxCli', () => {
source: 'metabase',
sourceConnectionId: 'prod_metabase',
sourceUrl: 'https://metabase.example.com',
sourceApiKeyRef: 'env:METABASE_API_KEY',
sourceApiKeyRef: 'env:METABASE_API_KEY', // pragma: allowlist secret
sourceWarehouseConnectionId: 'warehouse',
metabaseDatabaseId: 1,
}),
@ -1727,8 +1727,8 @@ describe('runKtxCli', () => {
{
results: [
{
key: 'metrics/revenue',
path: 'knowledge/global/metrics/revenue.md',
key: 'metrics-revenue',
path: 'knowledge/global/metrics-revenue.md',
scope: 'GLOBAL',
summary: 'Revenue metric definition',
score: 0.02459016393442623,
@ -1754,8 +1754,8 @@ describe('runKtxCli', () => {
expect(JSON.parse(io.stdout())).toEqual({
results: [
expect.objectContaining({
key: 'metrics/revenue',
path: 'knowledge/global/metrics/revenue.md',
key: 'metrics-revenue',
path: 'knowledge/global/metrics-revenue.md',
matchReasons: ['lexical', 'token'],
}),
],

View file

@ -61,7 +61,7 @@ describe('runKtxKnowledge', () => {
{
command: 'write',
projectDir,
key: 'metrics/revenue',
key: 'metrics-revenue',
scope: 'GLOBAL',
userId: 'local',
summary: 'Revenue',
@ -73,24 +73,53 @@ describe('runKtxKnowledge', () => {
writeIo.io,
),
).resolves.toBe(0);
expect(writeIo.stdout()).toContain('Wrote knowledge/global/metrics/revenue.md');
expect(writeIo.stdout()).toContain('Wrote knowledge/global/metrics-revenue.md');
const readIo = makeIo();
await expect(
runKtxKnowledge({ command: 'read', projectDir, key: 'metrics/revenue', userId: 'local' }, readIo.io),
runKtxKnowledge({ command: 'read', projectDir, key: 'metrics-revenue', userId: 'local' }, readIo.io),
).resolves.toBe(0);
expect(readIo.stdout()).toContain('# metrics/revenue');
expect(readIo.stdout()).toContain('# metrics-revenue');
expect(readIo.stdout()).toContain('Revenue is paid order value.');
const listIo = makeIo();
await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local' }, listIo.io)).resolves.toBe(0);
expect(listIo.stdout()).toContain('GLOBAL\tmetrics/revenue\tRevenue');
expect(listIo.stdout()).toContain('GLOBAL\tmetrics-revenue\tRevenue');
const searchIo = makeIo();
await expect(
runKtxKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local' }, searchIo.io),
).resolves.toBe(0);
expect(searchIo.stdout()).toContain('metrics/revenue');
expect(searchIo.stdout()).toContain('metrics-revenue');
});
it('rejects slash-delimited write keys with a flat-key suggestion', async () => {
const projectDir = join(tempDir, 'project');
await initKtxProject({ projectDir, projectName: 'warehouse' });
const writeIo = makeIo();
await expect(
runKtxKnowledge(
{
command: 'write',
projectDir,
key: 'orbit/company-overview',
scope: 'GLOBAL',
userId: 'local',
summary: 'Orbit',
content: 'Orbit overview.',
tags: [],
refs: [],
slRefs: [],
},
writeIo.io,
),
).resolves.toBe(1);
expect(writeIo.stderr()).toContain(
'Invalid wiki key "orbit/company-overview". Wiki keys must be flat; use "orbit-company-overview".',
);
expect(writeIo.stdout()).toBe('');
});
it('explains empty search results for a project without wiki pages', async () => {
@ -116,7 +145,7 @@ describe('runKtxKnowledge', () => {
{
command: 'write',
projectDir,
key: 'historic-sql/active-contract-arr-open-tickets',
key: 'active-contract-arr-open-tickets',
scope: 'GLOBAL',
userId: 'local',
summary: 'Active Contract ARR Ranked by Open Support Ticket Count',
@ -138,7 +167,7 @@ describe('runKtxKnowledge', () => {
),
).resolves.toBe(0);
expect(searchIo.stdout()).toContain('historic-sql/active-contract-arr-open-tickets');
expect(searchIo.stdout()).toContain('active-contract-arr-open-tickets');
expect(searchIo.stderr()).toBe('');
});
});