Skip to content

docs(doubles): the spy assertion family disagrees with itself on argument order #984

Description

@Chemaclass

Summary

Within one family, two assertions put the spy in different positions:

assert_have_been_called_times "expected_count""command"# count first
assert_have_been_called_with "command""expected_args"# command first

Both are documented correctly. The problem is that they are documented differently, and nothing at the call site tells them apart — assert_have_been_called_times 2 my_cmd and assert_have_been_called_with my_cmd "arg" look like the same shape to a reader skimming a test file.

Why it bites rather than just reads oddly

A swapped pair does not error. It compares two strings that happen to be in the wrong slots and reports a plain assertion failure, so the output is a plausible-looking mismatch rather than a hint that the call is malformed:

Expected 'my_cmd' but got '2'

Someone reading that will check what their spy recorded before they check the argument order.

The wider catalogue has the same split, which is why this is worth writing down rather than fixing by feel:

AssertionSubject position
assert_same "expected" "actual"expected first
assert_contains "needle" "haystack"needle first
assert_array_contains "needle" "haystack"needle first
assert_have_been_called_times "count" "command"count first
assert_have_been_called_with "command" "args"command first
assert_json_contains "key" "expected" "json"subject last

The majority convention is expected-value first, subject second. assert_have_been_called_with and assert_json_contains are the outliers.

What I am not proposing

Renaming or reordering these. They are public API, they are correct as documented, and silently swapping argument meaning would break every existing suite in the worst possible way — tests that keep passing while asserting something else. Any reordering would need a deprecation cycle, and I do not think the churn is worth it.

Proposal

Make the inconsistency visible where it is encountered, rather than only in the reference table.

  1. Say it in the docs.docs/doubles.md and the spy section of docs/assertions.md should state plainly that _times takes the count first while _with takes the spy first, instead of leaving the reader to infer it from two adjacent signatures. One sentence, positioned where someone is choosing between them.

  2. Name the arguments in the failure message. Today a swapped call produces a bare value mismatch. If the failure said which side was read as the spy and which as the expectation, the mistake is self-evident:

    Expected spy 'my_cmd' to have been called 2 times, got 0
    

    This is the higher-value half — it helps at the moment of confusion, and it costs nothing at runtime.

  3. Consider a guard for the obviously-swapped case.assert_have_been_called_times with a non-numeric first argument is always a mistake — a call count is a number. Failing that with a usage error would catch the exact swap this issue is about. Overlaps with the arity work in the missing-argument issue; whoever picks up either should look at both.

Constraints

  • Public API: signatures do not change. Only messages and docs.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md.
  • Failure output is compared verbatim in this suite, so message changes are mechanically verifiable; grep tests/ for the current strings before editing.
  • Bash 3.0+.

Acceptance criteria

  • Docs state the _times / _with order difference explicitly, where the reader chooses between them
  • Spy failure messages name what was read as the spy and what as the expectation
  • A decision is recorded on whether a non-numeric count is rejected as a usage error
  • No signature changes; existing suites behave identically
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

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" + '
docs(doubles): the spy assertion family disagrees with itself on argument order · Issue #984 · TypedDevs/bashunit · GitHub
Skip to content

docs(doubles): the spy assertion family disagrees with itself on argument order #984

Description

@Chemaclass

Summary

Within one family, two assertions put the spy in different positions:

assert_have_been_called_times "expected_count""command"# count first
assert_have_been_called_with "command""expected_args"# command first

Both are documented correctly. The problem is that they are documented differently, and nothing at the call site tells them apart — assert_have_been_called_times 2 my_cmd and assert_have_been_called_with my_cmd "arg" look like the same shape to a reader skimming a test file.

Why it bites rather than just reads oddly

A swapped pair does not error. It compares two strings that happen to be in the wrong slots and reports a plain assertion failure, so the output is a plausible-looking mismatch rather than a hint that the call is malformed:

Expected 'my_cmd' but got '2'

Someone reading that will check what their spy recorded before they check the argument order.

The wider catalogue has the same split, which is why this is worth writing down rather than fixing by feel:

AssertionSubject position
assert_same "expected" "actual"expected first
assert_contains "needle" "haystack"needle first
assert_array_contains "needle" "haystack"needle first
assert_have_been_called_times "count" "command"count first
assert_have_been_called_with "command" "args"command first
assert_json_contains "key" "expected" "json"subject last

The majority convention is expected-value first, subject second. assert_have_been_called_with and assert_json_contains are the outliers.

What I am not proposing

Renaming or reordering these. They are public API, they are correct as documented, and silently swapping argument meaning would break every existing suite in the worst possible way — tests that keep passing while asserting something else. Any reordering would need a deprecation cycle, and I do not think the churn is worth it.

Proposal

Make the inconsistency visible where it is encountered, rather than only in the reference table.

  1. Say it in the docs.docs/doubles.md and the spy section of docs/assertions.md should state plainly that _times takes the count first while _with takes the spy first, instead of leaving the reader to infer it from two adjacent signatures. One sentence, positioned where someone is choosing between them.

  2. Name the arguments in the failure message. Today a swapped call produces a bare value mismatch. If the failure said which side was read as the spy and which as the expectation, the mistake is self-evident:

    Expected spy 'my_cmd' to have been called 2 times, got 0
    

    This is the higher-value half — it helps at the moment of confusion, and it costs nothing at runtime.

  3. Consider a guard for the obviously-swapped case.assert_have_been_called_times with a non-numeric first argument is always a mistake — a call count is a number. Failing that with a usage error would catch the exact swap this issue is about. Overlaps with the arity work in the missing-argument issue; whoever picks up either should look at both.

Constraints

  • Public API: signatures do not change. Only messages and docs.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md.
  • Failure output is compared verbatim in this suite, so message changes are mechanically verifiable; grep tests/ for the current strings before editing.
  • Bash 3.0+.

Acceptance criteria

  • Docs state the _times / _with order difference explicitly, where the reader chooses between them
  • Spy failure messages name what was read as the spy and what as the expectation
  • A decision is recorded on whether a non-numeric count is rejected as a usage error
  • No signature changes; existing suites behave identically
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

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('^' + ".*" + ' docs(doubles): the spy assertion family disagrees with itself on argument order · Issue #984 · TypedDevs/bashunit · GitHub
Skip to content

docs(doubles): the spy assertion family disagrees with itself on argument order #984

Description

@Chemaclass

Summary

Within one family, two assertions put the spy in different positions:

assert_have_been_called_times "expected_count""command"# count first
assert_have_been_called_with "command""expected_args"# command first

Both are documented correctly. The problem is that they are documented differently, and nothing at the call site tells them apart — assert_have_been_called_times 2 my_cmd and assert_have_been_called_with my_cmd "arg" look like the same shape to a reader skimming a test file.

Why it bites rather than just reads oddly

A swapped pair does not error. It compares two strings that happen to be in the wrong slots and reports a plain assertion failure, so the output is a plausible-looking mismatch rather than a hint that the call is malformed:

Expected 'my_cmd' but got '2'

Someone reading that will check what their spy recorded before they check the argument order.

The wider catalogue has the same split, which is why this is worth writing down rather than fixing by feel:

AssertionSubject position
assert_same "expected" "actual"expected first
assert_contains "needle" "haystack"needle first
assert_array_contains "needle" "haystack"needle first
assert_have_been_called_times "count" "command"count first
assert_have_been_called_with "command" "args"command first
assert_json_contains "key" "expected" "json"subject last

The majority convention is expected-value first, subject second. assert_have_been_called_with and assert_json_contains are the outliers.

What I am not proposing

Renaming or reordering these. They are public API, they are correct as documented, and silently swapping argument meaning would break every existing suite in the worst possible way — tests that keep passing while asserting something else. Any reordering would need a deprecation cycle, and I do not think the churn is worth it.

Proposal

Make the inconsistency visible where it is encountered, rather than only in the reference table.

  1. Say it in the docs.docs/doubles.md and the spy section of docs/assertions.md should state plainly that _times takes the count first while _with takes the spy first, instead of leaving the reader to infer it from two adjacent signatures. One sentence, positioned where someone is choosing between them.

  2. Name the arguments in the failure message. Today a swapped call produces a bare value mismatch. If the failure said which side was read as the spy and which as the expectation, the mistake is self-evident:

    Expected spy 'my_cmd' to have been called 2 times, got 0
    

    This is the higher-value half — it helps at the moment of confusion, and it costs nothing at runtime.

  3. Consider a guard for the obviously-swapped case.assert_have_been_called_times with a non-numeric first argument is always a mistake — a call count is a number. Failing that with a usage error would catch the exact swap this issue is about. Overlaps with the arity work in the missing-argument issue; whoever picks up either should look at both.

Constraints

  • Public API: signatures do not change. Only messages and docs.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md.
  • Failure output is compared verbatim in this suite, so message changes are mechanically verifiable; grep tests/ for the current strings before editing.
  • Bash 3.0+.

Acceptance criteria

  • Docs state the _times / _with order difference explicitly, where the reader chooses between them
  • Spy failure messages name what was read as the spy and what as the expectation
  • A decision is recorded on whether a non-numeric count is rejected as a usage error
  • No signature changes; existing suites behave identically
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

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('^' + ".*" + ' docs(doubles): the spy assertion family disagrees with itself on argument order · Issue #984 · TypedDevs/bashunit · GitHub
Skip to content

docs(doubles): the spy assertion family disagrees with itself on argument order #984

Description

@Chemaclass

Summary

Within one family, two assertions put the spy in different positions:

assert_have_been_called_times "expected_count""command"# count first
assert_have_been_called_with "command""expected_args"# command first

Both are documented correctly. The problem is that they are documented differently, and nothing at the call site tells them apart — assert_have_been_called_times 2 my_cmd and assert_have_been_called_with my_cmd "arg" look like the same shape to a reader skimming a test file.

Why it bites rather than just reads oddly

A swapped pair does not error. It compares two strings that happen to be in the wrong slots and reports a plain assertion failure, so the output is a plausible-looking mismatch rather than a hint that the call is malformed:

Expected 'my_cmd' but got '2'

Someone reading that will check what their spy recorded before they check the argument order.

The wider catalogue has the same split, which is why this is worth writing down rather than fixing by feel:

AssertionSubject position
assert_same "expected" "actual"expected first
assert_contains "needle" "haystack"needle first
assert_array_contains "needle" "haystack"needle first
assert_have_been_called_times "count" "command"count first
assert_have_been_called_with "command" "args"command first
assert_json_contains "key" "expected" "json"subject last

The majority convention is expected-value first, subject second. assert_have_been_called_with and assert_json_contains are the outliers.

What I am not proposing

Renaming or reordering these. They are public API, they are correct as documented, and silently swapping argument meaning would break every existing suite in the worst possible way — tests that keep passing while asserting something else. Any reordering would need a deprecation cycle, and I do not think the churn is worth it.

Proposal

Make the inconsistency visible where it is encountered, rather than only in the reference table.

  1. Say it in the docs.docs/doubles.md and the spy section of docs/assertions.md should state plainly that _times takes the count first while _with takes the spy first, instead of leaving the reader to infer it from two adjacent signatures. One sentence, positioned where someone is choosing between them.

  2. Name the arguments in the failure message. Today a swapped call produces a bare value mismatch. If the failure said which side was read as the spy and which as the expectation, the mistake is self-evident:

    Expected spy 'my_cmd' to have been called 2 times, got 0
    

    This is the higher-value half — it helps at the moment of confusion, and it costs nothing at runtime.

  3. Consider a guard for the obviously-swapped case.assert_have_been_called_times with a non-numeric first argument is always a mistake — a call count is a number. Failing that with a usage error would catch the exact swap this issue is about. Overlaps with the arity work in the missing-argument issue; whoever picks up either should look at both.

Constraints

  • Public API: signatures do not change. Only messages and docs.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md.
  • Failure output is compared verbatim in this suite, so message changes are mechanically verifiable; grep tests/ for the current strings before editing.
  • Bash 3.0+.

Acceptance criteria

  • Docs state the _times / _with order difference explicitly, where the reader chooses between them
  • Spy failure messages name what was read as the spy and what as the expectation
  • A decision is recorded on whether a non-numeric count is rejected as a usage error
  • No signature changes; existing suites behave identically
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

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" + ' docs(doubles): the spy assertion family disagrees with itself on argument order · Issue #984 · TypedDevs/bashunit · GitHub
Skip to content

docs(doubles): the spy assertion family disagrees with itself on argument order #984

Description

@Chemaclass

Summary

Within one family, two assertions put the spy in different positions:

assert_have_been_called_times "expected_count""command"# count first
assert_have_been_called_with "command""expected_args"# command first

Both are documented correctly. The problem is that they are documented differently, and nothing at the call site tells them apart — assert_have_been_called_times 2 my_cmd and assert_have_been_called_with my_cmd "arg" look like the same shape to a reader skimming a test file.

Why it bites rather than just reads oddly

A swapped pair does not error. It compares two strings that happen to be in the wrong slots and reports a plain assertion failure, so the output is a plausible-looking mismatch rather than a hint that the call is malformed:

Expected 'my_cmd' but got '2'

Someone reading that will check what their spy recorded before they check the argument order.

The wider catalogue has the same split, which is why this is worth writing down rather than fixing by feel:

AssertionSubject position
assert_same "expected" "actual"expected first
assert_contains "needle" "haystack"needle first
assert_array_contains "needle" "haystack"needle first
assert_have_been_called_times "count" "command"count first
assert_have_been_called_with "command" "args"command first
assert_json_contains "key" "expected" "json"subject last

The majority convention is expected-value first, subject second. assert_have_been_called_with and assert_json_contains are the outliers.

What I am not proposing

Renaming or reordering these. They are public API, they are correct as documented, and silently swapping argument meaning would break every existing suite in the worst possible way — tests that keep passing while asserting something else. Any reordering would need a deprecation cycle, and I do not think the churn is worth it.

Proposal

Make the inconsistency visible where it is encountered, rather than only in the reference table.

  1. Say it in the docs.docs/doubles.md and the spy section of docs/assertions.md should state plainly that _times takes the count first while _with takes the spy first, instead of leaving the reader to infer it from two adjacent signatures. One sentence, positioned where someone is choosing between them.

  2. Name the arguments in the failure message. Today a swapped call produces a bare value mismatch. If the failure said which side was read as the spy and which as the expectation, the mistake is self-evident:

    Expected spy 'my_cmd' to have been called 2 times, got 0
    

    This is the higher-value half — it helps at the moment of confusion, and it costs nothing at runtime.

  3. Consider a guard for the obviously-swapped case.assert_have_been_called_times with a non-numeric first argument is always a mistake — a call count is a number. Failing that with a usage error would catch the exact swap this issue is about. Overlaps with the arity work in the missing-argument issue; whoever picks up either should look at both.

Constraints

  • Public API: signatures do not change. Only messages and docs.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md.
  • Failure output is compared verbatim in this suite, so message changes are mechanically verifiable; grep tests/ for the current strings before editing.
  • Bash 3.0+.

Acceptance criteria

  • Docs state the _times / _with order difference explicitly, where the reader chooses between them
  • Spy failure messages name what was read as the spy and what as the expectation
  • A decision is recorded on whether a non-numeric count is rejected as a usage error
  • No signature changes; existing suites behave identically
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

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('^' + ".*" + ' docs(doubles): the spy assertion family disagrees with itself on argument order · Issue #984 · TypedDevs/bashunit · GitHub
Skip to content

docs(doubles): the spy assertion family disagrees with itself on argument order #984

Description

@Chemaclass

Summary

Within one family, two assertions put the spy in different positions:

assert_have_been_called_times "expected_count""command"# count first
assert_have_been_called_with "command""expected_args"# command first

Both are documented correctly. The problem is that they are documented differently, and nothing at the call site tells them apart — assert_have_been_called_times 2 my_cmd and assert_have_been_called_with my_cmd "arg" look like the same shape to a reader skimming a test file.

Why it bites rather than just reads oddly

A swapped pair does not error. It compares two strings that happen to be in the wrong slots and reports a plain assertion failure, so the output is a plausible-looking mismatch rather than a hint that the call is malformed:

Expected 'my_cmd' but got '2'

Someone reading that will check what their spy recorded before they check the argument order.

The wider catalogue has the same split, which is why this is worth writing down rather than fixing by feel:

AssertionSubject position
assert_same "expected" "actual"expected first
assert_contains "needle" "haystack"needle first
assert_array_contains "needle" "haystack"needle first
assert_have_been_called_times "count" "command"count first
assert_have_been_called_with "command" "args"command first
assert_json_contains "key" "expected" "json"subject last

The majority convention is expected-value first, subject second. assert_have_been_called_with and assert_json_contains are the outliers.

What I am not proposing

Renaming or reordering these. They are public API, they are correct as documented, and silently swapping argument meaning would break every existing suite in the worst possible way — tests that keep passing while asserting something else. Any reordering would need a deprecation cycle, and I do not think the churn is worth it.

Proposal

Make the inconsistency visible where it is encountered, rather than only in the reference table.

  1. Say it in the docs.docs/doubles.md and the spy section of docs/assertions.md should state plainly that _times takes the count first while _with takes the spy first, instead of leaving the reader to infer it from two adjacent signatures. One sentence, positioned where someone is choosing between them.

  2. Name the arguments in the failure message. Today a swapped call produces a bare value mismatch. If the failure said which side was read as the spy and which as the expectation, the mistake is self-evident:

    Expected spy 'my_cmd' to have been called 2 times, got 0
    

    This is the higher-value half — it helps at the moment of confusion, and it costs nothing at runtime.

  3. Consider a guard for the obviously-swapped case.assert_have_been_called_times with a non-numeric first argument is always a mistake — a call count is a number. Failing that with a usage error would catch the exact swap this issue is about. Overlaps with the arity work in the missing-argument issue; whoever picks up either should look at both.

Constraints

  • Public API: signatures do not change. Only messages and docs.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md.
  • Failure output is compared verbatim in this suite, so message changes are mechanically verifiable; grep tests/ for the current strings before editing.
  • Bash 3.0+.

Acceptance criteria

  • Docs state the _times / _with order difference explicitly, where the reader chooses between them
  • Spy failure messages name what was read as the spy and what as the expectation
  • A decision is recorded on whether a non-numeric count is rejected as a usage error
  • No signature changes; existing suites behave identically
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

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('^' + ".*" + ' docs(doubles): the spy assertion family disagrees with itself on argument order · Issue #984 · TypedDevs/bashunit · GitHub
Skip to content

docs(doubles): the spy assertion family disagrees with itself on argument order #984

Description

@Chemaclass

Summary

Within one family, two assertions put the spy in different positions:

assert_have_been_called_times "expected_count""command"# count first
assert_have_been_called_with "command""expected_args"# command first

Both are documented correctly. The problem is that they are documented differently, and nothing at the call site tells them apart — assert_have_been_called_times 2 my_cmd and assert_have_been_called_with my_cmd "arg" look like the same shape to a reader skimming a test file.

Why it bites rather than just reads oddly

A swapped pair does not error. It compares two strings that happen to be in the wrong slots and reports a plain assertion failure, so the output is a plausible-looking mismatch rather than a hint that the call is malformed:

Expected 'my_cmd' but got '2'

Someone reading that will check what their spy recorded before they check the argument order.

The wider catalogue has the same split, which is why this is worth writing down rather than fixing by feel:

AssertionSubject position
assert_same "expected" "actual"expected first
assert_contains "needle" "haystack"needle first
assert_array_contains "needle" "haystack"needle first
assert_have_been_called_times "count" "command"count first
assert_have_been_called_with "command" "args"command first
assert_json_contains "key" "expected" "json"subject last

The majority convention is expected-value first, subject second. assert_have_been_called_with and assert_json_contains are the outliers.

What I am not proposing

Renaming or reordering these. They are public API, they are correct as documented, and silently swapping argument meaning would break every existing suite in the worst possible way — tests that keep passing while asserting something else. Any reordering would need a deprecation cycle, and I do not think the churn is worth it.

Proposal

Make the inconsistency visible where it is encountered, rather than only in the reference table.

  1. Say it in the docs.docs/doubles.md and the spy section of docs/assertions.md should state plainly that _times takes the count first while _with takes the spy first, instead of leaving the reader to infer it from two adjacent signatures. One sentence, positioned where someone is choosing between them.

  2. Name the arguments in the failure message. Today a swapped call produces a bare value mismatch. If the failure said which side was read as the spy and which as the expectation, the mistake is self-evident:

    Expected spy 'my_cmd' to have been called 2 times, got 0
    

    This is the higher-value half — it helps at the moment of confusion, and it costs nothing at runtime.

  3. Consider a guard for the obviously-swapped case.assert_have_been_called_times with a non-numeric first argument is always a mistake — a call count is a number. Failing that with a usage error would catch the exact swap this issue is about. Overlaps with the arity work in the missing-argument issue; whoever picks up either should look at both.

Constraints

  • Public API: signatures do not change. Only messages and docs.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md.
  • Failure output is compared verbatim in this suite, so message changes are mechanically verifiable; grep tests/ for the current strings before editing.
  • Bash 3.0+.

Acceptance criteria

  • Docs state the _times / _with order difference explicitly, where the reader chooses between them
  • Spy failure messages name what was read as the spy and what as the expectation
  • A decision is recorded on whether a non-numeric count is rejected as a usage error
  • No signature changes; existing suites behave identically
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

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); } })(); })(); docs(doubles): the spy assertion family disagrees with itself on argument order · Issue #984 · TypedDevs/bashunit · GitHub
Skip to content

docs(doubles): the spy assertion family disagrees with itself on argument order #984

Description

@Chemaclass

Summary

Within one family, two assertions put the spy in different positions:

assert_have_been_called_times "expected_count""command"# count first
assert_have_been_called_with "command""expected_args"# command first

Both are documented correctly. The problem is that they are documented differently, and nothing at the call site tells them apart — assert_have_been_called_times 2 my_cmd and assert_have_been_called_with my_cmd "arg" look like the same shape to a reader skimming a test file.

Why it bites rather than just reads oddly

A swapped pair does not error. It compares two strings that happen to be in the wrong slots and reports a plain assertion failure, so the output is a plausible-looking mismatch rather than a hint that the call is malformed:

Expected 'my_cmd' but got '2'

Someone reading that will check what their spy recorded before they check the argument order.

The wider catalogue has the same split, which is why this is worth writing down rather than fixing by feel:

AssertionSubject position
assert_same "expected" "actual"expected first
assert_contains "needle" "haystack"needle first
assert_array_contains "needle" "haystack"needle first
assert_have_been_called_times "count" "command"count first
assert_have_been_called_with "command" "args"command first
assert_json_contains "key" "expected" "json"subject last

The majority convention is expected-value first, subject second. assert_have_been_called_with and assert_json_contains are the outliers.

What I am not proposing

Renaming or reordering these. They are public API, they are correct as documented, and silently swapping argument meaning would break every existing suite in the worst possible way — tests that keep passing while asserting something else. Any reordering would need a deprecation cycle, and I do not think the churn is worth it.

Proposal

Make the inconsistency visible where it is encountered, rather than only in the reference table.

  1. Say it in the docs.docs/doubles.md and the spy section of docs/assertions.md should state plainly that _times takes the count first while _with takes the spy first, instead of leaving the reader to infer it from two adjacent signatures. One sentence, positioned where someone is choosing between them.

  2. Name the arguments in the failure message. Today a swapped call produces a bare value mismatch. If the failure said which side was read as the spy and which as the expectation, the mistake is self-evident:

    Expected spy 'my_cmd' to have been called 2 times, got 0
    

    This is the higher-value half — it helps at the moment of confusion, and it costs nothing at runtime.

  3. Consider a guard for the obviously-swapped case.assert_have_been_called_times with a non-numeric first argument is always a mistake — a call count is a number. Failing that with a usage error would catch the exact swap this issue is about. Overlaps with the arity work in the missing-argument issue; whoever picks up either should look at both.

Constraints

  • Public API: signatures do not change. Only messages and docs.
  • Per-assertion path must stay fork-free — see .claude/rules/perf-fork-budget.md.
  • Failure output is compared verbatim in this suite, so message changes are mechanically verifiable; grep tests/ for the current strings before editing.
  • Bash 3.0+.

Acceptance criteria

  • Docs state the _times / _with order difference explicitly, where the reader chooses between them
  • Spy failure messages name what was read as the spy and what as the expectation
  • A decision is recorded on whether a non-numeric count is rejected as a usage error
  • No signature changes; existing suites behave identically
  • make sa · make lint · ./bashunit --parallel --simple --strict tests/ · bash build.sh bin -v

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions