Files
gstack/crew/test/ship-queue-train-cost.test.ts
T
Garry Tanandcapy-ai[bot] 6417abf7a6 Move the tools only Capy crews use into a crew/ folder (GST-1833) (#4145)
* Load the Capy crew skills only on Capy machines (GST-1833)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Add changelog entry for Capy-only crew skills (GST-1833)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Move the Capy-only crew tooling into crew/ and run its tests only when needed (GST-1833)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Test that the Capy launch file puts crew/bin on PATH

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Count any ~/.capy as a Capy machine, so a Mac running Capy keeps /captain

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Fix the tests and fixtures that broke when crew tooling moved into crew/

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Plan every crew test when a pull request's merge-base can't be read

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Add a test that checks each crew-only module left the root tree for crew/

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Run crew tests on pull requests that change bin/ commands or workflows they drive

Review-Finding: p1/specialist-batched#1 reader=testing
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Keep running every crew test on every pull request; the skip comes in a later change

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Keep copied installs and older release bisects working after the crew/ move

Setup now copies crew/lib into every host's runtime root beside bin and lib, so Windows installs (which copy instead of linking) can still start gstack-ship-receipt, gstack-pregate and the other commands that import it. The release bisect runs the train-gate script from wherever the checked-out commit has it, so a bisect over commits from before the move still gets a verdict.

Review-Finding: p1/outside-voice#1 reader=outside-voice
Review-Finding: p1/outside-voice#2 reader=outside-voice
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Say in the changelog that Windows installs keep working after the crew/ move

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Point the harness guide's merge-queue rows at crew/lib/ where those modules moved

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Point the plain-title test at the wave module's new home in crew/ (GST-1833)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-08 14:26:14 +00:00

50 lines
3.4 KiB
TypeScript

/**
* The train verdict's cost line: the known sum plus how many of the record's
* cases carry no cost, so a partial sum is never read as the run's spend.
* Train 76db1d1f10c4 (2026-10-26) printed cost=$24.41 while its slice report
* said "cost $32.92 known + 54 of 136 trial(s) cost unknown": 54 E2E records
* named by display label had no case attributed (scripts/lib/paid-report.ts).
*/
import { afterEach, describe, expect, test } from 'bun:test';
import { EXIT } from '../../lib/headless-artifacts';
import { cleanup } from '../../test/helpers/restamp-fixture';
import { GREEN_GATE, PASS, TRAIN_POLICY, line, nonceOf, openPr, queue, readyReceipt, trainFixture, type Rule, type TrainFixture } from '../../test/helpers/ship-queue-train-fixture';
const fixtures: TrainFixture[] = [];
const make = () => { const f = trainFixture(TRAIN_POLICY); fixtures.push(f); return f; };
afterEach(() => { while (fixtures.length) cleanup(fixtures.pop()!.repo); });
function greenTrain(fx: TrainFixture, gate: Rule) {
const h1 = openPr(fx, '1');
const h2 = openPr(fx, '2');
readyReceipt(fx, '1', h1);
readyReceipt(fx, '2', h2);
fx.write(d => { d.rules['evals.yml'] = gate; });
return nonceOf(queue(fx, ['train', 'build', '--prs', `1@${h1},2@${h2}`, '--free-timeout', '1']).out);
}
describe('train verdict cost line', () => {
// Value: protects=a train verdict never presents a partial sum as the run's cost (train 76db1d1f10c4 on 2026-10-26: verdict $24.41, the run's slice reports $32.92 known with 54 trials unattributed);
// fails_when=the ATTEMPT line prints cost=$ without naming the cost-unknown cases, or names them when every panel has a cost; why_new=the line only had the known sum; seam=the gate record's panels; no_claim=why a trial's cost is unknown
test('the verdict prints the known cost and how many cases have none, and nothing extra when every panel is priced', () => {
const fx = make();
const unpriced = { ...GREEN_GATE, record: { ...GREEN_GATE.record, panels: [PASS('c1'), { ...PASS('c2'), cost_usd: null }, { ...PASS('j1', 'judge', 0.1), cost_usd: null }] } };
const nonce = greenTrain(fx, unpriced);
const v = queue(fx, ['train', 'verdict', '--train', nonce]);
expect([v.status, line(v.out, 'ATTEMPT:')]).toEqual([EXIT.ok, 'ATTEMPT: a1 gate run 101 GREEN cost=$0.70 known + 2 of 3 case(s) cost unknown (rule 2/2, judge 1/1)']);
const all = make();
const priced = greenTrain(all, GREEN_GATE);
expect(line(queue(all, ['train', 'verdict', '--train', priced]).out, 'ATTEMPT:')).toBe('ATTEMPT: a1 gate run 101 GREEN cost=$1.50 (rule 2/2, judge 1/1)');
}, 120_000);
// Value: protects=a train whose every case is priced at $0.00 (reused or free trials) still reports its cost as known;
// fails_when=the null guard on cost_usd becomes a falsy check, so a zero sum prints nothing and reads like an unpriced run;
// existing_gap=the two rows above price every panel above zero or leave some unpriced, never a known zero; seam=none
test('a train priced at zero prints cost=$0.00 rather than no cost at all', () => {
const fx = make();
const free = { ...GREEN_GATE, record: { ...GREEN_GATE.record, panels: [PASS('c1', 'rule', 0), PASS('c2', 'rule', 0), PASS('j1', 'judge', 0)] } };
const nonce = greenTrain(fx, free);
expect(line(queue(fx, ['train', 'verdict', '--train', nonce]).out, 'ATTEMPT:')).toBe('ATTEMPT: a1 gate run 101 GREEN cost=$0.00 (rule 2/2, judge 1/1)');
}, 60_000);
});