mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 15:27:56 +01:00
v1.93.27.0 feat(pregate): pregate-literal markers waive one deliberate old-value line (#3490) (#3891)
* feat(pregate): pregate-literal markers waive one deliberate old-value line (#3490) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore(changelog): fragment named after the branch Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (GST-634: a bare */ is not a reason; every marker on and above the line counts) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: a reason may not start with * (GST-634: **/ and * */ closers are not reasons) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: a pregate-literal reason cannot start with * (GST-634 docs audit) 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
2a801cf6f9
commit
e85005482a
8 files changed
+129
-10
No files matched your search
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.93.27.0] - 2026-11-02
|
||||||
|
|
||||||
|
**A test can keep an old value on purpose without turning off `literals` (#3490).** When a minimum version goes up, the test that checks the old version is refused has to keep the old value, and pregate failed it with `PREGATE_LITERALS`. The only way out was `warn: ["literals"]`, which turns the check off for every constant. Now that one line can carry `// pregate-literal: NAME=<old value> <reason>`.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **`pregate-literal:` markers.** A marker on the flagged line, or on the comment line directly above it, waives that line when it names the changed constant and its old value and gives a reason. Every other line in the file is still checked, and text inside a string is not a marker. The policy has no waiver key, so neither a base nor a pushed `pregate.json` can widen it.
|
||||||
|
- **Waivers are listed.** `literals` prints each waived line as `waived: <file>:<line>: <NAME> <old> (<reason>)` and adds `marker added on this branch` when the merge-base file lacks the marker, so a reviewer sees every new waiver.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **docs/pregate.md** and **docs/troubleshooting.md** (`PREGATE_LITERALS`) describe the marker.
|
||||||
|
|
||||||
## [1.93.26.0] - 2026-11-02
|
## [1.93.26.0] - 2026-11-02
|
||||||
|
|
||||||
**Editing the GBrain setup guide no longer runs every paid eval case on a PR screen.** The PR eval selector didn't recognize `USING_GBRAIN_WITH_GSTACK.md`, so it treated the file as an unknown dependency and fell back to the full suite. Two screens, #3727 and #3802, paid about $67 for it. No skill prompt or paid case reads the guide (only the README, `docs/gbrain-quick-tour.md` and free tests do), so it now selects nothing. A root doc that a paid prompt quotes, such as `ETHOS.md`, still selects its cases. The train gate's full census still runs every case, so coverage is unchanged (GST-1027).
|
**Editing the GBrain setup guide no longer runs every paid eval case on a PR screen.** The PR eval selector didn't recognize `USING_GBRAIN_WITH_GSTACK.md`, so it treated the file as an unknown dependency and fell back to the full suite. Two screens, #3727 and #3802, paid about $67 for it. No skill prompt or paid case reads the guide (only the README, `docs/gbrain-quick-tour.md` and free tests do), so it now selects nothing. A root doc that a paid prompt quotes, such as `ETHOS.md`, still selects its cases. The train gate's full census still runs every case, so coverage is unchanged (GST-1027).
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# gstack digest v1.93.26.0 — regenerate/re-copy after upgrading gstack
|
# gstack digest v1.93.27.0 — regenerate/re-copy after upgrading gstack
|
||||||
|
|
||||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||||
for agent hosts without a full skill install. The full skills add workflows,
|
for agent hosts without a full skill install. The full skills add workflows,
|
||||||
|
|||||||
@@ -228,6 +228,18 @@ Two stages with separate clocks, both printed:
|
|||||||
elsewhere in the file is not a consumer. A call-site name such as `attempts`
|
elsewhere in the file is not a consumer. A call-site name such as `attempts`
|
||||||
or `limit` is an ordinary word, so for it the file must also name the changed
|
or `limit` is an ordinary word, so for it the file must also name the changed
|
||||||
module.
|
module.
|
||||||
|
A test that keeps the old value on purpose, such as the refused input after a
|
||||||
|
minimum version goes up, marks that one line with a comment on it or on the
|
||||||
|
comment line directly above it:
|
||||||
|
`// pregate-literal: MIN_RUNTIME='1.4.0' refused input below the minimum`
|
||||||
|
(`#` and `/*` comments work too). The marker waives only that line, and only
|
||||||
|
when it names the changed constant and its old value and gives a reason (a
|
||||||
|
reason cannot start with `*`, so a bare `*/`, `**/` or `* */` closer is not
|
||||||
|
one); text inside a string is not a marker. Every other line in the file is still
|
||||||
|
checked. The check lists each waived line as
|
||||||
|
`waived: <file>:<line>: <NAME> <old> (<reason>)`, adding `marker added on
|
||||||
|
this branch` when the merge-base file lacks the marker. The policy has no
|
||||||
|
waiver key, so neither a base nor a pushed `pregate.json` can widen it.
|
||||||
|
|
||||||
- `timing`: added or changed test lines that compare an elapsed wall-clock or
|
- `timing`: added or changed test lines that compare an elapsed wall-clock or
|
||||||
CPU time against a bound, or sleep and then assert, print
|
CPU time against a bound, or sleep and then assert, print
|
||||||
|
|||||||
@@ -3707,7 +3707,12 @@ directory, or under a scratch name (`zz-*`, `tmp*`, `scratch*`).
|
|||||||
appears with its old value in a test file the branch did not update — nightly-
|
appears with its old value in a test file the branch did not update — nightly-
|
||||||
and Heavy-only files included.
|
and Heavy-only files included.
|
||||||
|
|
||||||
**Fix.** Update every listed `file:line`, or keep the old value.
|
**Fix.** Update every listed `file:line`, or keep the old value. When a test
|
||||||
|
keeps the old value on purpose (the refused input after a minimum version goes
|
||||||
|
up), mark that line: `// pregate-literal: NAME=<old value> <reason>` on the line
|
||||||
|
or on the comment line directly above it. The marker waives only that line, and
|
||||||
|
the check lists it as `waived:`. Don't use `warn: ["literals"]` for this, since
|
||||||
|
that turns the check off for every constant.
|
||||||
|
|
||||||
<a id="pregate-no-lane"></a>
|
<a id="pregate-no-lane"></a>
|
||||||
### `PREGATE_NO_LANE`
|
### `PREGATE_NO_LANE`
|
||||||
|
|||||||
@@ -14,6 +14,23 @@ import { isTestFile, listRepoFiles, scansLiterals } from '../import-graph';
|
|||||||
import type { CheckDef, CheckResult, PregateContext } from '../types';
|
import type { CheckDef, CheckResult, PregateContext } from '../types';
|
||||||
|
|
||||||
export interface ChangedLiteral { name: string; old: string; new: string; file: string }
|
export interface ChangedLiteral { name: string; old: string; new: string; file: string }
|
||||||
|
/** A consumer line a `pregate-literal:` marker waived; `added` when the merge-base file lacks the marker. */
|
||||||
|
export interface LiteralWaiver { at: string; reason: string; added: boolean }
|
||||||
|
|
||||||
|
const MARKER = /pregate-literal:[ \t]*([A-Za-z_]\w*)=(\S+)[ \t]+([^\s*].*?)[ \t]*(?:\*+\/[ \t]*)?$/;
|
||||||
|
|
||||||
|
/** The marker in this line's trailing comment (`//`, `#` or `/*` outside a string), or null. */
|
||||||
|
function markerIn(line: string): { marker: string; name: string; value: string; reason: string } | null {
|
||||||
|
const at = line.indexOf('pregate-literal:');
|
||||||
|
if (at < 0) return null;
|
||||||
|
const before = line.slice(0, at);
|
||||||
|
const open = /(?:\/\/|#|\/\*)[ \t]*$/.exec(before);
|
||||||
|
if (!open) return null;
|
||||||
|
const code = before.slice(0, open.index);
|
||||||
|
if (['\'', '"', '`'].some(q => code.split(q).length % 2 === 0)) return null;
|
||||||
|
const m = MARKER.exec(line.slice(at));
|
||||||
|
return m ? { marker: line.slice(at).trim(), name: m[1]!, value: m[2]!, reason: m[3]! } : null;
|
||||||
|
}
|
||||||
|
|
||||||
const CONST_PATTERNS: RegExp[] = [
|
const CONST_PATTERNS: RegExp[] = [
|
||||||
/^\s*export\s+(?:const|let|var)\s+([A-Z][A-Z0-9_]{1,})\s*(?::\s*[^=]+?)?\s*=\s*(.+?);?\s*$/,
|
/^\s*export\s+(?:const|let|var)\s+([A-Z][A-Z0-9_]{1,})\s*(?::\s*[^=]+?)?\s*=\s*(.+?);?\s*$/,
|
||||||
@@ -83,8 +100,12 @@ function unquote(v: string): string { return /^['"`]/.test(v) ? v.slice(1, -1) :
|
|||||||
* has no new-value row: the branch extended the table. A table it did not
|
* has no new-value row: the branch extended the table. A table it did not
|
||||||
* extend, a file that already held a new-value row, and an unreadable base
|
* extend, a file that already held a new-value row, and an unreadable base
|
||||||
* still count, and the scan goes on past skipped rows to the first real consumer.
|
* still count, and the scan goes on past skipped rows to the first real consumer.
|
||||||
|
*
|
||||||
|
* A consumer line is waived by a `pregate-literal: NAME=<old value> <reason>` comment on that line or
|
||||||
|
* on the comment line directly above it, naming this constant and its old value (#3490: the refused
|
||||||
|
* input after a minimum-version bump). Each waiver goes to `waived`; the scan goes on to the next line.
|
||||||
*/
|
*/
|
||||||
export function consumersOf(repoRoot: string, testFiles: readonly string[], change: ChangedLiteral, baseText?: (rel: string) => string | null | undefined): string[] {
|
export function consumersOf(repoRoot: string, testFiles: readonly string[], change: ChangedLiteral, baseText?: (rel: string) => string | null | undefined, waived?: LiteralWaiver[]): string[] {
|
||||||
const value = unquote(change.old);
|
const value = unquote(change.old);
|
||||||
const valueRe = /^-?\d/.test(value) ? new RegExp(`(?<![\\w.])${escape(value)}(?![\\w.])`) : new RegExp(`['"\`]${escape(value)}['"\`]`);
|
const valueRe = /^-?\d/.test(value) ? new RegExp(`(?<![\\w.])${escape(value)}(?![\\w.])`) : new RegExp(`['"\`]${escape(value)}['"\`]`);
|
||||||
const row = (raw: string) => new RegExp(`^[ \\t]*\\[[ \\t]*${/^['"`]/.test(raw) ? `['"\`]${escape(unquote(raw))}['"\`]` : escape(raw)}[ \\t]*,`, 'm');
|
const row = (raw: string) => new RegExp(`^[ \\t]*\\[[ \\t]*${/^['"`]/.test(raw) ? `['"\`]${escape(unquote(raw))}['"\`]` : escape(raw)}[ \\t]*,`, 'm');
|
||||||
@@ -131,7 +152,15 @@ export function consumersOf(repoRoot: string, testFiles: readonly string[], chan
|
|||||||
if (extended === undefined) { const base = baseText?.(rel); extended = base === null || (base !== undefined && !newRowRe.test(base)); }
|
if (extended === undefined) { const base = baseText?.(rel); extended = base === null || (base !== undefined && !newRowRe.test(base)); }
|
||||||
return extended;
|
return extended;
|
||||||
};
|
};
|
||||||
for (let i = 0; i < lines.length; i++) if (valueRe.test(lines[i]!) && (!nameRequired || oldRowRe.test(lines[i]!) || named(i)) && !skip(i)) { hits.push(`${rel}:${i + 1}`); break; }
|
const waiver = (i: number) => {
|
||||||
|
const m = [markerIn(lines[i]!), i > 0 && comment(lines[i - 1]!) ? markerIn(lines[i - 1]!) : null].find(c => c && c.name === change.name && unquote(c.value) === value);
|
||||||
|
if (!m) return false;
|
||||||
|
const before = baseText?.(rel);
|
||||||
|
waived?.push({ at: `${rel}:${i + 1}`, reason: m.reason, added: typeof before !== 'string' || !before.includes(m.marker) });
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const carries = (i: number) => { const m = markerIn(lines[i]!); return valueRe.test(m ? lines[i]!.slice(0, lines[i]!.indexOf('pregate-literal:')) : lines[i]!); };
|
||||||
|
for (let i = 0; i < lines.length; i++) if (carries(i) && (!nameRequired || oldRowRe.test(lines[i]!) || named(i)) && !skip(i) && !waiver(i)) { hits.push(`${rel}:${i + 1}`); break; }
|
||||||
}
|
}
|
||||||
return hits;
|
return hits;
|
||||||
}
|
}
|
||||||
@@ -150,19 +179,23 @@ function run(ctx: PregateContext): CheckResult {
|
|||||||
if (!changes.length) return { id: 'literals', stage: 'preflight', status: 'pass', inputs, detail: `no exported constant or named timeout literal changed in ${candidates.length} modified source file(s)` };
|
if (!changes.length) return { id: 'literals', stage: 'preflight', status: 'pass', inputs, detail: `no exported constant or named timeout literal changed in ${candidates.length} modified source file(s)` };
|
||||||
const testFiles = listRepoFiles(ctx.repoRoot).filter(f => scansLiterals(f) && !f.startsWith('bin/'));
|
const testFiles = listRepoFiles(ctx.repoRoot).filter(f => scansLiterals(f) && !f.startsWith('bin/'));
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
const notes: string[] = [];
|
||||||
for (const c of changes) {
|
for (const c of changes) {
|
||||||
|
const waived: LiteralWaiver[] = [];
|
||||||
const hits = consumersOf(ctx.repoRoot, testFiles, c, rel => {
|
const hits = consumersOf(ctx.repoRoot, testFiles, c, rel => {
|
||||||
const from = ctx.touched.find(t => t.path === rel)?.renamedFrom ?? rel;
|
const from = ctx.touched.find(t => t.path === rel)?.renamedFrom ?? rel;
|
||||||
const r = spawnSync('git', ['show', `${ctx.mergeBase}:${from}`], { cwd: ctx.repoRoot, encoding: 'utf8', timeout: 30_000, maxBuffer: 64 * 1024 * 1024, env: ctx.env });
|
const r = spawnSync('git', ['show', `${ctx.mergeBase}:${from}`], { cwd: ctx.repoRoot, encoding: 'utf8', timeout: 30_000, maxBuffer: 64 * 1024 * 1024, env: ctx.env });
|
||||||
if (r.status === 0) return r.stdout;
|
if (r.status === 0) return r.stdout;
|
||||||
const ls = spawnSync('git', ['ls-tree', ctx.mergeBase, '--', from], { cwd: ctx.repoRoot, encoding: 'utf8', timeout: 30_000, env: ctx.env });
|
const ls = spawnSync('git', ['ls-tree', ctx.mergeBase, '--', from], { cwd: ctx.repoRoot, encoding: 'utf8', timeout: 30_000, env: ctx.env });
|
||||||
return ls.status === 0 && !ls.stdout.trim() ? null : undefined;
|
return ls.status === 0 && !ls.stdout.trim() ? null : undefined;
|
||||||
});
|
}, waived);
|
||||||
|
for (const w of waived) notes.push(`waived: ${w.at}: ${c.name} ${c.old} (${w.reason}${w.added ? '; marker added on this branch' : ''})`);
|
||||||
ctx.explain.push(`literals: ${c.file} ${c.name} ${c.old} → ${c.new}: ${hits.length} test file(s) still carry ${c.old}`);
|
ctx.explain.push(`literals: ${c.file} ${c.name} ${c.old} → ${c.new}: ${hits.length} test file(s) still carry ${c.old}`);
|
||||||
for (const h of hits) lines.push(`${h}: ${c.name} still ${c.old} (now ${c.new} in ${c.file})`);
|
for (const h of hits) lines.push(`${h}: ${c.name} still ${c.old} (now ${c.new} in ${c.file})`);
|
||||||
}
|
}
|
||||||
if (lines.length) return { id: 'literals', stage: 'preflight', status: 'fail', inputs, lines, code: 'PREGATE_LITERALS', detail: `${lines.length} test location(s) still carry an old literal`, fix: 'update each listed test to the new value (nightly- and Heavy-only files included), or keep the old constant' };
|
const waivers = notes.length ? `; ${notes.length} line(s) waived by pregate-literal markers` : '';
|
||||||
return { id: 'literals', stage: 'preflight', status: 'pass', inputs, detail: `${changes.length} changed literal(s), no test still carries an old value (${testFiles.length} test-tree files scanned)` };
|
if (lines.length) return { id: 'literals', stage: 'preflight', status: 'fail', inputs, lines: [...lines, ...notes], code: 'PREGATE_LITERALS', detail: `${lines.length} test location(s) still carry an old literal${waivers}`, fix: "update each listed test to the new value (nightly- and Heavy-only files included), keep the old constant, or, when the old value is the test's input on purpose, mark that line with `// pregate-literal: NAME=<old value> <reason>`" };
|
||||||
|
return { id: 'literals', stage: 'preflight', status: 'pass', inputs, ...(notes.length ? { lines: notes } : {}), detail: `${changes.length} changed literal(s), no test still carries an old value (${testFiles.length} test-tree files scanned)${waivers}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const literalsCheck: CheckDef = { id: 'literals', stage: 'preflight', tier: 1, downgradable: true, needsRepoCommands: false, run };
|
export const literalsCheck: CheckDef = { id: 'literals', stage: 'preflight', tier: 1, downgradable: true, needsRepoCommands: false, run };
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gstack",
|
"name": "gstack",
|
||||||
"version": "1.93.26",
|
"version": "1.93.27",
|
||||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import * as fs from 'node:fs';
|
|||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import { EXIT } from '../lib/headless-artifacts';
|
import { EXIT } from '../lib/headless-artifacts';
|
||||||
import { changedLiterals, constantsIn, consumersOf } from '../lib/pregate/checks/literals';
|
import { changedLiterals, constantsIn, consumersOf, type LiteralWaiver } from '../lib/pregate/checks/literals';
|
||||||
import { parseSummary, planRuns } from '../lib/pregate/checks/lanes';
|
import { parseSummary, planRuns } from '../lib/pregate/checks/lanes';
|
||||||
import { DEFAULT_CONFIG, validateConfig } from '../lib/pregate/config';
|
import { DEFAULT_CONFIG, validateConfig } from '../lib/pregate/config';
|
||||||
import { isDocsEmptyLane } from '../lib/pregate/docs-lane';
|
import { isDocsEmptyLane } from '../lib/pregate/docs-lane';
|
||||||
@@ -298,6 +298,51 @@ describe('literals (pure)', () => {
|
|||||||
expect(consumersOf(root, Object.keys(files), { name: 'attempts', old: '1', new: '3', file: 'lanes/review-noloss/judge-run.ts' })).toEqual(['tests/judge-run.test.ts:3']);
|
expect(consumersOf(root, Object.keys(files), { name: 'attempts', old: '1', new: '3', file: 'lanes/review-noloss/judge-run.ts' })).toEqual(['tests/judge-run.test.ts:3']);
|
||||||
fs.rmSync(root, { recursive: true, force: true });
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
// Value: protects=a test that keeps an old value on purpose (the refused input after a minimum-version bump) can waive that one line
|
||||||
|
// with a `pregate-literal: NAME=value reason` comment, while every other old-value line, a marker for another constant or value,
|
||||||
|
// a marker with no reason (a bare `*/`, `**/` or `* */` closer is not a reason) and marker text inside a string still count, and a line marked
|
||||||
|
// for one constant can still be waived for another by the comment line above; fails_when=the waiver widens past its line or a stray
|
||||||
|
// marker clears a real consumer (#3490, gbrain#7690); why_new=the only escape was warn: ["literals"] for every constant;
|
||||||
|
// seam=none; no_claim=the policy file (no key exists; see the config test)
|
||||||
|
test('consumersOf: a pregate-literal marker waives only its own line, for its own constant and old value, with a reason (#3490)', () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-literals-waiver-'));
|
||||||
|
const nl = String.fromCharCode(10);
|
||||||
|
const files: Record<string, string> = {
|
||||||
|
'test/runtime.test.ts': [
|
||||||
|
"import { MIN_RUNTIME, checkRuntime } from '../src/runtime';",
|
||||||
|
"expect(checkRuntime('1.4.0')).toBe('refused'); // pregate-literal: MIN_RUNTIME='1.4.0' refused input below the minimum",
|
||||||
|
"// pregate-literal: MIN_RUNTIME=1.4.0 the oldest runtime we still name in the error",
|
||||||
|
"expect(message('1.4.0')).toContain('upgrade');",
|
||||||
|
"expect(MIN_RUNTIME).toBe('1.4.0');",
|
||||||
|
''].join(nl),
|
||||||
|
'test/other-name.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); // pregate-literal: MAX_RUNTIME='1.4.0' wrong constant", ''].join(nl),
|
||||||
|
'test/other-value.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); // pregate-literal: MIN_RUNTIME='1.3.9' wrong value", ''].join(nl),
|
||||||
|
'test/no-reason.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); // pregate-literal: MIN_RUNTIME='1.4.0'", ''].join(nl),
|
||||||
|
'test/in-string.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "const src = \"expect(v('1.4.0')); // pregate-literal: MIN_RUNTIME='1.4.0' looks waived\"; check('1.4.0');", ''].join(nl),
|
||||||
|
'test/two-above.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "// pregate-literal: MIN_RUNTIME='1.4.0' too far above", '', "expect(checkRuntime('1.4.0')).toBe('refused');", ''].join(nl),
|
||||||
|
'test/all-waived.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); # pregate-literal: MIN_RUNTIME='1.4.0' refused input", ''].join(nl),
|
||||||
|
'test/block-no-reason.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); /* pregate-literal: MIN_RUNTIME='1.4.0' */", ''].join(nl),
|
||||||
|
'test/block-stars.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); /* pregate-literal: MIN_RUNTIME='1.4.0' **/", ''].join(nl),
|
||||||
|
'test/block-star-space.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); /* pregate-literal: MIN_RUNTIME='1.4.0' * */", ''].join(nl),
|
||||||
|
'test/block-stars-reason.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "expect(checkRuntime('1.4.0')).toBe('refused'); /* pregate-literal: MIN_RUNTIME='1.4.0' refused input **/", ''].join(nl),
|
||||||
|
'test/two-constants.test.ts': ["import { MIN_RUNTIME } from '../src/runtime';", "// pregate-literal: MIN_RUNTIME='1.4.0' lower bound of the refused range", "expect(inRange('1.4.0', '2.0.0')).toBe(false); /* pregate-literal: MAX_RUNTIME='2.0.0' upper bound */", ''].join(nl),
|
||||||
|
};
|
||||||
|
for (const [rel, text] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); fs.writeFileSync(path.join(root, rel), text); }
|
||||||
|
const change = { name: 'MIN_RUNTIME', old: "'1.4.0'", new: "'1.4.4'", file: 'src/runtime.ts' };
|
||||||
|
const waived: LiteralWaiver[] = [];
|
||||||
|
const base = (rel: string) => rel === 'test/runtime.test.ts' ? files[rel]!.split(nl).slice(0, 2).join(nl) : null;
|
||||||
|
expect(consumersOf(root, Object.keys(files), change, base, waived)).toEqual([
|
||||||
|
'test/runtime.test.ts:5', 'test/other-name.test.ts:2', 'test/other-value.test.ts:2', 'test/no-reason.test.ts:2', 'test/in-string.test.ts:2', 'test/two-above.test.ts:4', 'test/block-no-reason.test.ts:2', 'test/block-stars.test.ts:2', 'test/block-star-space.test.ts:2',
|
||||||
|
]);
|
||||||
|
expect(waived).toEqual([
|
||||||
|
{ at: 'test/runtime.test.ts:2', reason: 'refused input below the minimum', added: false },
|
||||||
|
{ at: 'test/runtime.test.ts:4', reason: 'the oldest runtime we still name in the error', added: true },
|
||||||
|
{ at: 'test/all-waived.test.ts:2', reason: 'refused input', added: true },
|
||||||
|
{ at: 'test/block-stars-reason.test.ts:2', reason: 'refused input', added: true },
|
||||||
|
{ at: 'test/two-constants.test.ts:3', reason: 'lower bound of the refused range', added: true },
|
||||||
|
]);
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
test('consumersOf: a history row is skipped only when this branch added the new-value row to the same table; other old-value lines still count (GST-344)', () => {
|
test('consumersOf: a history row is skipped only when this branch added the new-value row to the same table; other old-value lines still count (GST-344)', () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-literals-history-'));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-literals-history-'));
|
||||||
const nl = String.fromCharCode(10);
|
const nl = String.fromCharCode(10);
|
||||||
@@ -500,6 +545,19 @@ describe('gstack-pregate (bin)', () => {
|
|||||||
expect(r.err).toContain('(PREGATE_LITERALS)');
|
expect(r.err).toContain('(PREGATE_LITERALS)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Value: protects=end to end, a marked refused-input line passes literals and the run names the waiver, flagging one this branch
|
||||||
|
// added; a branch policy cannot add a literals waiver; fails_when=the check ignores markers, hides a waiver, or the policy grows a
|
||||||
|
// literals key (#3490); why_new=no waiver existed; seam=none; no_claim=gstack-gate-diff tagging
|
||||||
|
test('#3490: a pregate-literal marker passes literals for its line and the run lists the waiver as added on this branch', () => {
|
||||||
|
const repo = fixture();
|
||||||
|
commit(repo, { 'test/nightly/retries.test.js': "const test = require('node:test');\nconst assert = require('node:assert');\nconst { MAX_RETRIES, retry } = require('../../lib/retry.js');\n// pregate-literal: MAX_RETRIES=3 the old cap is the input the guard must refuse\ntest('refuses 3', () => assert.throws(() => retry(() => 0, { MAX_RETRIES: 3 })));\n" });
|
||||||
|
commit(repo, { 'lib/retry.js': "exports.MAX_RETRIES = 4;\nexports.TIMEOUT_MS = 5000;\nexports.retry = (fn) => fn();\n", 'test/retry.test.js': "const test = require('node:test');\nconst assert = require('node:assert');\nconst { MAX_RETRIES } = require('../lib/retry.js');\ntest('retries four times', () => assert.strictEqual(MAX_RETRIES, 4));\n" });
|
||||||
|
const r = pregate(repo, ['--stage', 'preflight', '--only', 'literals']);
|
||||||
|
expect(r.status).toBe(EXIT.ok);
|
||||||
|
expect(r.out).toContain('literals: waived: test/nightly/retries.test.js:5: MAX_RETRIES 3 (the old cap is the input the guard must refuse; marker added on this branch)');
|
||||||
|
expect(validateConfig({ ...BASE_PREGATE, literals: { allow: [{ name: 'MAX_RETRIES', value: '3' }] } }).map(e => e.path)).toEqual(['literals']);
|
||||||
|
});
|
||||||
|
|
||||||
test('GST-344: a history table the branch extends passes literals through the merge-base read; one it does not extend fails', () => {
|
test('GST-344: a history table the branch extends passes literals through the merge-base read; one it does not extend fails', () => {
|
||||||
const history = (...ns: number[]) => ["const { MAX_RETRIES } = require('../lib/retry.js');", 'const versions = [', ...ns.map(n => ` [${n}, 'digest-${n}'],`), '];', ''].join('\n');
|
const history = (...ns: number[]) => ["const { MAX_RETRIES } = require('../lib/retry.js');", 'const versions = [', ...ns.map(n => ` [${n}, 'digest-${n}'],`), '];', ''].join('\n');
|
||||||
const repo = fixture({ baseFiles: { 'test/history.test.js': history(2, 3) } });
|
const repo = fixture({ baseFiles: { 'test/history.test.js': history(2, 3) } });
|
||||||
|
|||||||
Reference in new issue
Block a user