fix: resume UAT checkpoint from paused placeholder (#1350)
This commit is contained in:
6
.changeset/1300-uat-paused-checkpoint.md
Normal file
6
.changeset/1300-uat-paused-checkpoint.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 1350
|
||||
---
|
||||
|
||||
**UAT resume now accepts paused checkpoints** — `uat render-checkpoint` treats a non-structured paused `Current Test` placeholder as a resume signal and derives the checkpoint from the first pending UAT test instead of failing as malformed. (#1300)
|
||||
64
src/uat.cts
64
src/uat.cts
@@ -200,6 +200,13 @@ function parseCurrentTest(content: string): CurrentTest {
|
||||
const expectedInlineMatch = section.match(/^expected:\s*(.+)\s*$/m);
|
||||
|
||||
if (!numberMatch || !nameMatch || (!expectedBlockMatch && !expectedInlineMatch)) {
|
||||
if (!numberMatch && !nameMatch && !expectedBlockMatch && !expectedInlineMatch) {
|
||||
const pendingTest = parseFirstPendingTest(content);
|
||||
if (pendingTest) {
|
||||
return pendingTest;
|
||||
}
|
||||
error('Current Test section is non-structured and no pending UAT test remains to resume');
|
||||
}
|
||||
error('Current Test section is malformed');
|
||||
}
|
||||
|
||||
@@ -222,6 +229,63 @@ function parseCurrentTest(content: string): CurrentTest {
|
||||
};
|
||||
}
|
||||
|
||||
function parseFirstPendingTest(content: string): CurrentTest | null {
|
||||
const testsMatch = content.match(/##\s*Tests\s*\n([\s\S]*?)(?=\n##\s|$)/i);
|
||||
if (!testsMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const testsSection = testsMatch[1];
|
||||
const headingPattern = /^###\s*(\d+)\.\s*([^\n]+)\s*$/gm;
|
||||
const headings: Array<{ index: number; number: number; name: string }> = [];
|
||||
let headingMatch: RegExpExecArray | null;
|
||||
while ((headingMatch = headingPattern.exec(testsSection)) !== null) {
|
||||
headings.push({
|
||||
index: headingMatch.index,
|
||||
number: parseInt(headingMatch[1], 10),
|
||||
name: headingMatch[2].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < headings.length; i += 1) {
|
||||
const current = headings[i];
|
||||
const next = headings[i + 1];
|
||||
const block = testsSection.slice(current.index, next ? next.index : undefined);
|
||||
if (!/^result:\s*\[?pending\]?\s*$/im.test(block)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const expected = parseExpectedFromTestBlock(block);
|
||||
if (!expected) {
|
||||
error(`Pending UAT test ${current.number} is missing an expected field`);
|
||||
}
|
||||
|
||||
return {
|
||||
complete: false,
|
||||
number: current.number,
|
||||
name: sanitizeForDisplay(current.name),
|
||||
expected: sanitizeForDisplay(expected),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseExpectedFromTestBlock(block: string): string | null {
|
||||
const expectedBlockMatch = block.match(/^expected:\s*\|\n([\s\S]*?)(?=^\w[\w-]*:\s)/m)
|
||||
|| block.match(/^expected:\s*\|\n([\s\S]+)/m);
|
||||
if (expectedBlockMatch) {
|
||||
return expectedBlockMatch[1]
|
||||
.split('\n')
|
||||
.map((line: string) => line.replace(/^ {2}/, ''))
|
||||
.join('\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
const expectedInlineMatch = block.match(/^expected:\s*(.+)\s*$/m);
|
||||
return expectedInlineMatch ? expectedInlineMatch[1].trim() : null;
|
||||
}
|
||||
|
||||
// ─── buildCheckpoint ──────────────────────────────────────────────────────────
|
||||
|
||||
function buildCheckpoint(currentTest: { number: number; name: string; expected: string }): string {
|
||||
|
||||
@@ -533,6 +533,99 @@ expected: |
|
||||
assert.ok(result.output.includes('It ends at the section boundary.'));
|
||||
});
|
||||
|
||||
test('resumes paused Current Test placeholder from first pending test (#1300)', () => {
|
||||
fs.writeFileSync(uatPath, [
|
||||
'---',
|
||||
'status: partial',
|
||||
'phase: 01-test-phase',
|
||||
'started: 2026-06-15T00:00:00Z',
|
||||
'updated: 2026-06-15T00:00:00Z',
|
||||
'---',
|
||||
'',
|
||||
'## Current Test',
|
||||
'',
|
||||
'[testing paused — 2 items outstanding]',
|
||||
'',
|
||||
'## Tests',
|
||||
'',
|
||||
'### 1. First test',
|
||||
'expected: something observable',
|
||||
'result: pass',
|
||||
'',
|
||||
'### 2. Second test',
|
||||
'expected: another observable thing',
|
||||
'result: [pending]',
|
||||
'',
|
||||
'## Summary',
|
||||
'',
|
||||
'total: 2',
|
||||
'passed: 1',
|
||||
'issues: 0',
|
||||
'pending: 1',
|
||||
'skipped: 0',
|
||||
'blocked: 0',
|
||||
'',
|
||||
'## Gaps',
|
||||
'',
|
||||
'[none yet]',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools(['uat', 'render-checkpoint', '--file', '.planning/phases/01-test-phase/01-UAT.md'], tmpDir);
|
||||
assert.strictEqual(result.success, true, `render-checkpoint failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.test_number, 2);
|
||||
assert.strictEqual(output.test_name, 'Second test');
|
||||
assert.strictEqual(output.file_path, '.planning/phases/01-test-phase/01-UAT.md');
|
||||
});
|
||||
|
||||
test('raw checkpoint mode accepts paused Current Test placeholder (#1300)', () => {
|
||||
fs.writeFileSync(uatPath, [
|
||||
'---',
|
||||
'status: partial',
|
||||
'phase: 01-test-phase',
|
||||
'---',
|
||||
'',
|
||||
'## Current Test',
|
||||
'',
|
||||
'[testing paused — 1 item outstanding]',
|
||||
'',
|
||||
'## Tests',
|
||||
'',
|
||||
'### 1. First pending test',
|
||||
'expected: raw mode checkpoint is available',
|
||||
'result: [pending]',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools(['uat', 'render-checkpoint', '--file', '.planning/phases/01-test-phase/01-UAT.md', '--raw'], tmpDir);
|
||||
assert.strictEqual(result.success, true, `render-checkpoint failed: ${result.error}`);
|
||||
assert.ok(result.output.length > 0, 'raw mode must emit a checkpoint');
|
||||
});
|
||||
|
||||
test('non-structured Current Test with no pending tests reports actionable resume error (#1300)', () => {
|
||||
fs.writeFileSync(uatPath, [
|
||||
'---',
|
||||
'status: partial',
|
||||
'phase: 01-test-phase',
|
||||
'---',
|
||||
'',
|
||||
'## Current Test',
|
||||
'',
|
||||
'[testing paused — 0 items outstanding]',
|
||||
'',
|
||||
'## Tests',
|
||||
'',
|
||||
'### 1. Already handled test',
|
||||
'expected: completed behavior',
|
||||
'result: pass',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools(['uat', 'render-checkpoint', '--file', '.planning/phases/01-test-phase/01-UAT.md'], tmpDir);
|
||||
assert.strictEqual(result.success, false, 'Should fail when a paused placeholder has no pending test to resume');
|
||||
assert.ok(result.error.includes('no pending UAT test remains'));
|
||||
assert.ok(!result.error.includes('Current Test section is malformed'));
|
||||
});
|
||||
|
||||
test('fails when testing is already complete', () => {
|
||||
fs.writeFileSync(uatPath, `---
|
||||
status: complete
|
||||
|
||||
Reference in New Issue
Block a user