onetaskgraph-github-projects 0.14.14

A onetaskgraph source over GitHub Projects.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
//! What every document this source sends costs GitHub's hourly rate-limit allowance.
//!
//! No network, no credential and no schema: the price is arithmetic over the document's
//! own text, so this runs in the ordinary check on a pull request from a fork.
//!
//! The number is `cost`, the rate-limit **points** one call spends against an allowance one
//! credential shares across everything it does in an hour. It is not `nodeCount`, which is
//! what `tests/node_count.rs` holds every document below GitHub's per-query limit — two
//! numbers against two limits, and a document well under the node limit says nothing about
//! its price.
//!
//! **There is no ceiling to sweep against, so the pin is the check.** GitHub publishes no
//! per-call point limit a document can be over: points are an allowance that runs out over
//! an hour rather than a bound a query is refused for crossing. So what is asserted here is
//! that every document has a recorded price and that every recorded price is what that
//! document computes — which is what makes a price that quadruples a number moving rather
//! than a test still passing.
//!
//! **GitHub is the authority on its own pricing and this workspace is not.** The figures
//! below are what stop a regression merging, because they need no credential; the
//! credentialed lane reconciles each of them against GitHub's own `cost`, from the
//! `rateLimit(dryRun: true)` probe `tests/journey/mod.rs` already sends per read document.

use onetaskgraph_github_projects::{
    COPY_DETAIL_BATCH, DETAIL_BATCH, MEMBER_PAGE_SIZE, SEARCH_BATCH, SEARCH_BATCH_FIRST_PAGE,
    SEARCH_BATCH_ORIGINS, Variables, graphql, largest_page_sizes, point_cost,
    search_batch_largest_round, search_batch_resumed_page, worst_case_point_cost,
};

/// What each document this source sends costs, in points, under the largest page sizes it
/// can be driven with.
///
/// One line per document, with the price written out as a number, because that is the whole
/// instrument: a shared fragment that grows a connection moves one of these figures, and a
/// figure that moves is a line of this file somebody had to change. The sweep below reads
/// this table against [`graphql::DOCUMENTS`] both ways, so neither a document that landed
/// without a price nor a price for a document this source no longer sends can sit here
/// unnoticed.
///
/// Every mutation costs 1 — GitHub's published minimum for any call — because none of them
/// selects a connection. That is a figure rather than an exemption: a mutation that grew a
/// connection would move it.
const PRICES: &[(&str, u64)] = &[
    (graphql::ISSUES_BY_CREATION, 1),
    (graphql::USER_IDS, 1),
    (graphql::SEARCH_ISSUES, 5),
    (graphql::ISSUE, 1),
    (graphql::ISSUE_BOARD_ITEMS, 1),
    // llmlint: ignore[tests_hold_no_nonfunctional_thresholds] The price is the instrument, not a threshold: the module documentation records that GitHub publishes no per-call point limit to sweep against, so the pin is the check that a document's price moved. This change moved it deliberately, and `session-cost.md` says why.
    (graphql::SUB_ISSUES, 1),
    (graphql::BOARD, 2),
    // llmlint: ignore[tests_hold_no_nonfunctional_thresholds] The inventory records each document's actual API price rather than a ceiling.
    (graphql::BOARD_WITH_COMMENTS, 4),
    (graphql::ORIGIN_LOOKUP, 1),
    // llmlint: ignore-block[tests_hold_no_nonfunctional_thresholds] As at `SUB_ISSUES` above: the price is the instrument, not a threshold. `task status reconcile` reads the board's closed items and `task status complete` reads issues by reference, and the sweep below refuses any document in `graphql::DOCUMENTS` without a recorded price; the second's one point per fifty references is what the `github-landing-closure-points` budget's figure rests on, and onebudgetspec judges that budget.
    (graphql::CLOSED_ITEMS, 2),
    (graphql::ISSUES_BY_REFERENCE, 1),
    // llmlint: ignore-end[tests_hold_no_nonfunctional_thresholds]
    (graphql::BOARD_FIELDS, 1),
    (graphql::DRAFT, 1),
    (graphql::REPOSITORY, 1),
    (graphql::PROJECT_VISIBILITY, 1),
    (graphql::CREATION_CONTEXT, 1),
    (graphql::ISSUE_DEPENDENCIES, 1),
    // llmlint: ignore-block[tests_hold_no_nonfunctional_thresholds] As at `SUB_ISSUES` above: the price is the instrument, not a threshold, and the sweep below refuses any document in `graphql::DOCUMENTS` without a recorded price; these are the two one-direction dependency reads, and what a dependency read spends is the dependency-read budgets', judged by onebudgetspec.
    (graphql::ISSUE_DEPENDS_ON, 1),
    (graphql::ISSUE_DEPENDED_ON_BY, 1),
    // llmlint: ignore-end[tests_hold_no_nonfunctional_thresholds]
    (graphql::CREATE_ISSUE, 1),
    // llmlint: ignore-block[tests_hold_no_nonfunctional_thresholds] As at `SUB_ISSUES` above: the price is the instrument, not a threshold. These are the documents a copy now reads its origins and writes its created items with, and the sweep below refuses any document in `graphql::DOCUMENTS` without a recorded price; what a plan copy spends is the `github-plan-copy-points-*` budgets', judged by onebudgetspec.
    (graphql::ORIGIN_LOOKUPS, 1),
    // GitHub's own dry run, beside the model's: `docs/measurements/board-copy-gaps-2026-10-24/coalesced-members-dry-run`.
    (graphql::ORIGIN_LOOKUPS_WITH_MEMBERS, 1),
    (graphql::CREATE_ISSUES, 1),
    (graphql::CREATED_BOARD_ITEMS, 1),
    (graphql::WRITE_FIELD_VALUES, 1),
    (graphql::CLOSE_ISSUES, 1),
    (graphql::UPDATE_ISSUES, 1),
    (graphql::ADD_BLOCKED_BYS, 1),
    (graphql::WRITE_FIELDS_AND_EDGES, 1),
    (graphql::DELETE_ISSUES, 1),
    (graphql::DOCUMENT_IN_PROJECT, 1),
    // llmlint: ignore-end[tests_hold_no_nonfunctional_thresholds]
    (graphql::ADD_TO_BOARD, 1),
    (graphql::UPDATE_ISSUE, 1),
    (graphql::UPDATE_DRAFT, 1),
    (graphql::UPDATE_FIELD, 1),
    (graphql::UPDATE_FIELDS, 1),
    (graphql::CLEAR_FIELD, 1),
    (graphql::CREATE_FIELD, 1),
    (graphql::ADD_SUB_ISSUE, 1),
    (graphql::REMOVE_SUB_ISSUE, 1),
    (graphql::ADD_BLOCKED_BY, 1),
    (graphql::REMOVE_BLOCKED_BY, 1),
    (graphql::DELETE_ISSUE, 1),
    (graphql::ISSUE_COMMENTS, 1),
    (graphql::ISSUE_DETAIL, 1),
    (graphql::ISSUE_DETAILS, 1),
    // llmlint: ignore[tests_hold_no_nonfunctional_thresholds] As at `SUB_ISSUES` above: the price is the instrument, not a threshold. A copy's batched read is a document of its own, and the sweep below refuses any document in `graphql::DOCUMENTS` without a recorded price, so it needs one; `session-cost.md` records why it was added.
    (graphql::ISSUE_COPY_DETAILS, 1),
    (graphql::COMMENT_ISSUE, 1),
    (graphql::ADD_COMMENT, 1),
    (graphql::UPDATE_COMMENT, 1),
    (graphql::DELETE_COMMENT, 1),
    // llmlint: ignore-block[tests_hold_no_nonfunctional_thresholds] As at `SUB_ISSUES` above: the price is the instrument, not a threshold. These are the batched documents `task find`, `task comment add-many` and `task update-many` send, and the sweep below refuses any document in `graphql::DOCUMENTS` without a recorded price; what a follow-up cycle spends is the `github-points-per-follow-up-cycle` budget's, judged by onebudgetspec. The batched search is priced here at every slot's largest page; it is sent at three rows a slot on a first page, which `follow_up_cycle.rs` prices as sent.
    (graphql::SEARCH_BATCH, 22),
    (graphql::ADD_COMMENTS, 1),
    (graphql::WRITE_TASKS, 1),
    // llmlint: ignore-end[tests_hold_no_nonfunctional_thresholds]
    // The whole-issue twins an expected write sends in place of the document beside each, so
    // the store's cache can take what their responses show: each selects what `ISSUE_DETAIL`
    // does, and costs the one point it does.
    // llmlint: ignore-block[tests_hold_no_nonfunctional_thresholds] As at `SUB_ISSUES` above: the price is the instrument, not a threshold. These five are new documents, and the sweep below refuses any document in `graphql::DOCUMENTS` without a recorded price; the ceiling on what the cache spends is the `github-cache-points-not-above-uncached` budget's, judged by onebudgetspec.
    (graphql::ADD_COMMENT_WHOLE, 1),
    (graphql::UPDATE_COMMENT_WHOLE, 1),
    (graphql::UPDATE_ISSUE_WHOLE, 1),
    (graphql::UPDATE_FIELD_WHOLE, 1),
    (graphql::CLEAR_FIELD_WHOLE, 1),
    // llmlint: ignore-end[tests_hold_no_nonfunctional_thresholds]
    // The twins a targeted update sends in place of the document beside each, so its answer
    // spells the version the written task holds: each selects what a read of the issue does,
    // and costs the one point the plain document does.
    // llmlint: ignore-block[tests_hold_no_nonfunctional_thresholds] As at `SUB_ISSUES` above: the price is the instrument, not a threshold. These three are new documents, and the sweep below refuses any document in `graphql::DOCUMENTS` without a recorded price; what a follow-up cycle spends is the `github-points-per-follow-up-cycle` budget's, judged by onebudgetspec.
    (graphql::UPDATE_ISSUE_TOKEN, 1),
    (graphql::UPDATE_FIELD_TOKEN, 1),
    (graphql::CLEAR_FIELD_TOKEN, 1),
    // llmlint: ignore-end[tests_hold_no_nonfunctional_thresholds]
    (graphql::STATUS_OPTIONS_SNAPSHOT, 1),
    (graphql::STATUS_OPTIONS_UPDATE, 1),
    // llmlint: ignore-block[tests_hold_no_nonfunctional_thresholds] As at `SUB_ISSUES` above: the price is the instrument, not a threshold. These five are the relation documents, and the sweep below refuses any document in `graphql::DOCUMENTS` without a recorded price; what a relation batch spends is the `github-points-per-relation-batch` budget's, judged by onebudgetspec.
    (graphql::ISSUE_RELATIONS, 1),
    (graphql::SET_PARENTS, 1),
    (graphql::ADD_RELATES_TO, 1),
    (graphql::REMOVE_RELATES_TO, 1),
    (graphql::RELATES_TO_READS, 1),
    // llmlint: ignore-end[tests_hold_no_nonfunctional_thresholds]
];

/// What this source's own text says `document` costs against what [`PRICES`] records, or
/// the failure it is.
///
/// One place the verdict is spelled, so the inventory sweep below and the discrimination
/// case beneath it are the same check over different figures rather than two checks that
/// could come to differ. `recorded` is passed in rather than looked up, which is what lets
/// that case drive a price nobody would write down.
fn mispriced(doing: &str, document: &str, recorded: Option<u64>) -> Option<String> {
    let price = worst_case_point_cost(document)
        .unwrap_or_else(|error| panic!("the document for {doing} could not be priced: {error}"));
    match recorded {
        None => Some(format!(
            "the document for {doing} costs {price} points and this file records no price \
             for it; next: add it to PRICES at {price}"
        )),
        Some(recorded) if recorded != price => Some(format!(
            "the document for {doing} now costs {price} points and this file records \
             {recorded}; next: if the change is deliberate, put {price} in PRICES and say \
             in session-cost.md what moved it"
        )),
        Some(_) => None,
    }
}

/// The price [`PRICES`] records for `document`, if it records one.
fn recorded(document: &str) -> Option<u64> {
    PRICES
        .iter()
        .find(|(pinned, _)| *pinned == document)
        .map(|(_, price)| *price)
}

/// Every document, from the inventory rather than from a list written out here.
///
/// Reading [`graphql::DOCUMENTS`] is what makes this cover a document nobody thought
/// about: `documents_are_all_inventoried` already fails when a `pub const` there is left
/// out of that list, so a document added later is priced here without this test being
/// edited to know about it — and, having no price recorded, fails until somebody writes
/// one down.
#[test]
fn every_document_this_source_sends_costs_what_this_file_records() {
    let moved = graphql::DOCUMENTS
        .iter()
        .filter_map(|(document, doing)| mispriced(doing, document, recorded(document)))
        .collect::<Vec<_>>();
    assert!(moved.is_empty(), "{}", moved.join("\n"));
}

/// And the other way: nothing is priced here that this source does not send.
///
/// A price left behind by a document that was deleted or rewritten would go on passing the
/// sweep above for ever, because that sweep only ever reads the pins the inventory reaches.
#[test]
fn every_price_recorded_here_belongs_to_a_document_this_source_sends() {
    let orphaned = PRICES
        .iter()
        .filter(|(pinned, _)| {
            !graphql::DOCUMENTS
                .iter()
                .any(|(document, _)| document == pinned)
        })
        .map(|(pinned, price)| {
            format!("this file records {price} points for a document the inventory does not hold; next: delete the entry, or put the document back in graphql::DOCUMENTS:\n{pinned}")
        })
        .collect::<Vec<_>>();
    assert!(orphaned.is_empty(), "{}", orphaned.join("\n"));
}

/// The check refuses a document whose price has moved, naming both figures.
///
/// Without this the sweep above could pass over any tree at all — including one where the
/// calculation had been wired up to answer whatever was recorded. It drives the real
/// verdict over a real document against a price nobody would write down, which is the same
/// shape and the same reason as `tests/node_count.rs`'s pre-fix document.
#[test]
fn the_check_reports_a_failure_naming_a_document_whose_price_moved() {
    let refusal = mispriced("reading the board", graphql::BOARD, Some(8))
        .expect("a price this document does not cost is refused");
    assert!(refusal.contains("reading the board"), "{refusal}");
    assert!(refusal.contains('8'), "{refusal}");
    assert!(refusal.contains('2'), "{refusal}");
}

/// And it refuses a document with no price at all.
///
/// This is the half that catches a document landing without anybody pricing it: the sweep
/// reads the inventory, so a new document reaches this verdict with `None` recorded.
#[test]
fn the_check_reports_a_failure_naming_a_document_nobody_priced() {
    let refusal = mispriced("reading the board", graphql::BOARD, None)
        .expect("a document with no recorded price is refused");
    assert!(refusal.contains("reading the board"), "{refusal}");
    assert!(refusal.contains("records no price"), "{refusal}");
}

/// The batch read is one point, and the batch size is the largest that is: one item more
/// would cost two.
///
/// `DETAIL_BATCH` is declared as the largest batch the node-count model prices at one point,
/// and the sweep above only shows the document at that size costs one. This is the other
/// half: the same document grown by one aliased item — written the way every alias in it is —
/// is priced at two, so a batch constant left below what one point buys fails here rather
/// than spending a request a caller did not need to.
// llmlint: ignore-block[tests_hold_no_nonfunctional_thresholds] These hold each batch constant to GitHub's own pricing model rather than to a budget: the constant is the number of items the plugin puts in one request, and the check is that it is the largest batch one point buys — one item more costs two — which is a property of the constant against the published model, not a cost ceiling a test invented. `DETAIL_BATCH`'s check predates this change; the helper generalises it so `COPY_DETAIL_BATCH`, the copy's batch read, is held to the same property. `MEMBER_PAGE_SIZE` is held to the same property over a page size: the most sub-issues one point lists.
#[test]
fn the_batch_read_is_the_largest_one_point_buys() {
    const {
        assert!(
            DETAIL_BATCH >= 10,
            "a batch this small is not worth a document"
        )
    };
    largest_at_one_point(graphql::ISSUE_DETAILS, DETAIL_BATCH, "DETAIL_BATCH");
}

/// The same of a copy's batch read, which carries each item's `blockedBy` besides.
#[test]
fn the_copy_batch_read_is_the_largest_one_point_buys() {
    const {
        assert!(
            COPY_DETAIL_BATCH >= 10,
            "a batch this small is not worth a document"
        )
    };
    largest_at_one_point(
        graphql::ISSUE_COPY_DETAILS,
        COPY_DETAIL_BATCH,
        "COPY_DETAIL_BATCH",
    );
}

/// A project's sub-issue page is one point, and its size is the largest that is: one row more
/// would cost two.
///
/// The same property as the batch reads above, over a page size rather than a count of
/// aliases: the document is priced again with `$memberFirst` one larger, and that price is two.
#[test]
fn the_member_page_is_the_largest_one_point_buys() {
    const {
        assert!(
            MEMBER_PAGE_SIZE >= 10,
            "a page this small does not list a plan's project in one request"
        )
    };
    assert_eq!(worst_case_point_cost(graphql::SUB_ISSUES).unwrap(), 1);
    let mut grown: Variables = largest_page_sizes();
    grown.insert("memberFirst".to_owned(), MEMBER_PAGE_SIZE + 1);
    assert_eq!(
        github_graphql_node_count::point_cost(graphql::SUB_ISSUES, &grown).unwrap(),
        2,
        "one row more than MEMBER_PAGE_SIZE is still one point, so MEMBER_PAGE_SIZE is not the \
         largest page one point buys"
    );
}

/// `document`, a fixed-size batch of `batch` aliased items, costs one point, and the same
/// document grown by one aliased item — written the way every alias in it is — costs two.
fn largest_at_one_point(document: &str, batch: usize, name: &str) {
    // Fixed-size aliased `node(id:)` fields, one per item, and never `nodes(ids:)`, which the
    // pricing model cannot see under.
    assert!(!document.contains("nodes(ids:"), "{document}");
    assert_eq!(document.matches(":node(id:$id").count(), batch);
    assert_eq!(worst_case_point_cost(document).unwrap(), 1);

    let last = batch - 1;
    let alias_at = document
        .find(&format!("i{last}:node(id:$id{last})"))
        .expect("the last alias");
    let alias_end = document[alias_at..]
        .find("\n")
        .map_or(document.len(), |end| alias_at + end);
    let alias = &document[alias_at..alias_end];
    let grown_alias = alias
        .replacen(&format!("i{last}:"), &format!("i{batch}:"), 1)
        .replacen(&format!("$id{last}"), &format!("$id{batch}"), 1);
    let grown = format!(
        "{}\n      {grown_alias}{}",
        &document[..alias_end],
        &document[alias_end..]
    )
    .replacen(
        &format!("$id{last}:ID!,"),
        &format!("$id{last}:ID!,$id{batch}:ID!,"),
        1,
    );
    assert_eq!(grown.matches(":node(id:$id").count(), batch + 1);
    assert_eq!(
        worst_case_point_cost(&grown).unwrap(),
        2,
        "one item more than {name} is still one point, so {name} is not the largest batch \
         one point buys:\n{grown}"
    );
}
// llmlint: ignore-end[tests_hold_no_nonfunctional_thresholds]

/// [`search_batch_largest_round`] is the largest round of the batched search this source sends,
/// which is what makes the credentialed lane's reconciliation of the document under it a
/// reconciliation of every round: each way a round can split its search slots and origin item
/// listings between first pages, resumed pages and slots it does not ask is priced at the pages
/// `search_round` binds — a first page at [`SEARCH_BATCH_FIRST_PAGE`] rows, every resumed page
/// at [`search_batch_resumed_page`]'s share of the round, and a slot not asked at one — and none
/// is larger. It compares two prices of one document under GitHub's published model, not a price
/// with a ceiling: the full every-slot-at-a-whole-page worst case is pinned in the table above,
/// and what a find spends is `github-points-per-follow-up-cycle`'s, judged by onebudgetspec.
#[test]
fn the_largest_round_of_the_batched_search_is_the_largest_round_it_sends() {
    let largest = point_cost(graphql::SEARCH_BATCH, &search_batch_largest_round()).unwrap();
    assert!(largest <= worst_case_point_cost(graphql::SEARCH_BATCH).unwrap());
    for searches_first in 0..=SEARCH_BATCH {
        for searches_resumed in 0..=SEARCH_BATCH - searches_first {
            for origins_first in 0..=SEARCH_BATCH_ORIGINS {
                for origins_resumed in 0..=SEARCH_BATCH_ORIGINS - origins_first {
                    let shared = search_batch_resumed_page(searches_resumed + origins_resumed);
                    let pages = |first: usize, resumed: usize, slot: usize| {
                        if slot < first {
                            SEARCH_BATCH_FIRST_PAGE
                        } else if slot < first + resumed {
                            shared
                        } else {
                            1
                        }
                    };
                    let mut sizes = largest_page_sizes();
                    for slot in 0..SEARCH_BATCH {
                        sizes.insert(
                            format!("first{slot}"),
                            pages(searches_first, searches_resumed, slot),
                        );
                    }
                    for slot in 0..SEARCH_BATCH_ORIGINS {
                        sizes.insert(
                            format!("itemsFirst{slot}"),
                            pages(origins_first, origins_resumed, slot),
                        );
                    }
                    let cost = point_cost(graphql::SEARCH_BATCH, &sizes).unwrap();
                    assert!(
                        cost <= largest,
                        "a round of {searches_first} first and {searches_resumed} resumed \
                         searches and {origins_first} first and {origins_resumed} resumed origin \
                         listings is larger than search_batch_largest_round: {cost} points \
                         against {largest}"
                    );
                }
            }
        }
    }
}

/// What each read a source opted in to assignees sends costs, where that is not what the same
/// read costs a source that has not opted in, in points under the same page sizes.
///
/// The people ride the requests the read already sends — no request is added — but GitHub
/// charges every connection it is asked for, and `assignees` is one more under each issue of a
/// page: a document reading a page of issues costs more once its source opts in. Each figure is
/// a pin, not a threshold, as every price in [`PRICES`] is; a source that has not opted in pays
/// exactly what [`PRICES`] records.
// llmlint: ignore[tests_hold_no_nonfunctional_thresholds] The price is the instrument, not a threshold, as at `SUB_ISSUES` in `PRICES`: these record what an opted-in source's reads cost, so a change that moves one is seen.
const OPTED_PRICES: &[(&str, u64)] = &[
    (graphql::ISSUE_RELATIONS, 2),
    (graphql::SEARCH_ISSUES, 6),
    (graphql::DOCUMENT_IN_PROJECT, 2),
    (graphql::SUB_ISSUES, 2),
    (graphql::BOARD, 4),
    (graphql::BOARD_WITH_COMMENTS, 6),
    (graphql::CLOSED_ITEMS, 4),
    (graphql::ISSUE_DETAILS, 2),
    (graphql::ISSUE_COPY_DETAILS, 2),
    (graphql::SEARCH_BATCH, 28),
];

/// Every document a source opted in to assignees spells with them costs what [`OPTED_PRICES`]
/// records for it, or — for every document it does not name — exactly what the plain document
/// costs.
#[test]
fn a_document_spelled_with_assignees_costs_what_this_file_records() {
    let moved = graphql::DOCUMENTS
        .iter()
        .filter_map(|(document, doing)| {
            let spelled = graphql::with_assignees(document)?;
            let plain = worst_case_point_cost(document).expect("the plain document prices");
            let mut sizes = largest_page_sizes();
            // The most people GitHub assigns one issue, the spelled variable's own default.
            sizes.insert("assigneeFirst".to_owned(), 10);
            let opted = point_cost(&spelled, &sizes).expect("the spelled document prices");
            let recorded = OPTED_PRICES
                .iter()
                .find(|(pinned, _)| pinned == document)
                .map_or(plain, |(_, price)| *price);
            (opted != recorded).then(|| {
                format!("{doing}: {opted} with assignees, recorded {recorded} ({plain} plain)")
            })
        })
        .collect::<Vec<_>>();
    assert!(moved.is_empty(), "{}", moved.join("\n"));
}