mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-10 02:30:39 +01:00
train S1: merge #3892 at 13601c0440 v1.94.16.0
This commit is contained in:
commit
224cf12e6d
18 files changed
+414
-39
No files matched your search
@@ -8,7 +8,7 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { EXIT } from '../lib/headless-artifacts';
|
||||
import { BUMP_NEVER_KEY_VERSION, CHANGELOG_FRAGMENTS_KEY_VERSION, DEFAULT_POLICY, POLICY_KEYS, POLICY_KEY_INTRODUCED, REVIEW_SCOPE_KEY_VERSION, globToRegExp, matchesAny, minimumReader, shipConsequences, validatePolicy } from '../lib/ship-policy';
|
||||
import { BUMP_NEVER_KEY_VERSION, CHANGELOG_FRAGMENTS_KEY_VERSION, DEFAULT_POLICY, POLICY_KEYS, POLICY_KEY_INTRODUCED, REVIEW_SCOPE_KEY_VERSION, STAMP_PLACEHOLDERS_KEY_VERSION, globToRegExp, matchesAny, minimumReader, shipConsequences, validatePolicy } from '../lib/ship-policy';
|
||||
import { fillDue } from '../lib/ship-queue-train';
|
||||
import { cmpVersion, parseVersion } from '../lib/version-source';
|
||||
import { BASE_POLICY, cleanup, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/restamp-fixture';
|
||||
@@ -63,6 +63,32 @@ describe('validatePolicy (schema + containment, pure)', () => {
|
||||
expect(codes({ preregistration_shas: ['not-a-sha'] })).toEqual(['POLICY_CONTAINMENT:preregistration_shas[0]']);
|
||||
});
|
||||
|
||||
// Value: protects=stamp_placeholders defaults to none, needs its reader floor, and admits only short one-line literals
|
||||
// that are neither the {version} slot nor a version (GST-1026); fails_when=a non-default value validates without
|
||||
// min_gstack_version, or a version, a slot, a multi-line or an oversized entry is accepted; seam=validatePolicy
|
||||
test('stamp_placeholders: empty by default, needs its floor, and admits only short literal placeholders (GST-1026)', () => {
|
||||
const codes = (p: unknown) => validatePolicy(p).map(e => `${e.code}:${e.path}`);
|
||||
const at = (stamp_placeholders: unknown) => codes({ stamp_placeholders, min_gstack_version: STAMP_PLACEHOLDERS_KEY_VERSION });
|
||||
expect(DEFAULT_POLICY.stamp_placeholders).toEqual([]);
|
||||
expect(POLICY_KEY_INTRODUCED.stamp_placeholders).toBe(STAMP_PLACEHOLDERS_KEY_VERSION);
|
||||
expect(codes({ stamp_placeholders: [] })).toEqual([]);
|
||||
expect(codes({ stamp_placeholders: ['TBD'] })).toEqual(['POLICY_INVALID:stamp_placeholders']);
|
||||
expect(minimumReader({ stamp_placeholders: ['TBD'] })).toBe(STAMP_PLACEHOLDERS_KEY_VERSION);
|
||||
expect(at(['TBD', 'UNRELEASED'])).toEqual([]);
|
||||
expect(at('TBD')).toEqual(['POLICY_INVALID:stamp_placeholders']);
|
||||
expect(at([7])).toEqual(['POLICY_INVALID:stamp_placeholders[0]']);
|
||||
expect(at(Array.from({ length: 17 }, (_, i) => `P${i}`))).toEqual(['POLICY_CONTAINMENT:stamp_placeholders']);
|
||||
expect(at(['x'.repeat(33)])).toEqual(['POLICY_CONTAINMENT:stamp_placeholders[0]']);
|
||||
expect(at(['x'.repeat(32)])).toEqual([]);
|
||||
expect(at(['TB\nD'])).toEqual(['POLICY_CONTAINMENT:stamp_placeholders[0]']);
|
||||
expect(at([''])).toEqual(['POLICY_CONTAINMENT:stamp_placeholders[0]']);
|
||||
expect(at([' '])).toEqual(['POLICY_CONTAINMENT:stamp_placeholders[0]']);
|
||||
expect(at(['{version}'])).toEqual(['POLICY_INVALID:stamp_placeholders[0]']);
|
||||
expect(at(['next-{version}'])).toEqual(['POLICY_INVALID:stamp_placeholders[0]']);
|
||||
expect(at(['1.2.3.0'])).toEqual(['POLICY_INVALID:stamp_placeholders[0]']);
|
||||
expect(at(['0.60.553'])).toEqual(['POLICY_INVALID:stamp_placeholders[0]']);
|
||||
});
|
||||
|
||||
// Value: protects=bump never is refused without its floor (an older gstack names the cause) and next to any policy that stamps a version at every merge, reports its reader floor, and prints its Step 12 consequence; fails_when=never validates bare, or beside stamp-at-merge, train or changelog_fragments;
|
||||
// why_new=GST-380 added the value to an older key, which the per-key floor check skips; seam=validatePolicy, minimumReader, shipConsequences; no_claim=Step 12's behavior (test/ship-release.test.ts)
|
||||
test('bump never needs its floor and rules out every policy that stamps at merge', () => {
|
||||
|
||||
@@ -53,6 +53,25 @@ describe('E18: a repository without a version source or CHANGELOG', () => {
|
||||
expect(JSON.parse(fs.readFileSync(path.join(out, 'ship-run.json'), 'utf8')).version).toBe('none');
|
||||
});
|
||||
|
||||
// Value: protects=a receipt write --version named in a repository with no version source keeps that version through a
|
||||
// carry (GST-1030 review); fails_when=the carry refuses because HEAD has no readable version, or drops the version;
|
||||
// seam=bin/gstack-ship-receipt cmdCarry
|
||||
test('a carry keeps the version write --version named when HEAD has no version source (GST-1030)', () => {
|
||||
const repo = fixture();
|
||||
for (const f of ['VERSION', 'package.json', 'package-lock.json']) fs.rmSync(path.join(repo.work, f));
|
||||
repo.git('add', '-A'); repo.git('commit', '-qm', 'changelog only'); repo.git('push', '-q', 'origin', 'main');
|
||||
makePr(repo, 'pr-unversioned');
|
||||
const body = path.join(repo.root, 'body.md');
|
||||
fs.writeFileSync(body, '## Summary\n');
|
||||
expect(runBin(repo, 'gstack-ship-receipt', ['write', '--pr', '3', '--base', 'main', '--session-kind', 'x', '--version', '2.0.0', '--body', body]).status).toBe(EXIT.ok);
|
||||
fs.mkdirSync(path.join(repo.work, 'test'), { recursive: true });
|
||||
fs.writeFileSync(path.join(repo.work, 'test', 'a.test.ts'), 'test a\n');
|
||||
repo.git('add', '-A'); repo.git('commit', '-qm', 'a test-only change');
|
||||
const carried = runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
|
||||
expect(carried.out, carried.err).toContain('CARRY: carried');
|
||||
expect(extractReceipt(fs.readFileSync(body, 'utf8')).receipt?.version).toBe('2.0.0');
|
||||
});
|
||||
|
||||
test('a repository with a version source keeps its version, and one with only a CHANGELOG records no version rather than "none"', () => {
|
||||
const repo = fixture();
|
||||
const w = runBin(repo, 'gstack-ship-receipt', ['write', '--base', 'main', '--session-kind', 'x', '--json']);
|
||||
@@ -248,6 +267,25 @@ describe('carry follow-ups: git floor, named refusal, branch-touched walk, digit
|
||||
expect(stampOnlyText({ before, after: lines(key('1.2.3.0'), key('1.2.4.0')), oldVersion: '1.2.4.0', newVersion: '1.2.4.0', templates })).toBe(false);
|
||||
});
|
||||
|
||||
// Value: protects=the carry classifier reads a declared placeholder moved to the new version on the branch's own row
|
||||
// as a stamp, through the same predicate as finish and the tree receipt (GST-1026); fails_when=stampOnlyText ignores
|
||||
// the policy's placeholders, or accepts an undeclared one or a base row; seam=lib/ship-receipt.ts stampOnlyText
|
||||
test('stampOnlyText reads a declared placeholder moved on the branch\'s own row as a stamp, and nothing else (GST-1026)', () => {
|
||||
const templates = ["since: '{version}'"];
|
||||
const row = (v: string, id: string) => ` { since: '${v}', id: '${id}' },`;
|
||||
const text = (...rows: string[]) => ['export const ROWS = [', ...rows, '];', ''].join('\n');
|
||||
const before = text(row('TBD', 'base'), row('TBD', 'mine'));
|
||||
const side = { fork: '0.60.552.0', added: new Set([row('TBD', 'mine')]), at: new Set([3]) };
|
||||
const change = (after: string, extra: Record<string, unknown> = {}) => stampOnlyText({ before, after, oldVersion: '0.60.552.0', newVersion: '0.60.553.0', templates, side, placeholders: ['TBD'], ...extra });
|
||||
expect(change(text(row('TBD', 'base'), row('0.60.553.0', 'mine')))).toBe(true);
|
||||
expect(change(text(row('TBD', 'base'), row('0.60.553.0', 'mine')), { oldVersion: '0.60.553.0' })).toBe(true);
|
||||
expect(change(text(row('TBD', 'base'), row('0.60.553.0', 'mine')), { placeholders: [] })).toBe(false);
|
||||
expect(change(text(row('TBD', 'base'), row('0.60.553.0', 'mine')), { placeholders: ['WIP'] })).toBe(false);
|
||||
expect(change(text(row('TBD', 'base'), row('0.60.553.0', 'mine')), { templates: [] })).toBe(false);
|
||||
expect(change(text(row('TBD', 'base'), row('0.60.553.0', 'mine')), { side: undefined })).toBe(false);
|
||||
expect(change(text(row('0.60.553.0', 'base'), row('TBD', 'mine')))).toBe(false);
|
||||
});
|
||||
|
||||
test('planCarry names at most CARRY_NAMED_PATHS paths', () => {
|
||||
const many = Array.from({ length: CARRY_NAMED_PATHS + 3 }, (_, i) => `src/f${i}.ts`);
|
||||
const plan = planCarry({ from: 'a'.repeat(40), head: 'b'.repeat(40), changed: many.map(p => ({ path: p, class: 'product' as const })), productPatchId: { from: 'x', head: 'y' }, testPatchId: { from: '', head: '' }, changedProduct: many, mergeCommits: 0, rangeDiff: '' });
|
||||
|
||||
@@ -14,6 +14,8 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { EXIT } from '../lib/headless-artifacts';
|
||||
import { rewriteStamps, tentativeMentions } from '../lib/restamp';
|
||||
import { STAMP_PLACEHOLDERS_KEY_VERSION } from '../lib/ship-policy';
|
||||
import { extractReceipt } from '../lib/ship-receipt';
|
||||
import type { GitFixtureTemplate } from './helpers/git-fixture-template';
|
||||
import { BASE_POLICY, cleanup, cloneFixture, fixtureTemplate, makePr, mergeMain, mergeToMain, readFile, restamp, runBin, type FixtureRepo } from './helpers/restamp-fixture';
|
||||
|
||||
@@ -425,10 +427,165 @@ describe('a branch-added row left at the fork version after the base moved (GST-
|
||||
const { repo, body } = carryAfterStamp('restamp-carry-mode-', [row('1.2.3.0', 'base_row')], row('1.2.3.0', 'mine'),
|
||||
[row('1.2.3.0', 'base_row'), '', row('1.2.5.0', 'mine')]);
|
||||
fs.chmodSync(path.join(repo.work, NOTICE), 0o755);
|
||||
repo.git('commit', '-qam', 'stamp 1.2.5.0, and make the file executable');
|
||||
repo.git('add', '-A');
|
||||
repo.git('update-index', '--chmod=+x', '--', NOTICE);
|
||||
repo.git('commit', '-qm', 'stamp 1.2.5.0, and make the file executable');
|
||||
expect(repo.git('ls-tree', 'HEAD', '--', NOTICE)).toStartWith('100755 ');
|
||||
const carried = runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
|
||||
expect(carried.status, carried.out).toBe(EXIT.refused);
|
||||
expect(carried.err).toContain('RECEIPT_CARRY_PRODUCT_CHANGED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('declared placeholders and the receipt carry (GST-1026, GST-1030)', () => {
|
||||
/** gbrain's policy (GST-1026): the since: template, and TBD declared as a placeholder when `declared`. */
|
||||
const placeholderPolicy = (declared: boolean) => ({ ...receiptPolicy(true), ...(declared ? { stamp_placeholders: ['TBD'], min_gstack_version: STAMP_PLACEHOLDERS_KEY_VERSION } : {}) });
|
||||
|
||||
/** A branch row written as since: 'TBD' at the claim 1.2.4.0, then the stamp to 1.2.5.0 with the row restamped by hand (gbrain's release:restamp). */
|
||||
function tbdStamp(prefix: string, pol: Record<string, unknown>, ownRow: string): { repo: FixtureRepo; gated: string } {
|
||||
const repo = makeFixtureRepo({ prefix, policy: pol }); repos.push(repo);
|
||||
write(repo, NOTICE, notice([row('TBD', 'base_row')]));
|
||||
repo.git('add', '-A'); repo.git('commit', '-qm', 'notice rows'); repo.git('push', '-q', 'origin', 'main');
|
||||
makePr(repo, 'pr-tbd');
|
||||
expect(restamp(repo, ['--version', '1.2.4.0']).status).toBe(EXIT.ok);
|
||||
repo.git('commit', '-qam', 'stamp 1.2.4.0');
|
||||
write(repo, NOTICE, notice([row('TBD', 'base_row'), '', ownRow]));
|
||||
repo.git('commit', '-qam', 'mine: a TBD row');
|
||||
const gated = repo.git('rev-parse', 'HEAD');
|
||||
expect(restamp(repo, ['--version', '1.2.5.0']).status).toBe(EXIT.ok);
|
||||
return { repo, gated };
|
||||
}
|
||||
|
||||
// Value: protects=the tree receipt (stamp, verify, gate-ahead replay) accepts gbrain's restamp of the branch's own
|
||||
// since: 'TBD' row when the policy declares TBD, and refuses it undeclared, on a base row, or on a base row whose text
|
||||
// the branch duplicates (GST-1026); fails_when=classifyFile reads the TBD move as a hand edit, or accepts it off the
|
||||
// branch's own rows; seam=lib/tree-receipt.ts stampOnlyHunks
|
||||
test('the tree receipt accepts a declared TBD row restamped on the branch, and refuses the same move on a base row (GST-1026)', () => {
|
||||
const own = tbdStamp('restamp-tbd-receipt-', placeholderPolicy(true), row('TBD', 'mine'));
|
||||
write(own.repo, NOTICE, readFile(own.repo, NOTICE).replace(row('TBD', 'mine'), row('1.2.5.0', 'mine')));
|
||||
own.repo.git('commit', '-qam', 'stamp 1.2.5.0: the TBD row');
|
||||
const accepted = runBin(own.repo, 'gstack-tree-receipt', ['--gated', own.gated, '--base', 'main']);
|
||||
expect(accepted.out, accepted.err).toContain('TREE: same-modulo-stamps');
|
||||
write(own.repo, NOTICE, readFile(own.repo, NOTICE).replace(row('TBD', 'base_row'), row('1.2.5.0', 'base_row')));
|
||||
own.repo.git('commit', '-qam', 'move the base TBD row by hand');
|
||||
const base = runBin(own.repo, 'gstack-tree-receipt', ['--gated', own.gated, '--base', 'main']);
|
||||
expect(base.out, base.err).toContain('TREE: changed');
|
||||
expect(base.out).toContain("id: 'base_row'");
|
||||
});
|
||||
|
||||
test('the tree receipt refuses an undeclared TBD row restamped on the branch (GST-1026)', () => {
|
||||
const undeclared = tbdStamp('restamp-tbd-undeclared-', placeholderPolicy(false), row('TBD', 'mine'));
|
||||
write(undeclared.repo, NOTICE, readFile(undeclared.repo, NOTICE).replace(row('TBD', 'mine'), row('1.2.5.0', 'mine')));
|
||||
undeclared.repo.git('commit', '-qam', 'stamp 1.2.5.0: the TBD row');
|
||||
const refused = runBin(undeclared.repo, 'gstack-tree-receipt', ['--gated', undeclared.gated, '--base', 'main']);
|
||||
expect(refused.out, refused.err).toContain('TREE: changed');
|
||||
});
|
||||
|
||||
test('the tree receipt refuses a restamp of the base copy of a TBD row the branch duplicates (GST-1026)', () => {
|
||||
const dup = tbdStamp('restamp-tbd-dup-', placeholderPolicy(true), row('TBD', 'base_row'));
|
||||
write(dup.repo, NOTICE, notice([row('1.2.5.0', 'base_row'), '', row('TBD', 'base_row')]));
|
||||
dup.repo.git('commit', '-qam', 'move the base copy of the duplicated TBD row');
|
||||
const copy = runBin(dup.repo, 'gstack-tree-receipt', ['--gated', dup.gated, '--base', 'main']);
|
||||
expect(copy.out, copy.err).toContain('TREE: changed');
|
||||
});
|
||||
|
||||
/** A receipt written at the claim 1.2.4.0 after `own` changed the branch, then the stamp to 1.2.5.0, uncommitted. */
|
||||
function receiptThenStamp(prefix: string, pol: Record<string, unknown>, own: (repo: FixtureRepo) => void): { repo: FixtureRepo; body: string } {
|
||||
const repo = makeFixtureRepo({ prefix, policy: pol }); repos.push(repo);
|
||||
write(repo, NOTICE, notice([row('TBD', 'base_row')]));
|
||||
fs.rmSync(path.join(repo.work, 'package-lock.json'));
|
||||
repo.git('add', '-A'); repo.git('commit', '-qm', 'notice rows, no lockfile'); repo.git('push', '-q', 'origin', 'main');
|
||||
makePr(repo, 'pr-carry');
|
||||
expect(restamp(repo, ['--version', '1.2.4.0']).status).toBe(EXIT.ok);
|
||||
repo.git('commit', '-qam', 'stamp 1.2.4.0');
|
||||
own(repo);
|
||||
repo.git('add', '-A'); repo.git('commit', '-qm', 'mine');
|
||||
const body = path.join(repo.root, 'body.md');
|
||||
fs.writeFileSync(body, '## Summary\n');
|
||||
const w = runBin(repo, 'gstack-ship-receipt', ['write', '--pr', '3', '--base', 'main', '--session-kind', 'x', '--body', body]);
|
||||
expect(w.status, w.err).toBe(EXIT.ok);
|
||||
expect(restamp(repo, ['--version', '1.2.5.0']).status).toBe(EXIT.ok);
|
||||
return { repo, body };
|
||||
}
|
||||
const carry = (repo: FixtureRepo, body: string) => runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
|
||||
|
||||
// Value: protects=the receipt carry follows gbrain's restamp of the branch's own since: 'TBD' row when the policy
|
||||
// declares TBD, and refuses it undeclared (GST-1026); fails_when=productSpec's own-row check ignores the policy's
|
||||
// placeholders and refuses RECEIPT_CARRY_PRODUCT_CHANGED; seam=bin/gstack-ship-receipt productSpec ownRows
|
||||
test.each([['declared', true], ['undeclared', false]])('the carry and a TBD row restamped on the branch: %s', (_, declared) => {
|
||||
const { repo, body } = receiptThenStamp(`restamp-tbd-carry-${declared}-`, placeholderPolicy(declared), r => write(r, NOTICE, notice([row('TBD', 'base_row'), '', row('TBD', 'mine')])));
|
||||
write(repo, NOTICE, readFile(repo, NOTICE).replace(row('TBD', 'mine'), row('1.2.5.0', 'mine')));
|
||||
repo.git('commit', '-qam', 'stamp 1.2.5.0: the TBD row');
|
||||
const carried = carry(repo, body);
|
||||
if (declared) expect(carried.out, carried.err).toContain('CARRY: carried');
|
||||
else expect([carried.status, carried.err.includes('RECEIPT_CARRY_PRODUCT_CHANGED')], carried.err).toEqual([EXIT.refused, true]);
|
||||
});
|
||||
|
||||
// Value: protects=a carried receipt names the head's version, the one write would record there, after a main merge
|
||||
// and a restamp (GST-1030 point 1, GGST-1); fails_when=carryReceipt copies version from the old receipt;
|
||||
// seam=bin/gstack-ship-receipt cmdCarry
|
||||
test('the carried receipt records the head\'s version after a main merge and a restamp (GST-1030)', () => {
|
||||
const { repo, body } = receiptThenStamp('restamp-carry-version-', policy(true), r => write(r, 'src.ts', 'export const x = 0;\n'));
|
||||
repo.git('reset', '-q', '--hard');
|
||||
expect(readFile(repo, 'VERSION')).toBe('1.2.4.0\n');
|
||||
makePr(repo, 'pr-other');
|
||||
expect(restamp(repo, ['--next']).status).toBe(EXIT.ok);
|
||||
repo.git('commit', '-qam', 'stamp other');
|
||||
mergeToMain(repo, 'pr-other');
|
||||
repo.git('checkout', '-q', 'pr-carry');
|
||||
mergeMain(repo);
|
||||
expect(restamp(repo, ['--next']).status).toBe(EXIT.ok);
|
||||
repo.git('commit', '-qam', 'restamp after the lost slot');
|
||||
expect(readFile(repo, 'VERSION')).toBe('1.2.5.0\n');
|
||||
expect(extractReceipt(fs.readFileSync(body, 'utf8')).receipt?.version).toBe('1.2.4.0');
|
||||
const carried = carry(repo, body);
|
||||
expect(carried.out, carried.err).toContain('CARRY: carried');
|
||||
expect(extractReceipt(fs.readFileSync(body, 'utf8')).receipt?.version).toBe('1.2.5.0');
|
||||
});
|
||||
|
||||
/** package.json with its dependency pin set to `pin`, every other field kept. */
|
||||
const pinned = (repo: FixtureRepo, pin: string) => {
|
||||
const pkg = JSON.parse(readFile(repo, 'package.json'));
|
||||
pkg.dependencies = { ...pkg.dependencies, 'left-pad': pin };
|
||||
write(repo, 'package.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||
};
|
||||
|
||||
// Value: protects=a pin-bump PR's receipt carries across a main merge and a restamp when package.json's only change since
|
||||
// review is its version field, and refuses when the pin itself moved (GST-1030, GBEV-3, gbrain-evals #306);
|
||||
// fails_when=the carry counts the PR's package.json as product and refuses RECEIPT_CARRY_PRODUCT_CHANGED on a version-only
|
||||
// move, or lets a changed dependency pin ride; seam=bin/gstack-ship-receipt cmdCarry walk
|
||||
test.each([['the pin unchanged since review', false], ['the pin moved since review', true]])('the carry and a pin-bump PR after a main merge: %s (GST-1030)', (_, moved) => {
|
||||
const { repo, body } = receiptThenStamp(`restamp-carry-pin-${moved}-`, policy(true), r => pinned(r, '1.3.0'));
|
||||
repo.git('reset', '-q', '--hard');
|
||||
makePr(repo, 'pr-other');
|
||||
expect(restamp(repo, ['--next']).status).toBe(EXIT.ok);
|
||||
repo.git('commit', '-qam', 'stamp other');
|
||||
mergeToMain(repo, 'pr-other');
|
||||
repo.git('checkout', '-q', 'pr-carry');
|
||||
mergeMain(repo);
|
||||
expect(restamp(repo, ['--next']).status).toBe(EXIT.ok);
|
||||
if (moved) pinned(repo, '1.3.1');
|
||||
repo.git('commit', '-qam', 'restamp after the lost slot');
|
||||
expect(JSON.parse(readFile(repo, 'package.json')).version).toBe('1.2.5');
|
||||
const carried = carry(repo, body);
|
||||
if (moved) expect([carried.status, carried.err.includes('RECEIPT_CARRY_PRODUCT_CHANGED')], carried.err).toEqual([EXIT.refused, true]);
|
||||
else expect(carried.out, carried.err).toContain('CARRY: carried');
|
||||
});
|
||||
|
||||
// Value: protects=a version-only restamp of EVIDENCE.md is release metadata to the carry when the policy declares it in
|
||||
// stamp_files, and product when it does not (GST-1030 point 2, GGST-1); fails_when=the carry counts the declared
|
||||
// file's stamp as a product change (RECEIPT_CARRY_PRODUCT_CHANGED), or lets an undeclared file's version edit ride;
|
||||
// seam=bin/gstack-ship-receipt productSpec, lib/ship-receipt.ts stampOnlyText
|
||||
test.each([['declared in stamp_files', true], ['undeclared', false]])('the carry and a version-only restamp of EVIDENCE.md: %s (GST-1030)', (_, declared) => {
|
||||
const base = policy(true);
|
||||
const pol = declared ? { ...base, stamp_files: [...base.stamp_files, 'EVIDENCE.md'] } : base;
|
||||
const { repo, body } = receiptThenStamp(`restamp-carry-evidence-${declared}-`, pol, r => write(r, 'EVIDENCE.md', '# Evidence\n\nVerified on v1.2.4.0 with the free suite\n'));
|
||||
if (!declared) write(repo, 'EVIDENCE.md', '# Evidence\n\nVerified on v1.2.5.0 with the free suite\n');
|
||||
expect(readFile(repo, 'EVIDENCE.md')).toBe('# Evidence\n\nVerified on v1.2.5.0 with the free suite\n');
|
||||
repo.git('commit', '-qam', 'stamp 1.2.5.0');
|
||||
const carried = carry(repo, body);
|
||||
if (declared) expect(carried.out, carried.err).toContain('CARRY: carried');
|
||||
else expect([carried.status, carried.err.includes('RECEIPT_CARRY_PRODUCT_CHANGED')], carried.err).toEqual([EXIT.refused, true]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { EXIT } from '../lib/headless-artifacts';
|
||||
import { comparePath } from '../lib/ship-patch';
|
||||
import { STAMP_PLACEHOLDERS_KEY_VERSION } from '../lib/ship-policy';
|
||||
import { extractReceipt, placeReceiptFirst } from '../lib/ship-receipt';
|
||||
import { cleanup, makeFixtureRepo, runBin, write, type FixtureRepo } from './helpers/pregate-fixture';
|
||||
import { lineOf, PREFLIGHT, RELEASE, shipFixture, type ShipFx } from './helpers/ship-bin-fixture';
|
||||
@@ -266,6 +267,37 @@ describe('a restamp after a lost slot (GST-895, GBRA-218, gbrain #8134)', () =>
|
||||
const base = kept([['0.9.0.0', 'old'], ['1.0.1.0', 'base'], ['1.0.0.0', 'mine']]);
|
||||
expect([base.status, lineOf(base.stdout, 'STOP')]).toEqual([3, behaviorStop]);
|
||||
});
|
||||
|
||||
// Value: protects=gbrain's restamp of the PR's own `since: 'TBD'` row to the new version routes as metadata when the
|
||||
// policy declares TBD in stamp_placeholders (GST-1026, gbrain #8444); an undeclared placeholder, a base row moved the
|
||||
// same way, a duplicated base copy, or a line no stamp_pattern matches still stops; fails_when=stampedFiles reads the
|
||||
// move as behavior (gbrain #8444) or accepts it off the branch's own rows; seam=lib/ship-bin-common.ts stampedFiles
|
||||
test('a declared placeholder on the branch row is metadata; undeclared, a base row or no template stops (GST-1026)', () => {
|
||||
const declared = (placeholders: string[] | undefined, templates = ["since: '{version}'"]) => JSON.stringify({ stamp_files: ['VERSION', 'src/notice.ts'], stamp_patterns: templates,
|
||||
...(placeholders ? { stamp_placeholders: placeholders, min_gstack_version: STAMP_PLACEHOLDERS_KEY_VERSION } : {}) });
|
||||
const restamped = (pol: string, baseRows: [string, string][], ownRows: [string, string][], rows: [string, string][]) => {
|
||||
const f = fx({ '.gstack/ship-policy.json': pol, VERSION: '1.0.0.0\n', 'src/notice.ts': notice(...baseRows) },
|
||||
{ VERSION: '1.0.1.0\n', 'src/notice.ts': notice(...baseRows, ...ownRows) });
|
||||
const reviewed = wtree(f);
|
||||
const post = snapshot(f);
|
||||
f.write({ VERSION: '1.0.2.0\n', 'src/notice.ts': notice(...rows) }); f.commit('restamp: TBD rows to 1.0.2.0');
|
||||
return finish(f, reviewed, post, ['--reviewed-base', mergeBase(f)]);
|
||||
};
|
||||
const base: [string, string][] = [['0.9.0.0', 'old'], ['TBD', 'base']];
|
||||
const metadata = / behavior=0 docs=0 metadata=2 tests=0 /;
|
||||
const own = restamped(declared(['TBD']), base, [['TBD', 'mine']], [...base, ['1.0.2.0', 'mine']]);
|
||||
expect(lineOf(own.stdout, 'ROUTE'), own.stdout).toEqual(expect.stringMatching(metadata));
|
||||
const undeclared = restamped(declared(undefined), base, [['TBD', 'mine']], [...base, ['1.0.2.0', 'mine']]);
|
||||
expect([undeclared.status, lineOf(undeclared.stdout, 'STOP')]).toEqual([3, behaviorStop]);
|
||||
const other = restamped(declared(['WIP']), base, [['TBD', 'mine']], [...base, ['1.0.2.0', 'mine']]);
|
||||
expect([other.status, lineOf(other.stdout, 'STOP')]).toEqual([3, behaviorStop]);
|
||||
const baseRow = restamped(declared(['TBD']), base, [['TBD', 'mine']], [['0.9.0.0', 'old'], ['1.0.2.0', 'base'], ['1.0.2.0', 'mine']]);
|
||||
expect([baseRow.status, lineOf(baseRow.stdout, 'STOP')]).toEqual([3, behaviorStop]);
|
||||
const dup = restamped(declared(['TBD']), base, [['TBD', 'base']], [['0.9.0.0', 'old'], ['1.0.2.0', 'base'], ['TBD', 'base']]);
|
||||
expect([dup.status, lineOf(dup.stdout, 'STOP')]).toEqual([3, behaviorStop]);
|
||||
const noTemplate = restamped(declared(['TBD'], ['version: {version}']), base, [['TBD', 'mine']], [...base, ['1.0.2.0', 'mine']]);
|
||||
expect([noTemplate.status, lineOf(noTemplate.stdout, 'STOP')]).toEqual([3, behaviorStop]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('adjacent base edits (GST-384, #3631, founderstar#441)', () => {
|
||||
|
||||
@@ -121,6 +121,36 @@ describe('tree receipt: stamps, stamp-sensitive gates and negative fixtures', ()
|
||||
expect(stampOnlyLine("mine since: '1.2.3.0'", "mine since: '1.2.4.0'", '1.2.4.0', '1.2.4.0', tpl, mine, 5)).toBe(true);
|
||||
});
|
||||
|
||||
// Value: protects=a declared placeholder on the branch's own row moves to the new version as a stamp, only inside a
|
||||
// stamp_patterns template (gbrain's `since: 'TBD'` row, GST-1026); fails_when=stampOnlyLine reads the move as a hand
|
||||
// edit, or accepts an undeclared placeholder, a base row, a duplicated base line, a built-in form or a line no
|
||||
// template matches; seam=lib/stamp-only.ts stampOnlyLine placeholders
|
||||
test('a declared placeholder on a branch row moves to the new version inside a stamp_patterns template, and nowhere else (GST-1026)', () => {
|
||||
const tpl = ["since: '{version}'"];
|
||||
const row = " { since: 'TBD', id: 'mine' },";
|
||||
const moved = " { since: '0.60.553.0', id: 'mine' },";
|
||||
const side = (r: string, at = 10) => ({ fork: '0.60.552.0', added: new Set([r]), at: new Set([at]) });
|
||||
const line = (r: string, a: string, opts: { templates?: string[]; at?: number; placeholders?: string[]; oldV?: string } = {}) =>
|
||||
stampOnlyLine(r, a, opts.oldV ?? '0.60.552.0', '0.60.553.0', opts.templates ?? tpl, side(r), opts.at ?? 10, opts.placeholders ?? ['TBD']);
|
||||
expect(line(row, moved)).toBe(true);
|
||||
expect(line(row, moved, { oldV: '0.60.553.0' })).toBe(true);
|
||||
expect(line(row, moved, { placeholders: [] })).toBe(false);
|
||||
expect(line(row, moved, { placeholders: ['WIP'] })).toBe(false);
|
||||
expect(line(" { since: 'WIP', id: 'mine' },", moved)).toBe(false);
|
||||
expect(line(row, moved, { at: 2 })).toBe(false);
|
||||
expect(stampOnlyLine(row, moved, '0.60.552.0', '0.60.553.0', tpl, undefined, undefined, ['TBD'])).toBe(false);
|
||||
expect(stampOnlyLine(row, moved, '0.60.552.0', '0.60.553.0', tpl, side(" { since: 'TBD', id: 'other' },"), 10, ['TBD'])).toBe(false);
|
||||
expect(line(row, moved, { templates: [] })).toBe(false);
|
||||
expect(line(" { release: 'TBD', id: 'mine' },", " { release: '0.60.553.0', id: 'mine' },")).toBe(false);
|
||||
expect(line(' { since: TBD, id: mine },', ' { since: 0.60.553.0, id: mine },')).toBe(false);
|
||||
expect(line(row, " { since: '0.60.553.0', id: 'yours' },")).toBe(false);
|
||||
expect(line(row, " { since: '0.60.554.0', id: 'mine' },")).toBe(false);
|
||||
const both = " { since: 'TBD', note: 'Added in v0.60.552.0' },";
|
||||
expect(line(both, " { since: '0.60.553.0', note: 'Added in v0.60.553.0' },")).toBe(true);
|
||||
expect(line(both, " { since: '0.60.553.0', note: 'Added in v0.60.551.0' },")).toBe(false);
|
||||
expect(line(' // TBD: tidy', ' // 0.60.553.0: tidy', { templates: ['{version}'] })).toBe(false);
|
||||
});
|
||||
|
||||
test('negative fixture: a dependency pin equal to the old version is `changed`, not a stamp, even inside stamp_paths', () => {
|
||||
const repo = fixture();
|
||||
const g = gated(repo);
|
||||
|
||||
Reference in new issue
Block a user