agent-roast 0.12.0 → 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 +5 -1
- package/dist/{chunk-AP5Y4CH6.js → chunk-FA47T742.js} +47 -10
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
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
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1268
|
-
|
|
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
|
|
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
|
|
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
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-roast",
|
|
3
|
-
"version": "0.12.
|
|
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",
|