mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-06 00:44:07 +01:00
fix: cycle-2 review fixes (private screen scratch, no bare-merge receipt reuse, 12-hex --head, one receipt scrub) and align the unattended hook text with the GSTACK_RESULT writers (GSTA-91)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
ec438fe8b1
commit
1490d9d438
13 files changed
+74
-28
No files matched your search
+1
-1
@@ -15,7 +15,7 @@
|
||||
- **Gate edit (size budgets):** `test/helpers/carve-guards.ts` raises ship's `maxSkeletonBytes` 81,000 → 81,600 and `maxSizeRatio` 1.6690 → 1.6740 for Step 22 and the Step 18 routing, offset in part by tightening Steps 14, 16, 18 and 20 prose (measured skeleton 81,535, union 314,027; the 2026-10-14 tightening had left 2 bytes of headroom). No threshold, trial count or case kind changed.
|
||||
|
||||
### Tests
|
||||
- `test/ship-queue-train.test.ts`: READY, train build (dry run and live) and verify --train refuse a missing, invalid, other-head and other-PR receipt and admit the current one; the pinned-head receipt admits a stamp commit; verify refuses `QUEUE_PROOF_MISSING` when gh cannot read the PR; `--screen` refuses a lagging or missing merge ref and never follows a 0-case screen. The train fixture's `refs/pull/<n>/merge` is now a real merge commit (`mergeRefCommit`), as GitHub keeps it. `test/gstack-ship-queue.test.ts`: `ready` at a stamp commit (slot 1, and slot 2 with main merged in) accepts the pin's receipt. `test/gstack-ship-receipt.test.ts`: the `bindReceipt` head-matching table (12-character floor, other PR, one-line `SHIP_RECEIPT:`), `result --body` refusals, `--head`, a non-git cwd and gh failures. The train fixture's `openPr` now writes a receipt for the head it opens (`shipped: false` leaves the body empty). `test/install-verdict.test.ts` pins the GSTA-85 fix in both twins; `test/gstack-skill-start.test.ts` pins the new unattended /ship block.
|
||||
- `test/ship-queue-train.test.ts`: READY, train build (dry run and live) and verify --train refuse a missing, invalid, other-head and other-PR receipt and admit the current one; the pinned-head receipt admits a stamp commit; verify refuses `QUEUE_PROOF_MISSING` when gh cannot read the PR; `--screen` refuses a lagging or missing merge ref and never follows a 0-case screen. The train fixture's `refs/pull/<n>/merge` is now a real merge commit (`mergeRefCommit`), as GitHub keeps it. `test/gstack-ship-queue.test.ts`: `ready` at a stamp commit (slot 1, and slot 2 with main merged in) accepts the pin's receipt; a bare merge of main with no stamp on top, or an edit on the pin side under the slot-2 stamp, is `QUEUE_RECEIPT_STALE`. `test/auq-error-fallback-hook.test.ts`: the unattended directive names the same `GSTACK_RESULT` writers as docs/unattended.md and `gstack-skill-start` (GSTA-91). `test/gstack-ship-receipt.test.ts`: the `bindReceipt` head-matching table (12-character floor, other PR, one-line `SHIP_RECEIPT:`), `result --body` refusals, `--head`, a non-git cwd and gh failures. The train fixture's `openPr` now writes a receipt for the head it opens (`shipped: false` leaves the body empty). `test/install-verdict.test.ts` pins the GSTA-85 fix in both twins; `test/gstack-skill-start.test.ts` pins the new unattended /ship block.
|
||||
|
||||
## [1.91.224.0] - 2026-10-25
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ function cmdResult(rest: string[], json: boolean): number {
|
||||
return EXIT.refused;
|
||||
};
|
||||
if (!head) { process.stderr.write('gstack-ship-receipt result: --head <sha> is required outside a git repository\n'); return EXIT.usage; }
|
||||
if (!/^[0-9a-f]{7,64}$/i.test(head)) { process.stderr.write(`gstack-ship-receipt result: --head must be a hex commit id, got ${JSON.stringify(head.slice(0, 40))}\n`); return EXIT.usage; }
|
||||
if (!/^[0-9a-f]{12,64}$/i.test(head)) { process.stderr.write(`gstack-ship-receipt result: --head must be a hex commit id of at least 12 characters, got ${JSON.stringify(head.slice(0, 40))}\n`); return EXIT.usage; }
|
||||
const incomplete = (why: string) => {
|
||||
process.stderr.write(`gstack-ship-receipt result: ${why}\n`);
|
||||
process.stdout.write(json ? JSON.stringify({ skill: 'ship', status: 'incomplete', run: '-', message: why }) + '\n' : 'GSTACK_RESULT: skill=ship status=incomplete run=-\n');
|
||||
|
||||
+4
-2
@@ -334,8 +334,10 @@ on it: the receipt written for the pinned head stays the binding one. `verify
|
||||
--train` accepts a receipt whose `head` is either the slot's pinned head or
|
||||
the current head, and the re-READY after the stamp push (`ready` at the stamp
|
||||
commit) accepts the receipt for the pin when HEAD is the pin plus stamp files,
|
||||
or (from slot 2 on) the pin merged with main plus stamp files; any other head
|
||||
is `QUEUE_RECEIPT_STALE`.
|
||||
or (from slot 2 on) the pin merged with main plus stamp files on top of the
|
||||
merge; any other head is `QUEUE_RECEIPT_STALE`, including a merge of main with
|
||||
no stamp above it (a lane's own push under rule 14) and a merge whose pin side
|
||||
carries an edit made after /ship.
|
||||
|
||||
This is a process gate, not an authenticity check: the receipt is a block in
|
||||
the PR body, which the PR author writes, so the codes prove that /ship was run
|
||||
|
||||
@@ -152,8 +152,10 @@ export function directiveFor(kind: SessionKind): string {
|
||||
'SESSION_KIND=unattended — auto-choose the `(recommended)` option and append it to decisions.jsonl ' +
|
||||
'with kind "auto"; do not emit prose, do not BLOCK. A question with no recommendation, a consent, ' +
|
||||
'a destructive or irreversible option, or an approval gate is appended with kind "approval" and status ' +
|
||||
'"pending" — never chosen, never approved — and the skill ends at its next gate with ' +
|
||||
'GSTACK_RESULT status=gate_pending (Unattended session block).'
|
||||
'"pending" — never chosen, never approved — and the skill ends at its next gate. Only /autoplan and ' +
|
||||
'/eval-plan print `GSTACK_RESULT: skill=<name> status=gate_pending run=<dir>` there, and /ship prints its ' +
|
||||
'own line at Step 22 (`gstack-ship-receipt result`); every other skill prints no GSTACK_RESULT line, so do ' +
|
||||
'not invent one (Unattended session block; docs/unattended.md names each skill\'s record).'
|
||||
);
|
||||
case 'headless':
|
||||
// #2733 review (multi-specialist): a spawned-marked subagent under a
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* full-gate check run. Dispatch itself goes through scripts/eval-bg.ts.
|
||||
*/
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import type { Exec, RunSummary } from '../scripts/eval-bg';
|
||||
import type { QueueContext } from './ship-queue';
|
||||
@@ -68,22 +69,24 @@ export function downloadJson(ctx: QueueContext, slug: string, run: number, name:
|
||||
} catch { return null; } finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
||||
}
|
||||
|
||||
/** The named check run on a commit (the newest when several); null when absent or unreadable. */
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function hollowScreen(ctx: QueueContext, slug: string, run: number, artifact: string, scratch: string): string | null {
|
||||
export function hollowScreen(ctx: QueueContext, slug: string, run: number, artifact: string): string | null {
|
||||
const view = runView(ctx, slug, run);
|
||||
if (!view || view.status !== 'completed') return null;
|
||||
const rec = downloadJson(ctx, slug, run, artifact, scratch) as { census?: { planned?: { rule?: unknown; judge?: unknown } } } | 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;
|
||||
}
|
||||
|
||||
/** 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 }> }>(
|
||||
ctx.gh(['api', `repos/{owner}/{repo}/commits/${sha}/check-runs?check_name=${encodeURIComponent(name)}`]));
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
* replay (lib/gate-cost.ts) and printed as pending until it lands.
|
||||
*/
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { DISPATCH_INPUTS, awaitNonceRun, dedupeKey, dispatchWorkflow, findDuplicateRun, listDispatchRuns, makeNonce, type WorkflowDispatch } from '../scripts/eval-bg';
|
||||
import { EXIT } from './headless-artifacts';
|
||||
import { ready, type QueueContext } from './ship-queue';
|
||||
@@ -87,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, path.join(os.tmpdir(), 'gstack-screen-scratch')) : null;
|
||||
const hollow = existing ? hollowScreen(env.ctx, slug, existing.databaseId, env.train.record_artifact) : 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');
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
* scripts/eval-bg.ts's nonce.
|
||||
*/
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { DISPATCH_INPUTS, awaitNonceRun, dispatchWorkflow, listDispatchRuns, makeNonce, runForNonce, type WorkflowDispatch } from '../scripts/eval-bg';
|
||||
import { changelogDamage, mergeChangelogs, parseChangelog, type ChangelogSection } from './changelog-check';
|
||||
import type { ResultStatus } from './headless-artifacts';
|
||||
@@ -304,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, path.join(os.tmpdir(), 'gstack-screen-scratch')) : null;
|
||||
const hollow = screen.choice === 'screen' && screen.run && slug ? hollowScreen(ctx, slug, screen.run, env.train.record_artifact) : 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 });
|
||||
}
|
||||
|
||||
+9
-9
@@ -66,8 +66,9 @@ function treeReceipt(ctx: QueueContext, policy: LoadedPolicy, gated: string, cur
|
||||
* slot's stamp commit, docs/ship-policy.md#ship-before-ready), or, from slot
|
||||
* 2 on, the stamp over a merge of the base into it: /ship ran on the pin, the
|
||||
* stamp changes nothing it checked, and verify --train binds the same pair
|
||||
* against the train's own prefix. Any other receipt head stays
|
||||
* QUEUE_RECEIPT_STALE.
|
||||
* against the train's own prefix. A merge with no stamp on top is a lane's
|
||||
* own push (rule 14), so it reruns /ship like any other head. Any other
|
||||
* receipt head stays QUEUE_RECEIPT_STALE.
|
||||
*/
|
||||
function acceptedReceiptHeads(ctx: QueueContext, policy: LoadedPolicy, head: string, body: string): string[] {
|
||||
const ex = extractReceipt(body);
|
||||
@@ -80,13 +81,12 @@ function acceptedReceiptHeads(ctx: QueueContext, policy: LoadedPolicy, head: str
|
||||
if (stampOnly(full.out, head)) return [full.out, head];
|
||||
const baseSha = git(root, ['rev-parse', `origin/${ctx.base}`]).out;
|
||||
const onBase = (sha: string) => !!sha && git(root, ['merge-base', '--is-ancestor', sha, baseSha]).ok;
|
||||
for (const merge of [head, git(root, ['rev-parse', '--verify', '--quiet', `${head}^1`]).out]) {
|
||||
if (!merge || !stampOnly(merge, head)) continue;
|
||||
const parents = git(root, ['rev-list', '--parents', '-n', '1', merge]).out.split(' ').slice(1);
|
||||
if (parents.length !== 2) continue;
|
||||
const pinSide = onBase(parents[0]!) ? parents[1]! : onBase(parents[1]!) ? parents[0]! : '';
|
||||
if (pinSide && git(root, ['merge-base', '--is-ancestor', full.out, pinSide]).ok && stampOnly(full.out, pinSide)) return [full.out, head];
|
||||
}
|
||||
const merge = git(root, ['rev-parse', '--verify', '--quiet', `${head}^1`]).out;
|
||||
if (!merge || !stampOnly(merge, head)) return [head];
|
||||
const parents = git(root, ['rev-list', '--parents', '-n', '1', merge]).out.split(' ').slice(1);
|
||||
if (parents.length !== 2) return [head];
|
||||
const pinSide = onBase(parents[0]!) ? parents[1]! : onBase(parents[1]!) ? parents[0]! : '';
|
||||
if (pinSide && git(root, ['merge-base', '--is-ancestor', full.out, pinSide]).ok && stampOnly(full.out, pinSide)) return [full.out, head];
|
||||
return [head];
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -76,7 +76,7 @@ export function bindReceipt(body: string, pr: string | undefined, accepted: read
|
||||
const head = accepted.find(h => h === r.head || (r.head.length >= 12 && h.startsWith(r.head)));
|
||||
if (!head) {
|
||||
const want = accepted.map(h => h.slice(0, 12)).join(' or ');
|
||||
return { ok: false, code: 'QUEUE_RECEIPT_STALE', message: `${subject} ship receipt is for ${r.head.slice(0, 12)}, not the head ${want} being merged`, line };
|
||||
return { ok: false, code: 'QUEUE_RECEIPT_STALE', message: `${subject} ship receipt is for ${oneLine(r.head.slice(0, 12))}, not the head ${want} being merged`, line };
|
||||
}
|
||||
if (pr !== undefined && r.pr !== undefined && String(r.pr) !== pr) return { ok: false, code: 'QUEUE_RECEIPT_STALE', message: `${subject} ship receipt names pr=${r.pr}, not this PR`, line };
|
||||
return { ok: true, receipt: r, head, line };
|
||||
@@ -104,6 +104,9 @@ export function missingIssueLines(body: string, issues: NonNullable<ShipReceipt[
|
||||
}
|
||||
|
||||
/** The first lines of the completion message to the coordinator: identifiers only. */
|
||||
/** Receipt fields come from the PR body, so anything printed from one stays on its own line, control characters included. */
|
||||
const oneLine = (s: string) => s.replace(/[\x00-\x1f\x7f]+/g, ' ');
|
||||
|
||||
export function receiptSummaryLine(r: ShipReceipt): string {
|
||||
const parts = [`pr=${r.pr ?? '-'}`, `head=${r.head.slice(0, 12)}`, `version=${r.version ?? '-'}`, `base=${r.base}`];
|
||||
if (r.gated_tree) parts.push(`gated_tree=${r.gated_tree.slice(0, 12)}`);
|
||||
@@ -113,7 +116,7 @@ export function receiptSummaryLine(r: ShipReceipt): string {
|
||||
if (r.session_kind) parts.push(`session_kind=${r.session_kind}`);
|
||||
if (r.artifacts_consumed) parts.push(`artifacts_consumed=${r.artifacts_consumed}`);
|
||||
if (r.approval) parts.push(`approval=${approvalSummary(r.approval)}`);
|
||||
return `SHIP_RECEIPT: ${parts.join(' ').replace(/[\x00-\x1f\x7f]+/g, ' ')}`;
|
||||
return `SHIP_RECEIPT: ${oneLine(parts.join(' '))}`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { readFileSync } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { isErrorResponse, directiveFor } from '../hosts/claude/hooks/auq-error-fallback-hook.ts';
|
||||
import { SPAWNED_ESCAPE_SENTENCE } from '../hosts/claude/hooks/spawned-directive.ts';
|
||||
@@ -85,6 +86,18 @@ describe('directiveFor — per-session-kind instruction', () => {
|
||||
expect(d).not.toMatch(/BLOCKED/);
|
||||
});
|
||||
|
||||
test('unattended directive names the same GSTACK_RESULT writers as docs/unattended.md and gstack-skill-start (GSTA-91)', () => {
|
||||
const d = directiveFor('unattended');
|
||||
expect(d).toMatch(/Only \/autoplan and \/eval-plan print `GSTACK_RESULT: skill=<name> status=gate_pending run=<dir>`/);
|
||||
expect(d).toMatch(/\/ship prints its own line at Step 22/);
|
||||
expect(d).toMatch(/every other skill prints no GSTACK_RESULT line/);
|
||||
const doc = readFileSync(path.join(import.meta.dir, '..', 'docs', 'unattended.md'), 'utf8');
|
||||
expect(doc).toContain('**`/autoplan`**');
|
||||
expect(doc).toContain('**`/ship`**');
|
||||
expect(doc).toMatch(/`\/eval-plan` asks for a `gate_pending` line/);
|
||||
expect(doc).toMatch(/\*\*Every other workflow skill\*\*[\s\S]*prints the line/);
|
||||
});
|
||||
|
||||
test('spawned directive carries a self-contained destructive carve-out (#2733 review)', () => {
|
||||
// The "Spawned session block" it defers to exists only when a gstack
|
||||
// preamble ran; an AUQ error outside a skill still needs the exception.
|
||||
|
||||
@@ -299,7 +299,7 @@ describe('ready', () => {
|
||||
// Value: protects=the re-READY after a slot's stamp push accepts the receipt /ship wrote for the pinned head, and only then;
|
||||
// fails_when=ready binds the receipt to HEAD alone (every stamped READY turns QUEUE_RECEIPT_STALE) or accepts any ancestor's receipt;
|
||||
// why_new=the READY test above binds receipts to the exact head only; seam=none; no_claim=real gh and CI (shims; unit only)
|
||||
test('ready at the stamp commit accepts the receipt written for the pinned head; a real edit on top of the pin is stale', () => {
|
||||
test('ready at the stamp commit accepts the receipt written for the pinned head; a real edit on top of the pin, a bare merge of main, or an edit on the pin side under the slot-2 stamp is stale', () => {
|
||||
const repo = fixture();
|
||||
const pin = makePr(repo, 'pr-s');
|
||||
fs.writeFileSync(path.join(repo.work, 'VERSION'), '1.2.4.0\n');
|
||||
@@ -324,6 +324,22 @@ describe('ready', () => {
|
||||
const merged = queue(repo, ['ready', '--pr', '1', '--pregate', pregateFor(repo, slot2)], shim(slot2, shippedBody('1', pin)));
|
||||
expect([merged.status, merged.err]).toEqual([EXIT.ok, '']);
|
||||
expect(merged.out).toContain(`SHIP_RECEIPT: pr=1 head=${pin.slice(0, 12)} version=- base=main`);
|
||||
const staleRe = (h: string) => new RegExp(`PR #1's ship receipt is for ${pin.slice(0, 12)}, not the head ${h.slice(0, 12)} being merged.*\\(QUEUE_RECEIPT_STALE\\)`);
|
||||
const bareMerge = repo.git('rev-parse', 'HEAD^1');
|
||||
repo.git('reset', '-q', '--hard', bareMerge);
|
||||
const bare = queue(repo, ['ready', '--pr', '1', '--pregate', pregateFor(repo, bareMerge)], shim(bareMerge, shippedBody('1', pin)));
|
||||
expect(bare.status).toBe(EXIT.refused);
|
||||
expect(bare.err).toMatch(staleRe(bareMerge));
|
||||
repo.git('reset', '-q', '--hard', pin);
|
||||
fs.appendFileSync(path.join(repo.work, 'pr-s.txt'), 'real change before the merge\n');
|
||||
repo.git('commit', '-qam', 'edit on the pin side');
|
||||
repo.git('merge', '-q', '--no-edit', 'origin/main');
|
||||
fs.writeFileSync(path.join(repo.work, 'VERSION'), '1.2.5.0\n');
|
||||
repo.git('commit', '-qam', 'v1.2.5.0 stamp');
|
||||
const editedPin = repo.git('rev-parse', 'HEAD');
|
||||
const pinEdit = queue(repo, ['ready', '--pr', '1', '--pregate', pregateFor(repo, editedPin)], shim(editedPin, shippedBody('1', pin)));
|
||||
expect(pinEdit.status).toBe(EXIT.refused);
|
||||
expect(pinEdit.err).toMatch(staleRe(editedPin));
|
||||
repo.git('reset', '-q', '--hard', stamped);
|
||||
fs.appendFileSync(path.join(repo.work, 'pr-s.txt'), 'real change after /ship\n');
|
||||
repo.git('commit', '-qam', 'more');
|
||||
|
||||
@@ -229,7 +229,10 @@ describe('gstack-ship-receipt result (/ship terminal line, GBRA-108)', () => {
|
||||
expect(JSON.parse(fs.readFileSync(path.join(repo.root, 'pinned-out', 'ship-run.json'), 'utf8')).head).toBe('c'.repeat(40));
|
||||
const badHead = runBin(repo, 'gstack-ship-receipt', ['result', '--body', pinned, '--head', '../../etc'], env);
|
||||
expect([badHead.status, badHead.out]).toEqual([EXIT.usage, '']);
|
||||
expect(badHead.err).toContain('--head must be a hex commit id');
|
||||
expect(badHead.err).toContain('--head must be a hex commit id of at least 12 characters');
|
||||
const shortHead = runBin(repo, 'gstack-ship-receipt', ['result', '--body', pinned, '--head', 'abcdef1'], env);
|
||||
expect([shortHead.status, shortHead.out]).toEqual([EXIT.usage, '']);
|
||||
expect(shortHead.err).toContain('--head must be a hex commit id of at least 12 characters');
|
||||
const noGit = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-receipt-nogit-'));
|
||||
const outside = runBin(repo, 'gstack-ship-receipt', ['result', '--body', pinned], env, noGit);
|
||||
expect([outside.status, outside.err.trim()]).toEqual([EXIT.usage, 'gstack-ship-receipt result: --head <sha> is required outside a git repository']);
|
||||
|
||||
@@ -239,6 +239,14 @@ describe('ready under a train policy: the screen choice', () => {
|
||||
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, '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: [] } }; });
|
||||
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/);
|
||||
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);
|
||||
}, 120_000);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user