mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 15:04:37 +01:00
v1.91.255.0 fix(ship-queue): fill trains at 3 READY PRs or one hour, and price every case shard's trial (#3478)
* fix(ship-queue): fill trains at 3 READY PRs or one hour, and price every case shard's trial
The 5-PR / 3-hour fill left READY PRs waiting most of a day; .gstack/ship-policy.json
now fills at max_size 3 or fill_hours 1, whichever comes first (fillDue already
dispatches at either bound). train verdict printed cost=$24.41 for a train whose slice
report said cost $32.92 known + 54 of 136 trial(s) cost unknown: E2E records named by a
display label ("/benchmark workflow") were never attributed to their case id, so the
per-case sum was short. A case shard's records now belong to its one case; a multi-case
shard with display-labelled records keeps those trials unknown. The ATTEMPT: line names
how many cases have no cost instead of printing a partial sum as the total.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(ci): record the paid-report.ts harness decision and keep ship-queue-train-cost.test.ts off the Windows subset
scripts/lib/paid-report.ts is a pinned shared-harness file, so test/harness-version.test.ts wants a HARNESS_VERSION decision for its edit: non-behavioral, because a case shard's records are attributed to its one case for the trial's cost, class and error fields while no case's outcome, panel verdict or selection changes.
test/ship-queue-train-cost.test.ts drives bin/gstack-ship-queue against the #!/bin/sh gh shim, which windows-latest cannot run (windows-free-shard 2 on #3478 at d5e6a7d), so it joins test/ship-queue-train.test.ts in the Windows exclusion list.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test: pin a failed display-labelled trial's fields and the $0.00 cost line; say so in the CHANGELOG
The widened record filter also feeds failedRecord, so a failed display-labelled trial in a case shard now takes failure_class, exit_reason and error from its record (train-gate classes a red infra record INFRA instead of VERDICT). The CHANGELOG entry said only the cost fields changed; it now says both. A train priced at zero prints cost=$0.00.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* chore(harness): say which panel class the paid-report decision moves
The non-behavioral note now names the one verdict-side effect of attributing a case shard's records to its case: a red whose own record says infra or timeout is classed INFRA instead of VERDICT, which changes rerun and bisect eligibility and not a pass rate.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: document one-case-shard cost attribution and the train verdict cost line; put the undercounted train costs on record
/document-release (ship-owned audit) added the attribution rule and the ATTEMPT: cost-line form to docs/TESTING_INTERNALS.md, docs/troubleshooting.md#cost-unknown and docs/evals/census-red.md. The CHANGELOG headline now says every train cost the coordinator reported until this fix was the undercounted verdict figure (about $24 per train against about $33 real).
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: the train verdict prints the known+unknown cost form only when some but not every case is priced
The second /document-release audit tightened docs/TESTING_INTERNALS.md and docs/troubleshooting.md#cost-unknown: a train record with no priced case prints no cost at all (classifyGate's cost is null). The CHANGELOG bullet says the same.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* v1.91.255.0 stamp
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>
This commit is contained in:
1 parent
568cd5aa56
commit
b004c06dbc
16 files changed
+171
-16
No files matched your search
@@ -8,6 +8,7 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { EXIT } from '../lib/headless-artifacts';
|
||||
import { DEFAULT_POLICY, POLICY_KEYS, globToRegExp, matchesAny, validatePolicy } from '../lib/ship-policy';
|
||||
import { fillDue } from '../lib/ship-queue-train';
|
||||
import { BASE_POLICY, cleanup, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/restamp-fixture';
|
||||
|
||||
const repos: FixtureRepo[] = [];
|
||||
@@ -72,6 +73,18 @@ describe("gstack's own .gstack/ship-policy.json", () => {
|
||||
expect(raw.train).toMatchObject({ gate_workflow: 'evals.yml', free_workflow: 'free-tests.yml', full_gate_check: 'gstack/full-gate', record_artifact: 'gstack-train-record' });
|
||||
for (const wf of [raw.train.gate_workflow, raw.train.free_workflow]) expect(fs.existsSync(path.join(root, '.github/workflows', wf))).toBe(true);
|
||||
});
|
||||
|
||||
// Value: protects=the fill rule the coordinator relies on: a train dispatches at 3 READY PRs or one hour after the first READY, whichever comes first
|
||||
// (2026-10-26: 5 PRs / 3 hours left READY PRs waiting most of a day); fails_when=the live block drifts back to a larger or slower fill; why_new=the values were unpinned; seam=the policy file; no_claim=fillDue's arithmetic (ship-queue-train-logic)
|
||||
test('fills a train at 3 READY PRs or one hour after the first READY, whichever comes first', () => {
|
||||
const raw = JSON.parse(fs.readFileSync(path.join(import.meta.dir, '..', '.gstack', 'ship-policy.json'), 'utf8'));
|
||||
expect([raw.train.max_size, raw.train.fill_hours]).toEqual([3, 1]);
|
||||
const first = '2026-10-26T09:00:00.000Z';
|
||||
const slots = (n: number) => Array.from({ length: n }, (_, i) => ({ pr: i + 1, ready_at: new Date(Date.parse(first) + i * 60_000).toISOString() })) as any;
|
||||
expect(fillDue(slots(2), raw.train, '2026-10-26T09:59:00Z', false)).toBe('2026-10-26T10:00:00.000Z');
|
||||
expect(fillDue(slots(2), raw.train, '2026-10-26T10:00:00Z', false)).toBeNull();
|
||||
expect(fillDue(slots(3), raw.train, '2026-10-26T09:05:00Z', false)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('gstack-ship-policy (bin)', () => {
|
||||
|
||||
@@ -97,6 +97,53 @@ describe('census attribution from the 2026-10-03 artifacts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('a case shard\'s result records are its one case\'s cost (2026-10-26 train 76db1d1f10c4: 54 of 136 trials cost-unknown, verdict $24.41 under a $32.92 run)', () => {
|
||||
const shard = (dir: string, slug: string) => path.join(dir, 'paid-slice-1-a1', 'shards', slug);
|
||||
const results = (dir: string, slug: string, tests: Array<Record<string, unknown>>) =>
|
||||
fs.writeFileSync(path.join(shard(dir, slug), 'e2e-2026-10-26.json'), JSON.stringify({ tests, total_cost_usd: tests.reduce((sum, t) => sum + Number(t.cost_usd), 0) }));
|
||||
const collector = (dir: string) => JSON.parse(fs.readFileSync(path.join(dir, 'collector-outcomes.json'), 'utf8'));
|
||||
|
||||
test('a record named by its display label ("/qa fix loop") belongs to the shard\'s only case, so the trial and the verdict carry its cost', () => {
|
||||
const r = report('periodic', dir => results(dir, 'skill-e2e-qa-workflow--qa-fix-loop',
|
||||
[{ name: '/qa fix loop', suite: 'QA skill E2E', tier: 'e2e', passed: true, duration_ms: 1000, cost_usd: 1.23 }]));
|
||||
const trial = r.history.find(record => record.case === 'qa-fix-loop');
|
||||
expect(trial).toMatchObject({ outcome: 'passed', cost_usd: 1.23 });
|
||||
expect(trial.cost_known).toBeUndefined();
|
||||
expect(collector(r.dir).verdict.costUsd).toBe(1.23);
|
||||
expect(r.out).toContain('cost $1.23 known + 6 of 7 trial(s) cost unknown');
|
||||
});
|
||||
|
||||
test('a multi-case shard whose records carry display labels keeps those trials cost-unknown instead of guessing', () => {
|
||||
const r = report('periodic', dir => results(dir, 'skill-e2e-cso',
|
||||
[{ name: '/cso full audit', suite: 'cso', tier: 'e2e', passed: true, duration_ms: 1000, cost_usd: 0.5 },
|
||||
{ name: 'cso-infra-scope', suite: 'cso', tier: 'e2e', passed: true, duration_ms: 1000, cost_usd: 0.25 }]));
|
||||
expect(r.history.find(record => record.case === 'cso-full-audit')).toMatchObject({ cost_usd: 0, cost_known: false });
|
||||
expect(r.history.find(record => record.case === 'cso-infra-scope')).toMatchObject({ cost_usd: 0.25 });
|
||||
expect(r.history.find(record => record.case === 'cso-infra-scope').cost_known).toBeUndefined();
|
||||
expect(collector(r.dir).verdict.costUsd).toBe(0.75);
|
||||
});
|
||||
|
||||
// Value: protects=a failed case shard's trial reads its failure class, exit reason, error and cost from the display-labelled record the shard
|
||||
// wrote, not a generic 'assertion' with the JUnit message (the same attribution the cost fix made; train-gate's INFRA/VERDICT class follows failure_class);
|
||||
// fails_when=failedRecord is looked up through the old name/case_id-only filter again so the record's class and error are dropped;
|
||||
// existing_gap=both attribution rows above use passing records, so the failedRecord branch of the widened filter is unexercised; seam=none
|
||||
test('a failed display-labelled record in a case shard supplies the trial\'s failure class, exit reason and error, not a bare assertion', () => {
|
||||
const r = report('periodic', dir => {
|
||||
const qa = junit(dir, 'skill-e2e-qa-workflow--qa-fix-loop');
|
||||
fs.writeFileSync(qa, fs.readFileSync(qa, 'utf8').replace('failures="0"', 'failures="1"')
|
||||
.replace(/(<testcase name="qa-fix-loop"[^>]*?) \/>/, '$1><failure type="AssertionError" message="qa-fix-loop: expected a fix commit" /></testcase>'));
|
||||
results(dir, 'skill-e2e-qa-workflow--qa-fix-loop', [{ name: '/qa fix loop', suite: 'QA skill E2E', tier: 'e2e', passed: false, duration_ms: 1000, cost_usd: 2.5,
|
||||
failure_class: 'timeout', exit_reason: 'timeout', timeout_at_turn: 7, error: 'no terminal outcome within 1800000ms\nsecond line' }]);
|
||||
});
|
||||
expect(r.status, r.out).toBe(1);
|
||||
const trial = r.history.find(record => record.case === 'qa-fix-loop');
|
||||
expect(trial).toMatchObject({ outcome: 'failed', failure_class: 'timeout', exit_reason: 'timeout', error: 'no terminal outcome within 1800000ms', cost_usd: 2.5 });
|
||||
expect(trial.cost_known).toBeUndefined();
|
||||
expect(r.out).toContain('✗ qa-fix-loop rule FAIL timeout at turn 7 — no terminal outcome within 1800000ms');
|
||||
expect(trialRecordProblemsV1(trial)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('record builders attach ledger sessions and failure causes (plan 0.1/0.2)', () => {
|
||||
const noOp = (dir: string) => path.join(dir, 'paid-slice-1-a1', 'shards', 'skill-e2e-plan-mode-no-op');
|
||||
const failNoOp = (dir: string) => {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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 './helpers/restamp-fixture';
|
||||
import { GREEN_GATE, PASS, TRAIN_POLICY, line, nonceOf, openPr, queue, readyReceipt, trainFixture, type Rule, type TrainFixture } from './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);
|
||||
});
|
||||
Reference in new issue
Block a user