75 lines
6.2 KiB
TypeScript
75 lines
6.2 KiB
TypeScript
// Release notes in player language: a player-facing source beats the git log, and internal jargon is flagged.
|
|
import { afterEach, describe, expect, test } from 'bun:test';
|
|
import { jargonFindings, jargonMessage } from '../cli/lib/jargon';
|
|
import { changelogSection, checkLanguageNotes, cutNotes, findNotesSource, notesFromMarkdown } from '../cli/lib/whats-new';
|
|
import { run as whatsNew } from '../cli/whats-new';
|
|
import { BASE, capture, jsonOut, project, restore } from './helpers/store-mock';
|
|
|
|
afterEach(restore);
|
|
const words = (t: string, allow?: string[]) => jargonFindings(t, allow).map(f => `${f.kind}:${f.word}`);
|
|
|
|
describe('internal jargon', () => {
|
|
test('vendors, dev and version-control words, paths, issue ids and code identifiers', () => {
|
|
expect(words('Added Umami telemetry and a Keeper OS fix')).toEqual(['vendor:Umami', 'dev:telemetry', 'vendor:Keeper OS']);
|
|
expect(words('Sentry, Firebase, Capacitor, Tauri, Jenkins and Forgejo')).toEqual(['vendor:Sentry', 'vendor:Firebase', 'vendor:Capacitor', 'vendor:Tauri', 'vendor:Jenkins', 'vendor:Forgejo']);
|
|
expect(words('refactor the endpoint payload via the API in CI, PR merged, commit bump of deps, OTA fingerprint'))
|
|
.toEqual(['dev:refactor', 'dev:endpoint', 'dev:payload', 'dev:API', 'dev:CI', 'dev:PR', 'vcs:merged', 'vcs:commit', 'dev:bump', 'dev:deps', 'dev:OTA', 'dev:fingerprint']);
|
|
expect(words('Fixed src/game/save.ts and Player.gd')).toEqual(['path:src/game/save.ts', 'path:Player.gd']);
|
|
expect(words('Fixes #123 and GAME-42')).toEqual(['issue:#123', 'issue:GAME-42']);
|
|
expect(words('saveSlot now works, as does max_lives')).toEqual(['identifier:saveSlot', 'identifier:max_lives']);
|
|
});
|
|
test('player language passes: product names, links, dates, plain words', () => {
|
|
expect(words('New: 20 desert levels on iPhone and iPad! Faster loading. Help: https://example.com/help/api_v2 on 12/10.')).toEqual([]);
|
|
expect(words('Capital letters, a pricey item, a picnic and the kick-off')).toEqual([]); // no "api" / "ci" inside words
|
|
});
|
|
test('allowWords keeps a word, any case; messages name the word and a plain alternative', () => {
|
|
expect(words('New Firebase level and saveSlot', ['firebase', 'SAVESLOT'])).toEqual([]);
|
|
expect(jargonMessage(jargonFindings('endpoint')[0]!)).toBe('internal jargon (developer word): "endpoint"; instead: "server"');
|
|
expect(jargonMessage(jargonFindings('Umami')[0]!)).toBe('internal jargon (SDK / vendor name): "Umami"; instead: drop the name and say what changed for the player');
|
|
expect(jargonMessage(jargonFindings('merge')[0]!)).toBe('internal jargon (reads like a commit log): "merge"; drop it');
|
|
});
|
|
test('language check: jargon is a warning, never a blocking problem', () => {
|
|
const c = checkLanguageNotes({ id: 'en', apple: 'en-US' }, { lang: 'en', source: 'en', status: 'source', text: 'Telemetry via Umami.' });
|
|
expect(c.problems).toEqual([]);
|
|
expect(c.warnings).toHaveLength(2);
|
|
expect(checkLanguageNotes({ id: 'en', apple: 'en-US' }, { lang: 'en', source: 'en', status: 'source', text: 'Telemetry via Umami.' }, undefined, ['telemetry', 'umami']).warnings).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('player-facing sources', () => {
|
|
const CL = '# Changelog\n\n## Unreleased\n\n- **Night mode** for the map.\n\n## [1.2.0] - 2026-10-01\n\nBig update.\n\n- New `desert` world, see [the trailer](https://example.com).\n\n## 1.1.0\n\n- Old.\n';
|
|
test('changelog sections, markdown to text, cuts', () => {
|
|
expect(changelogSection(CL, '1.2.0')).toContain('New `desert` world');
|
|
expect(changelogSection(CL, 'v1.1.0')).toBe('- Old.');
|
|
expect(changelogSection(CL)).toBe('- **Night mode** for the map.');
|
|
expect(changelogSection(CL, '9.9.9')).toBeUndefined();
|
|
expect(notesFromMarkdown(changelogSection(CL, '1.2.0')!)).toBe('Big update.\n\n• New desert world, see the trailer.');
|
|
expect(cutNotes('aaa\nbbb\nccc', 7)).toBe('aaa\nbbb');
|
|
});
|
|
test('order: whatsNew.source, docs/RELEASE-<version>.md, CHANGELOG (version, then Unreleased), else none', () => {
|
|
const files: Record<string, string> = { 'CHANGELOG.md': CL, 'docs/RELEASE-1.2.0.md': '# 1.2.0\n\n- Desert world!\n', 'notes.md': 'Hand written.' };
|
|
const read = (rel: string) => files[rel];
|
|
expect(findNotesSource({ version: '1.2.0', source: 'notes.md', read })).toEqual({ kind: 'file', path: 'notes.md', text: 'Hand written.' });
|
|
expect(findNotesSource({ version: '1.2.0', read })).toEqual({ kind: 'release-doc', path: 'docs/RELEASE-1.2.0.md', text: '• Desert world!' });
|
|
expect(findNotesSource({ version: '1.1.0', read })?.text).toBe('• Old.');
|
|
expect(findNotesSource({ version: '2.0.0', read })?.text).toBe('• Night mode for the map.');
|
|
expect(findNotesSource({ read: () => undefined })).toBeUndefined();
|
|
expect(() => findNotesSource({ source: 'missing.md', read })).toThrow('whatsNew.source: missing.md does not exist');
|
|
});
|
|
test('command: the release doc wins over commit messages; --from-git and jargon warnings', async () => {
|
|
const p = project({ ...BASE, whatsNew: { allowWords: ['sentry'] } }, { 'package.json': '{"version":"1.2.0"}', 'docs/RELEASE-1.2.0.md': '# 1.2.0\n\n- 20 desert levels.\n- Sentry the guard dog joins you.\n' });
|
|
const git = (...a: string[]) => Bun.spawnSync(['git', ...a], { cwd: p.root, env: { ...process.env, GIT_AUTHOR_NAME: 'T', GIT_AUTHOR_EMAIL: 't@example.com', GIT_COMMITTER_NAME: 'T', GIT_COMMITTER_EMAIL: 't@example.com' } });
|
|
git('init', '-q'); git('add', '.'); git('commit', '-qm', 'chore: init'); git('tag', 'v1.1.0'); git('commit', '-q', '--allow-empty', '-m', 'feat: Umami telemetry endpoint');
|
|
let r = await capture(() => whatsNew(p.ctx(['--dry', '--json'])));
|
|
let j = jsonOut(r.out);
|
|
expect(j.source).toEqual({ kind: 'release-doc', path: 'docs/RELEASE-1.2.0.md' });
|
|
expect(j.play).toBe('• 20 desert levels.\n• Sentry the guard dog joins you.');
|
|
expect(r.err).not.toContain('jargon'); // "Sentry" is allowed by whatsNew.allowWords
|
|
r = await capture(() => whatsNew(p.ctx(['--dry', '--json', '--from-git'])));
|
|
j = jsonOut(r.out);
|
|
expect(j.source).toBeNull();
|
|
expect(j.play).toContain('Umami telemetry endpoint');
|
|
expect(r.err).toContain('internal jargon (SDK / vendor name): "Umami"');
|
|
expect(r.err).toContain('"endpoint"; instead: "server"');
|
|
});
|
|
});
|