using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using NUnit.Framework; using NightclubArcadia.Skills; using UnityEditor; namespace NightclubArcadia.Skills.Tests { /// /// Source lint over Assets/Dialogue/**/*.yarn. This is a tripwire, not a proof — /// it catches the easy-and-wrong wirings, not every possible one. /// public class CandidateYarnLintTests { const string DialogueRoot = "Assets/Dialogue"; const string DatabasePath = "Assets/Candidates/CandidateDatabase.asset"; static readonly Regex LeanWrite = new Regex( @"\$self_lean_([a-z][a-z0-9_]*)", RegexOptions.Compiled); static readonly Regex FoundWrite = new Regex( @"\$found_([a-z][a-z0-9_]*)", RegexOptions.Compiled); static readonly Regex LeanVsLean = new Regex( @"\$self_lean_[a-z][a-z0-9_]*\s*(>=|<=|==|!=|>|<)\s*\$self_lean_[a-z][a-z0-9_]*", RegexOptions.Compiled); static readonly Regex NodeSplit = new Regex( @"^title:\s*(.+)\s*$", RegexOptions.Compiled | RegexOptions.Multiline); CandidateDatabase LoadDatabase() { // Assets are checked in (generated by CandidateSystemSetup). Same pattern as // SkillRosterTests — the test assembly cannot reference the Editor setup type. var db = AssetDatabase.LoadAssetAtPath(DatabasePath); Assert.IsNotNull(db, $"Expected CandidateDatabase at {DatabasePath}"); return db; } static IEnumerable AllYarnFiles() { return Directory.GetFiles(DialogueRoot, "*.yarn", SearchOption.AllDirectories); } static string Read(string path) => File.ReadAllText(path); [Test] public void Lint_LeanAndFoundWrites_MatchRegistryIds() { var db = LoadDatabase(); var candidateIds = new HashSet( db.All.Select(c => c.Id), StringComparer.OrdinalIgnoreCase); var clueIds = new HashSet( db.All.SelectMany(c => c.Clues).Select(c => c.Id), StringComparer.OrdinalIgnoreCase); foreach (var path in AllYarnFiles()) { var text = Read(path); foreach (Match match in LeanWrite.Matches(text)) { var id = match.Groups[1].Value; Assert.IsTrue( candidateIds.Contains(id), $"{path}: $self_lean_{id} has no matching candidate id in the registry."); } foreach (Match match in FoundWrite.Matches(text)) { var id = match.Groups[1].Value; Assert.IsTrue( clueIds.Contains(id), $"{path}: $found_{id} has no matching clue id in the registry."); } } } [Test] public void Lint_AuthoredClues_HaveFoundWrites() { var db = LoadDatabase(); var allYarn = string.Join("\n", AllYarnFiles().Select(Read)); foreach (var candidate in db.All) { foreach (var clue in candidate.Clues) { if (clue.Status != CandidateClueStatus.Authored) { continue; } var needle = $"<>"; Assert.IsTrue( allYarn.Contains(needle), $"Authored clue '{clue.Id}' has no `{needle}` anywhere under {DialogueRoot}."); } } } [Test] public void Lint_NoLeanVsLeanComparison() { foreach (var path in AllYarnFiles()) { var text = Read(path); Assert.IsFalse( LeanVsLean.IsMatch(text), $"{path}: comparing two $self_lean_* values against each other is forbidden " + "(no derived winner). Comparing one lean against a constant is fine."); } } [Test] public void Lint_ClueAndLeanWrites_NotSkillGated() { foreach (var path in AllYarnFiles()) { var text = Read(path); var matches = NodeSplit.Matches(text); for (var i = 0; i < matches.Count; i++) { var start = matches[i].Index; var end = i + 1 < matches.Count ? matches[i + 1].Index : text.Length; var nodeBody = text.Substring(start, end - start); var title = matches[i].Groups[1].Value.Trim(); var writesLeanOrFound = nodeBody.Contains("$self_lean_") || nodeBody.Contains("$found_"); if (!writesLeanOrFound) { continue; } Assert.IsFalse( nodeBody.Contains("skill_rank("), $"{path} node '{title}': a node that writes $self_lean_* / $found_* " + "must not sit behind a skill_rank(...) guard."); } } } [Test] public void Lint_CommentaryNodes_NeverWriteLeanOrFound() { foreach (var path in AllYarnFiles()) { var text = Read(path); var matches = NodeSplit.Matches(text); for (var i = 0; i < matches.Count; i++) { var title = matches[i].Groups[1].Value.Trim(); if (!title.StartsWith("Commentary_", StringComparison.Ordinal)) { continue; } var start = matches[i].Index; var end = i + 1 < matches.Count ? matches[i + 1].Index : text.Length; var nodeBody = text.Substring(start, end - start); Assert.IsFalse( nodeBody.Contains("$self_lean_") || nodeBody.Contains("$found_"), $"{path} node '{title}': Commentary_* nodes must not write lean or found flags " + "(ambient advocacy only; clue lines are scene-authored)."); } } } } }