tests/Architecture/RpcAdmissionTests.cs

Unit test file that scans the codebase for methods marked with the [Rpc.Host] attribute and ensures each host RPC either goes through admission charging (Dispatch or TryBeginCommand) or is explicitly listed in a reviewed whitelist. It also checks the reviewed list does not contain stale entries and that the parser actually finds a reasonable number of host RPCs.

File AccessReflection
#nullable enable

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Hexagon.V2.Tests.Architecture;

/// <summary>
/// Admission-completeness guard for the client-to-host RPC surface. The engine applies no rate
/// limiting of its own, so Hexagon's per-connection budget is the only limiter that exists: an
/// [Rpc.Host] method that does not route through it is unmetered work any client can drive at
/// whatever rate the transport allows. Detection is not enough here - the one endpoint that was
/// unmetered got that way by being added without anyone noticing - so this fails the suite until
/// a new entry point is either charged or reviewed onto the list below with a reason.
/// </summary>
[TestClass]
public sealed class RpcAdmissionTests
{
	/// <summary>
	/// Entry points that legitimately do not charge the per-command budget, each with the reason
	/// it is safe. Anything not here must call Dispatch or a Try*/admission gate.
	/// </summary>
	private static readonly IReadOnlyDictionary<string, string> ReviewedUnchargedEntryPoints =
		new Dictionary<string, string>( StringComparer.Ordinal )
		{
			["ReportClientBootstrapDiagnostic"] =
				"Charged against the per-connection bootstrap-diagnostic controller, which caps total attempts.",
			["RequestEstablishSession"] =
				"Charged against the per-connection handshake controller; it runs before a command scope exists so it has no request ID."
		};

	[TestMethod]
	public void EveryHostRpcEntryPointIsChargedOrReviewed()
	{
		var unmetered = new List<string>();
		foreach ( var file in HostRpcSourceFiles() )
		{
			var root = CSharpSyntaxTree.ParseText( File.ReadAllText( file ) ).GetRoot();
			foreach ( var method in root.DescendantNodes().OfType<MethodDeclarationSyntax>() )
			{
				if ( !HasHostRpcAttribute( method ) ) continue;
				var name = method.Identifier.ValueText;
				if ( ReviewedUnchargedEntryPoints.ContainsKey( name ) ) continue;
				if ( ChargesAdmission( method ) ) continue;
				unmetered.Add( $"{Path.GetFileName( file )}:{name}" );
			}
		}

		Assert.IsEmpty( unmetered.OrderBy( value => value, StringComparer.Ordinal ).ToArray(),
			"These [Rpc.Host] entry points do no admission charging and are not on the reviewed " +
			$"list: {string.Join( ", ", unmetered )}" );
	}

	[TestMethod]
	public void TheReviewedListDoesNotOutliveTheEntryPointsItExcuses()
	{
		var declared = new HashSet<string>( StringComparer.Ordinal );
		foreach ( var file in HostRpcSourceFiles() )
		{
			var root = CSharpSyntaxTree.ParseText( File.ReadAllText( file ) ).GetRoot();
			foreach ( var method in root.DescendantNodes().OfType<MethodDeclarationSyntax>() )
				if ( HasHostRpcAttribute( method ) ) declared.Add( method.Identifier.ValueText );
		}

		var stale = ReviewedUnchargedEntryPoints.Keys
			.Where( name => !declared.Contains( name ) )
			.OrderBy( name => name, StringComparer.Ordinal )
			.ToArray();
		Assert.IsEmpty( stale,
			$"Reviewed entries name entry points that no longer exist: {string.Join( ", ", stale )}" );
	}

	[TestMethod]
	public void TheGuardSeesTheEntryPointsItIsSupposedToPolice()
	{
		// A guard that silently matches nothing passes forever. Pin that the parse actually finds
		// the surface, so a rename or a move fails here rather than vacating the check.
		var count = 0;
		foreach ( var file in HostRpcSourceFiles() )
		{
			var root = CSharpSyntaxTree.ParseText( File.ReadAllText( file ) ).GetRoot();
			count += root.DescendantNodes().OfType<MethodDeclarationSyntax>().Count( HasHostRpcAttribute );
		}

		Assert.IsGreaterThan( 10, count,
			"The [Rpc.Host] surface should be more than a handful of methods; the parse likely broke." );
	}

	private static bool HasHostRpcAttribute( MethodDeclarationSyntax method ) =>
		method.AttributeLists
			.SelectMany( list => list.Attributes )
			.Any( attribute => attribute.Name.ToString() is "Rpc.Host" or "Host" );

	/// <summary>
	/// Matches real invocations rather than source text. A substring scan over the body reads interior
	/// trivia too, so a comment mentioning the admission call was enough to satisfy the guard — which
	/// made a check on unmetered RPC entry points satisfiable by writing a comment.
	/// </summary>
	private static bool ChargesAdmission( MethodDeclarationSyntax method )
	{
		SyntaxNode? body = method.Body;
		body ??= method.ExpressionBody;
		if ( body is null ) return false;

		return body.DescendantNodes()
			.OfType<InvocationExpressionSyntax>()
			.Select( invocation => invocation.Expression switch
			{
				MemberAccessExpressionSyntax member => member.Name.Identifier.ValueText,
				IdentifierNameSyntax identifier => identifier.Identifier.ValueText,
				GenericNameSyntax generic => generic.Identifier.ValueText,
				_ => string.Empty
			} )
			.Any( name => name is "Dispatch" or "TryBeginCommand" );
	}

	private static string[] HostRpcSourceFiles()
	{
		var runtime = Path.Combine( RepositoryRoot(), "Code", "V2", "Runtime" );
		var files = Directory.GetFiles( runtime, "*.cs", SearchOption.AllDirectories );
		Assert.IsNotEmpty( files, $"No runtime sources found under '{runtime}'." );
		return files;
	}

	private static string RepositoryRoot()
	{
		var directory = new DirectoryInfo( AppContext.BaseDirectory );
		while ( directory is not null && !File.Exists( Path.Combine( directory.FullName, "hexagon.sbproj" ) ) )
			directory = directory.Parent;
		Assert.IsNotNull( directory, "Could not locate the Hexagon repository root." );
		return directory!.FullName;
	}
}