diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index dc0677aa47..56a8dad021 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -170,6 +170,7 @@ For boundary tests, use the smallest fixture that still crosses the boundary. If - Never assume global git identity exists. - Configure repo-local `user.name` and `user.email` inside the test setup. +- Disable repo-local commit/tag signing for fixture repositories so global signing settings cannot prompt or fail non-interactively. - Use helper methods or `ProcessStartInfo.ArgumentList`; do not depend on shell-specific quoting behavior. ### Database tests @@ -366,6 +367,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests" - global の git identity がある前提にしない。 - テストセットアップ内で repo-local の `user.name` と `user.email` を設定する。 +- fixture リポジトリでは repo-local の commit/tag signing を無効化し、global signing 設定が非対話実行でプロンプトや失敗を起こさないようにする。 - shell 依存の quoting ではなく、ヘルパーや `ProcessStartInfo.ArgumentList` を使う。 ### DB 系テスト diff --git a/changelog.d/unreleased/2729.fixed.md b/changelog.d/unreleased/2729.fixed.md new file mode 100644 index 0000000000..223816d06c --- /dev/null +++ b/changelog.d/unreleased/2729.fixed.md @@ -0,0 +1,19 @@ +--- +category: fixed +issues: + - 2729 +affected: + - tests/CodeIndex.Tests/TestProjectHelper.cs + - tests/CodeIndex.Tests/GitHelperTests.cs + - tests/CodeIndex.Tests/IndexCommandRunnerTests.cs + - tests/CodeIndex.Tests/GitTestProjectHelperTests.cs + - TESTING_GUIDE.md +--- + +## English + +- **Git-backed test fixtures no longer inherit commit signing (#2729)** — temporary repositories created by test helpers now disable commit and tag signing locally so fixture commits do not prompt for signing-key passphrases. + +## 日本語 + +- **Git を使うテスト fixture が commit signing を引き継がなくなりました (#2729)** — テストヘルパーが作る一時リポジトリでは commit / tag signing を repo-local に無効化し、fixture commit が署名キーのパスフレーズ入力を要求しないようにしました。 diff --git a/changelog.d/unreleased/2730.fixed.md b/changelog.d/unreleased/2730.fixed.md new file mode 100644 index 0000000000..d72ea9d238 --- /dev/null +++ b/changelog.d/unreleased/2730.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 2730 +affected: + - src/CodeIndex/Cli/GlobalToolLog.cs + - tests/CodeIndex.Tests/GlobalToolLogTests.cs +--- + +## English + +- **Persistent stderr mirroring no longer cascades disposed-writer failures (#2730)** — the global tool log tee now treats closed console/log writers as best-effort failures instead of throwing `ObjectDisposedException` back into test or CLI callers. + +## 日本語 + +- **永続 stderr mirror が閉じた writer の失敗を連鎖させなくなりました (#2730)** — global tool log の tee は、閉じた console/log writer をベストエフォートの失敗として扱い、`ObjectDisposedException` をテストや CLI 呼び出し元へ投げ返さないようになりました。 diff --git a/src/CodeIndex/Cli/GlobalToolLog.cs b/src/CodeIndex/Cli/GlobalToolLog.cs index 048a415ea2..4f7a521dda 100644 --- a/src/CodeIndex/Cli/GlobalToolLog.cs +++ b/src/CodeIndex/Cli/GlobalToolLog.cs @@ -638,26 +638,39 @@ private sealed class TeeTextWriter(TextWriter primary, TextWriter secondary) : T public override void Flush() { - primary.Flush(); - secondary.Flush(); + TryWrite(primary.Flush); + TryWrite(secondary.Flush); } public override void Write(char value) { - primary.Write(value); - secondary.Write(value); + TryWrite(() => primary.Write(value)); + TryWrite(() => secondary.Write(value)); } public override void Write(string? value) { - primary.Write(value); - secondary.Write(value); + TryWrite(() => primary.Write(value)); + TryWrite(() => secondary.Write(value)); } public override void WriteLine(string? value) { - primary.WriteLine(value); - secondary.WriteLine(value); + TryWrite(() => primary.WriteLine(value)); + TryWrite(() => secondary.WriteLine(value)); + } + + private static void TryWrite(Action write) + { + try + { + write(); + } + catch (Exception ex) when (ex is IOException or ObjectDisposedException) + { + // Best-effort mirror: a disposed console writer must not cascade into callers. + // mirror はベストエフォート。閉じた console writer が呼び出し側へ波及しないようにする。 + } } } } diff --git a/tests/CodeIndex.Tests/GitHelperTests.cs b/tests/CodeIndex.Tests/GitHelperTests.cs index 5569a2fd6b..6fead55cf4 100644 --- a/tests/CodeIndex.Tests/GitHelperTests.cs +++ b/tests/CodeIndex.Tests/GitHelperTests.cs @@ -699,6 +699,8 @@ private string CreateGitRepo() RunGit(repoDir, "init"); RunGit(repoDir, "config", "user.name", "CodeIndex Tests"); RunGit(repoDir, "config", "user.email", "tests@example.com"); + RunGit(repoDir, "config", "commit.gpgsign", "false"); + RunGit(repoDir, "config", "tag.gpgsign", "false"); return repoDir; } diff --git a/tests/CodeIndex.Tests/GitTestProjectHelperTests.cs b/tests/CodeIndex.Tests/GitTestProjectHelperTests.cs new file mode 100644 index 0000000000..b1bf61b7c2 --- /dev/null +++ b/tests/CodeIndex.Tests/GitTestProjectHelperTests.cs @@ -0,0 +1,41 @@ +namespace CodeIndex.Tests; + +public class GitTestProjectHelperTests +{ + [Fact] + public void InitializeGitRepo_DisablesCommitSigningForFixtureCommits() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_git_signing"); + var globalConfig = Path.Combine(projectRoot, "global-gitconfig"); + try + { + File.WriteAllText( + globalConfig, + """ + [commit] + gpgsign = true + [gpg] + format = ssh + [user] + signingkey = /definitely/missing/signing-key + """); + using var env = EnvironmentVariableScope.Capture("GIT_CONFIG_GLOBAL"); + env.Set("GIT_CONFIG_GLOBAL", globalConfig); + + TestProjectHelper.InitializeGitRepo(projectRoot); + File.WriteAllText(Path.Combine(projectRoot, "app.cs"), "class App {}\n"); + + TestProjectHelper.RunGit(projectRoot, "add", "app.cs"); + var commitSigning = TestProjectHelper.RunGit(projectRoot, "config", "--get", "commit.gpgsign").Trim(); + var tagSigning = TestProjectHelper.RunGit(projectRoot, "config", "--get", "tag.gpgsign").Trim(); + TestProjectHelper.RunGit(projectRoot, "commit", "-m", "fixture"); + + Assert.Equal("false", commitSigning); + Assert.Equal("false", tagSigning); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } +} diff --git a/tests/CodeIndex.Tests/GlobalToolLogTests.cs b/tests/CodeIndex.Tests/GlobalToolLogTests.cs index c08ca95cf0..fd322074e1 100644 --- a/tests/CodeIndex.Tests/GlobalToolLogTests.cs +++ b/tests/CodeIndex.Tests/GlobalToolLogTests.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Text; using System.Text.RegularExpressions; using CodeIndex.Cli; @@ -117,6 +118,52 @@ public void TryStart_WritesInvariantUtcTimestampAndStackTrace() } } + [Fact] + public void TryStart_ErrorMirrorIgnoresDisposedOriginalConsoleWriter() + { + var logRoot = Path.Combine(Path.GetTempPath(), $"cdidx_global_log_disposed_{Guid.NewGuid():N}"); + var originalError = Console.Error; + try + { + using var env = EnvironmentVariableScope.Capture( + "CDIDX_FORCE_GLOBAL_TOOL_LOG", + "CDIDX_DISABLE_PERSISTENT_LOG", + "CDIDX_GLOBAL_TOOL_LOG_DIR"); + env.Set("CDIDX_FORCE_GLOBAL_TOOL_LOG", "1"); + env.Set("CDIDX_DISABLE_PERSISTENT_LOG", null); + env.Set("CDIDX_GLOBAL_TOOL_LOG_DIR", logRoot); + + using var session = GlobalToolLog.TryStartForTesting( + ["status"], + "test", + afterWriterCreated: () => Console.SetError(new ThrowingTextWriter())); + + var exception = Record.Exception(() => Console.Error.WriteLine("mirrored error")); + + Assert.NotNull(session); + Assert.Null(exception); + } + finally + { + Console.SetError(originalError); + if (Directory.Exists(logRoot)) + Directory.Delete(logRoot, recursive: true); + } + } + private static void ThrowForGlobalToolLogTest() => throw new InvalidOperationException("global log stack trace test"); + + private sealed class ThrowingTextWriter : TextWriter + { + public override Encoding Encoding => Encoding.UTF8; + + public override void Flush() => throw new ObjectDisposedException(nameof(ThrowingTextWriter)); + + public override void Write(char value) => throw new ObjectDisposedException(nameof(ThrowingTextWriter)); + + public override void Write(string? value) => throw new ObjectDisposedException(nameof(ThrowingTextWriter)); + + public override void WriteLine(string? value) => throw new ObjectDisposedException(nameof(ThrowingTextWriter)); + } } diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs index 1dcafc6363..640a66305f 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs @@ -8844,6 +8844,8 @@ private static void RunGit(string workDir, params string[] args) { RunGit(workDir, "config", "user.name", "CodeIndex Tests"); RunGit(workDir, "config", "user.email", "tests@codeindex.local"); + RunGit(workDir, "config", "commit.gpgsign", "false"); + RunGit(workDir, "config", "tag.gpgsign", "false"); } } diff --git a/tests/CodeIndex.Tests/TestProjectHelper.cs b/tests/CodeIndex.Tests/TestProjectHelper.cs index b4caba5073..66d404de38 100644 --- a/tests/CodeIndex.Tests/TestProjectHelper.cs +++ b/tests/CodeIndex.Tests/TestProjectHelper.cs @@ -21,6 +21,8 @@ internal static void InitializeGitRepo(string projectRoot) RunGit(projectRoot, "init"); RunGit(projectRoot, "config", "user.name", "CodeIndex Tests"); RunGit(projectRoot, "config", "user.email", "tests@codeindex.local"); + RunGit(projectRoot, "config", "commit.gpgsign", "false"); + RunGit(projectRoot, "config", "tag.gpgsign", "false"); var excludePath = Path.Combine(projectRoot, ".git", "info", "exclude"); Directory.CreateDirectory(Path.GetDirectoryName(excludePath)!);