diff --git a/src/__tests__/rebuild-wiki-index.test.ts b/src/__tests__/rebuild-wiki-index.test.ts new file mode 100644 index 0000000..260b186 --- /dev/null +++ b/src/__tests__/rebuild-wiki-index.test.ts @@ -0,0 +1,64 @@ +import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { rebuildWikiIndex } from '../rebuild-wiki-index.js'; + +/** + * Regression test for the batch/local-import navigation bug: importing repos + * populated evidence/code// but the top-level router.md / index.md were + * never rebuilt, so they stayed frozen at the first single-repo import. + * rebuildWikiIndex must aggregate ALL repos under evidence/code/ and overwrite + * any stale top-level navigation. + */ +describe('rebuildWikiIndex', () => { + let wiki: string; + + beforeEach(async () => { + const tmp = await mkdtemp(path.join(os.tmpdir(), 'teamai-wiki-')); + wiki = path.join(tmp, 'teamwiki'); + const codeDir = path.join(wiki, 'evidence', 'code'); + await mkdir(codeDir, { recursive: true }); + + // Three imported repos, each with its own index.md carrying a Facts count. + for (const [slug, facts] of [['repo-alpha', 10], ['repo-beta', 20], ['repo-gamma', 30]] as const) { + const dir = path.join(codeDir, slug); + await mkdir(dir, { recursive: true }); + await writeFile(path.join(dir, 'index.md'), `# ${slug}\n\nFacts: ${facts}\n`); + } + + // Stale top-level nav: only one repo listed, wrong repo count. + await writeFile(path.join(wiki, 'index.md'), '# Team Wiki Index\n\n## Stats\n\n- 仓库: 1\n\nOnly repo-alpha (stale).\n'); + await writeFile(path.join(wiki, 'router.md'), '# Router\n\nOnly repo-alpha (stale).\n'); + }); + + afterEach(async () => { + await rm(path.dirname(wiki), { recursive: true, force: true }); + }); + + it('aggregates every repo under evidence/code and overwrites stale navigation', async () => { + await rebuildWikiIndex(wiki); + + const index = await readFile(path.join(wiki, 'index.md'), 'utf-8'); + const router = await readFile(path.join(wiki, 'router.md'), 'utf-8'); + + // All three repos appear in the rebuilt index. + expect(index).toContain('evidence/code/repo-alpha/index.md'); + expect(index).toContain('evidence/code/repo-beta/index.md'); + expect(index).toContain('evidence/code/repo-gamma/index.md'); + + // Repo count reflects the full set, and aggregated Facts total is 60. + expect(index).toContain('- 仓库: 3'); + expect(index).toContain('- Facts: 60'); + + // Stale content is gone from both files. + expect(index).not.toContain('stale'); + expect(router).not.toContain('stale'); + }); + + it('is a no-op when evidence/code does not exist', async () => { + const empty = path.join(path.dirname(wiki), 'empty-wiki'); + await mkdir(empty, { recursive: true }); + await expect(rebuildWikiIndex(empty)).resolves.toBeUndefined(); + }); +}); diff --git a/src/import-org.ts b/src/import-org.ts index f28fbe7..616447b 100644 --- a/src/import-org.ts +++ b/src/import-org.ts @@ -216,18 +216,22 @@ export async function importFromOrg(opts: ImportFromOrgOptions): Promise { `Batch import complete: ${result.succeeded} succeeded, ${result.failed.length} failed, ${result.skipped.length} skipped`, ); // Rebuild global router.md / index.md with full stats - try { - const { rebuildWikiIndex } = await import('./rebuild-wiki-index.js'); - const teamRepoPath = path.join(cwd, '.teamai', 'team-repo'); - const teamRepoWiki = path.join(teamRepoPath, 'teamwiki'); - if (await fs.pathExists(teamRepoWiki)) { - await rebuildWikiIndex(teamRepoWiki); - log.info('teamwiki router.md / index.md rebuilt'); - const { autoPushTeamRepo } = await import('./utils/git.js'); - await autoPushTeamRepo(teamRepoPath, '[teamai] Rebuild teamwiki index after batch import'); + if (!opts.dryRun) { + try { + const { rebuildWikiIndex } = await import('./rebuild-wiki-index.js'); + const { autoDetectInit } = await import('./config.js'); + const { localConfig } = await autoDetectInit(); + const teamRepoPath = localConfig.repo.localPath; + const teamRepoWiki = path.join(teamRepoPath, 'teamwiki'); + if (await fs.pathExists(teamRepoWiki)) { + await rebuildWikiIndex(teamRepoWiki); + log.info('teamwiki router.md / index.md rebuilt'); + const { autoPushTeamRepo } = await import('./utils/git.js'); + await autoPushTeamRepo(teamRepoPath, '[teamai] Rebuild teamwiki index after batch import'); + } + } catch (e) { + log.warn(`wiki index rebuild/push failed: ${e instanceof Error ? e.message : String(e)}`); } - } catch (e) { - log.debug(`wiki index rebuild/push failed: ${(e as Error).message}`); } } catch (err) { log.warn(`Batch import error (non-blocking): ${String(err)}`); diff --git a/src/import-repo-list.ts b/src/import-repo-list.ts index 202d3d8..2cae7eb 100644 --- a/src/import-repo-list.ts +++ b/src/import-repo-list.ts @@ -157,6 +157,24 @@ export async function importFromRepoList( } } + // 4.5 全量重建全局导航文件 router.md / index.md(基于完整 evidence/code/ 目录,覆盖 append 的中间状态) + if (!dryRun && succeeded.length > 0) { + try { + const { autoDetectInit } = await import('./config.js'); + const { localConfig } = await autoDetectInit(); + const teamwikiRoot = path.join(localConfig.repo.localPath, 'teamwiki'); + + if (await fs.pathExists(teamwikiRoot)) { + const { rebuildWikiIndex } = await import('./rebuild-wiki-index.js'); + await rebuildWikiIndex(teamwikiRoot); + log.info('teamwiki router.md / index.md rebuilt'); + } + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + log.warn(`[wiki] global index rebuild failed (non-blocking): ${msg}`); + } + } + // 5. 统一推送(graph 通过 MR 提交) if (!dryRun && succeeded.length > 0) { try { diff --git a/src/import-repo.ts b/src/import-repo.ts index a1ad4e6..4fa5e14 100644 --- a/src/import-repo.ts +++ b/src/import-repo.ts @@ -400,36 +400,6 @@ export async function importFromRepo(opts: ImportFromRepoOptions): Promise await fs.writeFile(domainsJsonPath, JSON.stringify({ domain: explicitDomain }, null, 2), 'utf8'); } } - // Update top-level router.md and index.md (append new project, do not overwrite) - const { routerTemplate, indexTemplate, HOT_TEMPLATE } = await import('./wiki-engine/adapters/templates.js'); - const routerPath = path.join(teamwikiRoot, 'router.md'); - const indexPath = path.join(teamwikiRoot, 'index.md'); - const projectLink = `[[evidence/code/${slug}/index]]`; - if (await fs.pathExists(routerPath)) { - const router = await fs.readFile(routerPath, 'utf8'); - if (!router.includes(projectLink)) { - const line = `- ${projectLink} — ${slug} code knowledge\n`; - await fs.writeFile(routerPath, router.trimEnd() + '\n' + line, 'utf8'); - } - } else { - await fs.writeFile(routerPath, routerTemplate([{ slug, label: slug }]), 'utf8'); - } - if (await fs.pathExists(indexPath)) { - const idx = await fs.readFile(indexPath, 'utf8'); - if (!idx.includes(slug)) { - const insertPoint = idx.indexOf('## Navigation'); - if (insertPoint > 0) { - const entry = `- [${slug}](./evidence/code/${slug}/index.md) — code knowledge graph\n\n`; - await fs.writeFile(indexPath, idx.slice(0, insertPoint) + entry + idx.slice(insertPoint), 'utf8'); - } - } - } else { - await fs.writeFile(indexPath, indexTemplate([{ slug, label: slug }]), 'utf8'); - } - if (!await fs.pathExists(path.join(teamwikiRoot, 'hot.md'))) { - await fs.writeFile(path.join(teamwikiRoot, 'hot.md'), HOT_TEMPLATE, 'utf8'); - } - log.info(chalk.green(`✓ teamwiki/ knowledge graph updated: ${slug}`)); } catch (err) { log.debug(`[wiki-engine] Graph generation failed (non-blocking): ${err instanceof Error ? err.message : err}`); @@ -474,6 +444,14 @@ export async function importFromRepo(opts: ImportFromRepoOptions): Promise } catch (e) { log.debug(`[graph] Single-repo aggregation skipped: ${(e as Error).message}`); } + // Rebuild global router.md / index.md from the full evidence/code set + try { + const { rebuildWikiIndex } = await import('./rebuild-wiki-index.js'); + await rebuildWikiIndex(teamwikiRoot); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + log.warn(`[wiki] global index rebuild failed (non-blocking): ${msg}`); + } } if (await fs.pathExists(teamRepoDir) && mrTeamConfig && mrLocalConfig) { const { autoPushViaMR } = await import('./utils/git.js'); diff --git a/src/import.ts b/src/import.ts index 855c9c2..00b77d4 100644 --- a/src/import.ts +++ b/src/import.ts @@ -225,6 +225,13 @@ export async function importCmd(opts: ImportOptions): Promise { skipAutoPush: true, sourceMrUrl: opts.fromMr, }); + try { + const { rebuildWikiIndex } = await import('./rebuild-wiki-index.js'); + await rebuildWikiIndex(teamwikiRoot); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + log.warn(`[wiki] global index rebuild failed (non-blocking): ${msg}`); + } ctx.didUpdate = true; } else { task.skip('No existing evidence for this repo'); @@ -317,6 +324,15 @@ export async function importCmd(opts: ImportOptions): Promise { const { aggregateGlobalGraph } = await import('./graph-aggregate.js'); await aggregateGlobalGraph(teamwikiRoot); + // Rebuild global router.md / index.md so newly imported repos appear in navigation + try { + const { rebuildWikiIndex } = await import('./rebuild-wiki-index.js'); + await rebuildWikiIndex(teamwikiRoot); + log.info('teamwiki router.md / index.md rebuilt'); + } catch (e) { + log.warn(`[wiki] global index rebuild failed (non-blocking): ${e instanceof Error ? e.message : String(e)}`); + } + await autoPushTeamRepo(teamRepoPath, `[teamai] Import from local dir: ${slug}`); log.success(`Pushed to team knowledge repo (${localConfig.repo.remote})`); }