mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
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.
This commit is contained in:
parent
65de75ebd7
commit
fd18caa26a
3 changed files with 101 additions and 5 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { promises as fs } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import type { SimpleGit } from 'simple-git';
|
||||
import { CheckRepoActions, type SimpleGit } from 'simple-git';
|
||||
import { noopLogger, resolveConfigDir, type KtxCoreConfig, type KtxLogger } from './config.js';
|
||||
import { createSimpleGit } from './git-env.js';
|
||||
|
||||
|
|
@ -98,10 +98,15 @@ export class GitService {
|
|||
|
||||
private async initialize(): Promise<void> {
|
||||
try {
|
||||
// Check if already initialized
|
||||
const isRepo = await this.git.checkIsRepo();
|
||||
// Adopt an existing repo ONLY when this directory is itself that repo's root.
|
||||
// When it sits below an enclosing repo, a plain checkIsRepo() is true and ktx
|
||||
// would silently piggyback on the enclosing tree — but every ktx relative path
|
||||
// (file-store writes, session worktrees, squash-merges, reindex scans) assumes
|
||||
// this directory IS the working-tree root. So treat "inside an enclosing repo"
|
||||
// the same as "no repo" and initialize a dedicated repo rooted here.
|
||||
const isRepoRoot = await this.git.checkIsRepo(CheckRepoActions.IS_REPO_ROOT);
|
||||
|
||||
if (!isRepo) {
|
||||
if (!isRepoRoot) {
|
||||
await this.git.init();
|
||||
this.logger.log('Initialized git repository');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
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 });
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdir, mkdtemp, readFile, realpath, rm, stat } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
|
@ -60,6 +61,30 @@ describe('KTX local project runtime', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('initializes a dedicated git repo at the project dir even when nested inside an enclosing repo', async () => {
|
||||
// A ktx project dir living below an existing git working tree (e.g. an analytics
|
||||
// subfolder of an app repo). ktx must own its own repo rooted at the project dir,
|
||||
// not silently adopt the enclosing repo — otherwise worktree writes resolve against
|
||||
// the enclosing root and land outside the project dir.
|
||||
const enclosing = join(tempDir, 'enclosing');
|
||||
await mkdir(enclosing, { recursive: true });
|
||||
execFileSync('git', ['init', '-q'], { cwd: enclosing });
|
||||
|
||||
const projectDir = join(enclosing, 'analytics');
|
||||
await initKtxProject({ projectDir, authorName: 'Agent', authorEmail: 'agent@example.com' });
|
||||
|
||||
await expect(stat(join(projectDir, '.git'))).resolves.toBeDefined();
|
||||
const toplevel = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
||||
cwd: projectDir,
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
expect(await realpath(toplevel)).toBe(await realpath(projectDir));
|
||||
|
||||
// ktx must not write its scaffold commits into the user's enclosing repo.
|
||||
const enclosingTracked = execFileSync('git', ['ls-files'], { cwd: enclosing, encoding: 'utf-8' });
|
||||
expect(enclosingTracked).not.toContain('ktx.yaml');
|
||||
});
|
||||
|
||||
it('rejects reinitializing an existing project unless force is set', async () => {
|
||||
const projectDir = join(tempDir, 'warehouse');
|
||||
await initKtxProject({ projectDir });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue