diff --git a/changelog.d/unreleased/3125.fixed.md b/changelog.d/unreleased/3125.fixed.md new file mode 100644 index 0000000000..8b347e85a9 --- /dev/null +++ b/changelog.d/unreleased/3125.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 3125 +affected: + - src/CodeIndex/Cli/ExportImportCommandRunner.cs + - tests/CodeIndex.Tests/ExportImportCommandRunnerTests.cs +--- + +## English + +- **`import` no longer reports failure after successful DB replacement solely because sidecar cleanup failed (#3125)** — destination WAL/SHM cleanup after the main database move is now best-effort. + +## 日本語 + +- **DB 本体の置換成功後に sidecar cleanup だけで `import` が失敗扱いにならなくなりました (#3125)** — main database move 後の destination WAL/SHM cleanup は best-effort として扱われます。 diff --git a/changelog.d/unreleased/3138.fixed.md b/changelog.d/unreleased/3138.fixed.md new file mode 100644 index 0000000000..82f0afe306 --- /dev/null +++ b/changelog.d/unreleased/3138.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 3138 +affected: + - src/CodeIndex/Cli/ExportImportCommandRunner.cs + - tests/CodeIndex.Tests/ExportImportCommandRunnerTests.cs +--- + +## English + +- **`export` archive and ctags output now use the absolutized output path (#3138)** — export validation, directory creation, atomic writes, and final reporting now share the same resolved destination path. + +## 日本語 + +- **`export` の archive / ctags 出力が absolute output path を使うようになりました (#3138)** — export の検証、ディレクトリ作成、atomic write、最終報告が同じ解決済み出力先を共有します。 diff --git a/changelog.d/unreleased/3147.fixed.md b/changelog.d/unreleased/3147.fixed.md new file mode 100644 index 0000000000..d267062b1e --- /dev/null +++ b/changelog.d/unreleased/3147.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 3147 +affected: + - src/CodeIndex/Cli/ReportCommandRunner.cs + - tests/CodeIndex.Tests/ReportCommandRunnerTests.cs +--- + +## English + +- **`report` now writes bundles through the same absolutized output path it reports (#3147)** — relative report bundle paths are fixed before bundle writing so current-directory drift cannot split summary metadata from the actual write target. + +## 日本語 + +- **`report` が表示する absolute output path と同じパスで bundle を書き込むようになりました (#3147)** — report bundle の相対パスは書き込み前に固定されるため、カレントディレクトリの変化で summary metadata と実際の書き込み先がずれなくなりました。 diff --git a/changelog.d/unreleased/3148.fixed.md b/changelog.d/unreleased/3148.fixed.md new file mode 100644 index 0000000000..fb9bd202e0 --- /dev/null +++ b/changelog.d/unreleased/3148.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 3148 +affected: + - src/CodeIndex/Cli/ReportCommandRunner.cs + - tests/CodeIndex.Tests/ReportCommandRunnerTests.cs +--- + +## English + +- **`report --db file:...` now reads existing SQLite file URI databases (#3148)** — report schema collection normalizes SQLite file URI inputs before filesystem checks and read-only SQLite opens. + +## 日本語 + +- **`report --db file:...` が既存の SQLite file URI DB を読めるようになりました (#3148)** — report の schema 収集は filesystem check と read-only SQLite open の前に SQLite file URI 入力を正規化します。 diff --git a/changelog.d/unreleased/3175.fixed.md b/changelog.d/unreleased/3175.fixed.md new file mode 100644 index 0000000000..003a665820 --- /dev/null +++ b/changelog.d/unreleased/3175.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 3175 +affected: + - src/CodeIndex/Cli/DbPathResolver.cs + - tests/CodeIndex.Tests/DbPathResolverTests.cs +--- + +## English + +- **DB path metadata probes now stay best-effort on filesystem errors (#3175)** — resolver metadata reads now suppress expected filesystem and path exceptions in addition to SQLite exceptions. + +## 日本語 + +- **DB path metadata probe が filesystem error でも best-effort のままになりました (#3175)** — resolver の metadata read は SQLite 例外に加えて、想定される filesystem / path 例外も握って上位の解決処理を落とさなくなりました。 diff --git a/changelog.d/unreleased/3221.fixed.md b/changelog.d/unreleased/3221.fixed.md new file mode 100644 index 0000000000..0ecfff51d0 --- /dev/null +++ b/changelog.d/unreleased/3221.fixed.md @@ -0,0 +1,19 @@ +--- +category: fixed +issues: + - 3221 +affected: + - src/CodeIndex/Cli/DbCommandRunner.cs + - src/CodeIndex/Cli/DiffCommandRunner.cs + - src/CodeIndex/Cli/DbPathResolver.cs + - tests/CodeIndex.Tests/DbCommandRunnerTests.cs + - tests/CodeIndex.Tests/DiffCommandRunnerTests.cs +--- + +## English + +- **DB and diff commands now preserve SQLite file URI display values (#3221)** — `cdidx db` and `cdidx diff` no longer pass `file:` URI inputs through filesystem path normalization when reporting human or JSON database paths. + +## 日本語 + +- **DB / diff コマンドが SQLite file URI の表示値を保持するようになりました (#3221)** — `cdidx db` と `cdidx diff` は human / JSON の database path 表示時に `file:` URI 入力を filesystem path 正規化へ渡さなくなりました。 diff --git a/src/CodeIndex/Cli/DbCommandRunner.cs b/src/CodeIndex/Cli/DbCommandRunner.cs index 05770a77f0..19077eb975 100644 --- a/src/CodeIndex/Cli/DbCommandRunner.cs +++ b/src/CodeIndex/Cli/DbCommandRunner.cs @@ -119,12 +119,13 @@ private static int RunIntegrityCheck(DbCommandOptions options, JsonSerializerOpt var issues = result.Rows; var ok = issues.Count == 1 && string.Equals(issues[0], "ok", StringComparison.Ordinal); var jsonContext = CliJsonSerializerContextFactory.Create(jsonOptions); + var displayDbPath = DbPathResolver.FormatDbPathForDisplay(dbPath); if (options.Json) { Console.WriteLine(JsonSerializer.Serialize( new DbIntegrityCheckJsonResult( - Path.GetFullPath(isUri ? dbPath : dbPath), + displayDbPath, ok, ok ? new List() : issues, result.Truncated, @@ -137,7 +138,7 @@ private static int RunIntegrityCheck(DbCommandOptions options, JsonSerializerOpt else { Console.WriteLine("Integrity check"); - Console.WriteLine($" database: {Path.GetFullPath(isUri ? dbPath : dbPath)}"); + Console.WriteLine($" database: {displayDbPath}"); Console.WriteLine($" result : {(ok ? "ok" : "corrupted")}"); if (!ok) { @@ -174,7 +175,7 @@ private static int RunSchema(DbCommandOptions options, JsonSerializerOptions jso try { var schema = ReadSchema(dbPath); - var fullPath = Path.GetFullPath(isUri ? dbPath : dbPath); + var fullPath = DbPathResolver.FormatDbPathForDisplay(dbPath); if (options.Json) { var jsonContext = CliJsonSerializerContextFactory.Create(jsonOptions); @@ -255,7 +256,7 @@ private static int RunPrune(DbCommandOptions options, JsonSerializerOptions json try { var result = PruneOrphans(dbPath, apply: options.PruneApply); - var fullPath = Path.GetFullPath(isUri ? dbPath : dbPath); + var fullPath = DbPathResolver.FormatDbPathForDisplay(dbPath); if (options.Json) { var jsonContext = CliJsonSerializerContextFactory.Create(jsonOptions); diff --git a/src/CodeIndex/Cli/DbPathResolver.cs b/src/CodeIndex/Cli/DbPathResolver.cs index 30e7b39612..7586471592 100644 --- a/src/CodeIndex/Cli/DbPathResolver.cs +++ b/src/CodeIndex/Cli/DbPathResolver.cs @@ -340,7 +340,7 @@ public static bool TryHasIndexedHeadCommitBranchStamp(string dbPath) var raw = cmd.ExecuteScalar(); return raw is string value && !string.IsNullOrWhiteSpace(value) ? value : null; } - catch (SqliteException) + catch (Exception ex) when (IsMetadataProbeException(ex)) { return null; } @@ -357,7 +357,7 @@ private static bool TryMetaKeyExists(string dbPath, string key) cmd.Parameters.AddWithValue("@key", key); return cmd.ExecuteScalar() != null; } - catch (SqliteException) + catch (Exception ex) when (IsMetadataProbeException(ex)) { return false; } @@ -425,12 +425,20 @@ private static bool SiblingRootMatchesIndexedContents(string dbPath, string full private static SqliteConnection OpenMetadataConnection(string dbPath) { + if (OpenMetadataConnectionForTesting != null) + return OpenMetadataConnectionForTesting(dbPath); + return new SqliteConnection(BuildSqliteConnectionString(dbPath, SqliteOpenMode.ReadOnly)); } + internal static Func? OpenMetadataConnectionForTesting { get; set; } + public static bool UriRequestsReadOnly(string uriText) => SqliteFileUri.RequestsReadOnly(uriText); + internal static string FormatDbPathForDisplay(string dbPath) + => SqliteFileUri.StartsWithFileScheme(dbPath) ? dbPath : Path.GetFullPath(dbPath); + private static bool PathsEqual(string left, string right) => PathCasing.PathsEqual(left, right); @@ -492,13 +500,21 @@ LIMIT 5 return samples; } - catch (SqliteException) + catch (Exception ex) when (IsMetadataProbeException(ex)) { // Fall back to persisted metadata / 永続化 metadata 側へフォールバック return []; } } + private static bool IsMetadataProbeException(Exception ex) + => ex is SqliteException + or IOException + or UnauthorizedAccessException + or ArgumentException + or NotSupportedException + or PathTooLongException; + private static SampleMatchResult CountMatchingSamples(string candidateRoot, IReadOnlyList samples) { var checksumMatches = 0; diff --git a/src/CodeIndex/Cli/DiffCommandRunner.cs b/src/CodeIndex/Cli/DiffCommandRunner.cs index f385411d0e..1f8ebaf6f1 100644 --- a/src/CodeIndex/Cli/DiffCommandRunner.cs +++ b/src/CodeIndex/Cli/DiffCommandRunner.cs @@ -667,11 +667,10 @@ private static long ExecuteLong(SqliteConnection connection, string sql) private static DiffDbHeader ReadHeader(string dbPath) { - var isUri = dbPath.StartsWith("file:", StringComparison.OrdinalIgnoreCase); using var connection = OpenReadOnlyConnection(dbPath); return new DiffDbHeader( - Path.GetFullPath(isUri ? dbPath : dbPath), + DbPathResolver.FormatDbPathForDisplay(dbPath), ExecuteLong(connection, "PRAGMA user_version"), ExecuteCountIfTableExists(connection, "files"), ExecuteCountIfTableExists(connection, "symbols"), diff --git a/src/CodeIndex/Cli/ExportImportCommandRunner.cs b/src/CodeIndex/Cli/ExportImportCommandRunner.cs index a2bb3e2774..f2ca1b1883 100644 --- a/src/CodeIndex/Cli/ExportImportCommandRunner.cs +++ b/src/CodeIndex/Cli/ExportImportCommandRunner.cs @@ -179,7 +179,7 @@ private static int RunExportArchive(string[] args, JsonSerializerOptions jsonOpt var snapshotPath = Path.Combine(Path.GetTempPath(), $"codeindex-export-{Guid.NewGuid():N}.db"); try { - var outputDirectory = Path.GetDirectoryName(Path.GetFullPath(outputPath)); + var outputDirectory = Path.GetDirectoryName(fullOutputPath); if (!string.IsNullOrWhiteSpace(outputDirectory)) Directory.CreateDirectory(outputDirectory); @@ -192,12 +192,12 @@ private static int RunExportArchive(string[] args, JsonSerializerOptions jsonOpt } SqliteConnection.ClearAllPools(); manifest = manifest with { DatabaseSha256 = ComputeSha256(snapshotPath) }; - WriteExportArchiveFile(outputPath, snapshotPath, manifest, jsonOptions); + WriteExportArchiveFile(fullOutputPath, snapshotPath, manifest, jsonOptions); if (wantsJson) - Console.WriteLine(JsonSerializer.Serialize(new ExportArchiveResult("1", Path.GetFullPath(outputPath), fullSourceDbPath), jsonOptions)); + Console.WriteLine(JsonSerializer.Serialize(new ExportArchiveResult("1", fullOutputPath, fullSourceDbPath), jsonOptions)); else - Console.WriteLine($"Exported CodeIndex archive to {outputPath}"); + Console.WriteLine($"Exported CodeIndex archive to {fullOutputPath}"); return CommandExitCodes.Success; } catch (Exception ex) @@ -254,11 +254,11 @@ private static int RunExportCtags(string[] args) { using var db = new DbContext(normalizedDbPath); db.TryMigrateForRead(); - var outputDirectory = Path.GetDirectoryName(Path.GetFullPath(outputPath)); + var outputDirectory = Path.GetDirectoryName(fullOutputPath); if (!string.IsNullOrWhiteSpace(outputDirectory)) Directory.CreateDirectory(outputDirectory); - WriteCtagsFile(outputPath, writer => + WriteCtagsFile(fullOutputPath, writer => { writer.WriteLine("!_TAG_FILE_FORMAT\t2\t/extended format/"); writer.WriteLine("!_TAG_FILE_SORTED\t1\t/0=unsorted, 1=sorted, 2=foldcase/"); @@ -281,7 +281,7 @@ FROM symbols s } }); - Console.WriteLine($"Exported ctags to {outputPath}"); + Console.WriteLine($"Exported ctags to {fullOutputPath}"); return CommandExitCodes.Success; } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or SqliteException) @@ -312,8 +312,9 @@ private static void AddTextEntry(ZipArchive archive, string name, string content internal static void WriteExportArchiveFile(string outputPath, string snapshotPath, ExportManifest manifest, JsonSerializerOptions jsonOptions) { + var fullOutputPath = Path.GetFullPath(outputPath); AtomicFileWriter.Write( - outputPath, + fullOutputPath, stream => { using var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true); @@ -330,8 +331,9 @@ internal static void WriteCtagsFile(string outputPath, Action writeC { ArgumentNullException.ThrowIfNull(writeContents); + var fullOutputPath = Path.GetFullPath(outputPath); AtomicFileWriter.Write( - outputPath, + fullOutputPath, stream => { using var writer = new StreamWriter( @@ -587,10 +589,23 @@ private static void DeleteSqliteSidecars(string dbPath) private static void TryDeleteFile(string path) { - if (File.Exists(path)) - File.Delete(path); + try + { + if (!File.Exists(path)) + return; + + if (DeleteSqliteSidecarForTesting != null) + DeleteSqliteSidecarForTesting(path); + else + File.Delete(path); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException or PathTooLongException) + { + } } + internal static Action? DeleteSqliteSidecarForTesting { get; set; } + private static bool IsSamePath(string left, string right) => string.Equals( Path.GetFullPath(left).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), diff --git a/src/CodeIndex/Cli/ReportCommandRunner.cs b/src/CodeIndex/Cli/ReportCommandRunner.cs index c22286b375..c890133d78 100644 --- a/src/CodeIndex/Cli/ReportCommandRunner.cs +++ b/src/CodeIndex/Cli/ReportCommandRunner.cs @@ -59,12 +59,13 @@ public static int Run(string[] cmdArgs, JsonSerializerOptions jsonOptions, strin try { + var fullOutputPath = Path.GetFullPath(options.OutputPath!); var resolvedVersion = appVersion ?? ConsoleUi.LoadVersion(); var bundle = BuildBundle(options, resolvedVersion); - WriteBundle(options.OutputPath!, bundle); + WriteBundle(fullOutputPath, bundle); var summary = new ReportBundleSummary( - Path.GetFullPath(options.OutputPath!), + fullOutputPath, resolvedVersion, bundle.Files.Count, bundle.SchemaTables.Count, @@ -194,16 +195,17 @@ internal static string BuildReadme(string version, bool includeLog, bool include internal static (string Text, List Tables, string? DbPath, bool DbIncluded) BuildSchemaSummary(string dbPath) { - if (!File.Exists(LongPath.EnsureWindowsPrefix(dbPath))) + var normalizedDbPath = DbPathResolver.NormalizeDbPath(dbPath); + if (!File.Exists(LongPath.EnsureWindowsPrefix(normalizedDbPath))) { var missingText = $"no SQLite index found at: {RedactedPlaceholder}\nRun `cdidx index ` first if you want schema details attached.\n"; - return (missingText, new List(), dbPath, false); + return (missingText, new List(), normalizedDbPath, false); } var tables = new List(); var connectionString = new SqliteConnectionStringBuilder { - DataSource = dbPath, + DataSource = normalizedDbPath, Mode = SqliteOpenMode.ReadOnly, }.ConnectionString; @@ -254,7 +256,7 @@ internal static (string Text, List Tables, string? DbPath, bo foreach (var t in tables) sb.AppendLine($"{t.Name} | {FormatSchemaRowCount(t)}"); - return (sb.ToString(), tables, dbPath, true); + return (sb.ToString(), tables, normalizedDbPath, true); } private static string FormatSchemaTableName(string name) @@ -392,12 +394,13 @@ private static string RedactKeyValue(string line, string key) internal static void WriteBundle(string outputPath, ReportBundle bundle, Action? beforeWriteEntries = null) { - var dir = Path.GetDirectoryName(outputPath); + var fullOutputPath = Path.GetFullPath(outputPath); + var dir = Path.GetDirectoryName(fullOutputPath); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); AtomicFileWriter.Write( - outputPath, + fullOutputPath, stream => { using var gz = new GZipStream(stream, CompressionLevel.Optimal, leaveOpen: true); diff --git a/tests/CodeIndex.Tests/DbCommandRunnerTests.cs b/tests/CodeIndex.Tests/DbCommandRunnerTests.cs index 6afb82d8e6..4a1bb14162 100644 --- a/tests/CodeIndex.Tests/DbCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/DbCommandRunnerTests.cs @@ -166,6 +166,54 @@ public void Run_IntegrityCheck_FileUriSemicolonPayloadDoesNotCreateDatabase_Issu } } + [Fact] + public void Run_IntegrityCheck_FileUriJsonReportsUriWithoutPathNormalization_Issue3221() + { + var dbPath = Path.Combine(Path.GetTempPath(), $"cdidx_db_uri_display_{Guid.NewGuid():N}.db"); + try + { + using (var db = new DbContext(dbPath)) + db.InitializeSchema(); + SqliteConnection.ClearAllPools(); + + var dbUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + var (exitCode, json) = RunAndCaptureJson(["--integrity-check", "--db", dbUri, "--json"]); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(dbUri, json.GetProperty("db_path").GetString()); + } + finally + { + SqliteConnection.ClearAllPools(); + if (File.Exists(dbPath)) + File.Delete(dbPath); + } + } + + [Fact] + public void Run_Schema_FileUriHumanOutputReportsUriWithoutPathNormalization_Issue3221() + { + var dbPath = Path.Combine(Path.GetTempPath(), $"cdidx_db_uri_schema_display_{Guid.NewGuid():N}.db"); + try + { + using (var db = new DbContext(dbPath)) + db.InitializeSchema(); + SqliteConnection.ClearAllPools(); + + var dbUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + var (exitCode, stdout, _) = RunAndCaptureStreams(["schema", "--db", dbUri]); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Contains($"database : {dbUri}", stdout); + } + finally + { + SqliteConnection.ClearAllPools(); + if (File.Exists(dbPath)) + File.Delete(dbPath); + } + } + [Fact] public void Run_MissingDb_JsonShapeIncludesHint() { diff --git a/tests/CodeIndex.Tests/DbPathResolverTests.cs b/tests/CodeIndex.Tests/DbPathResolverTests.cs index d6d70294cb..bb95bc2e01 100644 --- a/tests/CodeIndex.Tests/DbPathResolverTests.cs +++ b/tests/CodeIndex.Tests/DbPathResolverTests.cs @@ -402,6 +402,43 @@ public void ResolveProjectRootForQuery_PrefersStoredIndexedProjectRootMetadata() } } + [Fact] + public void MetadataStringProbesReturnNullOnFilesystemExceptions_Issue3175() + { + try + { + DbPathResolver.OpenMetadataConnectionForTesting = _ => throw new IOException("simulated metadata probe failure"); + + Assert.Null(DbPathResolver.TryReadIndexedHeadCommit("unreadable.db")); + Assert.False(DbPathResolver.TryHasIndexedHeadCommitBranchStamp("unreadable.db")); + } + finally + { + DbPathResolver.OpenMetadataConnectionForTesting = null; + } + } + + [Fact] + public void ResolveProjectRootForQuery_MetadataSampleProbeFilesystemErrorReturnsNull_Issue3175() + { + var dbContainerRoot = TestProjectHelper.CreateTempProject("cdidx_db_path_resolver_probe_failure"); + var dbPath = Path.Combine(dbContainerRoot, ".cdidx", "codeindex.db"); + try + { + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + DbPathResolver.OpenMetadataConnectionForTesting = _ => throw new IOException("simulated metadata sample failure"); + + var resolved = DbPathResolver.ResolveProjectRootForQuery(dbPath, dbPathExplicit: true); + + Assert.Null(resolved); + } + finally + { + DbPathResolver.OpenMetadataConnectionForTesting = null; + TestProjectHelper.DeleteDirectory(dbContainerRoot); + } + } + [Fact] public void ResolveProjectRootForQuery_ReadOnlyUri_PrefersStoredIndexedProjectRootMetadata() { diff --git a/tests/CodeIndex.Tests/DiffCommandRunnerTests.cs b/tests/CodeIndex.Tests/DiffCommandRunnerTests.cs index 5dbe2ba2cc..bc1de56c88 100644 --- a/tests/CodeIndex.Tests/DiffCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/DiffCommandRunnerTests.cs @@ -59,6 +59,49 @@ public void Run_OversizedFileUriQueryReturnsBoundedErrorBeforeReadingHeaders_Iss Assert.DoesNotContain(new string('a', SqliteFileUri.MaxDiagnosticValueLength + 1), stderr); } + [Fact] + public void Run_JsonFileUrisReportOriginalUriPaths_Issue3221() + { + var root = TestProjectHelper.CreateTempProject("cdidx_diff_uri_json"); + try + { + var dbPath = SeedDb(root, includeExtraFile: false); + var dbUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + + var (exitCode, output) = RunWithCapturedOut([dbUri, dbUri, "--json"]); + + Assert.Equal(0, exitCode); + using var document = JsonDocument.Parse(output); + Assert.Equal(dbUri, document.RootElement.GetProperty("left_db").GetString()); + Assert.Equal(dbUri, document.RootElement.GetProperty("right_db").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(root); + } + } + + [Fact] + public void Run_TextFileUrisReportOriginalUriPaths_Issue3221() + { + var root = TestProjectHelper.CreateTempProject("cdidx_diff_uri_text"); + try + { + var dbPath = SeedDb(root, includeExtraFile: false); + var dbUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + + var (exitCode, output) = RunWithCapturedOut([dbUri, dbUri]); + + Assert.Equal(0, exitCode); + Assert.Contains($"left : {dbUri}", output); + Assert.Contains($"right : {dbUri}", output); + } + finally + { + TestProjectHelper.DeleteDirectory(root); + } + } + [Fact] public void Run_LimitZeroStillDetectsDatabaseDrift_Issue2885() { diff --git a/tests/CodeIndex.Tests/ExportImportCommandRunnerTests.cs b/tests/CodeIndex.Tests/ExportImportCommandRunnerTests.cs index 115da5aab1..54b0df29a4 100644 --- a/tests/CodeIndex.Tests/ExportImportCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/ExportImportCommandRunnerTests.cs @@ -4,6 +4,7 @@ namespace CodeIndex.Tests; +[Collection("SQLite pool sensitive")] public class ExportImportCommandRunnerTests { [Fact] @@ -155,6 +156,34 @@ public void RunExportArchive_FailureOmitsRawExceptionMessage() } } + [Fact] + public void RunExportArchive_RelativeOutputReportsAndWritesFullPath_Issue3138() + { + var originalDirectory = Environment.CurrentDirectory; + var projectRoot = TestProjectHelper.CreateTempProject("export_archive_full_output"); + try + { + Directory.SetCurrentDirectory(projectRoot); + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var expectedOutput = Path.GetFullPath("codeindex.cdidx.zip"); + var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }; + + var (exitCode, stdout, stderr) = ConsoleCapture.Capture(() => + ExportImportCommandRunner.RunExport(["codeindex.cdidx.zip", "--db", dbPath, "--json"], jsonOptions, "test")); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.True(File.Exists(expectedOutput)); + using var document = JsonDocument.Parse(stdout); + Assert.Equal(expectedOutput, document.RootElement.GetProperty("archive_path").GetString()); + } + finally + { + Directory.SetCurrentDirectory(originalDirectory); + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void CreateDatabaseSnapshot_AppliesPrivateFileMode() { @@ -265,6 +294,35 @@ public void WriteCtagsFile_FailurePreservesExistingTagfile() } } + [Fact] + public void WriteCtagsFile_RelativeOutputUsesInitialFullPathWhenCurrentDirectoryChanges_Issue3138() + { + var originalDirectory = Environment.CurrentDirectory; + var workDir = TestProjectHelper.CreateTempProject("ctags_full_output"); + var driftDir = TestProjectHelper.CreateTempProject("ctags_full_output_drift"); + try + { + Directory.SetCurrentDirectory(workDir); + + ExportImportCommandRunner.WriteCtagsFile( + "tags", + writer => + { + Directory.SetCurrentDirectory(driftDir); + writer.WriteLine("!_TAG_FILE_FORMAT\t2\t/extended format/"); + }); + + Assert.True(File.Exists(Path.Combine(workDir, "tags"))); + Assert.False(File.Exists(Path.Combine(driftDir, "tags"))); + } + finally + { + Directory.SetCurrentDirectory(originalDirectory); + TestProjectHelper.DeleteDirectory(workDir); + TestProjectHelper.DeleteDirectory(driftDir); + } + } + [Fact] public void ReplaceImportedDatabase_MoveFailurePreservesExistingSidecars() { @@ -318,6 +376,35 @@ public void ReplaceImportedDatabase_SuccessDeletesDestinationSidecarsAfterMove() } } + [Fact] + public void ReplaceImportedDatabase_SidecarCleanupFailureDoesNotFailAfterMove_Issue3125() + { + var workDir = Path.Combine(Path.GetTempPath(), $"cdidx_import_cleanup_{Guid.NewGuid():N}"); + Directory.CreateDirectory(workDir); + try + { + var dbPath = Path.Combine(workDir, "codeindex.db"); + var tempPath = Path.Combine(workDir, "staged.db"); + File.WriteAllText(dbPath, "existing db"); + File.WriteAllText(dbPath + "-wal", "existing wal"); + File.WriteAllText(dbPath + "-shm", "existing shm"); + File.WriteAllText(tempPath, "imported db"); + ExportImportCommandRunner.DeleteSqliteSidecarForTesting = _ => throw new IOException("simulated sidecar cleanup failure"); + + ExportImportCommandRunner.ReplaceImportedDatabase(tempPath, dbPath); + + Assert.Equal("imported db", File.ReadAllText(dbPath)); + Assert.False(File.Exists(tempPath)); + Assert.True(File.Exists(dbPath + "-wal")); + Assert.True(File.Exists(dbPath + "-shm")); + } + finally + { + ExportImportCommandRunner.DeleteSqliteSidecarForTesting = null; + Directory.Delete(workDir, recursive: true); + } + } + [Fact] public void ReplaceImportedDatabase_AppliesPrivateFileMode() { diff --git a/tests/CodeIndex.Tests/ReportCommandRunnerTests.cs b/tests/CodeIndex.Tests/ReportCommandRunnerTests.cs index 4fd8f29c21..90c1f321ee 100644 --- a/tests/CodeIndex.Tests/ReportCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/ReportCommandRunnerTests.cs @@ -233,6 +233,34 @@ public void WriteBundle_FailurePreservesExistingBundle() } } + [Fact] + public void WriteBundle_RelativeOutputUsesInitialFullPathWhenCurrentDirectoryChanges_Issue3147() + { + var originalDirectory = Environment.CurrentDirectory; + var workDir = CreateWorkDir(); + var driftDir = CreateWorkDir(); + try + { + Directory.SetCurrentDirectory(workDir); + var bundle = new ReportBundle(); + bundle.AddText("metadata.txt", "ok"); + + ReportCommandRunner.WriteBundle( + "bundle.tgz", + bundle, + beforeWriteEntries: () => Directory.SetCurrentDirectory(driftDir)); + + Assert.True(File.Exists(Path.Combine(workDir, "bundle.tgz"))); + Assert.False(File.Exists(Path.Combine(driftDir, "bundle.tgz"))); + } + finally + { + Directory.SetCurrentDirectory(originalDirectory); + TryDeleteDirectory(workDir); + TryDeleteDirectory(driftDir); + } + } + [Fact] public void Run_WithRealDb_SchemaTxtListsTablesAndRowCounts() { @@ -269,6 +297,33 @@ public void Run_WithRealDb_SchemaTxtListsTablesAndRowCounts() } } + [Fact] + public void BuildSchemaSummary_FileUriReadsExistingDatabase_Issue3148() + { + var workDir = CreateWorkDir(); + var dbPath = Path.Combine(workDir, "codeindex.db"); + try + { + using (var db = new DbContext(dbPath)) + db.InitializeSchema(); + SqliteConnection.ClearAllPools(); + + var dbUri = new Uri(dbPath).AbsoluteUri + "?immutable=1"; + var (schemaText, tables, reportedDbPath, dbIncluded) = ReportCommandRunner.BuildSchemaSummary(dbUri); + + Assert.True(dbIncluded); + Assert.Equal(Path.GetFullPath(dbPath), reportedDbPath); + Assert.Contains(tables, table => table.Name == "files"); + Assert.Contains("files", schemaText); + Assert.DoesNotContain("no SQLite index found", schemaText); + } + finally + { + SqliteConnection.ClearAllPools(); + TryDeleteDirectory(workDir); + } + } + [Fact] public void BuildSchemaSummary_CapsTableEntries_Issue3146() {