mirror of
https://github.com/HeyPuter/builder.git
synced 2026-11-13 20:12:42 -10:00
333 lines
21 KiB
JavaScript
333 lines
21 KiB
JavaScript
import fs from 'node:fs';
|
|
|
|
// ---- Regression guard for the per-project issues logic (issues-core.js) ----
|
|
// IssuesCore owns the issues document shape, every mutation, the limits, and
|
|
// the send-to-AI message formatting. The UI layer (issues.js) applies each
|
|
// operation TWICE — optimistically to a cache and authoritatively to a fresh
|
|
// read — so beyond plain correctness these checks pin down the properties that
|
|
// double application depends on: immutability of inputs, determinism given
|
|
// explicit id/now, and idempotent re-application. It is intentionally pure /
|
|
// DOM-free; we evaluate the production bytes with a bare window and assert
|
|
// every branch, mirroring scripts/test-publish-state.mjs.
|
|
|
|
const SRC = new URL('../src/js/issues-core.js', import.meta.url);
|
|
const src = fs.readFileSync(SRC, 'utf8');
|
|
|
|
// issues-core.js is a classic browser-global script: it assigns
|
|
// window.IssuesCore. Evaluate it with a minimal window.
|
|
const window = {};
|
|
new Function('window', src)(window);
|
|
const Core = window.IssuesCore;
|
|
|
|
let failures = 0;
|
|
function check(name, cond) {
|
|
if (cond) console.log('ok - ' + name);
|
|
else { console.error('FAIL - ' + name); failures++; }
|
|
}
|
|
|
|
if (!Core || typeof Core.addIssue !== 'function') {
|
|
console.error('FAIL - window.IssuesCore was not defined');
|
|
process.exit(1);
|
|
}
|
|
|
|
const NOW = '2026-07-28T12:00:00.000Z';
|
|
const LATER = '2026-07-28T13:00:00.000Z';
|
|
|
|
// --- cleanIssueText -----------------------------------------------------------
|
|
check('clean: trims and keeps text', Core.cleanIssueText(' fix the header ').text === 'fix the header');
|
|
check('clean: normalizes CRLF', Core.cleanIssueText('a\r\nb').text === 'a\nb');
|
|
check('clean: collapses 3+ blank lines', Core.cleanIssueText('a\n\n\n\nb').text === 'a\n\nb');
|
|
check('clean: strips trailing spaces per line', Core.cleanIssueText('a \nb').text === 'a\nb');
|
|
check('clean: empty → error', Core.cleanIssueText(' ').error === 'empty');
|
|
check('clean: non-string → empty error', Core.cleanIssueText(null).error === 'empty');
|
|
check('clean: over the cap → too_long', Core.cleanIssueText('x'.repeat(Core.MAX_TEXT_LENGTH + 1)).error === 'too_long');
|
|
check('clean: exactly the cap is fine', Core.cleanIssueText('x'.repeat(Core.MAX_TEXT_LENGTH)).ok === true);
|
|
|
|
// --- normalizeDoc -------------------------------------------------------------
|
|
check('normalize: garbage → empty doc', Core.normalizeDoc('nope').issues.length === 0);
|
|
check('normalize: null → empty doc', Core.normalizeDoc(null).issues.length === 0);
|
|
check('normalize: sets schema 1', Core.normalizeDoc(null).schema === 1);
|
|
check('normalize: legacy bare array accepted',
|
|
Core.normalizeDoc([{ id: 'i_1_a', text: 'hi' }]).issues.length === 1);
|
|
check('normalize: entries without text dropped',
|
|
Core.normalizeDoc({ issues: [{ id: 'i_1_a' }, { id: 'i_2_b', text: ' ' }, 42, null] }).issues.length === 0);
|
|
check('normalize: over-long stored text is clamped, not dropped',
|
|
Core.normalizeDoc({ issues: [{ id: 'i_1_a', text: 'y'.repeat(Core.MAX_TEXT_LENGTH + 50) }] })
|
|
.issues[0].text.length === Core.MAX_TEXT_LENGTH);
|
|
check('normalize: bad status coerced to open',
|
|
Core.normalizeDoc({ issues: [{ id: 'i_1_a', text: 'x', status: 'weird' }] }).issues[0].status === 'open');
|
|
check('normalize: resolved status kept',
|
|
Core.normalizeDoc({ issues: [{ id: 'i_1_a', text: 'x', status: 'resolved' }] }).issues[0].status === 'resolved');
|
|
check('normalize: duplicate ids keep first',
|
|
Core.normalizeDoc({ issues: [{ id: 'i_1_a', text: 'first' }, { id: 'i_1_a', text: 'second' }] })
|
|
.issues.map(i => i.text).join(',') === 'first');
|
|
{
|
|
const doc = Core.normalizeDoc({ issues: [{ id: 'bad"id]<', text: 'x' }] });
|
|
check('normalize: unsafe id replaced with a safe deterministic one', doc.issues[0].id === 'i_legacy_0');
|
|
// Determinism matters beyond hygiene: the persistence layer applies each
|
|
// mutation to two separate reads of the same file, keyed by id — random
|
|
// replacement ids would make the second application miss its target.
|
|
check('normalize: replacement ids are stable across reads',
|
|
JSON.stringify(Core.normalizeDoc({ issues: [{ id: 'bad"id]<', text: 'x' }, { text: 'y' }] }))
|
|
=== JSON.stringify(Core.normalizeDoc({ issues: [{ id: 'bad"id]<', text: 'x' }, { text: 'y' }] })));
|
|
const collide = Core.normalizeDoc({ issues: [{ id: 'i_legacy_1', text: 'a' }, { id: 42, text: 'b' }] });
|
|
check('normalize: replacement id colliding with a stored id de-collides deterministically',
|
|
collide.issues.length === 2 && collide.issues[1].id === 'i_legacy_1_d');
|
|
}
|
|
{
|
|
// normalizeDoc and cleanIssueText share one scrub pipeline, so opening the
|
|
// inline editor on untouched stored text and blurring must round-trip
|
|
// byte-identical (a phantom "change" would wrongly clear the Sent stamp).
|
|
const messy = 'a \nb\n\n\n\nc\r\nd';
|
|
const stored = Core.normalizeDoc({ issues: [{ id: 'i_1_a', text: messy }] }).issues[0].text;
|
|
check('normalize/clean round trip: stored text re-cleans to itself',
|
|
Core.cleanIssueText(stored).text === stored);
|
|
// Clamping can expose trailing whitespace at the cut; a second normalize of
|
|
// the clamped result must be a fixed point.
|
|
const long = 'x'.repeat(Core.MAX_TEXT_LENGTH - 1) + ' \ny';
|
|
const once = Core.normalizeDoc({ issues: [{ id: 'i_1_a', text: long }] }).issues[0].text;
|
|
const twice = Core.normalizeDoc({ issues: [{ id: 'i_1_a', text: once }] }).issues[0].text;
|
|
check('normalize: clamping is idempotent across reads', once === twice);
|
|
}
|
|
check('normalize: non-string timestamps → null',
|
|
Core.normalizeDoc({ issues: [{ id: 'i_1_a', text: 'x', createdAt: 12345 }] }).issues[0].createdAt === null);
|
|
check('normalize: does NOT trim to MAX_ISSUES (no silent data loss)',
|
|
Core.normalizeDoc({ issues: Array.from({ length: Core.MAX_ISSUES + 5 }, (_, n) => ({ id: 'i_1_' + n, text: 't' + n })) })
|
|
.issues.length === Core.MAX_ISSUES + 5);
|
|
|
|
// --- addIssue -----------------------------------------------------------------
|
|
{
|
|
const empty = Core.emptyDoc();
|
|
const res = Core.addIssue(empty, ' Fix the broken button ', { id: 'i_1_add', now: NOW });
|
|
check('add: ok', res.ok === true);
|
|
check('add: input doc not mutated', empty.issues.length === 0);
|
|
check('add: text cleaned', res.issue.text === 'Fix the broken button');
|
|
check('add: status open', res.issue.status === 'open');
|
|
check('add: timestamps from opts.now', res.issue.createdAt === NOW && res.issue.updatedAt === NOW);
|
|
check('add: sentAt starts null', res.issue.sentAt === null);
|
|
check('add: deterministic given id+now',
|
|
JSON.stringify(Core.addIssue(empty, 'Fix the broken button', { id: 'i_1_add', now: NOW }).doc)
|
|
=== JSON.stringify(res.doc));
|
|
const again = Core.addIssue(res.doc, 'whatever', { id: 'i_1_add', now: LATER });
|
|
check('add: re-applying same id is an idempotent no-op', again.ok === true && again.doc.issues.length === 1
|
|
&& again.doc.issues[0].text === 'Fix the broken button');
|
|
check('add: generated ids match the house shape',
|
|
/^i_\d+_[a-z0-9]+$/.test(Core.addIssue(empty, 'x').issue.id));
|
|
check('add: empty text refused', Core.addIssue(empty, ' ').error === 'empty');
|
|
check('add: too-long text refused', Core.addIssue(empty, 'x'.repeat(Core.MAX_TEXT_LENGTH + 1)).error === 'too_long');
|
|
}
|
|
{
|
|
const full = { schema: 1, issues: Array.from({ length: Core.MAX_ISSUES }, (_, n) => ({ id: 'i_1_' + n, text: 't', status: 'open', createdAt: NOW, updatedAt: NOW, sentAt: null })) };
|
|
check('add: refused at the cap', Core.addIssue(full, 'one more').error === 'limit');
|
|
}
|
|
|
|
// --- updateIssueText ----------------------------------------------------------
|
|
{
|
|
const doc = Core.addIssue(Core.emptyDoc(), 'old text', { id: 'i_1_e', now: NOW }).doc;
|
|
doc.issues[0].sentAt = NOW; // pretend it was sent
|
|
const res = Core.updateIssueText(doc, 'i_1_e', ' new text ', { now: LATER });
|
|
check('edit: ok + cleaned', res.ok && res.issue.text === 'new text');
|
|
check('edit: input not mutated', doc.issues[0].text === 'old text');
|
|
check('edit: bumps updatedAt', res.issue.updatedAt === LATER);
|
|
check('edit: clears sentAt (AI never saw the new words)', res.issue.sentAt === null);
|
|
const same = Core.updateIssueText(res.doc, 'i_1_e', 'new text', { now: '2026-07-28T14:00:00.000Z' });
|
|
check('edit: unchanged text keeps updatedAt', same.issue.updatedAt === LATER);
|
|
check('edit: unknown id → not_found', Core.updateIssueText(doc, 'i_9_x', 'y').error === 'not_found');
|
|
check('edit: empty text refused (never becomes a delete)', Core.updateIssueText(doc, 'i_1_e', ' ').error === 'empty');
|
|
}
|
|
|
|
// --- setIssueStatus -------------------------------------------------------------
|
|
{
|
|
const doc = Core.addIssue(Core.emptyDoc(), 'a bug', { id: 'i_1_s', now: NOW }).doc;
|
|
const res = Core.setIssueStatus(doc, 'i_1_s', 'resolved', { now: LATER });
|
|
check('status: resolve works', res.ok && res.issue.status === 'resolved' && res.issue.updatedAt === LATER);
|
|
check('status: input not mutated', doc.issues[0].status === 'open');
|
|
const back = Core.setIssueStatus(res.doc, 'i_1_s', 'open', { now: LATER });
|
|
check('status: reopen works', back.ok && back.issue.status === 'open');
|
|
check('status: idempotent same-status keeps updatedAt',
|
|
Core.setIssueStatus(res.doc, 'i_1_s', 'resolved', { now: '2026-07-28T15:00:00.000Z' }).issue.updatedAt === LATER);
|
|
check('status: unknown id → not_found', Core.setIssueStatus(doc, 'i_9_x', 'resolved').error === 'not_found');
|
|
check('status: invalid status refused', Core.setIssueStatus(doc, 'i_1_s', 'closed').error === 'bad_status');
|
|
}
|
|
// --- the review state machine ---------------------------------------------------
|
|
// open → review (AI turn finished) → resolved (user confirms) | open (still
|
|
// broken). Only the user's confirmation may land an issue in 'resolved', so
|
|
// the resolved list never contains unverified claims.
|
|
{
|
|
let doc = Core.emptyDoc();
|
|
doc = Core.addIssue(doc, 'sent and untouched', { id: 'i_1_a', now: NOW }).doc;
|
|
doc = Core.addIssue(doc, 'edited after send', { id: 'i_2_b', now: NOW }).doc;
|
|
doc = Core.addIssue(doc, 'never sent', { id: 'i_3_c', now: NOW }).doc;
|
|
doc = Core.addIssue(doc, 'user resolved it first', { id: 'i_4_d', now: NOW }).doc;
|
|
doc = Core.markIssuesSent(doc, ['i_1_a', 'i_2_b', 'i_4_d'], { now: NOW }).doc;
|
|
doc = Core.updateIssueText(doc, 'i_2_b', 'new wording', { now: NOW }).doc; // clears its sentAt
|
|
doc = Core.setIssueStatus(doc, 'i_4_d', 'resolved', { now: NOW }).doc; // manual resolve
|
|
|
|
const res = Core.markSentIssuesForReview(doc, ['i_1_a', 'i_2_b', 'i_4_d', 'i_9_gone'], { now: LATER });
|
|
const by = id => res.doc.issues.find(i => i.id === id);
|
|
check('review: moves the sent, untouched issue to review — NOT resolved',
|
|
by('i_1_a').status === 'review' && by('i_1_a').updatedAt === LATER);
|
|
check('review: keeps the sent stamp', by('i_1_a').sentAt === NOW);
|
|
check('review: skips an issue edited after sending (new ask still open)',
|
|
by('i_2_b').status === 'open');
|
|
check('review: never touches unsent issues', by('i_3_c').status === 'open');
|
|
check('review: leaves a manual resolve alone (no AI credit)',
|
|
by('i_4_d').status === 'resolved' && by('i_4_d').resolvedBy === null);
|
|
check('review: reports how many it actually changed', res.reviewed === 1);
|
|
check('review: missing ids are skipped, not an error', res.ok === true);
|
|
check('review: input doc not mutated', doc.issues.find(i => i.id === 'i_1_a').status === 'open');
|
|
check('review: excluded from openIssues (never re-sent by "Send all")',
|
|
Core.openIssues(res.doc).every(i => i.id !== 'i_1_a'));
|
|
check('review: reviewIssues filters', Core.reviewIssues(res.doc).map(i => i.id).join(',') === 'i_1_a');
|
|
|
|
// Confirm: review → resolved, credited to the AI.
|
|
const conf = Core.confirmReviewIssues(res.doc, ['i_1_a', 'i_3_c', 'i_4_d', 'i_9_gone'], { now: LATER });
|
|
const c = id => conf.doc.issues.find(i => i.id === id);
|
|
check('confirm: review → resolved with AI credit',
|
|
c('i_1_a').status === 'resolved' && c('i_1_a').resolvedBy === 'ai');
|
|
check('confirm: only counts review items it changed', conf.confirmed === 1);
|
|
check('confirm: never touches open or already-resolved issues',
|
|
c('i_3_c').status === 'open' && c('i_4_d').resolvedBy === null);
|
|
check('confirm: input doc not mutated', res.doc.issues.find(i => i.id === 'i_1_a').status === 'review');
|
|
|
|
// The row circle on a review item also confirms (single-issue path).
|
|
const circled = Core.setIssueStatus(res.doc, 'i_1_a', 'resolved', { now: LATER }).issue;
|
|
check('confirm via circle: review → resolved gets AI credit', circled.resolvedBy === 'ai');
|
|
const manual = Core.setIssueStatus(res.doc, 'i_3_c', 'resolved', { now: LATER }).issue;
|
|
check('manual check-off: open → resolved carries NO AI credit', manual.resolvedBy === null);
|
|
|
|
// Still broken: review → open, all stamps cleared, re-armed for sending.
|
|
const broken = Core.setIssueStatus(res.doc, 'i_1_a', 'open', { now: LATER }).issue;
|
|
check('still broken: review → open clears sentAt', broken.sentAt === null && broken.status === 'open');
|
|
|
|
// Rewording a review item is a new ask: back to open.
|
|
const reworded = Core.updateIssueText(res.doc, 'i_1_a', 'different ask now', { now: LATER }).issue;
|
|
check('edit in review: changed text drops back to open',
|
|
reworded.status === 'open' && reworded.sentAt === null);
|
|
const untouched = Core.updateIssueText(res.doc, 'i_1_a', 'sent and untouched', { now: LATER }).issue;
|
|
check('edit in review: unchanged text stays in review', untouched.status === 'review');
|
|
}
|
|
{
|
|
// Reopening a resolved issue = "this is NOT fixed": the sent stamp and
|
|
// AI credit must not survive, or the reopened row would read as handled.
|
|
let doc = Core.addIssue(Core.emptyDoc(), 'a bug', { id: 'i_1_r', now: NOW }).doc;
|
|
doc = Core.markIssuesSent(doc, ['i_1_r'], { now: NOW }).doc;
|
|
doc = Core.markSentIssuesForReview(doc, ['i_1_r'], { now: NOW }).doc;
|
|
doc = Core.confirmReviewIssues(doc, ['i_1_r'], { now: NOW }).doc;
|
|
const reopened = Core.setIssueStatus(doc, 'i_1_r', 'open', { now: LATER }).issue;
|
|
check('reopen: clears sentAt', reopened.sentAt === null);
|
|
check('reopen: clears resolvedBy', reopened.resolvedBy === null);
|
|
// Rewriting a resolved issue's text is the same lie-prevention case:
|
|
// "Fixed by AI" must not survive onto words the AI never saw.
|
|
const editedResolved = Core.updateIssueText(doc, 'i_1_r', 'different words', { now: LATER }).issue;
|
|
check('edit resolved: clears resolvedBy', editedResolved.resolvedBy === null && editedResolved.status === 'resolved');
|
|
}
|
|
{
|
|
check('normalize: review status kept',
|
|
Core.normalizeDoc({ issues: [{ id: 'i_1_a', text: 'x', status: 'review' }] }).issues[0].status === 'review');
|
|
check('normalize: resolvedBy "ai" kept',
|
|
Core.normalizeDoc({ issues: [{ id: 'i_1_a', text: 'x', status: 'resolved', resolvedBy: 'ai' }] }).issues[0].resolvedBy === 'ai');
|
|
check('normalize: garbage resolvedBy → null',
|
|
Core.normalizeDoc({ issues: [{ id: 'i_1_a', text: 'x', resolvedBy: 'someone' }] }).issues[0].resolvedBy === null);
|
|
}
|
|
|
|
// --- deleteIssue / restoreIssue -------------------------------------------------
|
|
{
|
|
let doc = Core.emptyDoc();
|
|
doc = Core.addIssue(doc, 'first', { id: 'i_1_a', now: NOW }).doc;
|
|
doc = Core.addIssue(doc, 'second', { id: 'i_2_b', now: NOW }).doc;
|
|
doc = Core.addIssue(doc, 'third', { id: 'i_3_c', now: NOW }).doc;
|
|
const res = Core.deleteIssue(doc, 'i_2_b');
|
|
check('delete: ok, returns issue + index for undo', res.ok && res.issue.text === 'second' && res.index === 1);
|
|
check('delete: removed from doc', res.doc.issues.length === 2 && !res.doc.issues.some(i => i.id === 'i_2_b'));
|
|
check('delete: input not mutated', doc.issues.length === 3);
|
|
check('delete: unknown id → not_found', Core.deleteIssue(doc, 'i_9_x').error === 'not_found');
|
|
const undo = Core.restoreIssue(res.doc, res.issue, res.index);
|
|
check('restore: puts the issue back at its old index',
|
|
undo.ok && undo.doc.issues.map(i => i.id).join(',') === 'i_1_a,i_2_b,i_3_c');
|
|
check('restore: idempotent when already present',
|
|
Core.restoreIssue(undo.doc, res.issue, res.index).doc.issues.length === 3);
|
|
check('restore: out-of-range index clamps', Core.restoreIssue(res.doc, res.issue, 99).doc.issues.length === 3);
|
|
check('restore: garbage refused', Core.restoreIssue(doc, null).ok === false);
|
|
}
|
|
|
|
// --- markIssuesSent -------------------------------------------------------------
|
|
{
|
|
let doc = Core.emptyDoc();
|
|
doc = Core.addIssue(doc, 'a', { id: 'i_1_a', now: NOW }).doc;
|
|
doc = Core.addIssue(doc, 'b', { id: 'i_2_b', now: NOW }).doc;
|
|
const res = Core.markIssuesSent(doc, ['i_1_a', 'i_9_gone'], { now: LATER });
|
|
check('sent: stamps only the listed ids', res.ok
|
|
&& res.doc.issues[0].sentAt === LATER && res.doc.issues[1].sentAt === null);
|
|
check('sent: missing ids are skipped, not an error', res.ok === true);
|
|
check('sent: input not mutated', doc.issues[0].sentAt === null);
|
|
}
|
|
|
|
// --- unmarkIssuesSent -----------------------------------------------------------
|
|
// The revert for a "Sent" stamp whose turn never started (sign-in dismissed,
|
|
// another send mid-setup). Only OUR stamp may be taken back, so a later real
|
|
// send of the same issue is never undone.
|
|
{
|
|
let doc = Core.emptyDoc();
|
|
doc = Core.addIssue(doc, 'a', { id: 'i_1_a', now: NOW }).doc;
|
|
doc = Core.addIssue(doc, 'b', { id: 'i_2_b', now: NOW }).doc;
|
|
doc = Core.markIssuesSent(doc, ['i_1_a', 'i_2_b'], { now: NOW }).doc;
|
|
const res = Core.unmarkIssuesSent(doc, ['i_1_a', 'i_9_gone'], { now: NOW });
|
|
check('unsent: clears only the listed ids', res.ok && res.doc.issues[0].sentAt === null && res.doc.issues[1].sentAt === NOW);
|
|
check('unsent: input not mutated', doc.issues[0].sentAt === NOW);
|
|
const later = Core.markIssuesSent(doc, ['i_1_a'], { now: LATER }).doc;
|
|
const stale = Core.unmarkIssuesSent(later, ['i_1_a'], { now: NOW });
|
|
check('unsent: a newer real stamp is never undone by a stale revert', stale.doc.issues[0].sentAt === LATER);
|
|
const twice = Core.unmarkIssuesSent(res.doc, ['i_1_a'], { now: NOW });
|
|
check('unsent: idempotent', twice.ok && twice.doc.issues[0].sentAt === null);
|
|
check('unsent: signature changes when a stamp is taken back', Core.signature(doc) !== Core.signature(res.doc));
|
|
}
|
|
|
|
// --- open/resolved views ---------------------------------------------------------
|
|
{
|
|
let doc = Core.emptyDoc();
|
|
doc = Core.addIssue(doc, 'a', { id: 'i_1_a', now: NOW }).doc;
|
|
doc = Core.addIssue(doc, 'b', { id: 'i_2_b', now: NOW }).doc;
|
|
doc = Core.setIssueStatus(doc, 'i_1_a', 'resolved', { now: LATER }).doc;
|
|
check('views: openIssues filters', Core.openIssues(doc).map(i => i.id).join(',') === 'i_2_b');
|
|
check('views: resolvedIssues filters', Core.resolvedIssues(doc).map(i => i.id).join(',') === 'i_1_a');
|
|
check('views: tolerate null doc', Core.openIssues(null).length === 0 && Core.resolvedIssues(null).length === 0);
|
|
}
|
|
|
|
// --- signature -------------------------------------------------------------------
|
|
{
|
|
let doc = Core.addIssue(Core.emptyDoc(), 'a', { id: 'i_1_a', now: NOW }).doc;
|
|
const s0 = Core.signature(doc);
|
|
check('signature: stable for identical docs', s0 === Core.signature(Core.cloneDoc(doc)));
|
|
check('signature: changes on add', s0 !== Core.signature(Core.addIssue(doc, 'b', { id: 'i_2_b', now: NOW }).doc));
|
|
check('signature: changes on status flip', s0 !== Core.signature(Core.setIssueStatus(doc, 'i_1_a', 'resolved', { now: LATER }).doc));
|
|
check('signature: changes on edit', s0 !== Core.signature(Core.updateIssueText(doc, 'i_1_a', 'edited', { now: LATER }).doc));
|
|
check('signature: changes on sent stamp', s0 !== Core.signature(Core.markIssuesSent(doc, ['i_1_a'], { now: LATER }).doc));
|
|
check('signature: empty doc → empty string', Core.signature(Core.emptyDoc()) === '');
|
|
check('signature: tolerates null', Core.signature(null) === '');
|
|
}
|
|
|
|
// --- formatForAI ------------------------------------------------------------------
|
|
{
|
|
check('format: no issues → empty string', Core.formatForAI([]) === '');
|
|
check('format: tolerates null', Core.formatForAI(null) === '');
|
|
const one = Core.formatForAI([{ id: 'i', text: 'Button is broken' }]);
|
|
check('format: single issue, singular phrasing, no numbering',
|
|
one === 'Please fix the following issue:\n\nButton is broken');
|
|
const many = Core.formatForAI([
|
|
{ id: 'a', text: 'First bug' },
|
|
{ id: 'b', text: 'Second bug\nwith a second line' },
|
|
]);
|
|
check('format: multiple issues numbered', many.startsWith('Please fix the following issues:\n\n1. First bug\n2. Second bug'));
|
|
check('format: continuation lines indented under their number', many.includes('\n with a second line'));
|
|
check('format: blank/invalid entries skipped',
|
|
Core.formatForAI([{ id: 'a', text: ' ' }, { id: 'b', text: 'real' }, null])
|
|
=== 'Please fix the following issue:\n\nreal');
|
|
}
|
|
|
|
if (failures) {
|
|
console.error(`\n${failures} check(s) failed`);
|
|
process.exit(1);
|
|
}
|
|
console.log('\nAll issues-core checks passed');
|