mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 15:04:37 +01:00
v1.91.173.0 fix(ship-queue): train verify accepts a slot head with main merged in (GSTA-49) (#3327)
* fix(ship-queue): train verify accepts a slot head with main merged in (GSTA-49) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * v1.91.173.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
e7a1df9e18
commit
b616d6c53d
7 files changed
+73
-188
No files matched your search
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## [1.91.173.0] - 2026-10-20
|
||||
|
||||
**Merge trains can merge slot 2 onward.** After slot 1 merges, main carries the new stamp, so every later pin conflicts with main on the stamp files and GitHub can't squash it. A lane makes its PR mergeable by merging main into the pin before it stamps. `verify --pr <n> --train <nonce>` refused that head as moved off its pin. Now it also accepts a head whose tree equals the slot's train prefix (T<i>) modulo stamps: exactly the tree the train's gate covered, and what main becomes once the PR squash-merges. The first live train stopped at slot 2 on this (gate.train-ac4baf961759-43d6ed28f2bf-9a1dbb). Plan GSTA-49.
|
||||
|
||||
### Fixed
|
||||
- **`lib/ship-queue-train-verify.ts`** accepts a slot head that is either its pin plus stamps (as before) or T<i> modulo stamps, and the `head:` line says which. A head that matches neither is still `QUEUE_TRAIN_MOVED`, and the queue still freezes when earlier slots have merged.
|
||||
- **`docs/ship-policy.md`** gives the per-slot lane procedure from slot 2 on: merge `origin/main` into the pin, resolve the CHANGELOG conflict with `mergeChangelogs`, then run `gstack-restamp --next`.
|
||||
|
||||
### Gate integrity
|
||||
- `test/ship-queue-train.test.ts` builds a lane head with main merged into the pin and T2's tree stamped 1.2.5.0. It verifies, with `MERGE_CONDITION` at that head. The same head plus one file outside the train fails with `QUEUE_TRAIN_MOVED`. Without the fix, the positive case fails.
|
||||
|
||||
## [1.91.172.0] - 2026-10-20
|
||||
|
||||
**gstack now works the way Capy Captains run work: every decision goes to the Captain (or the person) in plain words, plan approvals arrive in a fixed before/after shape, each thread keeps a record of who approved what, and /ship's PR body shows before/after images of what changed.** Garry asked for this on 2026-10-20 ("make it much more obvious a/ how approvals work per thread, b/ for each plan approval i want a before/after, why it matters, what caused us to work on this, and big decisions you had to make, c/ when asking for merge guidance i want the PRs to show screenshots of what changed and why"). Everything below applies only when `GSTACK_CAPY=1` (the Capy launch line sets it). Other hosts keep their output, apart from one new `CAPY: off` STATUS line.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.91.172.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.91.173.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
+16
-182
@@ -401,6 +401,19 @@ EVAL_POLICY.infraRedispatch allows: `train rebuild --train <nonce> --infra`.
|
||||
The rebuild-once rule lives in the train module beside that constant until
|
||||
EVAL_POLICY records it. Bisect results never turn a train green.
|
||||
|
||||
**Stamping a slot.** When the slot is called, the lane stamps it at main plus PATCH. Slot 1's pin applies cleanly to main. From slot 2 on, main already carries the earlier slots' stamps, so the pin conflicts on CHANGELOG.md and GitHub can't merge it. The lane merges main into the pin first, then stamps:
|
||||
|
||||
```bash
|
||||
git fetch origin main
|
||||
git merge --no-ff --no-commit origin/main # expect a CHANGELOG.md conflict only
|
||||
bun --no-env-file --config=/dev/null -e "import { mergeChangelogs } from './lib/changelog-check'; import { spawnSync } from 'node:child_process'; import { writeFileSync } from 'node:fs'; const s = (n) => spawnSync('git', ['show', ':' + n + ':CHANGELOG.md'], { encoding: 'utf8', maxBuffer: 1 << 26, timeout: 30000 }).stdout; const m = mergeChangelogs(s(2), s(3)); if (!m) { console.error('CHANGELOG needs a hand merge'); process.exit(1); } writeFileSync('CHANGELOG.md', m);"
|
||||
git add CHANGELOG.md && git commit --no-edit
|
||||
gstack-restamp --next --base main --pr 42 --expect-base <main sha> --expect-order <order> --allow-repo-commands
|
||||
git add CHANGELOG.md VERSION package.json <release outputs> && git commit -m "v<X> stamp" && git push
|
||||
```
|
||||
|
||||
A conflict in any file other than CHANGELOG.md means the merge isn't the train's prefix. Stop and report it. `verify` accepts the resulting head because its tree equals T<i> modulo stamps; the `head:` line says "T<i> with main merged in".
|
||||
|
||||
**5. Coordinator: verify, merge, verify again.** For each slot in order:
|
||||
|
||||
```bash
|
||||
@@ -410,7 +423,8 @@ gstack-ship-queue verify --pr 41 --train <nonce> # VERIFY_TRAIN: #41 merged; m
|
||||
```
|
||||
|
||||
Before a merge, main's tree must equal the prefix before the PR modulo stamps
|
||||
and the PR must still be at its pin (stamps aside); after it, main's tree
|
||||
and the PR must be at its pin (stamps aside) or at T<i> modulo stamps (its pin
|
||||
with main merged in); after it, main's tree
|
||||
must equal the prefix after it. Anything else is `QUEUE_TRAIN_TREE_MISMATCH`
|
||||
(or `QUEUE_TRAIN_MOVED`). When nothing of the train has merged, that is a plain
|
||||
refusal: build a new train on current main. When earlier slots already
|
||||
@@ -579,184 +593,4 @@ a bundle the identity is unknown and the receipt is `not-eligible`. When the
|
||||
recorded predecessor head is no longer the predecessor PR's head (checked
|
||||
through `--predecessor-ref` or `gh pr view --predecessor-pr`) the receipt
|
||||
prints `PREDECESSOR MOVED <old> -> <new> — fix: gstack-restamp --after <pr>
|
||||
again (RESTAMP_PREDECESSOR_MOVED)` and refuses `eligible`.
|
||||
|
||||
`/ship` Step 16 consults the receipt; when it says `eligible`, a repeated full
|
||||
gate needs `--force-gate <reason>`, and the coordinator makes the call.
|
||||
|
||||
### The evidence bundle
|
||||
|
||||
`gstack-evidence` is machine-local by design, so the portable form is a bundle:
|
||||
|
||||
```bash
|
||||
gstack-evidence bundle --out evidence-bundle.json --label tests --label e2e --require tests,e2e \
|
||||
--selection tests='bun run test' --run-url tests=https://github.com/.../actions/runs/1
|
||||
```
|
||||
|
||||
A bundle binds to a committed head (`repo.commit`, `repo.tree`) and carries the
|
||||
comparison material (version source, CHANGELOG path, `stamp_paths`,
|
||||
`release_outputs`), the execution identity above, the required-lane manifest,
|
||||
and per lane the command, `cmd_sha256`, exit, timestamp, executed selection,
|
||||
run URL, log hash and whether it is `verified` (produced by the ledger on a
|
||||
clean committed tree at this head) or not (dirty tree, other commit, or an
|
||||
`--import`ed assertion). A fresh coordinator machine reads it with
|
||||
`gstack-tree-receipt --bundle` after the gate VM is gone; the receipt refuses
|
||||
reuse when the bundle was produced on another tree or lacks the identity
|
||||
(`EVIDENCE_IDENTITY_UNKNOWN`).
|
||||
|
||||
<a id="ship-receipt"></a>
|
||||
## The ship receipt
|
||||
|
||||
The handoff record is a fenced block, identifiers first, validated against
|
||||
`gstack-artifact schema ship-receipt`:
|
||||
|
||||
````
|
||||
```gstack-ship-receipt
|
||||
{ "schema_version": 1, "pr": 42, "head": "<sha>", "version": "1.2.7.0", "base": "main",
|
||||
"gated_tree": "<tree>", "predecessor": "<sha>", "tree": "same-modulo-stamps",
|
||||
"gate_reuse": "eligible (reusable: tests; rerun: digest)", "gate": { "tests": { "pass": 10, "fail": 0, "skip": 1 } },
|
||||
"ci": { "checks": 3, "failed": 0 }, "spend_usd": 0.42, "session_kind": "unattended",
|
||||
"artifacts_consumed": "1/1", "run": "<run id>", "thread": "<thread id>", "policy": "base origin/main@<sha>" }
|
||||
```
|
||||
````
|
||||
|
||||
`gstack-ship-receipt write --pr <n> --receipt <tree-receipt.json> --gate '<json>' --ci '<json>' --body <pr-body.md>`
|
||||
makes it the first section of the PR body (`/ship` Step 18) and prints
|
||||
`SHIP_RECEIPT: pr=… head=… version=…` for the first line of the completion
|
||||
message. `/land-and-deploy` reads it with `gstack-ship-receipt read --pr <n>
|
||||
--json` (`RECEIPT_MISSING`, `RECEIPT_INVALID`).
|
||||
|
||||
### The adoption census (P7)
|
||||
|
||||
```bash
|
||||
gstack-ship-receipt census --repo garrytan/gstack --since v1.91.70.0 --cohort threads.txt
|
||||
```
|
||||
|
||||
reads merged PRs since the tag through `gh`, parses each receipt, deduplicates
|
||||
by `run` and `thread`, and prints `CENSUS: … unattended=<n> consumed=<n>
|
||||
adoption=<consumed threads>/<cohort> durable=yes`. The cohort file lists one
|
||||
thread id per line (threads that never invoke gstack count in the
|
||||
denominator); without it the denominator is the threads the receipts name.
|
||||
|
||||
## History policy and preregistration (C7)
|
||||
|
||||
Under `history: merge-only`, `/ship` refuses a squash or rebase plan and merges
|
||||
the base branch instead: `gstack-ship-policy check-history --plan squash`
|
||||
prints `HISTORY: refused (…) (HISTORY_POLICY_VIOLATION)` with exit 3, and
|
||||
without `--plan` it prints `HISTORY: ok` or `HISTORY: rewritten …
|
||||
(HISTORY_POLICY_VIOLATION)` (exit 1) when the pushed branch is no longer an
|
||||
ancestor of HEAD. `gstack-evidence ancestor
|
||||
--from-policy --base main` prints one `ANCESTOR: ok|missing <sha>` line per
|
||||
`preregistration_shas` entry (`PREREGISTRATION_NOT_ANCESTOR`, exit 1), and the
|
||||
ship receipt records the result in `preregistration`.
|
||||
|
||||
## Contributor mode
|
||||
|
||||
`/review` runs `gstack-contributor-mode detect` at Step 1 (decision D10). The PR
|
||||
author is outside the maintainer set when `maintainers` in `.gstack/ship-policy.json`
|
||||
on the base branch is non-empty and does not list the login (`source=policy`);
|
||||
with an empty list, when `gh api repos/<owner>/<repo>/collaborators/<login>/permission`
|
||||
reports less than push permission (`source=collaborators`, an egress receipt
|
||||
`contributor-mode` precedes the call); when neither is available the mode is off
|
||||
and `CONTRIBUTOR_SOURCE_UNAVAILABLE` names what was missing. `--contributor`
|
||||
forces it on (`source=flag`). `CODEOWNERS` is file ownership, not maintenance
|
||||
authority, and is never read. The printed line is
|
||||
`CONTRIBUTOR_MODE: on|off author=<login> source=flag|policy|collaborators|off`,
|
||||
followed when on by the evidence-first instructions: a table per change
|
||||
(evidence value, risk surfaces, `accept` / `supersede` / `decline`), where
|
||||
`accept` keeps a direct path for a correct outside implementation judged by the
|
||||
same bar as a maintainer change, `supersede` rewrites from our own plan only after
|
||||
a plain-language approval, and the credit lines (`Contributed by @handle`,
|
||||
`Co-Authored-By:`, `Supersedes #N`) appear only when a diagnosis or a test carried
|
||||
over. `gstack-contributor-mode instructions` prints the same text without detecting.
|
||||
|
||||
## Banned terms
|
||||
|
||||
`.gstack/banned-terms.json` lists terms the repository forbids outside named
|
||||
locations: `terms` (a string, or `{ "term", "allowed_paths", "reason" }`) and a
|
||||
top-level `allowed_paths` that every term inherits. Globs follow the same
|
||||
containment rules as the ship policy (repo-relative, no `..`, under 256
|
||||
characters). `gstack-banned-terms check --base <ref>` scans the lines the branch
|
||||
adds since its merge base (or whole files with `--files <csv>`), reading the term
|
||||
list from the committed copy on the base by default (`--policy-from head` reads
|
||||
the working tree). Each finding is one line,
|
||||
`BANNED_TERM: "<term>" <file>:<line> allowed: <locations> — <text>`, and the
|
||||
last line is `BANNED_TERMS: ok | found=<n> | none` (`none`: the repository keeps
|
||||
no list). Exit 0 for ok or none, 1 when found, 3 when the file is invalid
|
||||
(`BANNED_TERMS_INVALID`, also from `validate`). `/review` turns each finding into
|
||||
a Step 4 finding and `/document-release` into a documentation finding;
|
||||
`gstack-banned-terms init` writes a commented example.
|
||||
|
||||
<a id="approval-classes"></a>
|
||||
## Approval classes
|
||||
|
||||
`.gstack/approval-classes.json` names the changes that need the owner's
|
||||
approval before they ship: upgrade-time data rewrites, trust or visibility
|
||||
changes, existing-user default changes, data repairs, anything the owner wants
|
||||
cited. Each entry in `classes` has a `name` (lowercase, dashes) and at least
|
||||
one matcher: `paths` (globs of changed files), `patterns` (literal text in an
|
||||
added line, case-sensitive) or `migrations: true` (the files the ship policy's
|
||||
`migrations` key describes). `allowed_paths` exempts files, such as tests;
|
||||
`description` is for people. Globs follow the ship policy's containment rules.
|
||||
|
||||
Classes come only from the copy committed on `origin/<base>`, so a branch can
|
||||
neither declare nor drop its own; `gstack-approval-classes validate` checks the
|
||||
working-tree copy before you commit it, and `init` writes an example.
|
||||
`gstack-approval-classes check --base <ref>` scans the branch's changes since
|
||||
the merge base and prints one `APPROVAL_CLASS: <class> <file>:<line>` line per
|
||||
hit, one `APPROVAL_SCOPE: <class>@<digest12>` line per declared class with hits,
|
||||
and `APPROVAL_CLASSES: ok | found=<n> | none` last (exit 0, 1 when found, 3 when
|
||||
the base file is invalid or `origin/<base>` does not resolve). Built-in
|
||||
detectors only report: the ship policy's migration files, when no class sets
|
||||
`migrations: true`, print as `(built-in, report-only)` and need no citation.
|
||||
|
||||
The digest covers the class's effects: each matched file with its blob at the
|
||||
scanned head, and each matched added line. The scan reads git's native diff
|
||||
(`--no-ext-diff --no-textconv`), so a configured external diff driver or text
|
||||
conversion cannot hide a line; when the content diff cannot be read, or its
|
||||
added lines disagree with git's own count, `check` and the receipt refuse with
|
||||
`APPROVAL_SOURCE_UNAVAILABLE` instead of passing. Adding, removing or editing an
|
||||
effect changes the token, so an approval given earlier no longer binds.
|
||||
|
||||
**Enforcement.** `gstack-ship-receipt write` refuses with
|
||||
`APPROVAL_CITATION_MISSING`, writing nothing, unless each declared class with
|
||||
hits has a line in the `--body` file (or an `--approval` value) of the form
|
||||
|
||||
```
|
||||
Approved by <owner> <YYYY-MM-DD> via <ref> [for <class>@<digest12>[,<class>@<digest12>]]
|
||||
```
|
||||
|
||||
that resolves and binds:
|
||||
|
||||
- A plan sha (7 to 64 hex characters) must match the plan of an autoplan run
|
||||
passed with `--approval-run <run dir>` whose gate is APPROVED, and the plan
|
||||
must name the scope token, or the class and every effect path.
|
||||
- A GitHub comment or review URL must be written by a `maintainers` login from
|
||||
the base's ship policy that equals the cited owner. A review must be in
|
||||
state `APPROVED`; a comment-only, dismissed or changes-requested review is
|
||||
not an approval. The evidence is the body's first non-blank line only. It
|
||||
must start at column 0 with `Approved` or `Approve` (any case, optionally
|
||||
in `**` bold; no list marker, quote or indent) and name the token, or the
|
||||
class and every effect path. Nothing later in the body counts, so a
|
||||
rejection that quotes or nests an example approval approves nothing. For
|
||||
example, a comment whose first line is
|
||||
`Approved data-repair@0123456789ab: preview-first, never at upgrade.`
|
||||
verifies; one that opens with any other line does not.
|
||||
- A token is authoritative. Evidence that names the class with a different
|
||||
digest approved earlier effects: it does not bind, and it never falls back
|
||||
to matching the class and paths.
|
||||
- Anything else (a thread code, a chat message, a URL gstack cannot read) is
|
||||
recorded `citation: unverified` for the coordinator, and only when the line
|
||||
attests which scope it covers with `for <class>@<digest12>` matching the
|
||||
current token.
|
||||
|
||||
The receipt's `approval` object records the source, `status` (`checked`, or
|
||||
`unavailable` when `origin/<base>` does not resolve), one record per class
|
||||
(scope token, effects, `citation: verified | unverified`, owner, date, ref,
|
||||
`binding: token | paths | attested`, evidence) and any report-only hits; the
|
||||
`SHIP_RECEIPT:` line ends with `approval=verified`, `approval=unverified:<classes>`,
|
||||
`approval=none` or `approval=unchecked`. A repository without the file gets no
|
||||
`approval` object unless a built-in detector reported.
|
||||
|
||||
A data repair is a class to declare, and /review checks its shape: explicit,
|
||||
preview-first, and never reachable from an upgrade or migration path.
|
||||
again (RESTAMP_PREDECESSOR_MOVED)` and refuses `eligible`.
|
||||
@@ -3,7 +3,9 @@
|
||||
* merge trains (docs/ship-policy.md#run-a-merge-train; plan GSTA-49 T1, P1).
|
||||
*
|
||||
* Each merge needs a green train record that includes the PR at its pinned
|
||||
* head, with main's tree equal to the prefix before the PR (pre-merge:
|
||||
* head, or at a head whose tree is the slot's prefix after it modulo stamps
|
||||
* (from slot 2 on, the lane merges main into its pin so GitHub can merge it),
|
||||
* with main's tree equal to the prefix before the PR (pre-merge:
|
||||
* MERGE_CONDITION) or after it (post-merge confirmation), modulo stamps
|
||||
* (lib/tree-receipt.ts). When a train stops partway (a mismatch or a moved
|
||||
* head after earlier slots merged) the queue freezes: the freeze is written
|
||||
@@ -95,8 +97,10 @@ export async function verifyTrain(ctx: QueueContext, o: { pr: string; train: str
|
||||
}
|
||||
const headRef = o.noFetch ? '' : (git(ctx.repoRoot, ['fetch', '--no-tags', '-q', env.remote, `+refs/pull/${o.pr}/head:refs/gstack/queue/pr-${o.pr}`]).ok ? `refs/gstack/queue/pr-${o.pr}` : '');
|
||||
const head = headRef ? git(ctx.repoRoot, ['rev-parse', headRef]).out : t.slots[i]!.head;
|
||||
const stampOnly = head === t.slots[i]!.head ? { same: true, detail: 'the pinned head' } : sameModuloStamps(env, t.slots[i]!.head, head);
|
||||
if (!stampOnly.same) return stopped(env, t, 'QUEUE_TRAIN_MOVED', `#${o.pr} moved past its pinned head ${t.slots[i]!.head.slice(0, 12)} (${head.slice(0, 12)}: ${stampOnly.detail})`, main, lines);
|
||||
const pinned = head === t.slots[i]!.head ? { same: true, detail: 'the pinned head' } : sameModuloStamps(env, t.slots[i]!.head, head);
|
||||
const asPrefix = pinned.same ? null : sameModuloStamps(env, after.commit, head);
|
||||
const stampOnly = asPrefix?.same ? { same: true, detail: `T${after.index} with main merged in, ${asPrefix.detail}` } : pinned;
|
||||
if (!stampOnly.same) return stopped(env, t, 'QUEUE_TRAIN_MOVED', `#${o.pr} moved past its pinned head ${t.slots[i]!.head.slice(0, 12)} and is not T${after.index} modulo stamps (${head.slice(0, 12)}: ${pinned.detail})`, main, lines);
|
||||
return outOk([...lines, `main: ${main} = T${before.index} (${pre.detail})`, `head: ${head} (${stampOnly.detail})`,
|
||||
`VERIFY_TRAIN: #${o.pr} may merge as slot ${i + 1} of train ${t.nonce} (green run ${gate.run?.id ?? '-'}, ${env.train.full_gate_check} success)`, `MERGE_CONDITION: sha=${head}`],
|
||||
`merge #${o.pr} with expected head ${head.slice(0, 12)} (gstack-restamp stamps at merge), then ${Q} verify --pr ${o.pr} --train ${t.nonce}`, run);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.91.172",
|
||||
"version": "1.91.173",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -521,4 +521,40 @@ describe('verify, freeze, repair and clear', () => {
|
||||
expect(JSON.parse(fs.readFileSync(freezeFile, 'utf8')).cleared_at).toBe('2026-10-18T12:00:00.000Z');
|
||||
expect(queue(fx, ['train', 'status']).out).not.toContain('QUEUE_TRAIN_FROZEN');
|
||||
}, 300_000);
|
||||
test('from slot 2 on, a head that merges main into the pin verifies when its tree is the slot\'s prefix modulo stamps', () => {
|
||||
const fx = make();
|
||||
const { h2, nonce } = twoPrTrain(fx);
|
||||
expect(queue(fx, ['train', 'verdict', '--train', nonce]).status).toBe(EXIT.ok);
|
||||
const t = trainJson(fx, nonce);
|
||||
const coord = coordinatorClone(fx.repo);
|
||||
const v = (pr: string) => queue(fx, ['verify', '--pr', pr, '--train', nonce], {}, coord);
|
||||
const main1 = publishMain(fx, t.prefixes[1].commit, '1.2.4.0');
|
||||
expect(v('1').status).toBe(EXIT.ok);
|
||||
|
||||
const { repo } = fx;
|
||||
const laneHead = (extra?: { file: string; text: string }) => {
|
||||
repo.git('reset', '-q', '--hard');
|
||||
repo.git('checkout', '-q', '-B', 'lane2', h2);
|
||||
repo.git('rm', '-rq', '.');
|
||||
repo.git('checkout', t.prefixes[2].commit, '--', '.');
|
||||
fs.writeFileSync(path.join(repo.work, 'VERSION'), '1.2.5.0\n');
|
||||
if (extra) fs.writeFileSync(path.join(repo.work, extra.file), extra.text);
|
||||
repo.git('add', '-A');
|
||||
const tree = repo.git('write-tree');
|
||||
const head = repo.git('commit-tree', tree, '-p', h2, '-p', main1, '-m', 'merge main into #2, stamp 1.2.5.0');
|
||||
repo.git('push', '-q', '-f', 'origin', `${head}:refs/pull/2/head`);
|
||||
return head;
|
||||
};
|
||||
|
||||
const merged = laneHead();
|
||||
const ok = v('2');
|
||||
expect(ok.status).toBe(EXIT.ok);
|
||||
expect(ok.out).toMatch(new RegExp(`^head: ${merged} \\(T2 with main merged in, same modulo stamps`, 'm'));
|
||||
expect(line(ok.out, 'MERGE_CONDITION:')).toBe(`MERGE_CONDITION: sha=${merged}`);
|
||||
|
||||
const stray = laneHead({ file: 'stray.txt', text: 'not in the train\n' });
|
||||
const moved = v('2');
|
||||
expect(moved.status).toBe(EXIT.fail);
|
||||
expect(moved.err).toMatch(new RegExp(`#2 moved past its pinned head ${h2.slice(0, 12)} and is not T2 modulo stamps \\(${stray.slice(0, 12)}: .*\\(QUEUE_TRAIN_MOVED\\)`));
|
||||
}, 300_000);
|
||||
});
|
||||
Reference in new issue
Block a user