mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-08 17:58:13 +01:00
fix: detect a hollow screen from the plan it uploads (no paid-plan artifact or nothing planned), correct two troubleshooting facts, and normalise result --pr
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
15f551c312
commit
da8bdb4289
10 files changed
+65
-37
No files matched your search
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
### Changed
|
||||
- **`gstack-ship-queue ready`** refuses with `QUEUE_RECEIPT_MISSING` when the PR body has no valid ```gstack-ship-receipt``` block and with `QUEUE_RECEIPT_STALE` when the receipt's `head` is not the head being READY'd (or its `pr` is another PR). The re-READY after a slot's stamp push accepts the receipt written for the pin when HEAD is the pin plus stamp files, or (from slot 2 on) the pin merged with main plus stamp files. The informational `SHIP_RECEIPT:` line still prints in the block, and it is one line whatever the receipt's strings contain (control characters are collapsed, so a crafted `base` cannot add a forged `MERGE_CONDITION:` line to `verify --train`'s output).
|
||||
- **`gstack-ship-queue ready --screen` refuses a stale merge ref** with the new `QUEUE_SCREEN_STALE_REF` (GSTA-95 on #3431): GitHub stops recomputing `refs/pull/<n>/merge` while a PR conflicts with the base (a CHANGELOG-only conflict after a stamp counts), so a screen dispatched there planned 0 cases, reported success and `ready` printed `SCREEN_DISPATCH:` as if a screen ran. `ready` now compares the merge ref's head parent with the PR head before dispatching (a missing merge ref carries the same code), never adopts a finished screen whose record says it planned 0 cases, and `train build` prints `SCREEN: #<n> run <id> planned 0 cases; counted as not-run` and seats that slot unscreened.
|
||||
- **`gstack-ship-queue ready --screen` refuses a stale merge ref** with the new `QUEUE_SCREEN_STALE_REF` (GSTA-95 on #3431): GitHub stops recomputing `refs/pull/<n>/merge` while a PR conflicts with the base (a CHANGELOG-only conflict after a stamp counts), so a screen dispatched there planned 0 cases, reported success and `ready` printed `SCREEN_DISPATCH:` as if a screen ran. `ready` now compares the merge ref's head parent with the PR head before dispatching (a missing merge ref carries the same code), never adopts a finished screen that planned no cases (a superseded run skips plan-slices and uploads no `paid-plan` artifact; a manifest with nothing planned counts the same), and `train build` prints `SCREEN: #<n> run <id> planned no cases …; counted as not-run` and seats that slot unscreened.
|
||||
- **`gstack-ship-queue train build`** refuses a pin whose PR has no current receipt with the same codes, naming the PR, before any write, push or dispatch, `--dry-run` included. **`verify --train`** refuses the slot the same way before `MERGE_CONDITION`, and accepts a receipt written for the slot's pinned head when the current head is its stamp commit (the stamp changes only stamp files, or merges main into the pin from slot 2 on), since no /ship runs on a stamp.
|
||||
- **`gstack-ship-receipt result`** (new) is /ship's terminal step: it binds the posted receipt to HEAD, writes `<state root>/ship/runs/<utc>-<head12>/{ship-run.json,ship-receipt.md}` (a /ship record, not a `run` v1 manifest: `gstack-artifact validate` does not apply) and prints `GSTACK_RESULT: skill=ship status=complete run=<dir>`, `status=refused run=-` with the receipt code, or `status=incomplete run=-` when gh cannot read the PR, so a parent's merge check reads `ship-run.json` instead of parsing the PR body. The ship skill's new Step 22 runs it after the pr-body and handoff sections.
|
||||
- **Unattended /ship may update an existing PR's body.** `gstack-skill-start`'s unattended block and `UNATTENDED_SHIP:` line, the ship skill's Step 18 and `docs/unattended.md` now say: when an open PR for the branch exists and HEAD is its pushed head, Step 18 takes the pr-body section's existing-PR path (body with the receipt first, and title); it never runs `pr-create` and never pushes a commit; anything else takes the handoff section. Before this the block said the receipt lived only in `.gstack/tmp/ship-receipt.md` while rule 22 and `ready` read it from the PR body.
|
||||
|
||||
@@ -202,8 +202,9 @@ function cmdRead(rest: string[], json: boolean): number {
|
||||
*/
|
||||
function cmdResult(rest: string[], json: boolean): number {
|
||||
const bodyFile = flag(rest, '--body');
|
||||
const pr = flag(rest, '--pr');
|
||||
if (pr !== undefined && !/^\d+$/.test(pr)) { process.stderr.write(`gstack-ship-receipt result: --pr must be a PR number, got ${JSON.stringify(pr.slice(0, 40))}\n`); return EXIT.usage; }
|
||||
const prFlag = flag(rest, '--pr');
|
||||
if (prFlag !== undefined && !/^[1-9]\d*$/.test(prFlag)) { process.stderr.write(`gstack-ship-receipt result: --pr must be a PR number, got ${JSON.stringify(prFlag.slice(0, 40))}\n`); return EXIT.usage; }
|
||||
const pr = prFlag;
|
||||
const repoRoot = git(['rev-parse', '--show-toplevel']);
|
||||
const head = flag(rest, '--head') ?? (repoRoot ? git(['rev-parse', 'HEAD'], repoRoot) : undefined);
|
||||
const refused = (code: 'QUEUE_RECEIPT_MISSING' | 'QUEUE_RECEIPT_STALE', message: string) => {
|
||||
|
||||
+5
-4
@@ -373,10 +373,11 @@ it. Before dispatching, `ready` fetches the merge ref and requires its second
|
||||
parent to be the PR head: a missing or lagging merge ref (GitHub stops
|
||||
recomputing it while the PR conflicts with the base, CHANGELOG-only conflicts
|
||||
included) is `QUEUE_SCREEN_STALE_REF`, because a screen dispatched there plans
|
||||
no cases and reports success. A finished screen whose record planned 0 cases
|
||||
is never followed as a screen: `ready --screen` dispatches a fresh one and
|
||||
`train build` prints `SCREEN: #<n> run <id> planned 0 cases; counted as
|
||||
not-run` and seats the slot unscreened.
|
||||
no cases and reports success. A finished screen that planned no cases (no
|
||||
`paid-plan` artifact, which a superseded run never uploads, or a plan manifest
|
||||
with nothing planned) is never followed as a screen: `ready --screen`
|
||||
dispatches a fresh one and `train build` prints `SCREEN: #<n> run <id> planned
|
||||
no cases …; counted as not-run` and seats the slot unscreened.
|
||||
|
||||
**2. Coordinator: build.** Name the pins exactly as READY reported them:
|
||||
|
||||
|
||||
+18
-10
@@ -1604,8 +1604,9 @@ message lists.
|
||||
|
||||
**Meaning.** On a Capy machine (`GSTACK_CAPY=1`, set by the `GSTACK_LAUNCH`
|
||||
line), an unattended or spawned workflow starts only with a passing install
|
||||
verdict (`<state root>/install-verdicts/<sha256 of the checkout path>.json`,
|
||||
named as `GSTACK_INSTALL_VERDICT`) for this checkout, its current HEAD and a
|
||||
verdict (`<state root>/install-verdicts/<sha256 of the checkout path>/<sha256
|
||||
of the project directory, or none>.json`, named as `GSTACK_INSTALL_VERDICT`)
|
||||
for this checkout, its current HEAD and a
|
||||
workflow at least as wide as the one starting: a verdict for `--for autoplan`
|
||||
does not cover `qa`'s browser, and a planning-only pins SKIP does not cover a
|
||||
workflow that runs the repo's tests. `gstack-skill-start` printed this row and
|
||||
@@ -1613,8 +1614,10 @@ workflow that runs the repo's tests. `gstack-skill-start` printed this row and
|
||||
an unattended run whose Bun on PATH is below the supported minimum
|
||||
(`INSTALL: fail runtime ... (RUNTIME_BELOW_MINIMUM)`) on any host.
|
||||
|
||||
**Fix.** Run the Capy skill installer with `--for <workflow>` on this machine
|
||||
and launch with the `GSTACK_LAUNCH` line it prints.
|
||||
**Fix.** Run the Capy skill installer (`scripts/install.sh --for <workflow>`)
|
||||
from the project directory the workflow will run in, since the verdict is
|
||||
keyed by that directory (GSTA-85), and launch with the `GSTACK_LAUNCH` line it
|
||||
prints.
|
||||
`GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1` admits anyway (printed as
|
||||
`INSTALL_VERDICT: overridden`); record it as a deviation.
|
||||
|
||||
@@ -4035,8 +4038,11 @@ before merging PRs; [ship-policy.md](ship-policy.md#ship-before-ready)).
|
||||
|
||||
**Fix.** Run /ship on the branch. Unattended, it ends by writing the receipt
|
||||
into the PR body (`gstack-ship-receipt write --pr <n> … --body <file>`, then
|
||||
`gstack-post pr-body`). Then rerun `gstack-ship-queue ready --pr <n>
|
||||
--screen|--no-screen` and send the new READY block.
|
||||
`gstack-post pr-body`) when an open PR exists for the branch at HEAD;
|
||||
otherwise it leaves the receipt in `.gstack/tmp/ship-receipt.md` and the
|
||||
parent posts it ([unattended.md](unattended.md)). Then rerun
|
||||
`gstack-ship-queue ready --pr <n> --screen|--no-screen` and send the new READY
|
||||
block.
|
||||
|
||||
<a id="queue-receipt-stale"></a>
|
||||
### `QUEUE_RECEIPT_STALE`
|
||||
@@ -4064,10 +4070,12 @@ counts), so a screen dispatched there checks out the old merge, sees itself
|
||||
superseded, plans 0 cases and reports success. GSTA-95 hit this on #3431:
|
||||
`ready` printed `SCREEN_DISPATCH:` as if a screen ran. `ready` now compares
|
||||
the merge ref's second parent with the PR head before dispatching, and a
|
||||
finished screen whose record says it planned 0 cases is never adopted as a
|
||||
screen: `ready --screen` dispatches a fresh one and `train build` prints
|
||||
`SCREEN: #<n> run <id> planned 0 cases; counted as not-run` and seats the slot
|
||||
unscreened.
|
||||
finished screen that planned no cases is never adopted as a screen. A screen
|
||||
run uploads no train record, so the evidence is its plan: a superseded run
|
||||
skips `plan-slices` and uploads no `paid-plan` artifact, and a manifest with
|
||||
nothing planned counts the same. `ready --screen` dispatches a fresh one and
|
||||
`train build` prints `SCREEN: #<n> run <id> planned no cases (no paid-plan
|
||||
artifact; …); counted as not-run` and seats the slot unscreened.
|
||||
|
||||
**Fix.** Resolve the conflict with the base branch (or wait for GitHub to
|
||||
recompute the merge ref), push, and run `gstack-ship-queue ready --pr <n>
|
||||
|
||||
@@ -136,7 +136,7 @@ export const RELEASE_RESULT_CODES = {
|
||||
QUEUE_RECEIPT_MISSING: {
|
||||
anchor: 'queue-receipt-missing',
|
||||
summary: 'the PR body carries no valid /ship receipt, so the PR was never shipped through gstack and cannot be READY’d, admitted to a train or merged',
|
||||
fix: 'run /ship on the branch (unattended it writes the receipt with gstack-ship-receipt write --pr <n> --body …), then gstack-ship-queue ready again; there is no flag to skip this',
|
||||
fix: 'run /ship on the branch (unattended it writes the receipt into an open PR at HEAD with gstack-ship-receipt write --pr <n> --body …, else leaves it in .gstack/tmp/ship-receipt.md for the parent to post), then gstack-ship-queue ready again; there is no flag to skip this',
|
||||
},
|
||||
QUEUE_RECEIPT_STALE: {
|
||||
anchor: 'queue-receipt-stale',
|
||||
|
||||
@@ -70,22 +70,32 @@ export function downloadJson(ctx: QueueContext, slug: string, run: number, name:
|
||||
}
|
||||
|
||||
/**
|
||||
* A finished screen run that planned no cases is not a screen: evals.yml plans
|
||||
* 0 cases and reports success when the merge ref it was dispatched for was
|
||||
* superseded (GSTA-95 on #3431). Returns the reason when the run is hollow,
|
||||
* null when it is live, still running, or its census cannot be read.
|
||||
* A finished screen run that planned no cases is not a screen: when the merge
|
||||
* ref it was dispatched for was superseded, evals.yml skips plan-slices and
|
||||
* concludes success (GSTA-95 on #3431). A screen run uploads no train record,
|
||||
* so the evidence is the plan itself: no `paid-plan` artifact, or a manifest
|
||||
* with nothing planned. Returns the reason when the run is hollow, null when
|
||||
* it is live, still running, or its plan cannot be read.
|
||||
*/
|
||||
export function hollowScreen(ctx: QueueContext, slug: string, run: number, artifact: string): string | null {
|
||||
export function hollowScreen(ctx: QueueContext, slug: string, run: number): string | null {
|
||||
const view = runView(ctx, slug, run);
|
||||
if (!view || view.status !== 'completed') return null;
|
||||
const list = json<{ artifacts?: Array<{ name?: string; expired?: boolean }> }>(ctx.gh(['api', `repos/${slug}/actions/runs/${run}/artifacts?per_page=100`]));
|
||||
if (!Array.isArray(list?.artifacts)) return null;
|
||||
const plan = list.artifacts.find(a => a.name === PLAN_ARTIFACT);
|
||||
if (!plan) return `run ${run} planned no cases (no ${PLAN_ARTIFACT} artifact; concluded ${view.conclusion ?? 'unknown'})`;
|
||||
if (plan.expired) return null;
|
||||
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-screen-'));
|
||||
let rec: { census?: { planned?: { rule?: unknown; judge?: unknown } } } | null;
|
||||
try { rec = downloadJson(ctx, slug, run, artifact, scratch) as typeof rec; } finally { fs.rmSync(scratch, { recursive: true, force: true }); }
|
||||
const planned = rec?.census?.planned;
|
||||
if (!planned || typeof planned.rule !== 'number' || typeof planned.judge !== 'number') return null;
|
||||
return planned.rule + planned.judge === 0 ? `run ${run} planned 0 cases (concluded ${view.conclusion ?? 'unknown'})` : null;
|
||||
let manifest: { entries?: Array<{ status?: unknown }> } | null;
|
||||
try { manifest = downloadJson(ctx, slug, run, PLAN_ARTIFACT, scratch) as typeof manifest; } finally { fs.rmSync(scratch, { recursive: true, force: true }); }
|
||||
if (!Array.isArray(manifest?.entries)) return null;
|
||||
const planned = manifest.entries.filter(e => e?.status === 'planned').length;
|
||||
return planned === 0 ? `run ${run} planned 0 cases (concluded ${view.conclusion ?? 'unknown'})` : null;
|
||||
}
|
||||
|
||||
/** The plan manifest every evals.yml run that planned anything uploads (plan-slices); a superseded screen never reaches that job. */
|
||||
const PLAN_ARTIFACT = 'paid-plan';
|
||||
|
||||
/** The named check run on a commit (the newest when several); null when absent or unreadable. */
|
||||
export function checkRun(ctx: QueueContext, sha: string, name: string): CheckInfo | null {
|
||||
const r = json<{ check_runs?: Array<{ name?: string; status?: string; conclusion?: string | null }> }>(
|
||||
|
||||
@@ -85,7 +85,7 @@ async function dispatchScreen(env: TrainEnv, o: ReadyOptions, mergeSha: string,
|
||||
if (!slug) return outRefused({ code: 'QUEUE_PROOF_MISSING', message: 'gh repo view could not name the repository' }, [], `${Q} ready --pr ${o.pr} --screen`, 'none');
|
||||
const exec = execFor(env.ctx);
|
||||
const existing = findDuplicateRun(listDispatchRuns(dispatch.workflow, exec, slug) ?? [], dedupeKey('pr', mergeSha, baseSha, scope));
|
||||
const hollow = existing ? hollowScreen(env.ctx, slug, existing.databaseId, env.train.record_artifact) : null;
|
||||
const hollow = existing ? hollowScreen(env.ctx, slug, existing.databaseId) : null;
|
||||
if (existing && !hollow) return { choice: { choice: 'screen', policy, nonce: /eval-bg (\S+?)\]/.exec(existing.displayTitle)?.[1] ?? nonce, run: existing.databaseId, merge_sha: mergeSha }, line: `SCREEN_DISPATCH: following run ${existing.databaseId} (same merge SHA, base and policy; no duplicate)` };
|
||||
const found = dispatchWorkflow(dispatch, env.ctx.base, exec, slug) ? await awaitNonceRun(dispatch.workflow, nonce, exec, slug) : null;
|
||||
if (!found) return outPending({ code: 'QUEUE_TRAIN_DISPATCH_UNKNOWN', message: `no ${dispatch.workflow} run named ${nonce} appeared after the screen dispatch` }, [], `${Q} ready --pr ${o.pr} --screen${o.broad ? ' --broad' : ''} (adopts the run by its key once it appears)`, 'none');
|
||||
|
||||
@@ -302,7 +302,7 @@ function admitSlots(env: TrainEnv, pins: Array<{ pr: string; sha: string }>): {
|
||||
if (!ready) return outRefused({ code: 'QUEUE_NOT_READY', message: `no READY receipt for #${p.pr} at ${info.headRefOid.slice(0, 12)} (local state or ${'refs/notes/gstack-trains'})` }, [], `the lane runs ${Q} ready --pr ${p.pr} --screen|--no-screen`, 'none');
|
||||
let screen = ready.screen;
|
||||
if (screen.choice === 'screen' && screen.run && slug === undefined) slug = repoSlug(ctx);
|
||||
const hollow = screen.choice === 'screen' && screen.run && slug ? hollowScreen(ctx, slug, screen.run, env.train.record_artifact) : null;
|
||||
const hollow = screen.choice === 'screen' && screen.run && slug ? hollowScreen(ctx, slug, screen.run) : null;
|
||||
if (hollow) { lines.push(`SCREEN: #${p.pr} ${hollow}; counted as not-run, the slot rides unscreened`); screen = { ...screen, choice: 'no-screen' }; }
|
||||
slots.push({ pr: p.pr, head: info.headRefOid, ready_at: ready.ready_at, screen });
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ export const TRAIN_POLICY = { ...BASE_POLICY, train: TRAIN_BLOCK, min_gstack_ver
|
||||
export interface Rule {
|
||||
fail?: boolean; silent?: boolean; failButCreate?: boolean; status?: string; conclusion?: string | null; event?: string; headSha?: string;
|
||||
record?: { panels: unknown[]; census?: unknown; verdict?: 'GREEN' | 'RED'; nonce?: string };
|
||||
/** The run's paid-plan manifest: planned entries (default 1); null uploads no paid-plan artifact, as a superseded screen does. */
|
||||
plan?: { planned: number } | null;
|
||||
check?: string | null;
|
||||
bisect?: { prefixes: string[]; patterns: Record<string, string>; costUsd?: number };
|
||||
}
|
||||
@@ -155,6 +157,7 @@ function shimSource(): string {
|
||||
" const run = { databaseId: id, workflow: w, headBranch: ref, headSha: head, status: rule.status || 'completed', conclusion: rule.conclusion === undefined ? 'success' : rule.conclusion,",
|
||||
" url: 'https://example.invalid/runs/' + id, event: rule.event || 'workflow_dispatch', displayTitle: inputs.nonce ? 'E2E Evals [eval-bg ' + inputs.nonce + ']' : w, inputs };",
|
||||
" d.runs.push(run); d.artifacts[id] = {};",
|
||||
" if (rule.plan !== null) d.artifacts[id]['paid-plan'] = { tier: 'gate', sliceCount: 1, entries: Array.from({ length: rule.plan ? rule.plan.planned : 1 }, (_, i) => ({ file: 'test/case-' + i + '.test.ts', status: 'planned' })) };",
|
||||
" if (rule.record && !inputs.prefix_shas) d.artifacts[id][d.artifact] = { schema: 1, kind: 'train-gate', nonce: rule.record.nonce || inputs.nonce, head_sha: head, tree: rev(head + '^{tree}'),",
|
||||
" verdict: rule.record.verdict || 'GREEN', census: rule.record.census, panels: rule.record.panels };",
|
||||
" if (rule.check && !inputs.prefix_shas) (d.checks[head] = d.checks[head] || []).push({ name: 'gstack/full-gate', status: 'completed', conclusion: rule.check });",
|
||||
@@ -176,6 +179,8 @@ function shimSource(): string {
|
||||
" fs.mkdirSync(opt('-D'), { recursive: true }); fs.writeFileSync(path.join(opt('-D'), 'record.json'), JSON.stringify(art)); save(); process.exit(0);",
|
||||
"}",
|
||||
"if (a[0] === 'api') {",
|
||||
" const arts = /actions\\/runs\\/(\\d+)\\/artifacts/.exec(a[1] || '');",
|
||||
" if (arts) { const names = Object.keys(d.artifacts[arts[1]] || {}); out({ total_count: names.length, artifacts: names.map(name => ({ name, expired: false })) }); }",
|
||||
" const named = /commits\\/([0-9a-f]+)\\/check-runs\\?check_name=([^&]+)/.exec(a[1] || '');",
|
||||
" if (named) { const list = (d.checks[named[1]] || []).filter(c => c.name === decodeURIComponent(named[2])); out({ total_count: list.length, check_runs: list }); }",
|
||||
" const paged = /commits\\/([0-9a-f]+)\\/(check-runs|status)\\?.*page=(\\d+)/.exec(a[1] || '');",
|
||||
|
||||
@@ -238,26 +238,29 @@ describe('ready under a train policy: the screen choice', () => {
|
||||
// fails_when=the census is not read (a hollow run is followed, or ORDER says screened); why_new=no test reads a screen run's record; seam=none; no_claim=evals.yml's real artifact (the shim's record stands in)
|
||||
test('a screen run that planned 0 cases is not followed by ready and is counted as not-run by train build', () => {
|
||||
const { fx, head, pregate } = readyFixture();
|
||||
fx.write(d => { d.rules['evals.yml'] = { status: 'completed', conclusion: 'success', record: { verdict: 'GREEN', census: { complete: true, planned: { rule: 0, judge: 0 }, executed: { rule: 0, judge: 0 } }, panels: [] } }; });
|
||||
fx.write(d => { d.rules['evals.yml'] = { status: 'completed', conclusion: 'success', plan: null }; });
|
||||
const first = queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--screen']);
|
||||
expect(first.status).toBe(EXIT.ok);
|
||||
expect(line(first.out, 'SCREEN_DISPATCH:')).toMatch(/^SCREEN_DISPATCH: evals\.yml run 100 /);
|
||||
const again = queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--screen']);
|
||||
expect(again.status).toBe(EXIT.ok);
|
||||
expect(line(again.out, 'SCREEN_DISPATCH:')).toMatch(/^SCREEN_DISPATCH: evals\.yml run 101 .* \(not following run 100 planned 0 cases \(concluded success\): a screen that planned nothing is not a screen\)$/);
|
||||
expect(line(again.out, 'SCREEN_DISPATCH:')).toMatch(/^SCREEN_DISPATCH: evals\.yml run 101 .* \(not following run 100 planned no cases \(no paid-plan artifact; concluded success\): a screen that planned nothing is not a screen\)$/);
|
||||
expect(dispatches(fx, 'evals.yml').length).toBe(2);
|
||||
const build = queue(fx, ['train', 'build', '--prs', `1@${head}`, '--dry-run']);
|
||||
expect(build.status).toBe(EXIT.ok);
|
||||
expect(line(build.out, 'SCREEN:')).toBe('SCREEN: #1 run 101 planned 0 cases (concluded success); counted as not-run, the slot rides unscreened');
|
||||
expect(line(build.out, 'SCREEN:')).toBe('SCREEN: #1 run 101 planned no cases (no paid-plan artifact; concluded success); counted as not-run, the slot rides unscreened');
|
||||
expect(line(build.out, 'ORDER:')).toBe('ORDER: 1:#1(unscreened)');
|
||||
fx.write(d => { d.rules['evals.yml'] = { status: 'completed', conclusion: 'success', record: { verdict: 'GREEN', census: { complete: true, planned: { rule: 0, judge: 3 }, executed: { rule: 0, judge: 3 } }, panels: [] } }; });
|
||||
fx.write(d => { d.rules['evals.yml'] = { status: 'completed', conclusion: 'success', plan: { planned: 0 } }; });
|
||||
const emptyPlan = queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--screen']);
|
||||
expect(line(emptyPlan.out, 'SCREEN_DISPATCH:')).toMatch(/^SCREEN_DISPATCH: evals\.yml run 102 .*not following run 101 planned no cases/);
|
||||
fx.write(d => { d.rules['evals.yml'] = { status: 'completed', conclusion: 'success', plan: { planned: 3 } }; });
|
||||
const live = queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--screen']);
|
||||
expect(live.status).toBe(EXIT.ok);
|
||||
expect(line(live.out, 'SCREEN_DISPATCH:')).toMatch(/^SCREEN_DISPATCH: evals\.yml run 102 .*not following run 101 planned 0 cases/);
|
||||
expect(line(live.out, 'SCREEN_DISPATCH:')).toMatch(/^SCREEN_DISPATCH: evals\.yml run 103 .*not following run 102 planned 0 cases \(concluded success\)/);
|
||||
const followed = queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--screen']);
|
||||
expect(followed.status).toBe(EXIT.ok);
|
||||
expect(line(followed.out, 'SCREEN_DISPATCH:')).toBe('SCREEN_DISPATCH: following run 102 (same merge SHA, base and policy; no duplicate)');
|
||||
expect(dispatches(fx, 'evals.yml').length).toBe(3);
|
||||
expect(line(followed.out, 'SCREEN_DISPATCH:')).toBe('SCREEN_DISPATCH: following run 103 (same merge SHA, base and policy; no duplicate)');
|
||||
expect(dispatches(fx, 'evals.yml').length).toBe(4);
|
||||
const liveBuild = queue(fx, ['train', 'build', '--prs', `1@${head}`, '--dry-run']);
|
||||
expect([liveBuild.status, line(liveBuild.out, 'ORDER:'), line(liveBuild.out, 'SCREEN:')]).toEqual([EXIT.ok, 'ORDER: 1:#1(screened)', '']);
|
||||
fx.write(d => { d.rules['evals.yml'] = { status: 'in_progress', conclusion: null }; });
|
||||
|
||||
Reference in new issue
Block a user