From b34030056850f5564553014847014d880a04d638 Mon Sep 17 00:00:00 2001 From: Josh Crites Date: Mon, 2 Mar 2026 17:04:32 -0500 Subject: [PATCH 1/3] feat: add MCP logging to sync operations for real-time progress visibility Adds structured logging throughout the clone/update lifecycle so MCP clients can surface progress messages (repo-by-repo status, git fetch/checkout stages, sparse-checkout paths, and error details). Also includes the gc.auto=0 fix for blobless clones to prevent the race condition. Co-Authored-By: Claude Opus 4.6 --- src/index.ts | 9 +++++++ src/tools/sync.ts | 18 ++++++++++--- src/utils/git.ts | 65 ++++++++++++++++++++++++++++++++++++----------- 3 files changed, 74 insertions(+), 18 deletions(-) diff --git a/src/index.ts b/src/index.ts index 59d9471..d995fd0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,6 +41,7 @@ const server = new Server( { capabilities: { tools: {}, + logging: {}, }, } ); @@ -197,10 +198,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { try { switch (name) { case "aztec_sync_repos": { + const log = (message: string, level: string = "info") => { + server.sendLoggingMessage({ + level: level as "info" | "debug" | "warning" | "error", + logger: "aztec-sync", + data: message, + }); + }; const result = await syncRepos({ version: args?.version as string | undefined, force: args?.force as boolean | undefined, repos: args?.repos as string[] | undefined, + log, }); return { content: [ diff --git a/src/tools/sync.ts b/src/tools/sync.ts index a8d0a68..146562b 100644 --- a/src/tools/sync.ts +++ b/src/tools/sync.ts @@ -3,7 +3,7 @@ */ import { AZTEC_REPOS, getAztecRepos, DEFAULT_AZTEC_VERSION, RepoConfig } from "../repos/config.js"; -import { cloneRepo, getReposStatus, getNoirCommitFromAztec, REPOS_DIR } from "../utils/git.js"; +import { cloneRepo, getReposStatus, getNoirCommitFromAztec, REPOS_DIR, Logger } from "../utils/git.js"; export interface SyncResult { success: boolean; @@ -24,8 +24,9 @@ export async function syncRepos(options: { force?: boolean; repos?: string[]; version?: string; + log?: Logger; }): Promise { - const { force = false, repos: repoNames, version } = options; + const { force = false, repos: repoNames, version, log } = options; // Get repos configured for the specified version const configuredRepos = version ? getAztecRepos(version) : AZTEC_REPOS; @@ -45,13 +46,19 @@ export async function syncRepos(options: { }; } + log?.(`Starting sync: ${reposToSync.length} repos, version=${effectiveVersion}, force=${force}`, "info"); + const results: SyncResult["repos"] = []; + let syncIndex = 0; async function syncRepo(config: RepoConfig, statusTransform?: (s: string) => string): Promise { + syncIndex++; + log?.(`Syncing ${syncIndex}/${reposToSync.length}: ${config.name}`, "info"); try { - const status = await cloneRepo(config, force); + const status = await cloneRepo(config, force, log); results.push({ name: config.name, status: statusTransform ? statusTransform(status) : status }); } catch (error) { + log?.(`${config.name}: Failed: ${error instanceof Error ? error.message : String(error)}`, "error"); results.push({ name: config.name, status: `Error: ${error instanceof Error ? error.message : String(error)}`, @@ -73,6 +80,9 @@ export async function syncRepos(options: { // Get the Noir commit from aztec-packages (if available) const noirCommit = await getNoirCommitFromAztec(); + if (noirCommit) { + log?.(`Resolved Noir commit from aztec-packages: ${noirCommit.substring(0, 7)}`, "info"); + } // Clone Noir repos with the commit from aztec-packages for (const config of noirRepos) { @@ -96,6 +106,8 @@ export async function syncRepos(options: { (r) => !r.status.toLowerCase().includes("error") ); + log?.(`Sync complete: ${results.length} repos, ${allSuccess ? "all succeeded" : "some failed"}`, "info"); + return { success: allSuccess, message: allSuccess diff --git a/src/utils/git.ts b/src/utils/git.ts index 3cc7c13..e5fdfce 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -8,6 +8,8 @@ import { join } from "path"; import { homedir } from "os"; import { RepoConfig } from "../repos/config.js"; +export type Logger = (message: string, level?: "info" | "debug" | "warning" | "error") => void; + /** Base directory for cloned repos */ export const REPOS_DIR = join( process.env.AZTEC_MCP_REPOS_DIR || join(homedir(), ".aztec-mcp"), @@ -41,7 +43,8 @@ export function isRepoCloned(repoName: string): boolean { */ export async function cloneRepo( config: RepoConfig, - force: boolean = false + force: boolean = false, + log?: Logger ): Promise { ensureReposDir(); const repoPath = getRepoPath(config.name); @@ -51,21 +54,32 @@ export async function cloneRepo( // Remove existing if force is set or version changed if ((force || versionMismatch) && existsSync(repoPath)) { + log?.(`${config.name}: Removing existing clone (force=${force}, versionMismatch=${versionMismatch})`, "debug"); rmSync(repoPath, { recursive: true, force: true }); } // If already cloned and version matches, just update if (isRepoCloned(config.name)) { - return await updateRepo(config.name); + log?.(`${config.name}: Already cloned, updating`, "debug"); + return await updateRepo(config.name, log); } - const git: SimpleGit = simpleGit(); - // Determine ref to checkout: commit > tag > branch const ref = config.commit || config.tag || config.branch || "default"; const refType = config.commit ? "commit" : config.tag ? "tag" : "branch"; + const isSparse = config.sparse && config.sparse.length > 0; + + log?.(`${config.name}: Cloning @ ${ref} (${refType}${isSparse ? ", sparse" : ""})`, "info"); + + const progressHandler = log + ? (data: { method: string; stage: string; progress: number }) => { + log(`${config.name}: ${data.method} ${data.stage} ${data.progress}%`, "debug"); + } + : undefined; + + const git: SimpleGit = simpleGit({ progress: progressHandler }); - if (config.sparse && config.sparse.length > 0) { + if (isSparse) { // Clone with sparse checkout for large repos if (config.commit) { // For commits, we need full history to fetch the commit @@ -75,10 +89,13 @@ export async function cloneRepo( "--no-checkout", ]); - const repoGit = simpleGit(repoPath); + const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler }); await repoGit.raw(["config", "gc.auto", "0"]); - await repoGit.raw(["sparse-checkout", "set", ...config.sparse]); + log?.(`${config.name}: Setting sparse checkout paths: ${config.sparse!.join(", ")}`, "debug"); + await repoGit.raw(["sparse-checkout", "set", ...config.sparse!]); + log?.(`${config.name}: Fetching commit ${config.commit.substring(0, 7)}`, "info"); await repoGit.fetch(["origin", config.commit]); + log?.(`${config.name}: Checking out commit`, "debug"); await repoGit.checkout(config.commit); } else if (config.tag) { await git.clone(config.url, repoPath, [ @@ -87,22 +104,28 @@ export async function cloneRepo( "--no-checkout", ]); - const repoGit = simpleGit(repoPath); + const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler }); await repoGit.raw(["config", "gc.auto", "0"]); - await repoGit.raw(["sparse-checkout", "set", ...config.sparse]); + log?.(`${config.name}: Setting sparse checkout paths: ${config.sparse!.join(", ")}`, "debug"); + await repoGit.raw(["sparse-checkout", "set", ...config.sparse!]); + log?.(`${config.name}: Fetching tag ${config.tag}`, "info"); await repoGit.fetch(["--depth=1", "origin", `refs/tags/${config.tag}:refs/tags/${config.tag}`]); + log?.(`${config.name}: Checking out tag`, "debug"); await repoGit.checkout(config.tag); // Apply sparse path overrides from different branches if (config.sparsePathOverrides) { for (const override of config.sparsePathOverrides) { + log?.(`${config.name}: Fetching override branch ${override.branch}`, "debug"); await repoGit.fetch(["--depth=1", "origin", override.branch]); try { + log?.(`${config.name}: Checking out override paths from ${override.branch}: ${override.paths.join(", ")}`, "debug"); await repoGit.checkout([`origin/${override.branch}`, "--", ...override.paths]); } catch (error) { const repoBase = config.url.replace(/\.git$/, ""); const parentDirs = [...new Set(override.paths.map((p) => p.split("/").slice(0, -1).join("/")))]; const browseLinks = parentDirs.map((d) => `${repoBase}/tree/${override.branch}/${d}`); + log?.(`${config.name}: sparsePathOverrides failed for branch "${override.branch}"`, "error"); throw new Error( `sparsePathOverrides failed for branch "${override.branch}": could not checkout paths [${override.paths.join(", ")}]. ` + `Check the actual folder names at: ${browseLinks.join(" , ")}`, @@ -118,25 +141,31 @@ export async function cloneRepo( ...(config.branch ? ["-b", config.branch] : []), ]); - const repoGit = simpleGit(repoPath); + const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler }); await repoGit.raw(["config", "gc.auto", "0"]); - await repoGit.raw(["sparse-checkout", "set", ...config.sparse]); + log?.(`${config.name}: Setting sparse checkout paths: ${config.sparse!.join(", ")}`, "debug"); + await repoGit.raw(["sparse-checkout", "set", ...config.sparse!]); } - return `Cloned ${config.name} @ ${ref} (${refType}, sparse: ${config.sparse.join(", ")})`; + log?.(`${config.name}: Clone complete`, "info"); + return `Cloned ${config.name} @ ${ref} (${refType}, sparse: ${config.sparse!.join(", ")})`; } else { // Clone for smaller repos if (config.commit) { // For commits, clone and checkout specific commit await git.clone(config.url, repoPath, ["--no-checkout"]); - const repoGit = simpleGit(repoPath); + const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler }); + log?.(`${config.name}: Fetching commit ${config.commit.substring(0, 7)}`, "info"); await repoGit.fetch(["origin", config.commit]); + log?.(`${config.name}: Checking out commit`, "debug"); await repoGit.checkout(config.commit); } else if (config.tag) { // Clone and checkout tag await git.clone(config.url, repoPath, ["--no-checkout"]); - const repoGit = simpleGit(repoPath); + const repoGit = simpleGit({ baseDir: repoPath, progress: progressHandler }); + log?.(`${config.name}: Fetching tag ${config.tag}`, "info"); await repoGit.fetch(["--depth=1", "origin", `refs/tags/${config.tag}:refs/tags/${config.tag}`]); + log?.(`${config.name}: Checking out tag`, "debug"); await repoGit.checkout(config.tag); } else { await git.clone(config.url, repoPath, [ @@ -145,6 +174,7 @@ export async function cloneRepo( ]); } + log?.(`${config.name}: Clone complete`, "info"); return `Cloned ${config.name} @ ${ref} (${refType})`; } } @@ -152,25 +182,30 @@ export async function cloneRepo( /** * Update an existing repository */ -export async function updateRepo(repoName: string): Promise { +export async function updateRepo(repoName: string, log?: Logger): Promise { const repoPath = getRepoPath(repoName); if (!isRepoCloned(repoName)) { throw new Error(`Repository ${repoName} is not cloned`); } + log?.(`${repoName}: Updating`, "info"); const git = simpleGit(repoPath); try { await git.fetch(["--depth=1"]); await git.reset(["--hard", "origin/HEAD"]); + log?.(`${repoName}: Update complete`, "info"); return `Updated ${repoName}`; } catch (error) { + log?.(`${repoName}: Fetch failed, trying pull`, "warning"); // If fetch fails, try a simple pull try { await git.pull(); + log?.(`${repoName}: Pull complete`, "info"); return `Updated ${repoName}`; } catch (pullError) { + log?.(`${repoName}: Update failed: ${pullError}`, "error"); return `Failed to update ${repoName}: ${pullError}`; } } From 20e58277508578e19f737caa015f3fa496cbdf6e Mon Sep 17 00:00:00 2001 From: Josh Crites Date: Mon, 2 Mar 2026 17:08:35 -0500 Subject: [PATCH 2/3] fix: handle logging edge cases from review feedback - Catch sendLoggingMessage rejections to prevent unhandled promise rejections when the client disconnects during sync - Only pass logger arg to cloneRepo when defined, preserving the two-argument call contract for callers without logging Co-Authored-By: Claude Opus 4.6 --- src/index.ts | 2 +- src/tools/sync.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index d995fd0..e958f44 100644 --- a/src/index.ts +++ b/src/index.ts @@ -203,7 +203,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { level: level as "info" | "debug" | "warning" | "error", logger: "aztec-sync", data: message, - }); + }).catch(() => {}); }; const result = await syncRepos({ version: args?.version as string | undefined, diff --git a/src/tools/sync.ts b/src/tools/sync.ts index 146562b..b78a135 100644 --- a/src/tools/sync.ts +++ b/src/tools/sync.ts @@ -55,7 +55,7 @@ export async function syncRepos(options: { syncIndex++; log?.(`Syncing ${syncIndex}/${reposToSync.length}: ${config.name}`, "info"); try { - const status = await cloneRepo(config, force, log); + const status = log ? await cloneRepo(config, force, log) : await cloneRepo(config, force); results.push({ name: config.name, status: statusTransform ? statusTransform(status) : status }); } catch (error) { log?.(`${config.name}: Failed: ${error instanceof Error ? error.message : String(error)}`, "error"); From 3560504ee12a148bfba0b85cf70c43a445be11d1 Mon Sep 17 00:00:00 2001 From: Josh Crites Date: Mon, 2 Mar 2026 20:29:19 -0500 Subject: [PATCH 3/3] feat: split sparsePathOverrides into separate clone and parallelize sync Instead of overlaying docs from the `next` branch into the blobless tag clone of aztec-packages (which triggered slow one-by-one lazy blob fetching or SIGSEGV), create a separate shallow clone (`aztec-packages-docs`) for the docs paths. All independent repos now clone in parallel via Promise.all after the blocking aztec-packages clone completes. Co-Authored-By: Claude Opus 4.6 --- src/tools/search.ts | 4 +- src/tools/sync.ts | 73 ++++++++++++++++++++++---------- src/utils/git.ts | 27 +++--------- src/utils/search.ts | 21 ++++++---- tests/tools/search.test.ts | 4 +- tests/tools/sync.test.ts | 28 +++++++++++-- tests/utils/git.test.ts | 85 +++----------------------------------- tests/utils/search.test.ts | 9 ++-- 8 files changed, 109 insertions(+), 142 deletions(-) diff --git a/src/tools/search.ts b/src/tools/search.ts index 380dc53..dd328eb 100644 --- a/src/tools/search.ts +++ b/src/tools/search.ts @@ -73,12 +73,12 @@ export function searchAztecDocs(options: { } { const { query, section, maxResults = 20 } = options; - if (!isRepoCloned("aztec-packages")) { + if (!isRepoCloned("aztec-packages-docs")) { return { success: false, results: [], message: - "aztec-packages is not cloned. Run aztec_sync_repos first to get documentation.", + "aztec-packages-docs is not cloned. Run aztec_sync_repos first to get documentation.", }; } diff --git a/src/tools/sync.ts b/src/tools/sync.ts index b78a135..1f99980 100644 --- a/src/tools/sync.ts +++ b/src/tools/sync.ts @@ -46,14 +46,35 @@ export async function syncRepos(options: { }; } - log?.(`Starting sync: ${reposToSync.length} repos, version=${effectiveVersion}, force=${force}`, "info"); + // Generate synthetic repo configs from sparsePathOverrides + const syntheticRepos: RepoConfig[] = []; + for (const repo of reposToSync) { + if (repo.sparsePathOverrides) { + for (const override of repo.sparsePathOverrides) { + syntheticRepos.push({ + name: `${repo.name}-docs`, + url: repo.url, + branch: override.branch, + sparse: override.paths, + description: `${repo.description} (docs from ${override.branch})`, + }); + } + } + } + + // Include synthetic repos in total count + const totalRepos = reposToSync.length + syntheticRepos.length; + log?.(`Starting sync: ${totalRepos} repos, version=${effectiveVersion}, force=${force}`, "info"); const results: SyncResult["repos"] = []; - let syncIndex = 0; - async function syncRepo(config: RepoConfig, statusTransform?: (s: string) => string): Promise { - syncIndex++; - log?.(`Syncing ${syncIndex}/${reposToSync.length}: ${config.name}`, "info"); + async function syncRepo( + config: RepoConfig, + index: number, + total: number, + statusTransform?: (s: string) => string, + ): Promise { + log?.(`Syncing ${index}/${total}: ${config.name}`, "info"); try { const status = log ? await cloneRepo(config, force, log) : await cloneRepo(config, force); results.push({ name: config.name, status: statusTransform ? statusTransform(status) : status }); @@ -66,16 +87,11 @@ export async function syncRepos(options: { } } - // Sort repos so aztec-packages is cloned first (needed to determine Noir version) + // Clone aztec-packages first (blocking - needed to determine Noir version) const aztecPackages = reposToSync.find((r) => r.name === "aztec-packages"); - const noirRepos = reposToSync.filter((r) => r.url.includes("noir-lang")); - const otherRepos = reposToSync.filter( - (r) => r.name !== "aztec-packages" && !r.url.includes("noir-lang") - ); - - // Clone aztec-packages first if present + let nextIndex = 1; if (aztecPackages) { - await syncRepo(aztecPackages); + await syncRepo(aztecPackages, nextIndex++, totalRepos); } // Get the Noir commit from aztec-packages (if available) @@ -84,24 +100,39 @@ export async function syncRepos(options: { log?.(`Resolved Noir commit from aztec-packages: ${noirCommit.substring(0, 7)}`, "info"); } - // Clone Noir repos with the commit from aztec-packages + // Build list of all remaining repos to clone in parallel + const parallelBatch: { config: RepoConfig; index: number; statusTransform?: (s: string) => string }[] = []; + + const noirRepos = reposToSync.filter((r) => r.url.includes("noir-lang")); + const otherRepos = reposToSync.filter( + (r) => r.name !== "aztec-packages" && !r.url.includes("noir-lang") + ); + for (const config of noirRepos) { const useAztecCommit = config.name === "noir" && noirCommit; const noirConfig: RepoConfig = useAztecCommit ? { ...config, commit: noirCommit, branch: undefined } : config; - - await syncRepo( - noirConfig, - useAztecCommit ? (s) => s.replace("(commit", "(commit from aztec-packages") : undefined - ); + parallelBatch.push({ + config: noirConfig, + index: nextIndex++, + statusTransform: useAztecCommit ? (s) => s.replace("(commit", "(commit from aztec-packages") : undefined, + }); } - // Clone other repos for (const config of otherRepos) { - await syncRepo(config); + parallelBatch.push({ config, index: nextIndex++ }); } + for (const config of syntheticRepos) { + parallelBatch.push({ config, index: nextIndex++ }); + } + + // Clone all remaining repos in parallel + await Promise.all( + parallelBatch.map((item) => syncRepo(item.config, item.index, totalRepos, item.statusTransform)) + ); + const allSuccess = results.every( (r) => !r.status.toLowerCase().includes("error") ); diff --git a/src/utils/git.ts b/src/utils/git.ts index e5fdfce..72ada50 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -58,8 +58,12 @@ export async function cloneRepo( rmSync(repoPath, { recursive: true, force: true }); } - // If already cloned and version matches, just update + // If already cloned and version matches, skip or update if (isRepoCloned(config.name)) { + if (config.tag || config.commit) { + log?.(`${config.name}: Already cloned at correct ${config.tag ? "tag" : "commit"}, skipping`, "debug"); + return `${config.name} already at ${config.commit || config.tag}`; + } log?.(`${config.name}: Already cloned, updating`, "debug"); return await updateRepo(config.name, log); } @@ -112,27 +116,6 @@ export async function cloneRepo( await repoGit.fetch(["--depth=1", "origin", `refs/tags/${config.tag}:refs/tags/${config.tag}`]); log?.(`${config.name}: Checking out tag`, "debug"); await repoGit.checkout(config.tag); - - // Apply sparse path overrides from different branches - if (config.sparsePathOverrides) { - for (const override of config.sparsePathOverrides) { - log?.(`${config.name}: Fetching override branch ${override.branch}`, "debug"); - await repoGit.fetch(["--depth=1", "origin", override.branch]); - try { - log?.(`${config.name}: Checking out override paths from ${override.branch}: ${override.paths.join(", ")}`, "debug"); - await repoGit.checkout([`origin/${override.branch}`, "--", ...override.paths]); - } catch (error) { - const repoBase = config.url.replace(/\.git$/, ""); - const parentDirs = [...new Set(override.paths.map((p) => p.split("/").slice(0, -1).join("/")))]; - const browseLinks = parentDirs.map((d) => `${repoBase}/tree/${override.branch}/${d}`); - log?.(`${config.name}: sparsePathOverrides failed for branch "${override.branch}"`, "error"); - throw new Error( - `sparsePathOverrides failed for branch "${override.branch}": could not checkout paths [${override.paths.join(", ")}]. ` + - `Check the actual folder names at: ${browseLinks.join(" , ")}`, - ); - } - } - } } else { await git.clone(config.url, repoPath, [ "--filter=blob:none", diff --git a/src/utils/search.ts b/src/utils/search.ts index 0840c1c..3b10bb6 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -81,19 +81,26 @@ export function searchDocs( ): SearchResult[] { const { section, maxResults = 30 } = options; - // Determine search path based on section + // Search versioned docs under developer_versioned_docs, narrowing by section if possible let repo: string | undefined; - if (section) { - const docsPath = join(REPOS_DIR, "aztec-packages", "docs", "docs", section); - if (existsSync(docsPath)) { - // Search within the specific section by using a narrowed path - repo = `aztec-packages/docs/docs/${section}`; + const versionedDocsGlob = join(REPOS_DIR, "aztec-packages-docs", "docs", "developer_versioned_docs"); + const versionDirs = existsSync(versionedDocsGlob) ? globbySync("version-*", { cwd: versionedDocsGlob, onlyDirectories: true }) : []; + const versionDir = versionDirs[0]; + + if (section && versionDir) { + const sectionPath = join(versionedDocsGlob, versionDir, section); + if (existsSync(sectionPath)) { + repo = `aztec-packages-docs/docs/developer_versioned_docs/${versionDir}/${section}`; } } + if (!repo && versionDir) { + repo = `aztec-packages-docs/docs/developer_versioned_docs/${versionDir}`; + } + return searchCode(query, { filePattern: "*.{md,mdx}", - repo: repo || "aztec-packages", + repo: repo || "aztec-packages-docs", maxResults, }); } diff --git a/tests/tools/search.test.ts b/tests/tools/search.test.ts index 1d28a90..8895c4a 100644 --- a/tests/tools/search.test.ts +++ b/tests/tools/search.test.ts @@ -97,11 +97,11 @@ describe("searchAztecCode", () => { }); describe("searchAztecDocs", () => { - it("returns failure when aztec-packages not cloned", () => { + it("returns failure when aztec-packages-docs not cloned", () => { mockIsRepoCloned.mockReturnValue(false); const result = searchAztecDocs({ query: "tutorial" }); expect(result.success).toBe(false); - expect(result.message).toContain("aztec-packages is not cloned"); + expect(result.message).toContain("aztec-packages-docs is not cloned"); }); it("delegates to searchDocs with correct options", () => { diff --git a/tests/tools/sync.test.ts b/tests/tools/sync.test.ts index 6788421..7bcc37f 100644 --- a/tests/tools/sync.test.ts +++ b/tests/tools/sync.test.ts @@ -10,6 +10,13 @@ vi.mock("../../src/repos/config.js", () => ({ name: "aztec-packages", url: "https://github.com/AztecProtocol/aztec-packages", tag: "v1.0.0", + sparse: ["noir-projects/aztec-nr", "yarn-project"], + sparsePathOverrides: [ + { + paths: ["docs/developer_versioned_docs/version-v1.0.0", "docs/static/api"], + branch: "next", + }, + ], description: "Main repo", }, { @@ -42,6 +49,12 @@ vi.mock("../../src/repos/config.js", () => ({ name: "aztec-packages", url: "https://github.com/AztecProtocol/aztec-packages", tag: version, + sparsePathOverrides: [ + { + paths: ["docs/developer_versioned_docs/version-" + version], + branch: "next", + }, + ], description: "Main repo", }, { @@ -88,11 +101,13 @@ describe("syncRepos", () => { await syncRepos({}); - // aztec-packages should be first + // aztec-packages should be first (blocking clone) expect(callOrder[0]).toBe("aztec-packages"); // noir repos should come after aztec-packages const noirIndex = callOrder.indexOf("noir"); expect(noirIndex).toBeGreaterThan(0); + // synthetic docs repo should be included + expect(callOrder).toContain("aztec-packages-docs"); }); it("extracts noir commit from aztec-packages and applies it", async () => { @@ -112,8 +127,8 @@ describe("syncRepos", () => { it("uses AZTEC_REPOS when no version specified", async () => { await syncRepos({}); - // Should clone repos from AZTEC_REPOS (5 repos) - expect(mockCloneRepo).toHaveBeenCalledTimes(5); + // Should clone repos from AZTEC_REPOS (5 repos + 1 synthetic docs repo) + expect(mockCloneRepo).toHaveBeenCalledTimes(6); expect(mockGetAztecRepos).not.toHaveBeenCalled(); }); @@ -126,8 +141,13 @@ describe("syncRepos", () => { it("filters to specific repos when repos option provided", async () => { await syncRepos({ repos: ["aztec-packages"] }); - expect(mockCloneRepo).toHaveBeenCalledTimes(1); + // aztec-packages + synthetic aztec-packages-docs + expect(mockCloneRepo).toHaveBeenCalledTimes(2); expect(mockCloneRepo.mock.calls[0][0].name).toBe("aztec-packages"); + const docsCall = mockCloneRepo.mock.calls.find((c: any[]) => c[0].name === "aztec-packages-docs"); + expect(docsCall).toBeDefined(); + expect(docsCall![0].branch).toBe("next"); + expect(docsCall![0].sparse).toEqual(["docs/developer_versioned_docs/version-v1.0.0", "docs/static/api"]); }); it("returns success:false when no repos match filter", async () => { diff --git a/tests/utils/git.test.ts b/tests/utils/git.test.ts index 4f5175f..9485c18 100644 --- a/tests/utils/git.test.ts +++ b/tests/utils/git.test.ts @@ -147,80 +147,6 @@ describe("cloneRepo", () => { expect(mockGitInstance.checkout).toHaveBeenCalledWith("v1.0.0"); }); - it("sparse + tag + sparsePathOverrides: fetches override branch and checks out paths", async () => { - const overrideConfig: RepoConfig = { - ...sparseConfig, - sparsePathOverrides: [ - { - paths: [ - "docs/developer_versioned_docs/version-v1.0.0", - "docs/static/api", - ], - branch: "next", - }, - ], - }; - mockExistsSync.mockReturnValue(false); - mockGitInstance.clone.mockResolvedValue(undefined); - mockGitInstance.raw.mockResolvedValue(undefined); - mockGitInstance.fetch.mockResolvedValue(undefined); - mockGitInstance.checkout.mockResolvedValue(undefined); - - const result = await cloneRepo(overrideConfig); - expect(result).toContain("Cloned aztec-packages"); - - // Normal tag checkout happens first - expect(mockGitInstance.checkout).toHaveBeenCalledWith("v1.0.0"); - - // Then override: fetch the branch and checkout paths from it - expect(mockGitInstance.fetch).toHaveBeenCalledWith([ - "--depth=1", - "origin", - "next", - ]); - expect(mockGitInstance.checkout).toHaveBeenCalledWith([ - "origin/next", - "--", - "docs/developer_versioned_docs/version-v1.0.0", - "docs/static/api", - ]); - }); - - it("sparse + tag + sparsePathOverrides: throws descriptive error with GitHub links on failure", async () => { - const overrideConfig: RepoConfig = { - ...sparseConfig, - sparsePathOverrides: [ - { - paths: [ - "docs/developer_versioned_docs/version-v1.0.0", - "docs/static/aztec-nr-api/devnet", - "docs/static/typescript-api/devnet", - ], - branch: "next", - }, - ], - }; - mockExistsSync.mockReturnValue(false); - mockGitInstance.clone.mockResolvedValue(undefined); - mockGitInstance.raw.mockResolvedValue(undefined); - mockGitInstance.fetch.mockResolvedValue(undefined); - // Tag checkout succeeds, override checkout fails - mockGitInstance.checkout - .mockResolvedValueOnce(undefined) // tag checkout - .mockRejectedValueOnce(new Error("pathspec did not match")); - - try { - await cloneRepo(overrideConfig); - expect.unreachable("should have thrown"); - } catch (e: any) { - expect(e.message).toMatch(/sparsePathOverrides failed for branch "next"/); - expect(e.message).toContain("docs/developer_versioned_docs/version-v1.0.0"); - expect(e.message).toContain("https://github.com/AztecProtocol/aztec-packages/tree/next/docs/developer_versioned_docs"); - expect(e.message).toContain("https://github.com/AztecProtocol/aztec-packages/tree/next/docs/static/aztec-nr-api"); - expect(e.message).toContain("https://github.com/AztecProtocol/aztec-packages/tree/next/docs/static/typescript-api"); - } - }); - it("sparse + commit: clones with sparse flags, fetches commit", async () => { const commitConfig: RepoConfig = { ...sparseConfig, @@ -303,18 +229,17 @@ describe("cloneRepo", () => { ); }); - it("already cloned + version match delegates to updateRepo", async () => { + it("already cloned + version match skips update for tag-pinned repos", async () => { // needsReclone: isRepoCloned returns true, tag matches mockExistsSync.mockReturnValue(true); // getRepoTag needs git.raw to return the tag mockGitInstance.raw.mockResolvedValue("v1.0.0\n"); - // isRepoCloned returns true -> delegates to updateRepo - // updateRepo does fetch + reset - mockGitInstance.fetch.mockResolvedValue(undefined); - mockGitInstance.reset.mockResolvedValue(undefined); const result = await cloneRepo(sparseConfig); - expect(result).toContain("Updated"); + expect(result).toContain("already at v1.0.0"); + // Should NOT call fetch/reset (updateRepo not invoked) + expect(mockGitInstance.fetch).not.toHaveBeenCalled(); + expect(mockGitInstance.reset).not.toHaveBeenCalled(); }); }); diff --git a/tests/utils/search.test.ts b/tests/utils/search.test.ts index f0aea1e..e87905e 100644 --- a/tests/utils/search.test.ts +++ b/tests/utils/search.test.ts @@ -236,15 +236,16 @@ describe("searchDocs", () => { it("narrows path when section exists", () => { mockExistsSync.mockReturnValue(true); + mockGlobbySync.mockReturnValue(["version-v1.0.0"] as any); mockExecSync.mockReturnValue(""); searchDocs("tutorial", { section: "tutorials" }); const call = mockExecSync.mock.calls[0][0] as string; - expect(call).toContain("aztec-packages/docs/docs/tutorials"); + expect(call).toContain("aztec-packages-docs/docs/developer_versioned_docs/version-v1.0.0/tutorials"); }); - it("falls back to aztec-packages when section doesn't exist", () => { + it("falls back to aztec-packages-docs when section doesn't exist", () => { // existsSync: first call for section path returns false, second for search path returns true mockExistsSync .mockReturnValueOnce(false) // section path doesn't exist @@ -254,8 +255,8 @@ describe("searchDocs", () => { searchDocs("tutorial", { section: "nonexistent" }); const call = mockExecSync.mock.calls[0][0] as string; - // Should search in aztec-packages, not the nonexistent section - expect(call).toContain("/fake/repos/aztec-packages"); + // Should search in aztec-packages-docs, not the nonexistent section + expect(call).toContain("/fake/repos/aztec-packages-docs"); }); });