diff --git a/src/Core/Reqnroll.IdeSupport.Common/Diagnostics/SynchronousFileLogger.cs b/src/Core/Reqnroll.IdeSupport.Common/Diagnostics/SynchronousFileLogger.cs
index 32b6009f..34d61dd4 100644
--- a/src/Core/Reqnroll.IdeSupport.Common/Diagnostics/SynchronousFileLogger.cs
+++ b/src/Core/Reqnroll.IdeSupport.Common/Diagnostics/SynchronousFileLogger.cs
@@ -5,14 +5,16 @@ namespace Reqnroll.IdeSupport.Common.Diagnostics;
public class SynchronousFileLogger : AsynchronousFileLogger
{
- public SynchronousFileLogger(string ide = "vs", string role = "ext")
- : base(new FileSystemForIDE(), TraceLevel.Verbose, ide, role)
+ public SynchronousFileLogger(string ide = "vs", string role = "ext", TraceLevel level = TraceLevel.Warning)
+ : base(new FileSystemForIDE(), level, ide, role)
{
EnsureLogFolder();
}
public override void Log(LogMessage message)
{
+ if (message.Level > Level) return;
+
try
{
WriteLogMessage(message);
diff --git a/src/LSP/Reqnroll.IdeSupport.LSP.Server/Hosting/ClientIdeContext.cs b/src/LSP/Reqnroll.IdeSupport.LSP.Server/Hosting/ClientIdeContext.cs
index b68b1531..82ed95da 100644
--- a/src/LSP/Reqnroll.IdeSupport.LSP.Server/Hosting/ClientIdeContext.cs
+++ b/src/LSP/Reqnroll.IdeSupport.LSP.Server/Hosting/ClientIdeContext.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics;
namespace Reqnroll.IdeSupport.LSP.Server.Hosting;
@@ -8,11 +9,21 @@ namespace Reqnroll.IdeSupport.LSP.Server.Hosting;
///
public sealed class ClientIdeContext
{
- public ClientIdeContext(string? ide) => Ide = ide;
+ public ClientIdeContext(string? ide, TraceLevel logLevel = TraceLevel.Warning)
+ {
+ Ide = ide;
+ LogLevel = logLevel;
+ }
/// The raw --ide value, or when absent.
public string? Ide { get; }
+ ///
+ /// The file/protocol log verbosity requested via --log-level, defaulting to
+ /// when the client did not specify one.
+ ///
+ public TraceLevel LogLevel { get; }
+
///
/// True when the connecting client is Visual Studio, whose built-in LSP semantic-token
/// colorizer cannot map custom token types — so the server pushes tokens to it instead of
diff --git a/src/LSP/Reqnroll.IdeSupport.LSP.Server/Hosting/Program.cs b/src/LSP/Reqnroll.IdeSupport.LSP.Server/Hosting/Program.cs
index 25284f07..f645c73e 100644
--- a/src/LSP/Reqnroll.IdeSupport.LSP.Server/Hosting/Program.cs
+++ b/src/LSP/Reqnroll.IdeSupport.LSP.Server/Hosting/Program.cs
@@ -1,4 +1,5 @@
-using MediatR;
+using System.Diagnostics;
+using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities;
@@ -34,10 +35,12 @@ public static async Task Main(string[] args)
// The semantic token legend no longer varies by IDE, but the identifier is retained for
// features that may need to vary their behaviour per IDE (e.g. future static-vs-dynamic
// capability registration decisions).
- var ideId = args
- .SkipWhile(a => !string.Equals(a, "--ide", StringComparison.OrdinalIgnoreCase))
- .Skip(1)
- .FirstOrDefault();
+ var ideId = ParseArg(args, "--ide");
+
+ // Each IDE's glue component may pass --log-level (Off/Error/Warning/Info/Verbose)
+ // when spawning the server. Defaults to Warning when absent so a normal session doesn't
+ // write maximum-verbosity logs indefinitely; pass --log-level Verbose for full tracing.
+ var logLevel = ParseLogLevel(args);
// Write any unhandled startup exception to a file next to the LSP inspector logs
// so crashes are self-diagnosing without needing to capture stderr.
@@ -53,7 +56,7 @@ public static async Task Main(string[] args)
// Production transport: the IDE talks to the server over stdio.
options.WithInput(Console.OpenStandardInput())
.WithOutput(Console.OpenStandardOutput());
- ConfigureServer(options, ideId);
+ ConfigureServer(options, ideId, logLevel);
});
using var preloadCts = new CancellationTokenSource();
@@ -104,11 +107,18 @@ public static async Task Main(string[] args)
/// when absent. Currently unused by the semantic-token pipeline
/// (the legend is shared across IDEs); retained for features that may vary behaviour per IDE.
///
- internal static void ConfigureServer(LanguageServerOptions options, string? clientIde = null)
+ ///
+ /// The --log-level verbosity requested by the client, defaulting to
+ /// . Drives both the file-backed
+ /// and the OmniSharp protocol-logging minimum level, so wire-level debugging and file logging
+ /// stay in lockstep unless a caller explicitly asks for more (e.g. --log-level Verbose).
+ ///
+ internal static void ConfigureServer(LanguageServerOptions options, string? clientIde = null,
+ TraceLevel logLevel = TraceLevel.Warning)
{
options.ConfigureLogging(logging =>
{
- logging.SetMinimumLevel(LogLevel.Trace);
+ logging.SetMinimumLevel(ToLogLevel(logLevel));
logging.AddLanguageProtocolLogging();
});
@@ -127,7 +137,7 @@ internal static void ConfigureServer(LanguageServerOptions options, string? clie
// notification to two handler instances (the transient from the scan and
// the singleton from the explicit call).
.AddMediatR(typeof(Program).Assembly)
- .AddReqnrollLspCoreServices(clientIde)
+ .AddReqnrollLspCoreServices(clientIde, logLevel)
.AddReqnrollProjectSystem()
.AddReqnrollEditorServices()
.AddReqnrollLspHandlers();
@@ -184,4 +194,31 @@ internal static void ConfigureServer(LanguageServerOptions options, string? clie
return Task.CompletedTask;
});
}
+
+ ///
+ /// Maps the verbosity scale onto
+ /// for the OmniSharp protocol-logging pipeline.
+ ///
+ internal static LogLevel ToLogLevel(TraceLevel level) => level switch
+ {
+ TraceLevel.Off => LogLevel.None,
+ TraceLevel.Error => LogLevel.Error,
+ TraceLevel.Warning => LogLevel.Warning,
+ TraceLevel.Info => LogLevel.Information,
+ TraceLevel.Verbose => LogLevel.Trace,
+ _ => LogLevel.Warning
+ };
+
+ /// Returns the value following in , or when absent.
+ internal static string? ParseArg(string[] args, string flag)
+ => args
+ .SkipWhile(a => !string.Equals(a, flag, StringComparison.OrdinalIgnoreCase))
+ .Skip(1)
+ .FirstOrDefault();
+
+ /// Parses --log-level from , defaulting to when absent or unrecognized.
+ internal static TraceLevel ParseLogLevel(string[] args)
+ => Enum.TryParse(ParseArg(args, "--log-level"), ignoreCase: true, out var parsedLevel)
+ ? parsedLevel
+ : TraceLevel.Warning;
}
diff --git a/src/LSP/Reqnroll.IdeSupport.LSP.Server/Hosting/ServiceCollectionExtensions.cs b/src/LSP/Reqnroll.IdeSupport.LSP.Server/Hosting/ServiceCollectionExtensions.cs
index f0990d0f..fed4a430 100644
--- a/src/LSP/Reqnroll.IdeSupport.LSP.Server/Hosting/ServiceCollectionExtensions.cs
+++ b/src/LSP/Reqnroll.IdeSupport.LSP.Server/Hosting/ServiceCollectionExtensions.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Reqnroll.IdeSupport.Common;
@@ -48,10 +49,11 @@ public static class ServiceCollectionExtensions
///
/// Registers core infrastructure and cross-cutting services.
///
- public static IServiceCollection AddReqnrollLspCoreServices(this IServiceCollection services, string? clientIde)
+ public static IServiceCollection AddReqnrollLspCoreServices(this IServiceCollection services, string? clientIde,
+ TraceLevel logLevel = TraceLevel.Warning)
{
return services
- .AddSingleton(new ClientIdeContext(clientIde))
+ .AddSingleton(new ClientIdeContext(clientIde, logLevel))
.AddSingleton()
.AddSingleton()
.AddSingleton(sp => NullMonitoringService.Instance)
diff --git a/src/LSP/Reqnroll.IdeSupport.LSP.Server/Logging/LspDeveroomLogger.cs b/src/LSP/Reqnroll.IdeSupport.LSP.Server/Logging/LspDeveroomLogger.cs
index 44428ca5..d57ee6b4 100644
--- a/src/LSP/Reqnroll.IdeSupport.LSP.Server/Logging/LspDeveroomLogger.cs
+++ b/src/LSP/Reqnroll.IdeSupport.LSP.Server/Logging/LspDeveroomLogger.cs
@@ -28,7 +28,7 @@ public LspDeveroomLogger(ClientIdeContext clientIdeContext)
};
_inner = new DeveroomCompositeLogger()
.Add(new DeveroomDebugLogger())
- .Add(new SynchronousFileLogger(idePrefix, "server"));
+ .Add(new SynchronousFileLogger(idePrefix, "server", clientIdeContext.LogLevel));
var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
var location = Assembly.GetExecutingAssembly().Location;
diff --git a/src/VSCode/package.json b/src/VSCode/package.json
index 2e876589..bfc6747d 100644
--- a/src/VSCode/package.json
+++ b/src/VSCode/package.json
@@ -163,7 +163,7 @@
"type": "string",
"enum": ["off", "messages", "verbose"],
"default": "off",
- "description": "Traces the communication between VS Code and the Reqnroll LSP server. When set to 'verbose', a log file is also written to the Reqnroll log directory."
+ "description": "Traces the communication between VS Code and the Reqnroll LSP server, and also controls the server's own log verbosity ('off' -> Warning, 'messages' -> Info, 'verbose' -> Verbose). When set to 'verbose', a log file is also written to the Reqnroll log directory. Requires a window reload to take effect on the running server."
}
}
},
diff --git a/src/VSCode/src/extension.ts b/src/VSCode/src/extension.ts
index 5f5dd8a1..185d5b0c 100644
--- a/src/VSCode/src/extension.ts
+++ b/src/VSCode/src/extension.ts
@@ -2,7 +2,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node';
-import { createTraceChannel } from './lspInspectorLogger';
+import { createTraceChannel, traceServerToLogLevel } from './lspInspectorLogger';
import { ProjectManager } from './projectManager';
import { StatusBarManager } from './statusBar';
import { doToggleComment } from './commentToggle';
@@ -197,7 +197,7 @@ export function activate(context: vscode.ExtensionContext): void {
// ── LSP client ─────────────────────────────────────────────────────────────
const serverOptions: ServerOptions = {
command: serverPath,
- args: ['--ide', 'vscode'],
+ args: ['--ide', 'vscode', '--log-level', traceServerToLogLevel()],
options: {
env: { ...process.env },
},
diff --git a/src/VSCode/src/lspInspectorLogger.ts b/src/VSCode/src/lspInspectorLogger.ts
index 6f3ad849..9e5a35fe 100644
--- a/src/VSCode/src/lspInspectorLogger.ts
+++ b/src/VSCode/src/lspInspectorLogger.ts
@@ -240,6 +240,23 @@ function parseLspTraceMessage(text: string): LspEntry | undefined {
* directory so that every JSON-RPC message captured by vscode-languageclient
* is persisted in lsp-viewer format alongside the VS Code Output panel entry.
*/
+/**
+ * Maps the `reqnroll.trace.server` setting onto the LSP server's `--log-level` argument, so the
+ * one lever VS Code users already have also controls the server's own file/protocol log
+ * verbosity instead of only the client-side wire trace.
+ */
+export function traceServerToLogLevel(): 'Warning' | 'Info' | 'Verbose' {
+ const level = vscode.workspace.getConfiguration('reqnroll').get('trace.server', 'off');
+ switch (level) {
+ case 'verbose':
+ return 'Verbose';
+ case 'messages':
+ return 'Info';
+ default:
+ return 'Warning';
+ }
+}
+
export function createTraceChannel(): vscode.LogOutputChannel {
const level = vscode.workspace.getConfiguration('reqnroll').get('trace.server', 'off');
diff --git a/src/VSCode/src/test/extension.test.ts b/src/VSCode/src/test/extension.test.ts
index 2128a05a..89192847 100644
--- a/src/VSCode/src/test/extension.test.ts
+++ b/src/VSCode/src/test/extension.test.ts
@@ -3,6 +3,7 @@ import * as vscode from 'vscode';
// Pull in all additional test suites so the single entry-point loads them all
import './projectManager.test';
+import './lspInspectorLogger.test';
suite('Reqnroll Extension Tests', () => {
const extensionId = 'reqnroll.reqnroll-ide-support';
diff --git a/src/VSCode/src/test/index.ts b/src/VSCode/src/test/index.ts
new file mode 100644
index 00000000..872ababc
--- /dev/null
+++ b/src/VSCode/src/test/index.ts
@@ -0,0 +1,35 @@
+import * as path from 'path';
+import Mocha from 'mocha';
+
+/**
+ * Bootstraps the Mocha `suite`/`test` globals inside the Extension Development Host and runs the
+ * bundled test file. `@vscode/test-electron` loads this module's `run` export as
+ * `extensionTestsPath` — pointing that directly at a compiled test file (as this used to) never
+ * registers Mocha's BDD globals, so every suite/test call throws `ReferenceError: suite is not
+ * defined` before a single test runs.
+ */
+export function run(): Promise {
+ // Default Mocha timeout (2000ms) races against extension.test.ts's own 2000ms activation
+ // wait; give suites headroom now that they actually run (this harness was previously never
+ // invoking them at all — see remarks above).
+ const mocha = new Mocha({ ui: 'tdd', color: true, timeout: 10000 });
+ const testsRoot = path.resolve(__dirname);
+
+ // extension.test.js pulls in the other suites (projectManager.test, lspInspectorLogger.test)
+ // via its own imports, so registering just the entry point is enough.
+ mocha.addFile(path.resolve(testsRoot, 'extension.test.js'));
+
+ return new Promise((resolve, reject) => {
+ try {
+ mocha.run((failures) => {
+ if (failures > 0) {
+ reject(new Error(`${failures} test(s) failed.`));
+ } else {
+ resolve();
+ }
+ });
+ } catch (err) {
+ reject(err instanceof Error ? err : new Error(String(err)));
+ }
+ });
+}
diff --git a/src/VSCode/src/test/lspInspectorLogger.test.ts b/src/VSCode/src/test/lspInspectorLogger.test.ts
new file mode 100644
index 00000000..4dd04e70
--- /dev/null
+++ b/src/VSCode/src/test/lspInspectorLogger.test.ts
@@ -0,0 +1,31 @@
+import * as assert from 'assert';
+import * as vscode from 'vscode';
+import { traceServerToLogLevel } from '../lspInspectorLogger';
+
+suite('traceServerToLogLevel', () => {
+ const config = vscode.workspace.getConfiguration('reqnroll');
+
+ teardown(async () => {
+ await config.update('trace.server', undefined, vscode.ConfigurationTarget.Global);
+ });
+
+ test('defaults to Warning when the setting is unset', async () => {
+ await config.update('trace.server', undefined, vscode.ConfigurationTarget.Global);
+ assert.strictEqual(traceServerToLogLevel(), 'Warning');
+ });
+
+ test("maps 'off' to Warning", async () => {
+ await config.update('trace.server', 'off', vscode.ConfigurationTarget.Global);
+ assert.strictEqual(traceServerToLogLevel(), 'Warning');
+ });
+
+ test("maps 'messages' to Info", async () => {
+ await config.update('trace.server', 'messages', vscode.ConfigurationTarget.Global);
+ assert.strictEqual(traceServerToLogLevel(), 'Info');
+ });
+
+ test("maps 'verbose' to Verbose", async () => {
+ await config.update('trace.server', 'verbose', vscode.ConfigurationTarget.Global);
+ assert.strictEqual(traceServerToLogLevel(), 'Verbose');
+ });
+});
diff --git a/src/VSCode/src/test/runTest.ts b/src/VSCode/src/test/runTest.ts
index 27966308..b13b5b51 100644
--- a/src/VSCode/src/test/runTest.ts
+++ b/src/VSCode/src/test/runTest.ts
@@ -7,8 +7,9 @@ async function main(): Promise {
// The folder containing the Extension Manifest (package.json)
const extensionDevelopmentPath = path.resolve(__dirname, '..', '..');
- // The path to the extension test script
- const extensionTestsPath = path.resolve(__dirname, 'extension.test');
+ // The path to the test bootstrapper (registers Mocha's suite/test globals, then loads
+ // extension.test.js which pulls in the other suites via its own imports).
+ const extensionTestsPath = path.resolve(__dirname, 'index');
// Download VS Code, unzip it and run the integration test
await runTests({
diff --git a/src/VisualStudio/Reqnroll.IdeSupport.VisualStudio.Extension/LspInterception/LspServerConnectionService.cs b/src/VisualStudio/Reqnroll.IdeSupport.VisualStudio.Extension/LspInterception/LspServerConnectionService.cs
index da705535..5ea31b83 100644
--- a/src/VisualStudio/Reqnroll.IdeSupport.VisualStudio.Extension/LspInterception/LspServerConnectionService.cs
+++ b/src/VisualStudio/Reqnroll.IdeSupport.VisualStudio.Extension/LspInterception/LspServerConnectionService.cs
@@ -110,6 +110,14 @@ internal static string ResolveServerExePath(string extensionAssemblyLocation)
"LSPServer",
"Reqnroll.IdeSupport.LSP.Server.exe");
+ ///
+ /// The command-line arguments passed to the LSP server process: --ide selects the
+ /// semantic token profile, --log-level keeps the server's own file/protocol logging in
+ /// step with the client's default rather than the server falling back to its own default
+ /// independently. Extracted as a constant so it's unit-testable without spawning a process.
+ ///
+ internal const string ServerArguments = "--ide visualstudio --log-level Warning";
+
private async Task StartAsync()
{
var serverExe = ResolveServerExePath(typeof(LspServerConnectionService).Assembly.Location);
@@ -134,9 +142,7 @@ internal static string ResolveServerExePath(string extensionAssemblyLocation)
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
- // Tell the LSP server which IDE is connecting so it selects the correct
- // semantic token profile (legend + DeveroomTag→token type mapping).
- Arguments = "--ide visualstudio",
+ Arguments = ServerArguments,
};
_serverProcess = Process.Start(psi)
diff --git a/tests/Core/Reqnroll.IdeSupport.Common.Tests/Diagnostics/SynchronousFileLoggerTests.cs b/tests/Core/Reqnroll.IdeSupport.Common.Tests/Diagnostics/SynchronousFileLoggerTests.cs
new file mode 100644
index 00000000..af0b0dbf
--- /dev/null
+++ b/tests/Core/Reqnroll.IdeSupport.Common.Tests/Diagnostics/SynchronousFileLoggerTests.cs
@@ -0,0 +1,79 @@
+using System.IO;
+
+namespace Reqnroll.IdeSupport.Common.Tests.Diagnostics;
+
+public class SynchronousFileLoggerTests
+{
+ [Fact]
+ public void Default_level_is_Warning()
+ {
+ var logger = new SynchronousFileLogger("test", $"default-{Guid.NewGuid():N}");
+ try
+ {
+ logger.Level.Should().Be(TraceLevel.Warning);
+ }
+ finally
+ {
+ DeleteLogFile(logger);
+ }
+ }
+
+ [Theory]
+ [InlineData(TraceLevel.Off)]
+ [InlineData(TraceLevel.Error)]
+ [InlineData(TraceLevel.Warning)]
+ [InlineData(TraceLevel.Info)]
+ [InlineData(TraceLevel.Verbose)]
+ public void Explicit_level_is_honored(TraceLevel level)
+ {
+ var logger = new SynchronousFileLogger("test", $"explicit-{Guid.NewGuid():N}", level);
+ try
+ {
+ logger.Level.Should().Be(level);
+ }
+ finally
+ {
+ DeleteLogFile(logger);
+ }
+ }
+
+ [Fact]
+ public void Messages_above_the_configured_level_are_dropped()
+ {
+ var logger = new SynchronousFileLogger("test", $"filter-{Guid.NewGuid():N}", TraceLevel.Warning);
+ try
+ {
+ logger.Log(new LogMessage(TraceLevel.Info, "should be dropped",
+ nameof(Messages_above_the_configured_level_are_dropped)));
+
+ File.Exists(logger.LogFilePath).Should().BeFalse(
+ "Info is below the Warning threshold and should never be written");
+ }
+ finally
+ {
+ DeleteLogFile(logger);
+ }
+ }
+
+ [Fact]
+ public void Messages_at_or_below_the_configured_level_are_written()
+ {
+ var logger = new SynchronousFileLogger("test", $"filter-{Guid.NewGuid():N}", TraceLevel.Warning);
+ try
+ {
+ logger.Log(new LogMessage(TraceLevel.Warning, "should be written",
+ nameof(Messages_at_or_below_the_configured_level_are_written)));
+
+ File.ReadAllText(logger.LogFilePath).Should().Contain("should be written");
+ }
+ finally
+ {
+ DeleteLogFile(logger);
+ }
+ }
+
+ private static void DeleteLogFile(SynchronousFileLogger logger)
+ {
+ try { File.Delete(logger.LogFilePath); } catch { /* best-effort cleanup */ }
+ }
+}
diff --git a/tests/LSP/Reqnroll.IdeSupport.LSP.Server.Tests/Hosting/ClientIdeContextTests.cs b/tests/LSP/Reqnroll.IdeSupport.LSP.Server.Tests/Hosting/ClientIdeContextTests.cs
new file mode 100644
index 00000000..b6126959
--- /dev/null
+++ b/tests/LSP/Reqnroll.IdeSupport.LSP.Server.Tests/Hosting/ClientIdeContextTests.cs
@@ -0,0 +1,33 @@
+using System.Diagnostics;
+using Reqnroll.IdeSupport.LSP.Server.Hosting;
+
+namespace Reqnroll.IdeSupport.LSP.Server.Tests.Hosting;
+
+public class ClientIdeContextTests
+{
+ [Fact]
+ public void Default_log_level_is_Warning()
+ {
+ new ClientIdeContext("visualstudio").LogLevel.Should().Be(TraceLevel.Warning);
+ }
+
+ [Theory]
+ [InlineData(TraceLevel.Off)]
+ [InlineData(TraceLevel.Error)]
+ [InlineData(TraceLevel.Warning)]
+ [InlineData(TraceLevel.Info)]
+ [InlineData(TraceLevel.Verbose)]
+ public void Explicit_log_level_is_honored(TraceLevel level)
+ {
+ new ClientIdeContext("vscode", level).LogLevel.Should().Be(level);
+ }
+
+ [Fact]
+ public void Ide_and_IsVisualStudio_are_unaffected_by_log_level()
+ {
+ var context = new ClientIdeContext("visualstudio", TraceLevel.Verbose);
+
+ context.Ide.Should().Be("visualstudio");
+ context.IsVisualStudio.Should().BeTrue();
+ }
+}
diff --git a/tests/LSP/Reqnroll.IdeSupport.LSP.Server.Tests/Hosting/ProgramTests.cs b/tests/LSP/Reqnroll.IdeSupport.LSP.Server.Tests/Hosting/ProgramTests.cs
new file mode 100644
index 00000000..98b6013d
--- /dev/null
+++ b/tests/LSP/Reqnroll.IdeSupport.LSP.Server.Tests/Hosting/ProgramTests.cs
@@ -0,0 +1,61 @@
+using System.Diagnostics;
+using Microsoft.Extensions.Logging;
+using Reqnroll.IdeSupport.LSP.Server.Hosting;
+
+namespace Reqnroll.IdeSupport.LSP.Server.Tests.Hosting;
+
+public class ProgramTests
+{
+ [Fact]
+ public void ParseLogLevel_defaults_to_Warning_when_flag_is_absent()
+ {
+ Program.ParseLogLevel(new[] { "--ide", "visualstudio" }).Should().Be(TraceLevel.Warning);
+ }
+
+ [Fact]
+ public void ParseLogLevel_defaults_to_Warning_for_no_args()
+ {
+ Program.ParseLogLevel(Array.Empty()).Should().Be(TraceLevel.Warning);
+ }
+
+ [Theory]
+ [InlineData("Off", TraceLevel.Off)]
+ [InlineData("error", TraceLevel.Error)]
+ [InlineData("WARNING", TraceLevel.Warning)]
+ [InlineData("Info", TraceLevel.Info)]
+ [InlineData("verbose", TraceLevel.Verbose)]
+ public void ParseLogLevel_parses_case_insensitively(string arg, TraceLevel expected)
+ {
+ Program.ParseLogLevel(new[] { "--ide", "vscode", "--log-level", arg }).Should().Be(expected);
+ }
+
+ [Fact]
+ public void ParseLogLevel_defaults_to_Warning_for_an_unrecognized_value()
+ {
+ Program.ParseLogLevel(new[] { "--log-level", "not-a-level" }).Should().Be(TraceLevel.Warning);
+ }
+
+ [Fact]
+ public void ParseArg_returns_the_value_following_the_flag()
+ {
+ Program.ParseArg(new[] { "--ide", "visualstudio", "--log-level", "Verbose" }, "--ide")
+ .Should().Be("visualstudio");
+ }
+
+ [Fact]
+ public void ParseArg_returns_null_when_the_flag_is_absent()
+ {
+ Program.ParseArg(new[] { "--ide", "vscode" }, "--log-level").Should().BeNull();
+ }
+
+ [Theory]
+ [InlineData(TraceLevel.Off, LogLevel.None)]
+ [InlineData(TraceLevel.Error, LogLevel.Error)]
+ [InlineData(TraceLevel.Warning, LogLevel.Warning)]
+ [InlineData(TraceLevel.Info, LogLevel.Information)]
+ [InlineData(TraceLevel.Verbose, LogLevel.Trace)]
+ public void ToLogLevel_maps_each_TraceLevel_to_the_matching_LogLevel(TraceLevel traceLevel, LogLevel expected)
+ {
+ Program.ToLogLevel(traceLevel).Should().Be(expected);
+ }
+}
diff --git a/tests/LSP/Reqnroll.IdeSupport.LSP.Server.Tests/Hosting/ServiceCollectionExtensionsTests.cs b/tests/LSP/Reqnroll.IdeSupport.LSP.Server.Tests/Hosting/ServiceCollectionExtensionsTests.cs
new file mode 100644
index 00000000..0a182825
--- /dev/null
+++ b/tests/LSP/Reqnroll.IdeSupport.LSP.Server.Tests/Hosting/ServiceCollectionExtensionsTests.cs
@@ -0,0 +1,31 @@
+using System.Diagnostics;
+using Microsoft.Extensions.DependencyInjection;
+using Reqnroll.IdeSupport.LSP.Server.Hosting;
+
+namespace Reqnroll.IdeSupport.LSP.Server.Tests.Hosting;
+
+public class ServiceCollectionExtensionsTests
+{
+ [Fact]
+ public void AddReqnrollLspCoreServices_registers_the_requested_log_level_on_ClientIdeContext()
+ {
+ var provider = new ServiceCollection()
+ .AddReqnrollLspCoreServices("vscode", TraceLevel.Verbose)
+ .BuildServiceProvider();
+
+ var context = provider.GetRequiredService();
+
+ context.Ide.Should().Be("vscode");
+ context.LogLevel.Should().Be(TraceLevel.Verbose);
+ }
+
+ [Fact]
+ public void AddReqnrollLspCoreServices_defaults_the_log_level_to_Warning()
+ {
+ var provider = new ServiceCollection()
+ .AddReqnrollLspCoreServices("visualstudio")
+ .BuildServiceProvider();
+
+ provider.GetRequiredService().LogLevel.Should().Be(TraceLevel.Warning);
+ }
+}
diff --git a/tests/VisualStudio/Reqnroll.VisualStudio.Tests/LspInterception/LspServerConnectionServiceTests.cs b/tests/VisualStudio/Reqnroll.VisualStudio.Tests/LspInterception/LspServerConnectionServiceTests.cs
index 2acfb871..a871be04 100644
--- a/tests/VisualStudio/Reqnroll.VisualStudio.Tests/LspInterception/LspServerConnectionServiceTests.cs
+++ b/tests/VisualStudio/Reqnroll.VisualStudio.Tests/LspInterception/LspServerConnectionServiceTests.cs
@@ -34,4 +34,10 @@ public void Resolution_is_relative_to_the_assembly_directory_not_the_working_dir
path.Should().Be(@"D:\some\other\deep\path\LSPServer\Reqnroll.IdeSupport.LSP.Server.exe");
}
+
+ [Fact]
+ public void Server_arguments_identify_the_ide_and_a_quiet_default_log_level()
+ {
+ LspServerConnectionService.ServerArguments.Should().Be("--ide visualstudio --log-level Warning");
+ }
}