|
5 | 5 | require "stringio" |
6 | 6 | require "guardrails/audit" |
7 | 7 | require "guardrails/audit/markdown_writer" |
| 8 | +require "guardrails/tokens" |
8 | 9 |
|
9 | 10 | RSpec.describe Guardrails::Audit::MarkdownWriter do |
10 | 11 | let(:root) { Pathname(Dir.mktmpdir) } |
11 | 12 | let(:now) { Time.utc(2026, 5, 4, 16, 30, 0) } |
12 | 13 | after { FileUtils.rm_rf(root) } |
13 | 14 |
|
14 | | - def violation(type:, file:, line: 1, column: 1, snippet: "x") |
| 15 | + def violation(type:, file:, line: 1, column: 1, snippet: "x", value: nil) |
15 | 16 | Guardrails::Audit::Violation.new( |
16 | | - type: type, file: file, line: line, column: column, snippet: snippet |
| 17 | + type: type, file: file, line: line, column: column, snippet: snippet, value: value |
17 | 18 | ) |
18 | 19 | end |
19 | 20 |
|
@@ -86,4 +87,79 @@ def violation(type:, file:, line: 1, column: 1, snippet: "x") |
86 | 87 | expect(output.string).to include("doc/guardrails-suggestions-20260504T163000Z.md") |
87 | 88 | end |
88 | 89 | end |
| 90 | + |
| 91 | + describe "token-aware suggestions" do |
| 92 | + def token(name:, value:, syntax: :scss_var, file: "tokens.scss", line: 1) |
| 93 | + Guardrails::Tokens::Token.new( |
| 94 | + name: name, value: value, syntax: syntax, file: file, line: line |
| 95 | + ) |
| 96 | + end |
| 97 | + |
| 98 | + it "suggests the matching token reference for raw_color when an exact match exists" do |
| 99 | + v = violation(type: :raw_color, file: "app/views/x.html.erb", value: "#0066ff", |
| 100 | + snippet: '<svg fill="#0066ff"></svg>') |
| 101 | + tokens = [token(name: "primary", value: "#0066ff", syntax: :scss_var)] |
| 102 | + |
| 103 | + described_class.new(root, output: StringIO.new, now: now, tokens: tokens).write([v]) |
| 104 | + |
| 105 | + content = root.join("doc/guardrails-suggestions-20260504T163000Z.md").read(encoding: Encoding::UTF_8) |
| 106 | + expect(content).to include("Use `$primary`") |
| 107 | + expect(content).to include("matches token `primary`") |
| 108 | + end |
| 109 | + |
| 110 | + it "suggests var(--name) for CSS custom property tokens" do |
| 111 | + v = violation(type: :raw_color, file: "app/views/x.html.erb", value: "#0066ff", |
| 112 | + snippet: '<svg fill="#0066ff"></svg>') |
| 113 | + tokens = [token(name: "primary-500", value: "#0066ff", syntax: :css_var)] |
| 114 | + |
| 115 | + described_class.new(root, output: StringIO.new, now: now, tokens: tokens).write([v]) |
| 116 | + |
| 117 | + content = root.join("doc/guardrails-suggestions-20260504T163000Z.md").read(encoding: Encoding::UTF_8) |
| 118 | + expect(content).to include("Use `var(--primary-500)`") |
| 119 | + end |
| 120 | + |
| 121 | + it "matches normalized hex (case + short form)" do |
| 122 | + v = violation(type: :raw_color, file: "app/views/x.html.erb", value: "#fa3", |
| 123 | + snippet: '<svg fill="#fa3"></svg>') |
| 124 | + tokens = [token(name: "secondary", value: "#FFAA33")] |
| 125 | + |
| 126 | + described_class.new(root, output: StringIO.new, now: now, tokens: tokens).write([v]) |
| 127 | + |
| 128 | + content = root.join("doc/guardrails-suggestions-20260504T163000Z.md").read(encoding: Encoding::UTF_8) |
| 129 | + expect(content).to include("matches token `secondary`") |
| 130 | + end |
| 131 | + |
| 132 | + it "matches Tailwind arbitrary values against token values" do |
| 133 | + v = violation(type: :tailwind_arbitrary, file: "app/views/x.html.erb", value: "#0066ff", |
| 134 | + snippet: '<div class="bg-[#0066ff]">x</div>') |
| 135 | + tokens = [token(name: "primary", value: "#0066ff", syntax: :scss_var)] |
| 136 | + |
| 137 | + described_class.new(root, output: StringIO.new, now: now, tokens: tokens).write([v]) |
| 138 | + |
| 139 | + content = root.join("doc/guardrails-suggestions-20260504T163000Z.md").read(encoding: Encoding::UTF_8) |
| 140 | + expect(content).to include("matches token `primary`") |
| 141 | + end |
| 142 | + |
| 143 | + it "falls back to the stock suggestion when no token matches" do |
| 144 | + v = violation(type: :raw_color, file: "app/views/x.html.erb", value: "#abcdef", |
| 145 | + snippet: '<svg fill="#abcdef"></svg>') |
| 146 | + tokens = [token(name: "primary", value: "#0066ff")] |
| 147 | + |
| 148 | + described_class.new(root, output: StringIO.new, now: now, tokens: tokens).write([v]) |
| 149 | + |
| 150 | + content = root.join("doc/guardrails-suggestions-20260504T163000Z.md").read(encoding: Encoding::UTF_8) |
| 151 | + expect(content).to include("Use a CSS custom property or SCSS variable") |
| 152 | + expect(content).not_to include("matches token") |
| 153 | + end |
| 154 | + |
| 155 | + it "uses stock suggestions when no tokens are provided" do |
| 156 | + v = violation(type: :raw_color, file: "app/views/x.html.erb", value: "#0066ff", |
| 157 | + snippet: '<svg fill="#0066ff"></svg>') |
| 158 | + |
| 159 | + described_class.new(root, output: StringIO.new, now: now).write([v]) |
| 160 | + |
| 161 | + content = root.join("doc/guardrails-suggestions-20260504T163000Z.md").read(encoding: Encoding::UTF_8) |
| 162 | + expect(content).to include("Use a CSS custom property or SCSS variable") |
| 163 | + end |
| 164 | + end |
89 | 165 | end |
0 commit comments