ktx/packages/cli/test/context/index-sync/reindex.nested-git-root.test.ts
Andrey Avtomonov fd18caa26a
fix(cli): own a dedicated git repo at the project dir when nested in an enclosing repo (#282)
GitService.initialize() used checkIsRepo(), which is true whenever the project
dir sits anywhere inside a git working tree. So when a ktx project lived in a
subdirectory of an enclosing repo, ktx skipped `git init` and silently adopted
the enclosing repo as its store.

Every ktx relative path assumes the project dir IS the working-tree root. During
ingest, wiki/SL pages are written through a session worktree (whose root is the
worktree dir, so the page is recorded at repo-relative `wiki/global/<key>.md`)
and then squash-merged into the main worktree. With an adopted enclosing repo,
the main worktree's root is the enclosing git root, so the merge wrote the page
to `<gitRoot>/wiki/global/` — outside the project dir. reindex scans
`<projectDir>/wiki/global/`, found nothing, and wiki_search silently returned
empty (knowledge_pages = 0) even though ingest reported success.

Detect the project dir's own root with checkIsRepo(IS_REPO_ROOT) and initialize
a dedicated repo there unless the project dir is already a repo root. This keeps
adopting a user-created repo when the project dir IS that repo's root, fixes the
silent wiki/SL/memory divergence at its source for every writer, and stops ktx
from committing its scaffold into the user's enclosing repo.

Regression tests cover both layers: a project nested in an enclosing repo gets
its own .git (and the enclosing repo stays untouched), and a wiki page written
through a session worktree + squash-merge lands in the project dir and is
discovered by reindex.
2026-06-09 23:37:24 +02:00

66 lines
3 KiB
TypeScript

import { execFileSync } from 'node:child_process';
import { mkdir, mkdtemp, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { reindexLocalIndexes } from '../../../src/context/index-sync/reindex.js';
import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js';
const AUTHOR = 'Agent';
const EMAIL = 'agent@example.com';
const WIKI_PAGE = '---\nsummary: Revenue\nusage_mode: auto\n---\n\nPaid orders.\n';
/**
* Regression for the "wiki silently unsearchable when the project dir is not the git root"
* bug: a ktx project initialized below an existing git working tree. ingest writes wiki
* pages through a session worktree and squash-merges into main, so the page must land
* inside the project dir (where reindex scans), not at the enclosing git root.
*/
describe('reindex with a ktx project nested inside an enclosing git repo', () => {
let tempDir: string;
let enclosing: string;
let projectDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'ktx-nested-git-root-'));
enclosing = join(tempDir, 'enclosing');
await mkdir(enclosing, { recursive: true });
execFileSync('git', ['init', '-q'], { cwd: enclosing });
projectDir = join(enclosing, 'analytics');
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
it('indexes a wiki page written through a session worktree and squash-merged into main', async () => {
const project: KtxLocalProject = await initKtxProject({
projectDir,
authorName: AUTHOR,
authorEmail: EMAIL,
});
// Mirror the ingest write path: create a session worktree, write the page on its
// branch through the worktree-scoped file store, then squash-merge into main.
const mainHead = await project.git.revParseHead();
const workdir = join(projectDir, '.ktx/worktrees/session-test');
const branch = 'session/test';
await project.git.addWorktree(workdir, branch, mainHead);
const worktreeStore = project.fileStore.forWorktree(workdir);
await worktreeStore.writeFile('wiki/global/revenue.md', WIKI_PAGE, AUTHOR, EMAIL, 'Add revenue page');
const merge = await project.git.squashMergeIntoMain(branch, AUTHOR, EMAIL, 'Merge session');
expect(merge.ok).toBe(true);
await project.git.removeWorktree(workdir);
await project.git.deleteBranch(branch, true);
// The page must land inside the project dir, not the enclosing git root.
await expect(stat(join(projectDir, 'wiki/global/revenue.md'))).resolves.toBeDefined();
await expect(stat(join(enclosing, 'wiki/global/revenue.md'))).rejects.toMatchObject({ code: 'ENOENT' });
// ...and reindex must discover and index it.
const summary = await reindexLocalIndexes(project, { force: false, embeddingService: null });
const global = summary.scopes.find((scope) => scope.label === 'global');
expect(global).toMatchObject({ scanned: 1, updated: 1 });
});
});