fix(ci-carry): a per_file remainder file that skips here too is not owed (GST-1314) (#3985)

* fix(ci-carry): a per_file remainder file that skips here too is not owed (GST-1314); anchor per_file paths, --no-env-file, declare gstack's per_file runner

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

* chore: name the changelog fragment after the branch

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

* fix(evidence): a truncated per_file log never counts a remainder file not owed (GST-1314)

Review-Finding: p1b/outside-voice#1 reader=outside-voice
Review-Finding: p1b/red-team#3 reader=red-team
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(changelog): GST-1314 fragment names the truncated-log refusal

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

* docs(pregate): a per_file log cut at 2 MB still refuses (GST-1314)

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:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-04 08:14:07 +00:00
1 parent c1ace7483b
commit 068087f33c
6 files changed
+108 -30

No files matched your search

+1 -1
View File
@@ -29,7 +29,7 @@
"skipped": "bun --no-env-file --config=/dev/null -e 'import { isPaidTestFile } from \"./test/helpers/paid-test-set.ts\"; import * as fs from \"node:fs\"; for (const f of fs.readFileSync(process.env.PREGATE_FILES_FILE, \"utf8\").split(/\\r?\\n/)) if (f && isPaidTestFile(f)) console.log(f);'"
},
"ship_lanes_from": {
"bun run test": { "profiles": ["ci-reuse/free-tests"], "inventory": "bun --no-env-file --config=/dev/null run scripts/test-free-shards.ts --list | sed -n 's/^ //p'" }
"bun run test": { "profiles": ["ci-reuse/free-tests"], "inventory": "bun --no-env-file --config=/dev/null run scripts/test-free-shards.ts --list | sed -n 's/^ //p'", "per_file": "bun --no-env-file test --timeout=30000 {files}" }
},
"warn": [],
"guards": [
+29 -10
View File
@@ -138,7 +138,8 @@ interface EvidenceRecord {
tests_unit?: "files";
/** Why a carried lane was not owed at all: every job it names was path-skipped by an unchanged gate (record-ci, GST-1190). */
not_owed?: string;
skipped_here?: { label: string; files: string[]; tests_ran: number | "unknown" };
/** The per_file run of the inventory files CI skipped entirely; not_owed: the ones that skipped every test here too (GST-1314). */
skipped_here?: { label: string; files: string[]; tests_ran: number | "unknown"; not_owed?: string[] };
}
/** Bytes of streamed output kept for the tests_ran parse (the 2MB log would truncate the summary). */
@@ -184,6 +185,15 @@ const ANSI = /\u001B\[[0-?]*[ -/]*[@-~]/g;
const strip = (l: string) => l.replace(ANSI, "").replace(/\r$/, "");
const count = (text: string, word: string): number => Number(new RegExp(`(\\d+) ${word}\\b`).exec(text)?.[1] ?? 0);
/** The tests bun's last terminal summary in a run's log registered, skips included ("Ran N tests across M files"); 0 when the log or the summary is missing, or the log is incomplete (capped at LOG_MAX_BYTES or cut by a write failure), since its last summary need not be the run's. */
function registeredBunTests(logPath: string | undefined): number {
let text = "";
try { text = logPath ? readFileSync(logPath, "utf8") : ""; } catch { return 0; }
if (/^\[gstack-evidence: log (truncated at 2MB|ended early)/m.test(text)) return 0;
const last = text.split("\n").map(strip).findLast((l) => /^Ran \d+ tests? across \d+ files?\. \[/.test(l));
return Number(/^Ran (\d+)/.exec(last ?? "")?.[1] ?? 0);
}
/**
* Bun: the last "Ran N tests across M files" block; executed = its " N pass" +
* " N fail" lines. When that block ends a `bun test --shard=i/N` process (bun's
@@ -733,14 +743,23 @@ async function cmdRecordCi(argv: string[]): Promise<number> {
let here: EvidenceRecord["skipped_here"];
if (v.remainder?.length) {
const hereLabel = `${label}.skipped`;
console.error(`gstack-evidence record-ci: ${v.remainder.length} inventory file(s) skipped every test in CI; running them here (label=${hereLabel})`);
const hereCommand = perFileCommand(m.per_file!, v.remainder);
const exit = await cmdRun(["--label", hereLabel, "--", hereCommand]);
if (exit !== 0) return none("skipped_failed", `the per_file run of ${v.remainder.length} skipped file(s) exited ${exit} (label=${hereLabel})`);
let rows: EvidenceRecord[] = [];
try { rows = readJsonl<EvidenceRecord>(ledgerPath().file); } catch { rows = []; }
here = { label: hereLabel, files: v.remainder, tests_ran: rows.findLast((r) => r.label === hereLabel && r.source === undefined && r.cmd_sha256 === sha256(hereCommand) && r.wtree === tree)?.tests_ran ?? "unknown" };
if (typeof here.tests_ran !== "number" || here.tests_ran <= 0) return none("skipped_zero_run", `the per_file run of ${v.remainder.length} skipped file(s) ran ${here.tests_ran} tests here (label=${hereLabel}); a lane passes on its runner count, never exit 0 alone`);
console.error(`gstack-evidence record-ci: ${v.remainder.length} inventory file(s) skipped every test in CI; running them here one at a time (label=${hereLabel})`);
let ran = 0;
const notOwed: string[] = [];
for (const file of v.remainder) {
const hereCommand = perFileCommand(m.per_file!, [file]);
const exit = await cmdRun(["--label", hereLabel, "--", hereCommand]);
if (exit !== 0) return none("skipped_failed", `the per_file run of ${file} exited ${exit} (label=${hereLabel})`);
let rows: EvidenceRecord[] = [];
try { rows = readJsonl<EvidenceRecord>(ledgerPath().file); } catch { rows = []; }
const row = rows.findLast((r) => r.label === hereLabel && r.source === undefined && r.cmd_sha256 === sha256(hereCommand) && r.wtree === tree);
const n = row?.tests_ran ?? "unknown";
if (typeof n === "number" && n > 0) { ran += n; continue; }
if (n === 0 && registeredBunTests(row?.log_path) > 0) { notOwed.push(file); continue; }
return none("skipped_zero_run", `the per_file run of ${file} ran ${n} tests here and registered none it skipped (label=${hereLabel}); a lane passes on its runner count, never exit 0 alone`);
}
here = { label: hereLabel, files: v.remainder, tests_ran: ran, ...(notOwed.length ? { not_owed: notOwed } : {}) };
if (notOwed.length) console.error(`gstack-evidence record-ci: not owed here: ${notOwed.length} remainder file(s) skipped every test on CI and here too (env- or tool-gated by design): ${notOwed.join(", ")}`);
for (const e of v.runs) {
const changed = runChanged(check.deps, e.run_id, e.run_attempt);
if (changed) return none("run_changed", changed);
@@ -761,7 +780,7 @@ async function cmdRecordCi(argv: string[]): Promise<number> {
commit: head, tree, dirty: false, wtree: tree, tests_ran: v.tests, source: "ci", runs, profiles: m.profiles, policy: loaded.label, ...(m.jobs ? { jobs: m.jobs } : {}), ...(v.unit ? { tests_unit: v.unit } : {}), ...(v.notOwed ? { not_owed: v.notOwed } : {}), ...(here ? { skipped_here: here } : {}),
};
appendJsonl(paths.file, rec, { mode: 0o600 });
console.error(`gstack-evidence: recorded label=${label} exit=0 tests_ran=${v.tests}${v.unit ? ` unit=${v.unit}` : ""} log=- source=ci runs=${runs.join(",")} policy=${loaded.label}${here ? ` skipped_here=${here.files.length} tests_ran_here=${here.tests_ran}` : ""}`);
console.error(`gstack-evidence: recorded label=${label} exit=0 tests_ran=${v.tests}${v.unit ? ` unit=${v.unit}` : ""} log=- source=ci runs=${runs.join(",")} policy=${loaded.label}${here ? ` skipped_here=${here.files.length} tests_ran_here=${here.tests_ran}${here.not_owed ? ` not_owed_here=${here.not_owed.length}` : ""}` : ""}`);
if (v.notOwed) console.error(`gstack-evidence: not owed: ${v.notOwed}`);
return 0;
}
+10
View File
@@ -0,0 +1,10 @@
**CI carry no longer refuses a skipped file that skips here too (GST-1314).** When CI skipped every test in a file, `gstack-evidence record-ci` reruns it here through the lane's `per_file` runner, and a file that skipped everything again ended the carry with `skipped_zero_run`, so the whole free suite reran. Free Tests run 41754278952 had six such files, all env-gated or tool-gated by design (0 pass, 20 skip on a Capy machine), so every gstack /ship carry would have fallen back to the full lane.
### Fixed
- **`gstack-evidence record-ci`:** runs the per_file remainder one file at a time. A file whose Bun summary registered tests and skipped every one here too is recorded as not owed (`skipped_here.not_owed` in the CI row, `not_owed_here=<n>` on the `LANE:` line) instead of refusing. A file that registers no test here still refuses with `skipped_zero_run`, even beside a file that ran, and so does a file whose log hit the 2 MB cap, since a cut log's last summary may not be the run's. A failing file's `skipped_failed` names it.
- **`per_file` paths** reach the runner anchored with `./`, because Bun reads a bare relative path as a substring filter (`test/x.test.ts` also matched `browse/test/x.test.ts`).
- **gstack's `bun run test` lane** declares `"per_file": "bun --no-env-file test --timeout=30000 {files}"`, so the runner is on main before the Free Tests writer starts listing skipped files (#3979). `--no-env-file` keeps a local `.env` from switching an env-gated file on.
- **Docs:** `docs/pregate.md#skipped-files` describes the per-file runs, the not-owed case and the anchoring.
### Tests
- `test/ship-ci-carry.test.ts`: run 41754278952's six remainder files, each printing its own skip-only Bun summary, carry with `not_owed_here=6` and reach the runner as `./<path>`; a remainder that mixes a file that ran with one that registers no test refuses with `skipped_zero_run` naming that file; a remainder file that registers no test refuses; a remainder file whose log passed 2 MB after a nested `Ran 5 tests` summary refuses instead of counting as not owed.
+19 -11
View File
@@ -775,20 +775,28 @@ once:
them here. A /ship lane or guard whose inventory names one carries the
covered files and runs only the skipped ones here, through the runner its
`ship_lanes_from` entry declares as `per_file` (a command with `{files}`,
such as `"per_file": "bun test --timeout=30000 {files}"`, the same form as the
tests stage's `runner`). That run is recorded under `<label>.skipped`, the
`LANE:` line adds `skipped_here=<n> file(s) tests_ran_here=<n>`, and the CI row
is written only after four checks pass. In order, they are the remainder's
exit (`skipped_failed`), its test count above zero (`skipped_zero_run`), every
carried run again (`run_changed`), and the working tree once more
such as `"per_file": "bun --no-env-file test --timeout=30000 {files}"`, the
same form as the tests stage's `runner`). It runs one file at a time, each path
anchored with `./` because Bun reads a bare relative path as a substring
filter. Those runs are recorded under `<label>.skipped`, the `LANE:` line adds
`skipped_here=<n> file(s) tests_ran_here=<n>`, and the CI row is written only
after four checks pass. In order, they are each file's exit
(`skipped_failed`), each file's test count above zero (`skipped_zero_run`),
every carried run again (`run_changed`), and the working tree once more
(`tree_changed`, for an edit made during those checks). An edit made while the
remainder itself runs leaves its ledger row without a tree, so it shows as
`skipped_zero_run`. Any of these refusals runs the whole lane here. An entry with no `per_file` refuses as
`skipped_zero_run`. One exception to the count check (GST-1314): a file whose
Bun summary registered tests and skipped every one of them here too, the way
an env-gated or tool-gated file skips by design, is not owed. The CI row lists
it under `skipped_here.not_owed` and the `LANE:` line adds
`not_owed_here=<n>`. A file that registers no test here still refuses, and so
does a file whose log hit the 2 MB cap, since a cut log's last summary may not
be the run's. Any of these refusals runs the whole lane here. An entry with no `per_file` refuses as
`inventory_skipped`, naming the files, and the lane runs here whole. A partly
skipped file ran tests and stays covered. gstack's own Free Tests writer does
not emit the list yet: it starts in the same follow-up pull request that
declares `per_file` for gstack's `bun run test` lane, so gstack's carry does not
change until both switch on together. A `tree.json`
skipped file ran tests and stays covered. gstack's `bun run test` lane
declares `per_file`, and its Free Tests writer starts emitting the list in a
follow-up pull request, so the runner is in place on the base before any run
lists a skipped file. A `tree.json`
without the key (older writers, other repos) is read as before, with every
listed file counted as run; add the list to your writer to close that gap.
The remote gate's clearance does not use it: that gate runs on a Linux runner
+4 -4
View File
@@ -164,14 +164,14 @@ export function recordCiResult(stderr: string): { runs?: string; tests?: string;
const none = lines.findLast(x => x.startsWith('CARRY_NONE: '))?.slice('CARRY_NONE: '.length);
if (at < 0) return none ? { none } : {};
const got = / tests_ran=(\d+|n\/a)( unit=files)? log=- source=ci runs=(\S+)/.exec(lines[at]!);
const here = / skipped_here=(\d+) tests_ran_here=(\S+)$/.exec(lines[at]!);
const here = / skipped_here=(\d+) tests_ran_here=(\S+)( not_owed_here=\d+)?$/.exec(lines[at]!);
const notOwed = lines.slice(at + 1).find(x => x.startsWith('gstack-evidence: not owed: '))?.slice('gstack-evidence: not owed: '.length);
return { runs: got?.[3], tests: got?.[1], files: !!got?.[2], ...(notOwed ? { notOwed } : {}), ...(here ? { here: `skipped_here=${here[1]} file(s) tests_ran_here=${here[2]}` } : {}) };
return { runs: got?.[3], tests: got?.[1], files: !!got?.[2], ...(notOwed ? { notOwed } : {}), ...(here ? { here: `skipped_here=${here[1]} file(s) tests_ran_here=${here[2]}${here[3] ?? ''}` } : {}) };
}
/** The per_file runner command for `files`: `{files}` replaced by the paths, each single-quoted for bash. */
/** The per_file runner command for `files`: `{files}` replaced by the paths, each anchored with ./ (Bun reads a bare relative path as a substring filter) and single-quoted for bash. */
export const perFileCommand = (template: string, files: string[]): string =>
template.replaceAll('{files}', files.map(f => `'${f.replace(/'/g, `'\\''`)}'`).join(' '));
template.replaceAll('{files}', files.map(f => `'${(/^(\/|\.\/)/.test(f) ? f : `./${f}`).replace(/'/g, `'\\''`)}'`).join(' '));
/** `<path>: <message>` errors for `ship_lanes_from`; empty when valid. Shared with lib/pregate/config.ts. */
export function shipLanesErrors(raw: unknown, profileNames: readonly string[]): Array<{ path: string; message: string }> {
+45 -4
View File
@@ -526,7 +526,7 @@ describe('GST-1190: a lane carries the covered files and runs only the skipped r
const out = preflight(f, [`free=${FREE}`]);
expect(out.status, out.stdout + out.stderr).toBe(0);
expect(lane(out.stdout, 'free')).toBe('LANE: free carried run=7/1 tests_ran=1 (files) skipped_here=1 file(s) tests_ran_here=1');
expect([args(f), runs(f, 'free-runs')]).toEqual(['test/b.test.ts\n', 0]);
expect([args(f), runs(f, 'free-runs')]).toEqual(['./test/b.test.ts\n', 0]);
const all = rows(f);
expect(all.find(r => r.source === 'ci')).toMatchObject({ label: 'free', tests_ran: 1, tests_unit: 'files', skipped_here: { label: 'free.skipped', files: ['test/b.test.ts'], tests_ran: 1 } });
expect(all.find(r => r.label === 'free.skipped')).toMatchObject({ exit: 0, tests_ran: 1 });
@@ -536,14 +536,14 @@ describe('GST-1190: a lane carries the covered files and runs only the skipped r
const f = shipFixture(withRunner(1)); fixtures.push(f);
world(f, { tree: { skipped_files: ['test/b.test.ts'] } });
const rec = f.run(path.join(BIN, 'gstack-evidence'), ['record-ci', '--label', 'free', '--base', 'main', '--', FREE]);
expect([rec.status, rec.stderr]).toEqual([1, expect.stringMatching(/^CARRY_NONE: skipped_failed: the per_file run of 1 skipped file\(s\) exited 1 \(label=free\.skipped\)$/m)]);
expect([rec.status, rec.stderr]).toEqual([1, expect.stringMatching(/^CARRY_NONE: skipped_failed: the per_file run of test\/b\.test\.ts exited 1 \(label=free\.skipped\)$/m)]);
expect(rows(f).filter(r => r.source === 'ci')).toEqual([]);
expect(preflight(f, [`free=${FREE}`]).stdout).toContain('CI_CARRY: none label=free (skipped_failed');
expect(runs(f, 'free-runs')).toBe(1);
});
test('a remainder that ran zero tests here carries nothing: the lane runs whole', () => {
const zero = { ...BASE, '.gstack/pregate.json': JSON.stringify({ run_profiles: { 'ci/free': PROFILE }, ship_lanes_from: { [FREE]: { profiles: ['ci/free'], inventory: "printf 'test/a.test.ts\\ntest/b.test.ts\\n'", per_file: "printf ' 0 pass\\n 1 skip\\n 0 fail\\nRan 1 test across 1 file. [1.00ms]\\n' >&2 # {files}" } } }, null, 2) + '\n' };
test('a remainder file that registers no test here carries nothing: the lane runs whole', () => {
const zero = { ...BASE, '.gstack/pregate.json': JSON.stringify({ run_profiles: { 'ci/free': PROFILE }, ship_lanes_from: { [FREE]: { profiles: ['ci/free'], inventory: "printf 'test/a.test.ts\\ntest/b.test.ts\\n'", per_file: "printf ' 0 pass\\n 0 fail\\nRan 0 tests across 1 file. [1.00ms]\\n' >&2 # {files}" } } }, null, 2) + '\n' };
const f = shipFixture(zero); fixtures.push(f);
world(f, { tree: { skipped_files: ['test/b.test.ts'] } });
const rec = f.run(path.join(BIN, 'gstack-evidence'), ['record-ci', '--label', 'free', '--base', 'main', '--', FREE]);
@@ -580,6 +580,47 @@ describe('GST-1190: a lane carries the covered files and runs only the skipped r
expect(rows(f).filter(r => r.source === 'ci')).toEqual([]);
});
// Value: protects=GST-1314: a remainder file that skips every test here too (env-gated or tool-gated by design) is recorded as not owed
// instead of refusing the whole carry, while a file that registers no test, or a mixed remainder hiding one, still refuses;
// fails_when=run 41754278952's six skipped files (0 pass, 20 skip locally) make every gstack carry end in skipped_zero_run again;
// why_new=GST-1314 replay (L6, #3979); seam=fake gh, gstack-evidence record-ci, per_file runner printing bun's per-file summary
const SIX: Array<[string, number]> = [["browse/test/security-bench.test.ts",3],["test/codex-resume-flag-semantics.test.ts",2],["test/cso-docker-integration.test.ts",5],["test/cso-node-lifecycle-integration.test.ts",1],["test/cso-stack-cold-integration.test.ts",1],["test/motion-pipeline.test.ts",8]];
const bunSummary = (f: ShipFx, cases: Array<[string, string]>) => fs.writeFileSync(path.join(f.root, 'perfile.sh'), `printf '%s\\n' "$@" >> ../perfile-args\ncase "$1" in\n${cases.map(([pat, out]) => ` *${pat}*) printf '${out}' >&2;;\n`).join('')}esac\n`);
const skipsOnly = (n: number) => ` 0 pass\\n ${n} skip\\n 0 fail\\nRan ${n} test${n === 1 ? '' : 's'} across 1 file. [1.00ms]\\n`;
const policy = (files: string[]) => ({ ...BASE, '.gstack/pregate.json': JSON.stringify({ run_profiles: { 'ci/free': PROFILE }, ship_lanes_from: { [FREE]: { profiles: ['ci/free'], inventory: `printf '${['test/a.test.ts', ...files].join('\\n')}\\n'`, per_file: 'sh ../perfile.sh {files}' } } }, null, 2) + '\n' });
test('GST-1314: run 41754278952\'s six remainder files skip every test here too, so they are not owed and the lane carries', () => {
const f = shipFixture(policy(SIX.map(([p]) => p))); fixtures.push(f);
bunSummary(f, SIX.map(([p, n]) => [p, skipsOnly(n)]));
world(f, { tree: { files: ['test/a.test.ts', ...SIX.map(([p]) => p)], skipped_files: SIX.map(([p]) => p) } });
const rec = f.run(path.join(BIN, 'gstack-evidence'), ['record-ci', '--label', 'free', '--base', 'main', '--', FREE]);
expect(rec.status, rec.stderr).toBe(0);
expect(rec.stderr).toContain('not owed here: 6 remainder file(s) skipped every test on CI and here too');
expect(args(f).trim().split('\n')).toEqual(SIX.map(([p]) => './' + p));
const ci = rows(f).find(r => r.source === 'ci');
expect(ci).toMatchObject({ label: 'free', tests_ran: 1, tests_unit: 'files', skipped_here: { label: 'free.skipped', files: SIX.map(([p]) => p), tests_ran: 0, not_owed: SIX.map(([p]) => p) } });
expect(lane(preflight(f, [`free=${FREE}`]).stdout, 'free')).toBe('LANE: free carried run=7/1 tests_ran=1 (files) skipped_here=6 file(s) tests_ran_here=0 not_owed_here=6');
});
test('GST-1314: a remainder that mixes a file that ran here with one that registers no test still refuses', () => {
const f = shipFixture(policy(['test/b.test.ts', 'test/c.test.ts'])); fixtures.push(f);
bunSummary(f, [['b.test', ' 2 pass\\n 0 fail\\nRan 2 tests across 1 file. [1.00ms]\\n'], ['c.test', ' 0 pass\\n 0 fail\\nRan 0 tests across 1 file. [1.00ms]\\n']]);
world(f, { tree: { files: ['test/a.test.ts', 'test/b.test.ts', 'test/c.test.ts'], skipped_files: ['test/b.test.ts', 'test/c.test.ts'] } });
const rec = f.run(path.join(BIN, 'gstack-evidence'), ['record-ci', '--label', 'free', '--base', 'main', '--', FREE]);
expect([rec.status, rec.stderr]).toEqual([1, expect.stringMatching(/^CARRY_NONE: skipped_zero_run: the per_file run of test\/c\.test\.ts /m)]);
expect(rows(f).filter(r => r.source === 'ci')).toEqual([]);
});
test('GST-1314: a remainder file whose log hit the 2MB cap is never counted not owed from the truncated log', () => {
const f = shipFixture(policy(['test/b.test.ts'])); fixtures.push(f);
fs.writeFileSync(path.join(f.root, 'perfile.sh'), `printf '%s\\n' "$@" >> ../perfile-args\nprintf ' 0 pass\\n 5 skip\\n 0 fail\\nRan 5 tests across 1 file. [1.00ms]\\n' >&2\nhead -c 2200000 /dev/zero | tr '\\0' x >&2\nprintf '\\n 0 pass\\n 0 fail\\nRan 0 tests across 1 file. [1.00ms]\\n' >&2\n`);
world(f, { tree: { files: ['test/a.test.ts', 'test/b.test.ts'], skipped_files: ['test/b.test.ts'] } });
const rec = f.run('sh', ['-c', '"$0" "$@" 2>../record-ci.err', path.join(BIN, 'gstack-evidence'), 'record-ci', '--label', 'free', '--base', 'main', '--', FREE]);
expect([rec.status, fs.readFileSync(path.join(f.root, 'record-ci.err'), 'utf-8')]).toEqual([1, expect.stringMatching(/^CARRY_NONE: skipped_zero_run: the per_file run of test\/b\.test\.ts ran 0 tests here /m)]);
expect(fs.readFileSync(rows(f).findLast(r => r.label === 'free.skipped').log_path, 'utf-8')).toContain('log truncated at 2MB');
expect(rows(f).filter(r => r.source === 'ci')).toEqual([]);
});
test('per_file is validated: it needs {files} and an inventory entry', async () => {
const { shipLanesErrors } = await import('../lib/ci-carry');
expect(shipLanesErrors({ x: { profiles: ['p'], inventory: 'ls', per_file: 'bun test {files}' } }, ['p'])).toEqual([]);