@@ -202,11 +202,12 @@ public async Task L2_StagesOneAndTwo_AreGreen_WithZeroArtifacts()
202202 }
203203
204204 /// <summary>
205- /// SDK-generated and hand-authored friend-assembly declarations must each
206- /// reach the compiled G# assembly exactly once.
205+ /// Diagnostic-run projects disable SDK AssemblyInfo generation, so the
206+ /// translator must retain only generated friend declarations while the
207+ /// hand-authored fully-qualified attribute stays in its translated source.
207208 /// </summary>
208209 [ Fact ]
209- public async Task L2_Compile_EmitsInternalsVisibleToExactlyOnce ( )
210+ public async Task L2_DiagnosticCompile_EmitsInternalsVisibleToExactlyOnce ( )
210211 {
211212 string compiler = FindCompiler ( ) ;
212213 if ( compiler is null )
@@ -216,60 +217,96 @@ public async Task L2_Compile_EmitsInternalsVisibleToExactlyOnce()
216217
217218 string corpus = ResolveCorpusDir ( ) ;
218219 string repoRoot = GsharpTestProjectRunner . FindRepoRoot ( ) ;
219- ( string NupkgPath , string Version ) ? sdk =
220- GsharpTestProjectRunner . ResolveLocalSdkPackage ( repoRoot ) ;
221- if ( sdk is null )
220+ if ( GsharpTestProjectRunner . ResolveLocalSdkPackage ( repoRoot ) is null )
222221 {
223222 return ;
224223 }
225224
226- CorpusApp originalL2 = CorpusDiscovery . FindById ( corpus , "corpus/L2-Library" ) ;
227225 string sourceRoot = NewOutputRoot ( "l2-friend-source" ) ;
228- string sourceProjectDir = Path . Combine ( sourceRoot , "L2-Library" ) ;
229- Directory . CreateDirectory ( sourceProjectDir ) ;
230- File . Copy (
231- Path . Combine ( corpus , "Directory.Build.props" ) ,
232- Path . Combine ( sourceRoot , "Directory.Build.props" ) ) ;
233- string projectPath = Path . Combine ( sourceProjectDir , Path . GetFileName ( originalL2 . ProjectPath ) ) ;
234- File . Copy ( originalL2 . ProjectPath , projectPath ) ;
235- File . WriteAllText (
236- projectPath ,
237- File . ReadAllText ( projectPath ) . Replace (
238- "<PropertyGroup>" ,
239- "<PropertyGroup>" + Environment . NewLine + " <TargetFramework>net10.0</TargetFramework>" ,
240- StringComparison . Ordinal ) ) ;
241- File . WriteAllText ( Path . Combine ( sourceProjectDir , "FriendAssembly.cs" ) , """
242- using System.Runtime.CompilerServices;
243-
244- [assembly: InternalsVisibleTo("L2-Library.Source.Tests")]
245-
246- public sealed class Marker
247- {
248- }
249- """ ) ;
250- var l2 = new CorpusApp (
251- originalL2 . Id ,
252- projectPath ,
253- originalL2 . TargetKind ) ;
254- string outRoot = NewOutputRoot ( "l2-friend-assembly" ) ;
226+ CorpusApp l2 = CreateFriendAssemblyFixture ( corpus , sourceRoot ) ;
227+ string outRoot = NewOutputRoot ( "l2-friend-diagnostic" ) ;
255228 var options = new PipelineOptions { GscPath = compiler , OutputRoot = outRoot } ;
256- var pipeline = new MigrationPipeline ( options , new IMigrationStage [ ] { new TranslateStage ( ) } ) ;
229+ var pipeline = new MigrationPipeline (
230+ options ,
231+ new IMigrationStage [ ] { new TranslateStage ( ) , new CompileStage ( ) } ) ;
257232
258233 RunResult result = await pipeline . RunAsync ( new [ ] { l2 } ) ;
259234 AppResult app = Assert . Single ( result . Apps ) ;
260235
261- Assert . True ( app . Succeeded ) ;
236+ Assert . True ( app . Succeeded , string . Join ( Environment . NewLine , app . Artifacts ) ) ;
262237 string appDir = Path . Combine ( outRoot , result . RunId , MigrationPipeline . SanitizeAppId ( l2 . Id ) ) ;
263- string generatedProjectPath = Path . Combine ( appDir , "L2-Library.gsproj" ) ;
238+ Assert . Equal (
239+ "@assembly:InternalsVisibleTo(\" L2-Library.Tests\" )" + Environment . NewLine ,
240+ File . ReadAllText ( Path . Combine ( appDir , "AssemblyInfo.gs" ) ) ) ;
241+ Assert . Contains (
242+ "@assembly:System.Runtime.CompilerServices.InternalsVisibleTo(\" L2-Library.Source.Tests\" )" ,
243+ File . ReadAllText ( Path . Combine ( appDir , "FriendAssembly.gs" ) ) ,
244+ StringComparison . Ordinal ) ;
245+
246+ string assemblyPath = Path . Combine ( appDir , "bin" , "Release" , "net10.0" , "L2-Library.dll" ) ;
247+ AssertFriendAssemblies ( assemblyPath ) ;
248+ }
249+
250+ /// <summary>
251+ /// Repository projects preserve the MSBuild item and must not receive a
252+ /// second generated G# annotation from translation.
253+ /// </summary>
254+ [ Fact ]
255+ public async Task L2_RepositoryCompile_EmitsInternalsVisibleToExactlyOnce ( )
256+ {
257+ string compiler = FindCompiler ( ) ;
258+ string repoRoot = GsharpTestProjectRunner . FindRepoRoot ( ) ;
259+ ( string NupkgPath , string Version ) ? sdk =
260+ GsharpTestProjectRunner . ResolveLocalSdkPackage ( repoRoot ) ;
261+ if ( compiler is null || sdk is null )
262+ {
263+ return ;
264+ }
265+
266+ string corpus = ResolveCorpusDir ( ) ;
267+ string sourceRoot = NewOutputRoot ( "l2-friend-repository-source" ) ;
268+ CorpusApp l2 = CreateFriendAssemblyFixture ( corpus , sourceRoot ) ;
269+ string destinationRoot = NewOutputRoot ( "l2-friend-repository" ) ;
270+ string projectDir = Path . Combine ( destinationRoot , "L2-Library" ) ;
271+ Directory . CreateDirectory ( projectDir ) ;
272+ var options = new PipelineOptions
273+ {
274+ GscPath = compiler ,
275+ SourceRoot = sourceRoot ,
276+ OutputRoot = destinationRoot ,
277+ OutputLayout = MigrationOutputLayout . Repository ,
278+ } ;
279+ var context = new StageExecutionContext (
280+ l2 ,
281+ options ,
282+ new GscInvoker ( compiler ) ,
283+ projectDir ,
284+ new TriageBuilder ( "run" , "2026-07-28T00:00:00Z" , "test" , l2 . Id ) ) ;
285+ StageOutcome translation = await new TranslateStage ( ) . ExecuteAsync ( context ) ;
286+
287+ Assert . Equal ( StageStatus . Passed , translation . Status ) ;
288+ Assert . False ( File . Exists ( Path . Combine ( projectDir , "AssemblyInfo.gs" ) ) ) ;
289+ Assert . Contains (
290+ "@assembly:System.Runtime.CompilerServices.InternalsVisibleTo(\" L2-Library.Source.Tests\" )" ,
291+ File . ReadAllText ( Path . Combine ( projectDir , "FriendAssembly.gs" ) ) ,
292+ StringComparison . Ordinal ) ;
293+
294+ string generatedProjectPath = Path . Combine ( projectDir , "L2-Library.gsproj" ) ;
264295 GSharpProjectTransformer . Transform (
265- projectPath ,
266- appDir ,
296+ l2 . ProjectPath ,
297+ projectDir ,
267298 "Gsharp.NET.Sdk/" + sdk . Value . Version ,
268299 generatedProjectPaths : null ) . Save (
269300 generatedProjectPath ,
270301 System . Xml . Linq . SaveOptions . DisableFormatting ) ;
302+ Assert . Single (
303+ System . Xml . Linq . XDocument . Load ( generatedProjectPath ) . Descendants ( ) ,
304+ element =>
305+ element . Name . LocalName == "InternalsVisibleTo" &&
306+ element . Attribute ( "Include" ) ? . Value == "L2-Library.Tests" ) ;
307+
271308 GsharpTestProjectRunner . EnsureInLocalFeed ( repoRoot , sdk . Value . NupkgPath ) ;
272- GsharpTestProjectRunner . WriteIsolationBoundary ( appDir ) ;
309+ GsharpTestProjectRunner . WriteIsolationBoundary ( projectDir ) ;
273310 ProcessRunResult build = ProcessRunner . Run (
274311 "dotnet" ,
275312 new [ ]
@@ -280,21 +317,12 @@ public sealed class Marker
280317 "Release" ,
281318 "-p:RestoreConfigFile=" + Path . Combine ( repoRoot , "nuget.config" ) ,
282319 } ,
283- appDir ,
320+ projectDir ,
284321 TimeSpan . FromMinutes ( 5 ) ) ;
285322 Assert . True ( build . ExitCode == 0 , build . Output ) ;
286323
287- string assemblyPath = Path . Combine ( appDir , "bin" , "Release" , "net10.0" , "L2-Library.dll" ) ;
288- Assembly assembly = Assembly . LoadFile ( assemblyPath ) ;
289- string [ ] friendAssemblies = assembly . GetCustomAttributesData ( )
290- . Where ( attribute => attribute . AttributeType == typeof ( InternalsVisibleToAttribute ) )
291- . Select ( attribute => ( string ) Assert . Single ( attribute . ConstructorArguments ) . Value )
292- . OrderBy ( name => name , StringComparer . Ordinal )
293- . ToArray ( ) ;
294-
295- Assert . Equal (
296- new [ ] { "L2-Library.Source.Tests" , "L2-Library.Tests" } ,
297- friendAssemblies ) ;
324+ string assemblyPath = Path . Combine ( projectDir , "bin" , "Release" , "net10.0" , "L2-Library.dll" ) ;
325+ AssertFriendAssemblies ( assemblyPath ) ;
298326 }
299327
300328 /// <summary>
@@ -559,6 +587,50 @@ public Task<StageOutcome> ExecuteAsync(StageExecutionContext context, Cancellati
559587 Task . FromResult ( StageOutcome . Failed ( Array . Empty < TriageArtifact > ( ) ) ) ;
560588 }
561589
590+ private static CorpusApp CreateFriendAssemblyFixture ( string corpus , string sourceRoot )
591+ {
592+ CorpusApp original = CorpusDiscovery . FindById ( corpus , "corpus/L2-Library" ) ;
593+ string projectDir = Path . Combine ( sourceRoot , "L2-Library" ) ;
594+ Directory . CreateDirectory ( projectDir ) ;
595+ File . Copy (
596+ Path . Combine ( corpus , "Directory.Build.props" ) ,
597+ Path . Combine ( sourceRoot , "Directory.Build.props" ) ) ;
598+ string projectPath = Path . Combine ( projectDir , Path . GetFileName ( original . ProjectPath ) ) ;
599+ File . Copy ( original . ProjectPath , projectPath ) ;
600+ File . WriteAllText (
601+ projectPath ,
602+ File . ReadAllText ( projectPath ) . Replace (
603+ "<PropertyGroup>" ,
604+ "<PropertyGroup>" + Environment . NewLine + " <TargetFramework>net10.0</TargetFramework>" ,
605+ StringComparison . Ordinal ) ) ;
606+ File . WriteAllText ( Path . Combine ( projectDir , "FriendAssembly.cs" ) , """
607+ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("L2-Library.Source.Tests")]
608+
609+ public sealed class Marker
610+ {
611+ }
612+ """ ) ;
613+ return new CorpusApp (
614+ original . Id ,
615+ projectPath ,
616+ original . TargetKind ,
617+ relativeProjectPath : Path . Combine ( "L2-Library" , Path . GetFileName ( projectPath ) ) ) ;
618+ }
619+
620+ private static void AssertFriendAssemblies ( string assemblyPath )
621+ {
622+ Assembly assembly = Assembly . LoadFile ( assemblyPath ) ;
623+ string [ ] friendAssemblies = assembly . GetCustomAttributesData ( )
624+ . Where ( attribute => attribute . AttributeType == typeof ( InternalsVisibleToAttribute ) )
625+ . Select ( attribute => ( string ) Assert . Single ( attribute . ConstructorArguments ) . Value )
626+ . OrderBy ( name => name , StringComparer . Ordinal )
627+ . ToArray ( ) ;
628+
629+ Assert . Equal (
630+ new [ ] { "L2-Library.Source.Tests" , "L2-Library.Tests" } ,
631+ friendAssemblies ) ;
632+ }
633+
562634 private static string FingerprintShort ( string fingerprint )
563635 {
564636 string hex = fingerprint . StartsWith ( "sha256:" , StringComparison . Ordinal )
0 commit comments