Files
gstack/test/ship-issue-intent.test.ts
T
Garry Tanandcapy-ai[bot] 8584d6ff49 v1.91.77.0 feat(ship): two-stage gstack-pregate, gstack-regen generated-file registry, intent-based issue links (multi-agent wave C-pregate) (#3121)
* v1.91.77.0 feat(ship): two-stage gstack-pregate, gstack-regen registry, intent-based issue links (multi-agent wave C-pregate)

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

* test(pregate): keep the bin-driving pregate tests off the Windows subset; pin files are free-only for eval selection

The three C-pregate tests that spawn bin/gstack-regen, bin/gstack-pregate,
bin/gstack-issue-links and bin/gstack-ship-receipt do so through
test/helpers/pregate-fixture.ts runBin, so the Windows curation's source
scan of the test file never saw a bin/ spawn and shipped them to
windows-latest, where CreateProcess cannot exec a shebang (14 failures in
windows-free-shard 2 and 5). Each is now a KNOWN_WINDOWS_INCOMPATIBLE entry
with the precise reason. .gstack/generated.json and .gstack/pregate.json
join FREE_ONLY_PR_FILES: no paid case reads gstack's own pin files (the
tools read origin/<base> of the repo they run in, and paid cases run in
fixture repos), so touching them no longer restores every gate case.

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

* test(harness-version): record the FREE_ONLY_PR_FILES pin-file entries as non-behavioral

scripts/test-pr-profile.ts is a pinned shared-harness file; the two
.gstack pin-file entries change selection only, never a case verdict.

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>
2026-10-11 01:57:16 +00:00

67 lines
4.8 KiB
TypeScript

/**
* C6: `Fixes #N` only on explicit closure intent. References, partial fixes,
* deferred asks and cross-repo ids are `Refs`; an unreadable closure issue is
* downgraded (ISSUE_UNREADABLE); failing CI run ids map to flake issues.
* The bin reads issues only through gstack-issue-guard (a gh shim on PATH).
*/
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { mentionsIn, renderIssueLines, resolveLinks } from '../lib/issue-links';
import { cleanup, commit, ghShim, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/pregate-fixture';
const repos: FixtureRepo[] = [];
const fixture = () => { const r = makeFixtureRepo(); repos.push(r); return r; };
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
describe('issue intent (pure)', () => {
test('trailers carry closure; bare mentions, partial fixes, deferred asks and cross-repo ids are references', () => {
const m = mentionsIn('fix: retry\n\nContext in #12. Partially fixes #13; deferring #17 to a follow-up.\nSee owner/other#9.\n\nFixes #14\nCloses: #15\nResolves GH-18\n');
expect(m.map(x => [x.repo ? `${x.repo}#${x.issue}` : `#${x.issue}`, x.intent])).toEqual([
['#12', 'refs'], ['#13', 'refs'], ['#17', 'refs'], ['owner/other#9', 'refs'], ['#14', 'fixes'], ['#15', 'fixes'], ['#18', 'fixes'],
]);
expect(mentionsIn('Fixes #14 and #15 together').map(x => x.intent)).toEqual(['refs', 'refs']);
expect(mentionsIn('v1.2.3 (#301) release notes').map(x => x.issue)).toEqual([301]);
});
test('resolution: explicit --issue is closure, unreadable closure becomes Refs, a flake issue is never a plain Ref', () => {
const links = resolveLinks(mentionsIn('Fixes #14\nFixes #15\nsee #40 and #16'), [16], n => n !== 15, [{ issue: 40, run: '7' }]);
expect(links).toEqual({ fixes: [14, 16], refs: [15], flakes: [{ issue: 40, run: '7' }], cross: [], unreadable: [15] });
expect(renderIssueLines(links)).toEqual(['Fixes #14', 'Fixes #16', 'Refs #15 (unreadable through gstack-issue-guard; not auto-closed)', 'Flake: #40 (run 7)']);
expect(renderIssueLines(resolveLinks([], [], () => true))).toEqual([]);
});
});
describe('gstack-issue-links (bin)', () => {
test('reads origin/<base>..HEAD, probes closure issues through gstack-issue-guard, maps CI run ids to flake issues, writes the receipt object', () => {
const repo = fixture();
commit(repo, { 'lib/util.js': "exports.add = (a, b) => a + b + 0;\n" }, 'fix add\n\nSee #12 for context, partially fixes #13.\n\nFixes #14\nCloses #15\nRefs other/repo#9\n');
const env = ghShim(repo, { issues: { '14': { title: 't', body: 'b', comments: [] }, '16': { title: 'u', body: '', comments: [] } }, issueList: [{ number: 40, body: 'Flake in run 123456' }, { number: 41, body: 'unrelated' }] });
const out = path.join(repo.work, '.gstack/tmp/issues.json');
const r = runBin(repo, 'gstack-issue-links', ['--base', 'main', '--ci-run', '123456', '--issue', '16', '--out', out], env);
expect(r.status).toBe(EXIT.fail);
expect(r.out).toBe(['Fixes #14', 'Fixes #16', 'Refs #12', 'Refs #13', 'Refs #15 (unreadable through gstack-issue-guard; not auto-closed)', 'Refs other/repo#9', 'Flake: #40 (run 123456)', ''].join('\n'));
expect(r.err).toContain('(#15)');
expect(r.err).toContain('(ISSUE_UNREADABLE)');
expect(r.err).toContain('closure #14 ← commit ');
expect(JSON.parse(fs.readFileSync(out, 'utf8'))).toEqual({ fixes: [14, 16], refs: [12, 13, 15], flakes: [{ issue: 40, run: '123456' }] });
const receipt = runBin(repo, 'gstack-ship-receipt', ['write', '--base', 'main', '--version', '1.0.0', '--session-kind', 'interactive', '--issues', `@${out}`, '--json'], env);
expect(receipt.status).toBe(EXIT.ok);
expect(JSON.parse(receipt.out).issues).toEqual({ fixes: [14, 16], refs: [12, 13, 15], flakes: [{ issue: 40, run: '123456' }] });
});
test('no mentions prints ISSUE_LINKS: none with exit 0; a bare mention never closes; bad flags are usage', () => {
const repo = fixture();
commit(repo, { 'lib/util.js': "exports.add = (a, b) => a + b + 0;\n" }, 'tidy\n\nmentions #12 only\n');
const env = ghShim(repo, { issues: { '12': { title: 't', body: 'b', comments: [] } } });
const r = runBin(repo, 'gstack-issue-links', ['--base', 'main', '--json'], env);
expect(r.status).toBe(EXIT.ok);
expect(JSON.parse(r.out)).toMatchObject({ fixes: [], refs: [12], flakes: [] });
const none = fixture();
expect(runBin(none, 'gstack-issue-links', ['--base', 'main']).out).toBe('ISSUE_LINKS: none\n');
expect(runBin(none, 'gstack-issue-links', ['--base', 'main', '--issue', 'abc']).status).toBe(EXIT.usage);
expect(runBin(none, 'gstack-issue-links', ['--help']).out).toContain('Exit codes: 0 ok · 1 fail · 2 usage · 3 refused or needs a flag');
});
});