Skip to content

feat(assert): a missing argument reports a failed assertion, not a usage error #983

Description

@Chemaclass

Summary

Call a two-argument assertion with one argument and it does not complain. It compares against the empty string and reports an ordinary failure:

functiontest_wrong_arg_count() {
assert_same "only-one"
}
✗ Failed: Wrong arg count
Expected 'only-one'
but got ''

That output is indistinguishable from a genuine failure where the code under test really did produce an empty string. The reader debugs their code; the bug is in the test.

Why this is the expensive kind of mistake

Empty is the single most common real value in shell — an unset variable, a command that printed nothing, a failed capture. So the false story this tells is also the most plausible one. Someone hitting it will go and check why their function returned nothing, because that is exactly what the message says happened.

It is also easy to reach: assert_same "$expected" after deleting the second argument during a refactor, or a typo'd "$actual" that expands to nothing and silently collapses the argument count.

Related and worth handling together: argument order in this catalogue is not uniform, so a swapped pair is another way to produce a confusing-but-plausible failure. assert_same and assert_contains take expected/needle first; assert_json_contains takes key expected json with the subject last. I hit that myself while researching this — assert_json_contains '{"a":[1,2,3]}' '.a|length' "3" reports Expected '.a | length' but got '{…', which is the assertion faithfully comparing the two arguments it was handed. Nothing is wrong except that no one can tell.

Proposal

Arity checking on the assertion entry points. When too few arguments are supplied, fail with a usage error rather than a comparison:

✗ Error: Wrong arg count
assert_same expects 2 arguments (expected, actual), got 1

Design points worth settling before implementing:

  • Report as an error, not a failure. This is a defect in the test, the same class as calling an assertion that does not exist — which this framework already surfaces as Error, not Failed. Reusing that channel keeps the distinction the reader needs.
  • Only under-supply, not over-supply. Several assertions take a trailing optional argument (a label override, an nth index), so a strict upper bound would be wrong or would need per-assertion tables. Under-supply is unambiguous and covers the real failure mode.
  • Do not use $# naively where an assertion legitimately accepts an empty string as its second argument — assert_empty "$x" and assert_same "" "$x" are valid. The check is on count supplied, which $# gives correctly, so this works; it just has to be $# and never [ -z "$2" ].

Doing this for all 76 assertions at once is a large mechanical change. A reasonable first cut is the comparison family that carries the risk: assert_same, assert_equals, assert_not_same, assert_not_equals, assert_contains, assert_not_contains, assert_greater_than and friends.

Constraints

  • Bash 3.0+; $# and case only, no compatibility surface.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md. An arity check is a builtin comparison, so this is free, but it must not become a shared helper invoked through $( ).
  • Public API: no signature changes. A call that is correct today must behave identically — the only behaviour change is for calls that are already broken.
  • The error path needs its own rendering; check how bashunit::assert::fail_with and the Error classification in runner/diagnostics.sh interact before inventing a third shape.
  • CHANGELOG.md under ### Changed, and note it explicitly: a suite that was silently passing a malformed assertion will start reporting it.

Acceptance criteria

  • assert_same "only-one" reports a usage error naming the expected argument count, not but got ''
  • Reported as Error, consistent with an undefined assertion, not as Failed
  • assert_same "" "$x" and other legitimately-empty arguments still work
  • Optional trailing arguments (label override, nth) are unaffected
  • No new fork on the assertion path — fork budget tests still pass
  • Scope of which assertions are covered in this pass is stated in the PR
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(assert): a missing argument reports a failed assertion, not a usage error · Issue #983 · TypedDevs/bashunit · GitHub
Skip to content

feat(assert): a missing argument reports a failed assertion, not a usage error #983

Description

@Chemaclass

Summary

Call a two-argument assertion with one argument and it does not complain. It compares against the empty string and reports an ordinary failure:

functiontest_wrong_arg_count() {
assert_same "only-one"
}
✗ Failed: Wrong arg count
Expected 'only-one'
but got ''

That output is indistinguishable from a genuine failure where the code under test really did produce an empty string. The reader debugs their code; the bug is in the test.

Why this is the expensive kind of mistake

Empty is the single most common real value in shell — an unset variable, a command that printed nothing, a failed capture. So the false story this tells is also the most plausible one. Someone hitting it will go and check why their function returned nothing, because that is exactly what the message says happened.

It is also easy to reach: assert_same "$expected" after deleting the second argument during a refactor, or a typo'd "$actual" that expands to nothing and silently collapses the argument count.

Related and worth handling together: argument order in this catalogue is not uniform, so a swapped pair is another way to produce a confusing-but-plausible failure. assert_same and assert_contains take expected/needle first; assert_json_contains takes key expected json with the subject last. I hit that myself while researching this — assert_json_contains '{"a":[1,2,3]}' '.a|length' "3" reports Expected '.a | length' but got '{…', which is the assertion faithfully comparing the two arguments it was handed. Nothing is wrong except that no one can tell.

Proposal

Arity checking on the assertion entry points. When too few arguments are supplied, fail with a usage error rather than a comparison:

✗ Error: Wrong arg count
assert_same expects 2 arguments (expected, actual), got 1

Design points worth settling before implementing:

  • Report as an error, not a failure. This is a defect in the test, the same class as calling an assertion that does not exist — which this framework already surfaces as Error, not Failed. Reusing that channel keeps the distinction the reader needs.
  • Only under-supply, not over-supply. Several assertions take a trailing optional argument (a label override, an nth index), so a strict upper bound would be wrong or would need per-assertion tables. Under-supply is unambiguous and covers the real failure mode.
  • Do not use $# naively where an assertion legitimately accepts an empty string as its second argument — assert_empty "$x" and assert_same "" "$x" are valid. The check is on count supplied, which $# gives correctly, so this works; it just has to be $# and never [ -z "$2" ].

Doing this for all 76 assertions at once is a large mechanical change. A reasonable first cut is the comparison family that carries the risk: assert_same, assert_equals, assert_not_same, assert_not_equals, assert_contains, assert_not_contains, assert_greater_than and friends.

Constraints

  • Bash 3.0+; $# and case only, no compatibility surface.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md. An arity check is a builtin comparison, so this is free, but it must not become a shared helper invoked through $( ).
  • Public API: no signature changes. A call that is correct today must behave identically — the only behaviour change is for calls that are already broken.
  • The error path needs its own rendering; check how bashunit::assert::fail_with and the Error classification in runner/diagnostics.sh interact before inventing a third shape.
  • CHANGELOG.md under ### Changed, and note it explicitly: a suite that was silently passing a malformed assertion will start reporting it.

Acceptance criteria

  • assert_same "only-one" reports a usage error naming the expected argument count, not but got ''
  • Reported as Error, consistent with an undefined assertion, not as Failed
  • assert_same "" "$x" and other legitimately-empty arguments still work
  • Optional trailing arguments (label override, nth) are unaffected
  • No new fork on the assertion path — fork budget tests still pass
  • Scope of which assertions are covered in this pass is stated in the PR
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(assert): a missing argument reports a failed assertion, not a usage error · Issue #983 · TypedDevs/bashunit · GitHub
Skip to content

feat(assert): a missing argument reports a failed assertion, not a usage error #983

Description

@Chemaclass

Summary

Call a two-argument assertion with one argument and it does not complain. It compares against the empty string and reports an ordinary failure:

functiontest_wrong_arg_count() {
assert_same "only-one"
}
✗ Failed: Wrong arg count
Expected 'only-one'
but got ''

That output is indistinguishable from a genuine failure where the code under test really did produce an empty string. The reader debugs their code; the bug is in the test.

Why this is the expensive kind of mistake

Empty is the single most common real value in shell — an unset variable, a command that printed nothing, a failed capture. So the false story this tells is also the most plausible one. Someone hitting it will go and check why their function returned nothing, because that is exactly what the message says happened.

It is also easy to reach: assert_same "$expected" after deleting the second argument during a refactor, or a typo'd "$actual" that expands to nothing and silently collapses the argument count.

Related and worth handling together: argument order in this catalogue is not uniform, so a swapped pair is another way to produce a confusing-but-plausible failure. assert_same and assert_contains take expected/needle first; assert_json_contains takes key expected json with the subject last. I hit that myself while researching this — assert_json_contains '{"a":[1,2,3]}' '.a|length' "3" reports Expected '.a | length' but got '{…', which is the assertion faithfully comparing the two arguments it was handed. Nothing is wrong except that no one can tell.

Proposal

Arity checking on the assertion entry points. When too few arguments are supplied, fail with a usage error rather than a comparison:

✗ Error: Wrong arg count
assert_same expects 2 arguments (expected, actual), got 1

Design points worth settling before implementing:

  • Report as an error, not a failure. This is a defect in the test, the same class as calling an assertion that does not exist — which this framework already surfaces as Error, not Failed. Reusing that channel keeps the distinction the reader needs.
  • Only under-supply, not over-supply. Several assertions take a trailing optional argument (a label override, an nth index), so a strict upper bound would be wrong or would need per-assertion tables. Under-supply is unambiguous and covers the real failure mode.
  • Do not use $# naively where an assertion legitimately accepts an empty string as its second argument — assert_empty "$x" and assert_same "" "$x" are valid. The check is on count supplied, which $# gives correctly, so this works; it just has to be $# and never [ -z "$2" ].

Doing this for all 76 assertions at once is a large mechanical change. A reasonable first cut is the comparison family that carries the risk: assert_same, assert_equals, assert_not_same, assert_not_equals, assert_contains, assert_not_contains, assert_greater_than and friends.

Constraints

  • Bash 3.0+; $# and case only, no compatibility surface.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md. An arity check is a builtin comparison, so this is free, but it must not become a shared helper invoked through $( ).
  • Public API: no signature changes. A call that is correct today must behave identically — the only behaviour change is for calls that are already broken.
  • The error path needs its own rendering; check how bashunit::assert::fail_with and the Error classification in runner/diagnostics.sh interact before inventing a third shape.
  • CHANGELOG.md under ### Changed, and note it explicitly: a suite that was silently passing a malformed assertion will start reporting it.

Acceptance criteria

  • assert_same "only-one" reports a usage error naming the expected argument count, not but got ''
  • Reported as Error, consistent with an undefined assertion, not as Failed
  • assert_same "" "$x" and other legitimately-empty arguments still work
  • Optional trailing arguments (label override, nth) are unaffected
  • No new fork on the assertion path — fork budget tests still pass
  • Scope of which assertions are covered in this pass is stated in the PR
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(assert): a missing argument reports a failed assertion, not a usage error · Issue #983 · TypedDevs/bashunit · GitHub
Skip to content

feat(assert): a missing argument reports a failed assertion, not a usage error #983

Description

@Chemaclass

Summary

Call a two-argument assertion with one argument and it does not complain. It compares against the empty string and reports an ordinary failure:

functiontest_wrong_arg_count() {
assert_same "only-one"
}
✗ Failed: Wrong arg count
Expected 'only-one'
but got ''

That output is indistinguishable from a genuine failure where the code under test really did produce an empty string. The reader debugs their code; the bug is in the test.

Why this is the expensive kind of mistake

Empty is the single most common real value in shell — an unset variable, a command that printed nothing, a failed capture. So the false story this tells is also the most plausible one. Someone hitting it will go and check why their function returned nothing, because that is exactly what the message says happened.

It is also easy to reach: assert_same "$expected" after deleting the second argument during a refactor, or a typo'd "$actual" that expands to nothing and silently collapses the argument count.

Related and worth handling together: argument order in this catalogue is not uniform, so a swapped pair is another way to produce a confusing-but-plausible failure. assert_same and assert_contains take expected/needle first; assert_json_contains takes key expected json with the subject last. I hit that myself while researching this — assert_json_contains '{"a":[1,2,3]}' '.a|length' "3" reports Expected '.a | length' but got '{…', which is the assertion faithfully comparing the two arguments it was handed. Nothing is wrong except that no one can tell.

Proposal

Arity checking on the assertion entry points. When too few arguments are supplied, fail with a usage error rather than a comparison:

✗ Error: Wrong arg count
assert_same expects 2 arguments (expected, actual), got 1

Design points worth settling before implementing:

  • Report as an error, not a failure. This is a defect in the test, the same class as calling an assertion that does not exist — which this framework already surfaces as Error, not Failed. Reusing that channel keeps the distinction the reader needs.
  • Only under-supply, not over-supply. Several assertions take a trailing optional argument (a label override, an nth index), so a strict upper bound would be wrong or would need per-assertion tables. Under-supply is unambiguous and covers the real failure mode.
  • Do not use $# naively where an assertion legitimately accepts an empty string as its second argument — assert_empty "$x" and assert_same "" "$x" are valid. The check is on count supplied, which $# gives correctly, so this works; it just has to be $# and never [ -z "$2" ].

Doing this for all 76 assertions at once is a large mechanical change. A reasonable first cut is the comparison family that carries the risk: assert_same, assert_equals, assert_not_same, assert_not_equals, assert_contains, assert_not_contains, assert_greater_than and friends.

Constraints

  • Bash 3.0+; $# and case only, no compatibility surface.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md. An arity check is a builtin comparison, so this is free, but it must not become a shared helper invoked through $( ).
  • Public API: no signature changes. A call that is correct today must behave identically — the only behaviour change is for calls that are already broken.
  • The error path needs its own rendering; check how bashunit::assert::fail_with and the Error classification in runner/diagnostics.sh interact before inventing a third shape.
  • CHANGELOG.md under ### Changed, and note it explicitly: a suite that was silently passing a malformed assertion will start reporting it.

Acceptance criteria

  • assert_same "only-one" reports a usage error naming the expected argument count, not but got ''
  • Reported as Error, consistent with an undefined assertion, not as Failed
  • assert_same "" "$x" and other legitimately-empty arguments still work
  • Optional trailing arguments (label override, nth) are unaffected
  • No new fork on the assertion path — fork budget tests still pass
  • Scope of which assertions are covered in this pass is stated in the PR
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(assert): a missing argument reports a failed assertion, not a usage error · Issue #983 · TypedDevs/bashunit · GitHub
Skip to content

feat(assert): a missing argument reports a failed assertion, not a usage error #983

Description

@Chemaclass

Summary

Call a two-argument assertion with one argument and it does not complain. It compares against the empty string and reports an ordinary failure:

functiontest_wrong_arg_count() {
assert_same "only-one"
}
✗ Failed: Wrong arg count
Expected 'only-one'
but got ''

That output is indistinguishable from a genuine failure where the code under test really did produce an empty string. The reader debugs their code; the bug is in the test.

Why this is the expensive kind of mistake

Empty is the single most common real value in shell — an unset variable, a command that printed nothing, a failed capture. So the false story this tells is also the most plausible one. Someone hitting it will go and check why their function returned nothing, because that is exactly what the message says happened.

It is also easy to reach: assert_same "$expected" after deleting the second argument during a refactor, or a typo'd "$actual" that expands to nothing and silently collapses the argument count.

Related and worth handling together: argument order in this catalogue is not uniform, so a swapped pair is another way to produce a confusing-but-plausible failure. assert_same and assert_contains take expected/needle first; assert_json_contains takes key expected json with the subject last. I hit that myself while researching this — assert_json_contains '{"a":[1,2,3]}' '.a|length' "3" reports Expected '.a | length' but got '{…', which is the assertion faithfully comparing the two arguments it was handed. Nothing is wrong except that no one can tell.

Proposal

Arity checking on the assertion entry points. When too few arguments are supplied, fail with a usage error rather than a comparison:

✗ Error: Wrong arg count
assert_same expects 2 arguments (expected, actual), got 1

Design points worth settling before implementing:

  • Report as an error, not a failure. This is a defect in the test, the same class as calling an assertion that does not exist — which this framework already surfaces as Error, not Failed. Reusing that channel keeps the distinction the reader needs.
  • Only under-supply, not over-supply. Several assertions take a trailing optional argument (a label override, an nth index), so a strict upper bound would be wrong or would need per-assertion tables. Under-supply is unambiguous and covers the real failure mode.
  • Do not use $# naively where an assertion legitimately accepts an empty string as its second argument — assert_empty "$x" and assert_same "" "$x" are valid. The check is on count supplied, which $# gives correctly, so this works; it just has to be $# and never [ -z "$2" ].

Doing this for all 76 assertions at once is a large mechanical change. A reasonable first cut is the comparison family that carries the risk: assert_same, assert_equals, assert_not_same, assert_not_equals, assert_contains, assert_not_contains, assert_greater_than and friends.

Constraints

  • Bash 3.0+; $# and case only, no compatibility surface.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md. An arity check is a builtin comparison, so this is free, but it must not become a shared helper invoked through $( ).
  • Public API: no signature changes. A call that is correct today must behave identically — the only behaviour change is for calls that are already broken.
  • The error path needs its own rendering; check how bashunit::assert::fail_with and the Error classification in runner/diagnostics.sh interact before inventing a third shape.
  • CHANGELOG.md under ### Changed, and note it explicitly: a suite that was silently passing a malformed assertion will start reporting it.

Acceptance criteria

  • assert_same "only-one" reports a usage error naming the expected argument count, not but got ''
  • Reported as Error, consistent with an undefined assertion, not as Failed
  • assert_same "" "$x" and other legitimately-empty arguments still work
  • Optional trailing arguments (label override, nth) are unaffected
  • No new fork on the assertion path — fork budget tests still pass
  • Scope of which assertions are covered in this pass is stated in the PR
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(assert): a missing argument reports a failed assertion, not a usage error · Issue #983 · TypedDevs/bashunit · GitHub
Skip to content

feat(assert): a missing argument reports a failed assertion, not a usage error #983

Description

@Chemaclass

Summary

Call a two-argument assertion with one argument and it does not complain. It compares against the empty string and reports an ordinary failure:

functiontest_wrong_arg_count() {
assert_same "only-one"
}
✗ Failed: Wrong arg count
Expected 'only-one'
but got ''

That output is indistinguishable from a genuine failure where the code under test really did produce an empty string. The reader debugs their code; the bug is in the test.

Why this is the expensive kind of mistake

Empty is the single most common real value in shell — an unset variable, a command that printed nothing, a failed capture. So the false story this tells is also the most plausible one. Someone hitting it will go and check why their function returned nothing, because that is exactly what the message says happened.

It is also easy to reach: assert_same "$expected" after deleting the second argument during a refactor, or a typo'd "$actual" that expands to nothing and silently collapses the argument count.

Related and worth handling together: argument order in this catalogue is not uniform, so a swapped pair is another way to produce a confusing-but-plausible failure. assert_same and assert_contains take expected/needle first; assert_json_contains takes key expected json with the subject last. I hit that myself while researching this — assert_json_contains '{"a":[1,2,3]}' '.a|length' "3" reports Expected '.a | length' but got '{…', which is the assertion faithfully comparing the two arguments it was handed. Nothing is wrong except that no one can tell.

Proposal

Arity checking on the assertion entry points. When too few arguments are supplied, fail with a usage error rather than a comparison:

✗ Error: Wrong arg count
assert_same expects 2 arguments (expected, actual), got 1

Design points worth settling before implementing:

  • Report as an error, not a failure. This is a defect in the test, the same class as calling an assertion that does not exist — which this framework already surfaces as Error, not Failed. Reusing that channel keeps the distinction the reader needs.
  • Only under-supply, not over-supply. Several assertions take a trailing optional argument (a label override, an nth index), so a strict upper bound would be wrong or would need per-assertion tables. Under-supply is unambiguous and covers the real failure mode.
  • Do not use $# naively where an assertion legitimately accepts an empty string as its second argument — assert_empty "$x" and assert_same "" "$x" are valid. The check is on count supplied, which $# gives correctly, so this works; it just has to be $# and never [ -z "$2" ].

Doing this for all 76 assertions at once is a large mechanical change. A reasonable first cut is the comparison family that carries the risk: assert_same, assert_equals, assert_not_same, assert_not_equals, assert_contains, assert_not_contains, assert_greater_than and friends.

Constraints

  • Bash 3.0+; $# and case only, no compatibility surface.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md. An arity check is a builtin comparison, so this is free, but it must not become a shared helper invoked through $( ).
  • Public API: no signature changes. A call that is correct today must behave identically — the only behaviour change is for calls that are already broken.
  • The error path needs its own rendering; check how bashunit::assert::fail_with and the Error classification in runner/diagnostics.sh interact before inventing a third shape.
  • CHANGELOG.md under ### Changed, and note it explicitly: a suite that was silently passing a malformed assertion will start reporting it.

Acceptance criteria

  • assert_same "only-one" reports a usage error naming the expected argument count, not but got ''
  • Reported as Error, consistent with an undefined assertion, not as Failed
  • assert_same "" "$x" and other legitimately-empty arguments still work
  • Optional trailing arguments (label override, nth) are unaffected
  • No new fork on the assertion path — fork budget tests still pass
  • Scope of which assertions are covered in this pass is stated in the PR
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(assert): a missing argument reports a failed assertion, not a usage error · Issue #983 · TypedDevs/bashunit · GitHub
Skip to content

feat(assert): a missing argument reports a failed assertion, not a usage error #983

Description

@Chemaclass

Summary

Call a two-argument assertion with one argument and it does not complain. It compares against the empty string and reports an ordinary failure:

functiontest_wrong_arg_count() {
assert_same "only-one"
}
✗ Failed: Wrong arg count
Expected 'only-one'
but got ''

That output is indistinguishable from a genuine failure where the code under test really did produce an empty string. The reader debugs their code; the bug is in the test.

Why this is the expensive kind of mistake

Empty is the single most common real value in shell — an unset variable, a command that printed nothing, a failed capture. So the false story this tells is also the most plausible one. Someone hitting it will go and check why their function returned nothing, because that is exactly what the message says happened.

It is also easy to reach: assert_same "$expected" after deleting the second argument during a refactor, or a typo'd "$actual" that expands to nothing and silently collapses the argument count.

Related and worth handling together: argument order in this catalogue is not uniform, so a swapped pair is another way to produce a confusing-but-plausible failure. assert_same and assert_contains take expected/needle first; assert_json_contains takes key expected json with the subject last. I hit that myself while researching this — assert_json_contains '{"a":[1,2,3]}' '.a|length' "3" reports Expected '.a | length' but got '{…', which is the assertion faithfully comparing the two arguments it was handed. Nothing is wrong except that no one can tell.

Proposal

Arity checking on the assertion entry points. When too few arguments are supplied, fail with a usage error rather than a comparison:

✗ Error: Wrong arg count
assert_same expects 2 arguments (expected, actual), got 1

Design points worth settling before implementing:

  • Report as an error, not a failure. This is a defect in the test, the same class as calling an assertion that does not exist — which this framework already surfaces as Error, not Failed. Reusing that channel keeps the distinction the reader needs.
  • Only under-supply, not over-supply. Several assertions take a trailing optional argument (a label override, an nth index), so a strict upper bound would be wrong or would need per-assertion tables. Under-supply is unambiguous and covers the real failure mode.
  • Do not use $# naively where an assertion legitimately accepts an empty string as its second argument — assert_empty "$x" and assert_same "" "$x" are valid. The check is on count supplied, which $# gives correctly, so this works; it just has to be $# and never [ -z "$2" ].

Doing this for all 76 assertions at once is a large mechanical change. A reasonable first cut is the comparison family that carries the risk: assert_same, assert_equals, assert_not_same, assert_not_equals, assert_contains, assert_not_contains, assert_greater_than and friends.

Constraints

  • Bash 3.0+; $# and case only, no compatibility surface.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md. An arity check is a builtin comparison, so this is free, but it must not become a shared helper invoked through $( ).
  • Public API: no signature changes. A call that is correct today must behave identically — the only behaviour change is for calls that are already broken.
  • The error path needs its own rendering; check how bashunit::assert::fail_with and the Error classification in runner/diagnostics.sh interact before inventing a third shape.
  • CHANGELOG.md under ### Changed, and note it explicitly: a suite that was silently passing a malformed assertion will start reporting it.

Acceptance criteria

  • assert_same "only-one" reports a usage error naming the expected argument count, not but got ''
  • Reported as Error, consistent with an undefined assertion, not as Failed
  • assert_same "" "$x" and other legitimately-empty arguments still work
  • Optional trailing arguments (label override, nth) are unaffected
  • No new fork on the assertion path — fork budget tests still pass
  • Scope of which assertions are covered in this pass is stated in the PR
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(assert): a missing argument reports a failed assertion, not a usage error · Issue #983 · TypedDevs/bashunit · GitHub
Skip to content

feat(assert): a missing argument reports a failed assertion, not a usage error #983

Description

@Chemaclass

Summary

Call a two-argument assertion with one argument and it does not complain. It compares against the empty string and reports an ordinary failure:

functiontest_wrong_arg_count() {
assert_same "only-one"
}
✗ Failed: Wrong arg count
Expected 'only-one'
but got ''

That output is indistinguishable from a genuine failure where the code under test really did produce an empty string. The reader debugs their code; the bug is in the test.

Why this is the expensive kind of mistake

Empty is the single most common real value in shell — an unset variable, a command that printed nothing, a failed capture. So the false story this tells is also the most plausible one. Someone hitting it will go and check why their function returned nothing, because that is exactly what the message says happened.

It is also easy to reach: assert_same "$expected" after deleting the second argument during a refactor, or a typo'd "$actual" that expands to nothing and silently collapses the argument count.

Related and worth handling together: argument order in this catalogue is not uniform, so a swapped pair is another way to produce a confusing-but-plausible failure. assert_same and assert_contains take expected/needle first; assert_json_contains takes key expected json with the subject last. I hit that myself while researching this — assert_json_contains '{"a":[1,2,3]}' '.a|length' "3" reports Expected '.a | length' but got '{…', which is the assertion faithfully comparing the two arguments it was handed. Nothing is wrong except that no one can tell.

Proposal

Arity checking on the assertion entry points. When too few arguments are supplied, fail with a usage error rather than a comparison:

✗ Error: Wrong arg count
assert_same expects 2 arguments (expected, actual), got 1

Design points worth settling before implementing:

  • Report as an error, not a failure. This is a defect in the test, the same class as calling an assertion that does not exist — which this framework already surfaces as Error, not Failed. Reusing that channel keeps the distinction the reader needs.
  • Only under-supply, not over-supply. Several assertions take a trailing optional argument (a label override, an nth index), so a strict upper bound would be wrong or would need per-assertion tables. Under-supply is unambiguous and covers the real failure mode.
  • Do not use $# naively where an assertion legitimately accepts an empty string as its second argument — assert_empty "$x" and assert_same "" "$x" are valid. The check is on count supplied, which $# gives correctly, so this works; it just has to be $# and never [ -z "$2" ].

Doing this for all 76 assertions at once is a large mechanical change. A reasonable first cut is the comparison family that carries the risk: assert_same, assert_equals, assert_not_same, assert_not_equals, assert_contains, assert_not_contains, assert_greater_than and friends.

Constraints

  • Bash 3.0+; $# and case only, no compatibility surface.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md. An arity check is a builtin comparison, so this is free, but it must not become a shared helper invoked through $( ).
  • Public API: no signature changes. A call that is correct today must behave identically — the only behaviour change is for calls that are already broken.
  • The error path needs its own rendering; check how bashunit::assert::fail_with and the Error classification in runner/diagnostics.sh interact before inventing a third shape.
  • CHANGELOG.md under ### Changed, and note it explicitly: a suite that was silently passing a malformed assertion will start reporting it.

Acceptance criteria

  • assert_same "only-one" reports a usage error naming the expected argument count, not but got ''
  • Reported as Error, consistent with an undefined assertion, not as Failed
  • assert_same "" "$x" and other legitimately-empty arguments still work
  • Optional trailing arguments (label override, nth) are unaffected
  • No new fork on the assertion path — fork budget tests still pass
  • Scope of which assertions are covered in this pass is stated in the PR
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions