Files
gstack/test/autoplan-budget.test.ts
Garry Tanandcapy-ai[bot] 148d28ea81 feat(ship): /ship and /autoplan size their review to the diff (GSTA-131) (#3527)
* wip(GSTA-131): review depth, fix-check transitions, dispatch counters, carry pregate, autoplan round policy

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

* wip(GSTA-131): reviewers stay at full effort; audits light

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

* feat(ship): Step 9 dispatches what gstack-review-plan plans; fix-check after below-floor fixes; cap two full passes plus one fix-check

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

* test: review-dispatch-compare has a free lane; review-depth fixtures registered free; resumed max_rounds case off the old default

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

* fix(GSTA-131): ship review pass-1 repairs: fix-check binds under its own skill, Step 11.5 accepts a fix-check or a planned adversarial skip, delta carry with a pregate, carry --pregate pinned to the checkout, quiet-after-cap revise regates, skill-dir markdown and rule docs classed code, gate signals from tests class up, plan fails closed on git errors, per-class compare ratios

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

* fix(GSTA-131): ship review pass-2 repairs: Step 11 follows the last full-pass plan, Important Rules restored, ship goldens regenerated, true per-class medians, review-policy transitions and once-per-invocation voices, docs/** assets stay docs-class, CLI and fix-check binding tests

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

* docs(changelog): GSTA-131 review depth, fix-check, delta /ship and autoplan rounds

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

* docs: ship-policy cap and counters, review-policy /review cycle cap, unattended passes (docs audit)

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

* fix(review): 400-word findings cap fits the /review size pins (five host pins lowered by 2); CHANGELOG max_rounds wording

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

* docs(changelog): gate integrity names the size-ledger entry and the lowered /review pins

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

* test: the round-budget case asserts the default through ROUNDS output only (pregate literals heuristic)

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

* test: curate ship-receipt-carry-pregate out of the Windows lane like its --pregate siblings

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

* docs(changelog): end the Windows curation sentence instead of a mid-sentence colon

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

* fix(ship): one decision each for the review-loop exits, the planned adversarial skip and Step 16's TODOS rerun (screen judge repair)

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

* fix(ship): fix-check repairs: pin the renamed Step 11 heading, keep the below-floor exit at the cap, name the plan run a repeat reuses, print the native skip on outside-only plans

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-10-28 05:45:54 +00:00

261 lines
18 KiB
TypeScript

/**
* Field list 2, W5: the autoplan run budget on the simulated Capy host. An
* unattended Capy run reserves the default per-voice estimate so --spend-cap
* binds, and a refused dispatch stops the run `incomplete` with a structured
* stop object that export, validate and resume understand; `budget` journals
* the owner's raise. `close --deferred` keeps a dispatched voice's
* reservation, routes an open Critical to the gate and carries open findings
* into tasks.jsonl; the round cap defers a rerun phase and turns a later
* revise into an owner gate item; confirm rounds carry the plan delta and
* revision-qualified, settled finding ids. Every spawn has a timeout.
*/
import { afterAll, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { DEFAULT_HOST_ESTIMATE_USD } from '../lib/autoplan-budget';
import { SMALL_PLAN } from './helpers/autoplan-run-fixture';
import { capyRunner, phaseVia, readJsonl, readRunJson, writePlan } from './helpers/autoplan-capy';
const r = capyRunner();
afterAll(() => r.cleanup());
let n = 0;
const V2 = SMALL_PLAN.replace('cap 30 s', 'cap 60 s, dead-letter after 8 attempts');
const NONE = { native: [], outside: [] };
function start(args: string[] = []): { out: string; dir: string; plan: string } {
const dir = path.join(r.host.base, `budget${++n}`);
const plan = writePlan(dir);
const out = path.join(dir, 'run');
const first = r.ap(['next', '--out', out, '--plan', plan, '--no-ui', ...args]);
if (first.status !== 0) throw new Error(`first next failed: ${first.stderr}\n${first.stdout}`);
return { out, dir, plan };
}
const spend = (out: string) => JSON.parse(fs.readFileSync(path.join(out, 'spend.json'), 'utf8'));
const unknownUsd = (out: string) => spend(out).settled.filter((s: any) => s.usd === 'unknown').reduce((t: number, s: any) => t + s.reserved_usd, 0);
const answer = (out: string, rev: number, reply: string) => r.ap(['answer', '--by', 'human', '--out', out, '--gate-rev', String(rev), '--reply', reply]);
const gate = (out: string) => JSON.parse(fs.readFileSync(path.join(out, 'gate.json'), 'utf8'));
const v2File = (dir: string, text = V2) => { const f = path.join(dir, `plan-v2-${++n}.md`); fs.writeFileSync(f, text); return f; };
describe('spend: the default host estimate binds and budget_exhausted is a resumable incomplete stop', () => {
test('Capy unattended reserves the default per voice; the cap stops the run incomplete; budget raise then resume continues', () => {
const dir = path.join(r.host.base, 'spend');
const plan = writePlan(dir);
const out = path.join(dir, 'run');
const first = r.ap(['next', '--out', out, '--plan', plan, '--no-ui', '--spend-cap', '12']);
expect(first.status, first.stderr).toBe(0);
expect(first.stdout).toMatch(/^ESTIMATE: 5\.00 per voice reserved for each host-subagent attempt/m);
expect(first.stdout).toMatch(/^ROUNDS: ceo=1\/2 eng=1\/2 max_rounds=2 host_spend=unknown$/m);
expect(spend(out).reserved.map((x: any) => x.estimate_usd)).toEqual([5, 5]);
expect(readRunJson(out).max_rounds).toBe(2);
phaseVia(r, out, 'ceo', NONE);
expect(unknownUsd(out)).toBe(10);
const stopped = r.ap(['next', '--out', out]);
expect(stopped.status).toBe(3);
expect(stopped.stderr).toMatch(/\(SPEND_CAP_EXCEEDED\)$/m);
expect(stopped.stdout).toMatch(/^STOP: reason=budget_exhausted phase=eng cap=12\.00 spent=0\.00 reserved=0\.00 unknown=2\(10\.00\) next_estimate=10\.00 rounds=ceo:1\/2,eng:1\/2$/m);
expect(stopped.stdout).toContain(`RECOVER: gstack-autoplan close --out ${out} --phase eng --deferred`);
expect(stopped.stdout).toContain(`RECOVER: gstack-autoplan budget --out ${out} --spend-cap <usd>`);
expect(stopped.stdout).toContain(`GSTACK_RESULT: skill=autoplan status=incomplete run=${out}`);
const run = readRunJson(out);
expect(run.status).toBe('incomplete');
expect(run.stop).toMatchObject({ reason: 'budget_exhausted', cap_usd: 12, rounds: { ceo: 1, eng: 1 }, spent: 0, reserved: 0, unknown: 10 });
expect(readJsonl(path.join(out, 'attempts.jsonl')).filter(e => e.event === 'dispatched' && e.phase === 'eng')).toEqual([]);
expect(r.ap(['export', '--out', out]).status).toBe(0);
const v = r.validate(out);
expect(v.status, v.stdout + v.stderr).toBe(0);
expect(v.stdout).toContain('status=incomplete');
const early = r.ap(['resume', '--out', out]);
expect(early.stdout).toMatch(/^STOPPED: budget_exhausted at /m);
expect(readRunJson(out).status).toBe('incomplete');
const noBy = r.ap(['budget', '--out', out, '--spend-cap', '40']);
expect(noBy.status).toBe(3);
expect(noBy.stderr).toMatch(/\(ANSWER_BY_REQUIRED\)$/m);
const raise = r.ap(['budget', '--out', out, '--spend-cap', '40', '--by', 'human']);
expect(raise.status, raise.stderr).toBe(0);
expect(raise.stdout).toMatch(/^BUDGET: spend_cap=12->40 by=human$/m);
expect(raise.stdout).toContain(`NEXT: gstack-autoplan resume --out ${out}`);
expect(spend(out).cap_usd).toBe(40);
const resumed = r.ap(['resume', '--out', out]);
expect(resumed.stdout).toMatch(/^STOP_LIFTED: budget raised after budget_exhausted$/m);
expect(readRunJson(out).stops).toHaveLength(1);
const eng = phaseVia(r, out, 'eng', NONE);
expect(eng.outside.stdout).toContain('status=gate_pending');
expect(fs.readFileSync(path.join(out, 'review-record.md'), 'utf8')).toMatch(/Stopped budget_exhausted at .* \(lifted\)/);
});
});
describe('close --deferred', () => {
test('a dispatched voice keeps its reservation, an open Critical becomes an owner item, open findings become tasks', () => {
const s = start(['--spend-cap', '100']);
r.review(path.join(s.out, 'ceo-native-prompt.md'), { FIXTURE_FINDINGS: JSON.stringify([{ severity: 'Critical', title: 'Retries can double-charge', fix: 'idempotency key' }, { severity: 'Medium', title: 'Jitter source unstated' }]) });
const run0 = readRunJson(s.out);
const bound = r.ap(['submit', '--out', s.out, '--phase', 'ceo', '--voice', 'native', '--result', run0.phases.ceo.native.attempts[0].result, '--model', 'anthropic/claude-opus-5-5', '--attempt', run0.phases.ceo.native.attempt]);
expect(bound.status, bound.stderr).toBe(0);
const outsideAttempt = run0.phases.ceo.outside.attempt;
const before = unknownUsd(s.out) + spend(s.out).reserved.reduce((t: number, x: any) => t + x.estimate_usd, 0);
const noBy = r.ap(['close', '--out', s.out, '--phase', 'ceo', '--deferred', 'owner stop']);
expect(noBy.status).toBe(3);
expect(noBy.stderr).toMatch(/\(ANSWER_BY_REQUIRED\)$/m);
const close = r.ap(['close', '--out', s.out, '--phase', 'ceo', '--deferred', 'owner stop', '--by', 'human']);
expect(close.status, close.stderr).toBe(0);
expect(close.stdout).toMatch(/^PHASE_DEFERRED: ceo round=1\/2 by=human open=2 \(Critical=1 Medium=1\) blockers=\S+-ceo-native-1 reason="owner stop"$/m);
expect(close.stdout).toContain('GSTACK_RESULT: skill=autoplan status=running');
const run = readRunJson(s.out);
expect(run.phases.ceo.outside.status).toBe('skipped');
expect(run.phases.ceo.outside.attempts[0].state).toBe('deferred');
const settled = spend(s.out).settled.find((x: any) => x.attempt === outsideAttempt);
expect(settled).toMatchObject({ usd: 'unknown', reserved_usd: DEFAULT_HOST_ESTIMATE_USD });
expect(spend(s.out).released).toEqual([]);
const after = unknownUsd(s.out) + spend(s.out).reserved.reduce((t: number, x: any) => t + x.estimate_usd, 0);
expect(after).toBe(before);
expect(r.ap(['resume', '--out', s.out]).status).toBe(0);
expect(unknownUsd(s.out)).toBe(after);
expect(readJsonl(path.join(s.out, 'attempts.jsonl')).some(e => e.event === 'attempt_deferred' && e.attempt === outsideAttempt)).toBe(true);
const again = r.ap(['close', '--out', s.out, '--phase', 'ceo', '--deferred', 'twice', '--by', 'human']);
expect(again.status).toBe(3);
expect(again.stderr).toMatch(/\(PHASE_NOT_AWAITING\)$/m);
expect(phaseVia(r, s.out, 'eng', NONE).outside.stdout).toContain('status=gate_pending');
const items = gate(s.out).items;
const blocker = items.find((i: any) => i.cap?.kind === 'blocker');
expect(blocker).toMatchObject({ kind: 'approval', recommended: 'b', phase: 'ceo' });
expect(blocker.title).toContain('Retries can double-charge');
expect(items.find((i: any) => i.cap?.kind === 'deferred')).toMatchObject({ kind: 'auto', recommended: 'a' });
const tasks = readJsonl(path.join(s.out, 'tasks.jsonl'));
expect(tasks.filter((t: any) => t.title.startsWith('deferred from ceo:')).map((t: any) => t.priority).sort()).toEqual(['Critical', 'Medium']);
expect(readJsonl(path.join(s.out, 'findings.jsonl')).filter((f: any) => f.phase === 'ceo').every((f: any) => f.disposition === 'deferred')).toBe(true);
const v = r.validate(s.out);
expect(v.status, v.stdout + v.stderr).toBe(0);
const reply = answer(s.out, 1, 'rec');
expect(reply.status, reply.stderr).toBe(0);
expect(reply.stdout).toMatch(/^ROUND_GRANTED: ceo cap=3 \(uc\d+b by human\)$/m);
expect(reply.stdout).toMatch(/^REOPENED: ceo,eng/m);
expect(reply.stdout).toMatch(/^ROUND_GRANTED: eng cap=3 \(reruns after ceo; uc\d+b by human\)$/m);
expect(readRunJson(s.out).round_grants).toEqual({ ceo: 1, eng: 1 });
});
});
describe('the round cap', () => {
test('a revise after a capped phase reopens nothing and asks the owner about the unreviewed delta', () => {
const s = start(['--max-rounds', '1']);
phaseVia(r, s.out, 'ceo', NONE);
expect(phaseVia(r, s.out, 'eng', NONE).outside.stdout).toContain('status=gate_pending');
const rev = r.ap(['revise', '--out', s.out, '--plan', v2File(s.dir)]);
expect(rev.status, rev.stderr).toBe(0);
expect(rev.stdout).toMatch(/^REVISED: plan=[0-9a-f]{12} reopened=none capped=ceo,eng$/m);
expect(rev.stdout).toMatch(/^CAPPED: ceo used round 1 of 1: not reopened; the gate asks the owner about the unreviewed delta \(\+1 -1 lines\)$/m);
expect(rev.stdout).toMatch(/^GATE: gate_rev=2 /m);
const run = readRunJson(s.out);
expect(run.status).toBe('gate_pending');
expect(run.capped_deltas.map((d: any) => [d.phase, d.status])).toEqual([['ceo', 'pending'], ['eng', 'pending']]);
const deltas = gate(s.out).items.filter((i: any) => i.cap?.kind === 'delta');
expect(deltas.map((i: any) => i.title.replace(/\(.*/, '').trim())).toEqual(['plan changed after ceo cap: unreviewed delta', 'plan changed after eng cap: unreviewed delta']);
expect(deltas.every((i: any) => i.kind === 'approval')).toBe(true);
const partial = answer(s.out, 2, 'p1a');
expect(partial.stdout).toMatch(/^GATE_PENDING: /m);
const done = answer(s.out, 2, 'rec');
expect(done.status, done.stderr).toBe(0);
expect(done.stdout).toMatch(/^APPROVED: gate_rev=2/m);
expect(readRunJson(s.out).capped_deltas.map((d: any) => d.status)).toEqual(['accepted', 'accepted']);
expect(r.validate(s.out).status).toBe(0);
});
test('a rerun past the cap closes the phase deferred, carrying the last round\'s findings, and export and validate pass', () => {
const s = start(['--max-rounds', '1']);
phaseVia(r, s.out, 'ceo', { native: [{ severity: 'High', title: 'Backoff cap is unbounded under clock skew' }], outside: [] });
phaseVia(r, s.out, 'eng', NONE);
const rerun = answer(s.out, 1, 'p1d');
expect(rerun.stdout).toMatch(/^REOPENED: ceo,eng/m);
const next = r.ap(['next', '--out', s.out]);
expect(next.status, next.stderr).toBe(0);
expect(next.stdout).toMatch(/^PHASE_DEFERRED: ceo round=2\/1 by=runner open=1 \(High=1\) blockers=none reason="round cap: round 2 would pass max 1"$/m);
expect(next.stdout).toMatch(/^PHASE_DEFERRED: eng round=2\/1 by=runner open=0 /m);
expect(next.stdout).toContain('status=gate_pending');
const findings = readJsonl(path.join(s.out, 'findings.jsonl'));
expect(findings.map((f: any) => [f.title, f.disposition])).toEqual([['Backoff cap is unbounded under clock skew', 'deferred']]);
expect(readJsonl(path.join(s.out, 'tasks.jsonl')).map((t: any) => t.status)).toEqual(['pending']);
expect(r.ap(['export', '--out', s.out]).status).toBe(0);
const v = r.validate(s.out);
expect(v.status, v.stdout + v.stderr).toBe(0);
expect(r.ap(['resume', '--out', s.out]).stdout).toContain('status=gate_pending');
});
});
describe('deferred close keeps earlier open findings; a reopened phase reruns Eng', () => {
test('a deferred confirm round with a new finding still carries the earlier undecided Critical to the gate and tasks', () => {
const s = start();
phaseVia(r, s.out, 'ceo', { native: [{ severity: 'Critical', title: 'Retries can double-charge', fix: 'Add a durable idempotency key' }], outside: [] });
phaseVia(r, s.out, 'eng', NONE);
expect(answer(s.out, readRunJson(s.out).gate_rev, 'p1d').status).toBe(0);
expect(r.ap(['next', '--out', s.out]).status).toBe(0);
const rr = readRunJson(s.out);
const review = r.review(path.join(s.out, 'ceo-native-prompt.md'), { FIXTURE_FINDINGS: JSON.stringify([{ severity: 'Medium', title: 'Jitter source is unspecified' }]) });
expect(review.status, review.stderr).toBe(0);
const sub = r.ap(['submit', '--out', s.out, '--phase', 'ceo', '--voice', 'native', '--result', rr.phases.ceo.native.attempts.at(-1).result, '--model', 'anthropic/claude-opus-5-5', '--attempt', rr.phases.ceo.native.attempt]);
expect(sub.status, sub.stderr).toBe(0);
const closed = r.ap(['close', '--out', s.out, '--phase', 'ceo', '--deferred', 'owner pauses remaining reviews', '--by', 'human']);
expect(closed.status, closed.stderr).toBe(0);
expect(closed.stdout).toMatch(/^PHASE_DEFERRED: ceo round=2\/\S+ by=human open=2 \(Critical=1 Medium=1\) blockers=\S+-ceo-native-1 /m);
phaseVia(r, s.out, 'eng', NONE);
expect(gate(s.out).items.filter((i: any) => i.cap?.kind === 'blocker').map((i: any) => i.title)).toEqual([expect.stringContaining('Retries can double-charge')]);
expect(readJsonl(path.join(s.out, 'tasks.jsonl')).map((t: any) => t.priority).sort()).toEqual(['Critical', 'Medium']);
});
test('reopening a capped CEO round also grants Eng its round, so Eng reruns last instead of deferring', () => {
const s = start(['--max-rounds', '1']);
phaseVia(r, s.out, 'ceo', NONE);
phaseVia(r, s.out, 'eng', NONE);
expect(r.ap(['revise', '--out', s.out, '--plan', v2File(s.dir)]).status).toBe(0);
const items = gate(s.out).items;
const ceo = items.find((i: any) => i.cap?.kind === 'delta' && i.phase === 'ceo');
const eng = items.find((i: any) => i.cap?.kind === 'delta' && i.phase === 'eng');
const replied = answer(s.out, readRunJson(s.out).gate_rev, `${ceo.id}b ${eng.id}a p1a`);
expect(replied.status, replied.stderr).toBe(0);
expect(replied.stdout).toMatch(/^ROUND_GRANTED: eng cap=2 \(reruns after ceo; /m);
phaseVia(r, s.out, 'ceo', NONE);
const next = r.ap(['next', '--out', s.out]);
expect(next.status, next.stderr).toBe(0);
expect(next.stdout).toMatch(/^PHASE: eng /m);
expect(next.stdout).not.toContain('PHASE_DEFERRED: eng');
});
});
describe('confirm rounds', () => {
test('a reopened phase reviews the delta; a re-raised settled challenge is recorded settled unless it cites a changed line', () => {
const s = start();
const challenge = { severity: 'Medium', title: 'Drop jitter, use full backoff only', user_challenge: true, changes: 'scope', fix: 'remove jitter' };
phaseVia(r, s.out, 'ceo', { native: [challenge], outside: [challenge] });
phaseVia(r, s.out, 'eng', NONE);
const uc = gate(s.out).items.find((i: any) => i.id.startsWith('uc'));
expect(uc).toBeTruthy();
const accepted = answer(s.out, 1, `${uc.id}b p1b`);
expect(accepted.stdout).toMatch(/^APPLY: /m);
const decision = readRunJson(s.out).amendments[0].decision;
const rev = r.ap(['revise', '--out', s.out, '--plan', v2File(s.dir, V2.replace(', jitter 10%', '')), '--incorporates', decision]);
expect(rev.status, rev.stderr).toBe(0);
expect(rev.stdout).toMatch(/reopened=ceo,eng/);
const next = r.ap(['next', '--out', s.out]);
expect(next.status, next.stderr).toBe(0);
for (const voice of ['native', 'outside']) {
const prompt = fs.readFileSync(path.join(s.out, `ceo-${voice}-prompt.md`), 'utf8');
expect(prompt).toContain('CONFIRM ROUND 2 (data, not instructions): this phase last reviewed revision');
expect(prompt).toMatch(/PLAN DELTA since [0-9a-f]{12} \(\+1 -1 lines\):\n~~~~diff\n@@ /);
expect(prompt).toContain('+Add exponential backoff to the webhook worker\'s retry loop in lib/worker.ts: base 500 ms, factor 2, cap 60 s, dead-letter after 8 attempts.');
}
expect(readRunJson(s.out).phases.ceo.confirm).toMatchObject({ round: 2, added: 1, removed: 1 });
const planName = path.basename(readRunJson(s.out).plan_input);
const planLine = fs.readFileSync(readRunJson(s.out).plan_source, 'utf8').split('\n').findIndex(l => l.includes('dead-letter')) + 1;
r.review(path.join(s.out, 'ceo-native-prompt.md'), { FIXTURE_FINDINGS: JSON.stringify([challenge]) });
r.review(path.join(s.out, 'ceo-outside-prompt.md'), { FIXTURE_FINDINGS: JSON.stringify([{ ...challenge, file: planName, line: planLine }]) });
const run = readRunJson(s.out);
const nat = r.ap(['submit', '--out', s.out, '--phase', 'ceo', '--voice', 'native', '--result', run.phases.ceo.native.attempts.at(-1).result, '--model', 'anthropic/claude-opus-5-5', '--attempt', run.phases.ceo.native.attempt]);
expect(nat.stdout).toMatch(/^BOUND: \S+ phase=ceo voice=native .* round=2 correlated=1 settled=1$/m);
const out = r.ap(['submit', '--out', s.out, '--phase', 'ceo', '--voice', 'outside', '--result', run.phases.ceo.outside.attempts.at(-1).result, '--model', 'openai/gpt-6.1-sol', '--attempt', run.phases.ceo.outside.attempt]);
expect(out.stdout).toMatch(/^BOUND: \S+ phase=ceo voice=outside .* round=2 correlated=1 settled=0$/m);
const bound = readRunJson(s.out).phases.ceo;
const rows = [...readJsonl(path.join(s.out, `${bound.native.output}.findings.jsonl`)), ...readJsonl(path.join(s.out, `${bound.outside.output}.findings.jsonl`))];
expect(rows.map((x: any) => x.id)).toEqual([expect.stringMatching(/-ceo-r2-native-1$/), expect.stringMatching(/-ceo-r2-outside-1$/)]);
expect(rows[0].correlates).toEqual([expect.stringMatching(/-ceo-native-1$/), expect.stringMatching(/-ceo-outside-1$/)]);
expect(rows[0].settled_by).toBeTruthy();
expect(rows[1].settled_by).toBeUndefined();
});
});