mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
222 lines
8.3 KiB
JavaScript
222 lines
8.3 KiB
JavaScript
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
import assert from 'node:assert';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import os from 'node:os';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { writeBuffer } from '../skill/scripts/live/manual-edits-buffer.mjs';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
const SCRIPT = path.join(REPO_ROOT, 'skill/scripts/live-wrap.mjs');
|
|
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wrap-buf-test-'));
|
|
fs.mkdirSync(path.join(tmpDir, 'src'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
function seedBuffer(entries) {
|
|
writeBuffer(tmpDir, { entries });
|
|
}
|
|
|
|
function entry({ pageUrl, ops }) {
|
|
return {
|
|
id: 'e' + Math.random().toString(36).slice(2, 8),
|
|
pageUrl,
|
|
element: { tagName: 'h1' },
|
|
ops,
|
|
stagedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
function runWrap(extraArgs) {
|
|
const args = [SCRIPT, '--id', 'aaaaaaaa', '--count', '3', ...extraArgs];
|
|
const stdout = execFileSync('node', args, { encoding: 'utf-8', cwd: tmpDir });
|
|
return JSON.parse(stdout.trim());
|
|
}
|
|
|
|
function runWrapExpectFailure(extraArgs) {
|
|
const args = [SCRIPT, '--id', 'aaaaaaaa', '--count', '3', ...extraArgs];
|
|
try {
|
|
execFileSync('node', args, { encoding: 'utf-8', cwd: tmpDir, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
throw new Error('expected wrap to fail');
|
|
} catch (err) {
|
|
if (err.status === undefined) throw err;
|
|
return { status: err.status, stderr: err.stderr.toString() };
|
|
}
|
|
}
|
|
|
|
describe('live-wrap.mjs buffer-aware "original" content', () => {
|
|
it('with matching --page-url, rewrites the wrap block to reflect the buffered edit', () => {
|
|
const file = path.join(tmpDir, 'src', 'page.html');
|
|
fs.writeFileSync(file, '<div>\n <h1 class="hero">Welcome</h1>\n</div>\n');
|
|
|
|
seedBuffer([
|
|
entry({ pageUrl: '/', ops: [{ ref: 'div>h1.1', tag: 'h1', classes: ['hero'], originalText: 'Welcome', newText: 'Hello there' }] }),
|
|
]);
|
|
|
|
runWrap(['--classes', 'hero', '--tag', 'h1', '--page-url', '/']);
|
|
|
|
const after = fs.readFileSync(file, 'utf-8');
|
|
assert.match(after, /Hello there/);
|
|
assert.doesNotMatch(after, /<h1 class="hero">Welcome<\/h1>/);
|
|
});
|
|
|
|
it('accepts --page-url=<url> when retrying after missing page-url guidance', () => {
|
|
const file = path.join(tmpDir, 'src', 'page.html');
|
|
fs.writeFileSync(file, '<div>\n <h1 class="hero">Welcome</h1>\n</div>\n');
|
|
|
|
seedBuffer([
|
|
entry({ pageUrl: '/', ops: [{ ref: 'div>h1.1', tag: 'h1', classes: ['hero'], originalText: 'Welcome', newText: 'Hello equals' }] }),
|
|
]);
|
|
|
|
runWrap(['--classes', 'hero', '--tag', 'h1', '--page-url=/']);
|
|
|
|
const after = fs.readFileSync(file, 'utf-8');
|
|
assert.match(after, /Hello equals/);
|
|
});
|
|
|
|
it('updates only the staged leaf when matching text repeats inside the selected original block', () => {
|
|
const file = path.join(tmpDir, 'src', 'page.html');
|
|
fs.writeFileSync(file, '<section class="hero">\n <h1>Welcome</h1>\n <p>Welcome</p>\n</section>\n');
|
|
|
|
seedBuffer([
|
|
entry({ pageUrl: '/', ops: [{ ref: 'section.hero>h1:nth-of-type(1)', tag: 'h1', originalText: 'Welcome', newText: 'Hello' }] }),
|
|
]);
|
|
|
|
runWrap(['--classes', 'hero', '--tag', 'section', '--page-url', '/']);
|
|
|
|
const after = fs.readFileSync(file, 'utf-8');
|
|
const originalWrapper = after.match(/data-impeccable-variant="original"[\s\S]*?<\/div>/)?.[0] || '';
|
|
assert.equal((originalWrapper.match(/Hello/g) || []).length, 1);
|
|
assert.match(originalWrapper, /<h1>Hello<\/h1>/);
|
|
assert.match(originalWrapper, /<p>Welcome<\/p>/);
|
|
});
|
|
|
|
it('with matching --page-url, refuses when a staged edit in the block is ambiguous', () => {
|
|
const file = path.join(tmpDir, 'src', 'page.html');
|
|
const original = '<section class="cards">\n <p class="label">Same</p>\n <p class="label">Same</p>\n</section>\n';
|
|
fs.writeFileSync(file, original);
|
|
|
|
seedBuffer([
|
|
entry({
|
|
pageUrl: '/',
|
|
ops: [{
|
|
ref: 'body>section.cards:nth-of-type(1)>p.label:nth-of-type(2)',
|
|
tag: 'p',
|
|
classes: ['label'],
|
|
originalText: 'Same',
|
|
newText: 'Second',
|
|
}],
|
|
}),
|
|
]);
|
|
|
|
const result = runWrapExpectFailure(['--classes', 'cards', '--tag', 'section', '--page-url', '/']);
|
|
assert.equal(result.status, 1);
|
|
const errPayload = JSON.parse(result.stderr.split('\n').filter((l) => l.trim().startsWith('{')).pop());
|
|
assert.equal(errPayload.error, 'manual_edit_buffer_apply_failed');
|
|
assert.equal(errPayload.pendingOps.length, 1);
|
|
assert.equal(fs.readFileSync(file, 'utf-8'), original);
|
|
});
|
|
|
|
it('with matching --page-url, ignores unrelated same-page staged edits outside the selected block', () => {
|
|
const file = path.join(tmpDir, 'src', 'page.html');
|
|
fs.writeFileSync(file, '<main>\n <section class="hero"><h1>Welcome</h1></section>\n <aside><p>Footer copy</p></aside>\n</main>\n');
|
|
|
|
seedBuffer([
|
|
entry({
|
|
pageUrl: '/',
|
|
ops: [{
|
|
ref: 'body>main:nth-of-type(1)>aside:nth-of-type(1)>p:nth-of-type(1)',
|
|
tag: 'p',
|
|
originalText: 'Footer copy',
|
|
newText: 'Footer edited',
|
|
sourceHint: { file: 'src/page.html', line: 3 },
|
|
}],
|
|
}),
|
|
]);
|
|
|
|
const result = runWrap(['--classes', 'hero', '--tag', 'section', '--page-url', '/']);
|
|
assert.ok(result.file, 'wrap should succeed when same-page staged edits cannot affect the target block');
|
|
});
|
|
|
|
it('with mismatched --page-url, does not leak the edit', () => {
|
|
const file = path.join(tmpDir, 'src', 'page.html');
|
|
fs.writeFileSync(file, '<div>\n <h1 class="hero">Welcome</h1>\n</div>\n');
|
|
|
|
// Buffer has an edit for "/a" — wrap is called for "/b"
|
|
seedBuffer([
|
|
entry({ pageUrl: '/a', ops: [{ ref: 'div>h1.1', tag: 'h1', classes: ['hero'], originalText: 'Welcome', newText: 'LEAK' }] }),
|
|
]);
|
|
|
|
runWrap(['--classes', 'hero', '--tag', 'h1', '--page-url', '/b']);
|
|
|
|
const after = fs.readFileSync(file, 'utf-8');
|
|
assert.match(after, /Welcome/);
|
|
assert.doesNotMatch(after, /LEAK/);
|
|
});
|
|
|
|
it('with pending entries that affect the picked block, refuses without --page-url', () => {
|
|
const file = path.join(tmpDir, 'src', 'page.html');
|
|
fs.writeFileSync(file, '<div>\n <h1 class="hero">Welcome</h1>\n</div>\n');
|
|
|
|
seedBuffer([
|
|
entry({
|
|
pageUrl: '/',
|
|
ops: [{
|
|
ref: 'div>h1.1',
|
|
tag: 'h1',
|
|
classes: ['hero'],
|
|
originalText: 'Welcome',
|
|
newText: 'SHOULD_NOT_APPEAR',
|
|
sourceHint: { file: 'src/page.html', line: 2 },
|
|
}],
|
|
}),
|
|
]);
|
|
|
|
const result = runWrapExpectFailure(['--classes', 'hero', '--tag', 'h1']);
|
|
assert.equal(result.status, 1);
|
|
const errPayload = JSON.parse(result.stderr.split('\n').filter((l) => l.trim().startsWith('{')).pop());
|
|
assert.equal(errPayload.error, 'missing_page_url_with_pending_edits');
|
|
assert.equal(errPayload.pendingEntries, 1);
|
|
assert.equal(fs.readFileSync(file, 'utf-8'), '<div>\n <h1 class="hero">Welcome</h1>\n</div>\n');
|
|
});
|
|
|
|
it('without --page-url, does not block wrap for unrelated same-file pending edits', () => {
|
|
const file = path.join(tmpDir, 'src', 'page.html');
|
|
fs.writeFileSync(file, '<main>\n <section class="hero"><h1>Welcome</h1></section>\n <aside><p>Shared</p></aside>\n</main>\n');
|
|
|
|
seedBuffer([
|
|
entry({
|
|
pageUrl: '/other',
|
|
ops: [{
|
|
ref: 'body>main:nth-of-type(1)>aside:nth-of-type(1)>p:nth-of-type(1)',
|
|
tag: 'p',
|
|
originalText: 'Shared',
|
|
newText: 'Shared edited',
|
|
sourceHint: { file: 'src/page.html', line: 3 },
|
|
}],
|
|
}),
|
|
]);
|
|
|
|
const result = runWrap(['--classes', 'hero', '--tag', 'section']);
|
|
assert.ok(result.file, 'wrap should succeed when staged edits cannot affect the target block');
|
|
});
|
|
|
|
it('with empty buffer, --page-url is optional', () => {
|
|
const file = path.join(tmpDir, 'src', 'page.html');
|
|
fs.writeFileSync(file, '<div>\n <h1 class="hero">Welcome</h1>\n</div>\n');
|
|
// No seedBuffer call — empty buffer.
|
|
|
|
const result = runWrap(['--classes', 'hero', '--tag', 'h1']);
|
|
assert.ok(result.file, 'wrap should succeed and emit file path');
|
|
});
|
|
});
|