agent-roast 0.12.1 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -110,7 +110,7 @@ old debt with a baseline: [docs/reference.md](docs/reference.md#configuration).
110
110
  Measured, with the denominators and the caveats in [docs/benchmarks.md](docs/benchmarks.md). **Weakened checks are rare, mostly
111
111
  deliberate, and not specific to AI. agent-roast is a tripwire that stays silent until a check gets weaker.**
112
112
 
113
- - **64 of 64** hand-labeled cheating edits are caught and **44 of 44** harmless look-alikes (reformats, moves, raised
113
+ - **71 of 71** hand-labeled cheating edits are caught and **51 of 51** harmless look-alikes (reformats, moves, raised
114
114
  thresholds, environment-gated skips) stay silent. The cases were written with the detector, so they guard against regressions;
115
115
  they do not prove accuracy. Three gaps are documented and still open.
116
116
  - **12,645 commits** that touch tests or CI/config in 166 public repositories: blocking rules fired on 0.3% of AI-signed commits
@@ -120,6 +120,10 @@ deliberate, and not specific to AI. agent-roast is a tripwire that stays silent
120
120
  - **About one finding in five was a false alarm**, and most of the rest were deliberate, explained changes a reviewer should
121
121
  see rather than mistakes. The labels come from one reader (an AI assistant), so treat them as a first estimate.
122
122
  - `replay` ran on 179 public repositories that use coding agents: no commit would have been blocked in 149 of them.
123
+ - The rules for **eight more languages** (Go, Rust, Java, Kotlin, C#, Ruby, PHP, Swift) ran on the newest 300 commits of 39
124
+ repositories and every finding was read: 26 of 99 were skips that depend on the environment, now fixed; of the 73 left, 56 are
125
+ tests that really are switched off and 17 are deliberate test data. One reader, and the fixes were made on the same
126
+ repositories, so expect a few new false alarms elsewhere.
123
127
 
124
128
  ## How it compares
125
129
 
@@ -1191,7 +1191,8 @@ var GO_GUARD = {
1191
1191
  var RUBY_GUARD = {
1192
1192
  modifier: /^\s*(?:skip|pending)\b.*\b(?:if|unless)\b/,
1193
1193
  conditional: /^\s*(?:if|unless|else|elsif|case|when|begin|rescue|ensure|while|until)\b/,
1194
- test: /^\s*(?:it|specify|example|scenario|test|before|after|around)\b|^\s*def\s+test_/,
1194
+ // Hooks (before, after, around) are not listed: a hook declared under `elsif RUBY_VERSION ...` gates its skip like an example does.
1195
+ test: /^\s*(?:it|specify|example|scenario|test)\b|^\s*def\s+test_/,
1195
1196
  helper: /^\s*def\s+\w+/
1196
1197
  };
1197
1198
  var PHP_GUARD = {
@@ -1201,9 +1202,10 @@ var PHP_GUARD = {
1201
1202
  helper: /^\s*(?:(?:public|protected|private|static)\s+)*function\s+\w+\s*\(/
1202
1203
  };
1203
1204
  var SWIFT_GUARD = {
1205
+ preprocessor: { open: /^\s*#if\b/, close: /^\s*#endif\b/, other: /^\s*#else(?:if)?\b/ },
1204
1206
  conditional: /^\s*(?:\}\s*)?(?:if|else|guard|switch|case|for|while|do|catch)\b/,
1205
- test: /^\s*(?:@\w+\s+)*(?:(?:private|fileprivate|static)\s+)*func\s+test\w*\s*\(/,
1206
- helper: /^\s*(?:@\w+\s+)*(?:(?:private|fileprivate|static)\s+)*func\s+\w+\s*\(/
1207
+ test: /^\s*(?:@\w+\s+)*(?:(?:public|open|internal|private|fileprivate|static|final|override|class|nonisolated)\s+)*func\s+test\w*\s*\(/,
1208
+ helper: /^\s*(?:@\w+\s+)*(?:(?:public|open|internal|private|fileprivate|static|final|override|class|nonisolated)\s+)*func\s+\w+\s*\(/
1207
1209
  };
1208
1210
  var LANGUAGE_LINE_RULES = [
1209
1211
  { files: /_test\.go$/i, rules: [
@@ -1226,7 +1228,9 @@ var LANGUAGE_LINE_RULES = [
1226
1228
  { type: "trivial-assert", regex: /\bassertThat\s*\(\s*true\s*\)\s*\.\s*isTrue\s*\(/, name: "trivial assert (assertThat(true).isTrue())" }
1227
1229
  ] },
1228
1230
  { files: /\.cs$/i, rules: [
1229
- { type: "test-skip", regex: /\[\s*Ignore\b|\b(?:Fact|Theory)\s*\(\s*(?:[^)]*,\s*)?Skip\s*=/, name: "ignored .NET test ([Ignore] / Skip =)" },
1231
+ // `[Ignore]` is checked separately: it also names a mapping attribute (AutoMapper) and must sit next to a test attribute.
1232
+ // Skip = with SkipUnless, SkipWhen or SkipType depends on the environment.
1233
+ { type: "test-skip", regex: /\b(?:Fact|Theory)\s*\(\s*(?:[^)]*,\s*)?Skip\s*=(?![^\]]*\bSkip(?:Unless|When|Type)\b)/, name: "ignored .NET test (Skip =)" },
1230
1234
  { type: "trivial-assert", regex: /\bAssert\s*\.\s*(?:True|IsTrue)\s*\(\s*true\s*\)/, name: "trivial assert (Assert.True(true))" },
1231
1235
  { type: "trivial-assert", regex: /\bAssert\s*\.\s*(?:False|IsFalse)\s*\(\s*false\s*\)/, name: "trivial assert (Assert.False(false))" },
1232
1236
  { type: "trivial-assert", regex: /\bAssert\s*\.\s*(?:AreEqual|Equal)\s*\(\s*(\d+|true|false|null)\s*,\s*\1\s*\)/, name: "trivial assert (Assert.Equal(1, 1))" }
@@ -1236,7 +1240,9 @@ var LANGUAGE_LINE_RULES = [
1236
1240
  { type: "test-focus", regex: /^\s*(?:fit|fdescribe|fcontext|fspecify|fexample|fscenario)\b/, name: "focused RSpec example (fit / fdescribe)" },
1237
1241
  { type: "test-focus", regex: /\b(?:it|describe|context|specify|scenario)\b[^\n]*[,(]\s*(?::focus\b|focus:\s*true\b)/, name: "focused RSpec example (focus: true)" },
1238
1242
  { type: "test-skip", regex: /^\s*(?:xit|xdescribe|xcontext|xspecify|xexample|xscenario|xfeature)\b/, name: "disabled RSpec example (xit / xdescribe)" },
1239
- { type: "test-skip", regex: /\b(?:it|describe|context|specify|scenario)\b[^\n]*[,(]\s*(?::skip\b|skip:\s*(?:true\b|["']))/, name: "disabled RSpec example (skip: true)" },
1243
+ // `:skip => !feature?` or `skip: RUBY_VERSION < "3"` gates the example on its environment; only a bare flag, true or a reason
1244
+ // disables it. The line is matched with its strings blanked, so a reason shows as nothing between `skip:` and `do`, `{`, `,` or `)`.
1245
+ { type: "test-skip", regex: /\b(?:it|describe|context|specify|scenario)\b[^\n]*[,(]\s*(?::skip\b(?!\s*=>)|(?::skip\s*=>|skip:)\s*(?:true\b|(?=do\b|\{|,|\)|$)))/, name: "disabled RSpec example (skip: true)" },
1240
1246
  { type: "test-skip", regex: /^\s*skip(?:\s|\(|$)/, name: "skipped Ruby test (skip)", guard: RUBY_GUARD },
1241
1247
  { type: "test-skip", regex: /^\s*pending(?:\s+["'(]|\s*$)/, name: "pending Ruby example (pending)", guard: RUBY_GUARD },
1242
1248
  { type: "trivial-assert", regex: /\bexpect\s*\(\s*(true|false|1|0|nil)\s*\)\s*\.\s*to\s+(?:eq|eql|be|equal)\b\s*\(?\s*\1\b/, name: "trivial expectation (expect(true).to eq(true))" },
@@ -1251,29 +1257,53 @@ var LANGUAGE_LINE_RULES = [
1251
1257
  ] },
1252
1258
  { files: /\.swift$/i, rules: [
1253
1259
  { type: "test-skip", regex: /\bthrow\s+XCTSkip\s*\(/, name: "skipped XCTest (XCTSkip)", guard: SWIFT_GUARD },
1254
- { type: "test-skip", regex: /@(?:Test|Suite)\s*\([^)]*\.disabled\b/, name: "disabled Swift Testing test (.disabled)" },
1260
+ // `.disabled(if: condition)` depends on the environment; `.disabled()` and `.disabled("reason")` switch the test off.
1261
+ { type: "test-skip", regex: /@(?:Test|Suite)\s*\([^)]*\.disabled\s*\((?!\s*if\b)/, name: "disabled Swift Testing test (.disabled)" },
1255
1262
  { type: "trivial-assert", regex: /\bXCTAssertTrue\s*\(\s*true\s*\)/, name: "trivial assert (XCTAssertTrue(true))" },
1256
1263
  { type: "trivial-assert", regex: /\bXCTAssertFalse\s*\(\s*false\s*\)/, name: "trivial assert (XCTAssertFalse(false))" },
1257
1264
  { type: "trivial-assert", regex: /\bXCTAssertEqual\s*\(\s*(\d+|true|false|nil)\s*,\s*\1\s*\)/, name: "trivial assert (XCTAssertEqual(1, 1))" },
1258
1265
  { type: "trivial-assert", regex: /#expect\s*\(\s*true\s*\)/, name: "trivial expectation (#expect(true))" }
1259
1266
  ] }
1260
1267
  ];
1268
+ function inTestAttributeList(lines, index) {
1269
+ const marker = /\[\s*(?:Test|TestCase|TestCaseSource|TestMethod|DataTestMethod|TestFixture|TestClass|Fact|Theory)\b/;
1270
+ if (marker.test(lines[index].code)) return true;
1271
+ for (const step of [-1, 1]) {
1272
+ for (let i = index + step, seen = 0; i >= 0 && i < lines.length && seen < 6; i += step, seen++) {
1273
+ if (lines[i].number !== lines[i - step].number + step || !/^\s*\[/.test(lines[i].code)) break;
1274
+ if (marker.test(lines[i].code)) return true;
1275
+ }
1276
+ }
1277
+ return false;
1278
+ }
1261
1279
  function isGuardedBlockSkip(lines, profile) {
1262
1280
  const indentOf = (text) => text.length - text.trimStart().length;
1263
1281
  const mentionsCi = (raw) => /\bCI\b|\bGITHUB_ACTIONS\b/.test(raw);
1264
1282
  const own = lines[lines.length - 1];
1265
1283
  if (profile.modifier?.test(own.code) || profile.conditional.test(own.code)) return !mentionsCi(own.raw);
1284
+ const compiledConditionally = (from) => {
1285
+ const directives = profile.preprocessor;
1286
+ if (!directives) return false;
1287
+ let depth = 0;
1288
+ for (let i2 = from; i2 < lines.length - 1; i2++) {
1289
+ if (directives.open.test(lines[i2].code)) depth++;
1290
+ else if (directives.close.test(lines[i2].code)) depth = Math.max(0, depth - 1);
1291
+ }
1292
+ return depth > 0;
1293
+ };
1266
1294
  let limit = indentOf(own.code);
1267
- for (let i = lines.length - 2; i >= 0 && lines.length - 1 - i <= 200; i--) {
1268
- if (lines[i].number !== lines[i + 1].number - 1) return false;
1295
+ let i = lines.length - 2;
1296
+ for (; i >= 0 && lines.length - 1 - i <= 200; i--) {
1297
+ if (lines[i].number !== lines[i + 1].number - 1) return compiledConditionally(i + 1);
1269
1298
  const { code, raw } = lines[i];
1270
1299
  if (!code.trim() || indentOf(code) >= limit) continue;
1300
+ if (profile.preprocessor && (profile.preprocessor.open.test(code) || profile.preprocessor.close.test(code) || profile.preprocessor.other.test(code))) continue;
1271
1301
  if (profile.conditional.test(code)) return !mentionsCi(raw);
1272
- if (profile.test.test(code)) return false;
1302
+ if (profile.test.test(code)) return compiledConditionally(i + 1);
1273
1303
  if (profile.helper.test(code)) return true;
1274
1304
  limit = indentOf(code);
1275
1305
  }
1276
- return false;
1306
+ return compiledConditionally(i + 1);
1277
1307
  }
1278
1308
  var JS_TEST_API = /(?<![\w$.])(?:(?:it|test|describe|suite)(?:\s*\.\s*(?:concurrent|serial|parallel|describe|failing|sequential))*\s*\.\s*(skip|todo|only)|((?:xit|xtest|xdescribe|fit|fdescribe)))\b((?:\s*\.\s*(?:each|failing|concurrent|serial|parallel|sequential))*)\s*(?=\(|\u0001)/g;
1279
1309
  function isConditionalSkip(source, raw, open2) {
@@ -1470,6 +1500,13 @@ function auditFileDiffs(files, commitHash = "HEAD", commitDate = (/* @__PURE__ *
1470
1500
  }
1471
1501
  }
1472
1502
  }
1503
+ if (/\.cs$/i.test(file.path)) {
1504
+ maskedLines.forEach((line, index) => {
1505
+ if (/\[\s*Ignore\b/.test(line.code) && inTestAttributeList(maskedLines, index)) {
1506
+ infractions.push({ type: "test-skip", file: file.path, lineNumber: line.number, lineSnippet: line.raw.trim(), commitHash, commitDate, reason: "ignored .NET test ([Ignore])" });
1507
+ }
1508
+ });
1509
+ }
1473
1510
  let source = "";
1474
1511
  let code = "";
1475
1512
  let raw = "";
package/dist/cli.js CHANGED
@@ -37,7 +37,7 @@ import {
37
37
  terminalText,
38
38
  uninstallHooks,
39
39
  writeBaseline
40
- } from "./chunk-AP5Y4CH6.js";
40
+ } from "./chunk-FA47T742.js";
41
41
 
42
42
  // src/cli.ts
43
43
  import { Command } from "commander";
package/dist/index.js CHANGED
@@ -88,7 +88,7 @@ import {
88
88
  unquoteGitPath,
89
89
  validateExceptions,
90
90
  writeBaseline
91
- } from "./chunk-AP5Y4CH6.js";
91
+ } from "./chunk-FA47T742.js";
92
92
  export {
93
93
  AGENT_LABELS,
94
94
  CONFIG_FILENAMES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-roast",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "description": "Stops AI coding agents (and rushed humans) from turning a build green by weakening tests, coverage gates or CI. Hook for Claude Code, Codex and Cursor, pre-commit, GitHub Action. Offline.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",