Compare commits

...
Author SHA1 Message Date
Abdul WahabandCursor 8769bd4926 Fix: forward engine exit code from impeccable.cmd (#836)
Bare `exit /b` dropped the process code when the launcher was cmd /c's entry point (PowerShell, Node spawn), so Windows skill runs treated detector findings as success.

AI assistance: implemented by Cursor Grok 4.6.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 16:52:33 +05:00
f2c7051853 Fix: drop discard blank lines from live-accept oracle goldens (#793)
The four discard goldens encoded the wrapper newlines as empty lines around the restored element. Re-recorded from the engine so they match the trim in extract_original.

Written with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 17:57:54 +05:00
c0a0f8a9ad fix(live): discard restores the original without a blank line above and below it
`extract_original` split the wrapper's inner text on newlines and kept the
first and last pieces, which are the line breaks around the original
element inside `<div data-impeccable-variant="original">`. Every discard
therefore left one empty line before and one after the restored element.
`extract_variant` already trims those; the original now gets the same
trim. Test writes a JSX wrapper to disk, discards, and checks the file
byte for byte.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:57:54 +05:00
8 changed files with 121 additions and 14 deletions
+65 -8
View File
@@ -1093,12 +1093,26 @@ fn extract_inner_by_attr(text: &str, attr: &str) -> Option<String> {
None
}
/// Drop leading and trailing blank lines while keeping a single empty line
/// when the inner text is only whitespace.
fn trim_surrounding_blank_lines(lines: Vec<String>) -> Vec<String> {
let mut start = 0usize;
let mut end = lines.len();
while end - start > 1 && trim(&lines[start]).is_empty() {
start += 1;
}
while end - start > 1 && trim(&lines[end - 1]).is_empty() {
end -= 1;
}
lines[start..end].to_vec()
}
/// JS: extractOriginal(lines, block)
fn extract_original(lines: &[String], block: &MarkerBlock) -> Vec<String> {
let text = strip_style_and_join(lines, block);
match extract_inner_by_attr(&text, "data-impeccable-variant=\"original\"") {
None => Vec::new(),
Some(inner) => inner.split('\n').map(String::from).collect(),
Some(inner) => trim_surrounding_blank_lines(inner.split('\n').map(String::from).collect()),
}
}
@@ -1113,13 +1127,7 @@ fn extract_variant(
&text,
&format!("data-impeccable-variant=\"{}\"", variant_num),
)?;
let mut result: Vec<String> = inner.split('\n').map(String::from).collect();
while result.len() > 1 && trim(&result[0]).is_empty() {
result.remove(0);
}
while result.len() > 1 && trim(result.last().unwrap()).is_empty() {
result.pop();
}
let result = trim_surrounding_blank_lines(inner.split('\n').map(String::from).collect());
if result.is_empty() {
None
} else {
@@ -1249,6 +1257,55 @@ fn find_session_file(id: &str, cwd: &str) -> Option<(String, String, Vec<String>
Some((file, content, lines))
}
#[cfg(test)]
mod discard_tests {
use super::*;
#[test]
fn discard_restores_the_original_without_blank_lines_around_it() {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = std::env::temp_dir().join(format!(
"impeccable-discard-{}-{nanos}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("Login.tsx");
let src = [
" </div>",
" <div data-impeccable-variants=\"ab12cd34\" data-impeccable-variant-count=\"3\" style={{ display: \"contents\" }}>",
" {/* impeccable-variants-start ab12cd34 */}",
" <style data-impeccable-css=\"ab12cd34\">{`",
" @scope ([data-impeccable-variant=\"1\"]) { :scope > .x { color: red; } }",
" `}</style>",
" {/* Original */}",
" <div data-impeccable-variant=\"original\">",
" <Bar title=\"Log in\" />",
" </div>",
" {/* Variants: insert below this line */}",
" <div data-impeccable-variant=\"1\">",
" <Bar title=\"Log in\" />",
" </div>",
" {/* impeccable-variants-end ab12cd34 */}",
" </div>",
" </AuthPage>",
];
let lines: Vec<String> = src.iter().map(|s| s.to_string()).collect();
let path = file.to_string_lossy().into_owned();
std::fs::write(&file, lines.join("\n")).unwrap();
handle_discard_unlocked("ab12cd34", &lines, &path).unwrap();
let out = std::fs::read_to_string(&file).unwrap();
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(
out,
" </div>\n <Bar title=\"Log in\" />\n </AuthPage>"
);
}
}
#[cfg(test)]
mod bake_tests {
use super::*;
@@ -57,6 +57,28 @@ fn cmd_launcher_asset_naming_matches_engine() {
assert!(!cmd.contains("npm i -g"));
}
#[test]
fn cmd_launcher_forwards_engine_exit_code() {
// Bare `exit /b` drops the process exit code when this file is cmd.exe's
// entry point (cmd /c, PowerShell, Node spawn). Forward %errorlevel%
// after each engine invocation instead.
let cmd = launcher_file("impeccable.cmd");
for (i, line) in cmd.lines().enumerate() {
assert_ne!(
line.trim(),
"exit /b",
"impeccable.cmd line {}: bare exit /b drops the process code: {line}",
i + 1
);
}
let forward = "exit /b %errorlevel%";
assert_eq!(
cmd.matches(forward).count(),
2,
"PATH candidate and :run must both forward the engine exit code"
);
}
#[test]
fn cmd_launcher_has_no_multiline_parenthesized_blocks() {
// cmd.exe expands %var% inside a parenthesized block at parse time, so a
+2 -2
View File
@@ -59,7 +59,7 @@ if errorlevel 1 goto download
call :probe impeccable
if not "%probe_ok%"=="1" goto download
impeccable %*
exit /b
exit /b %errorlevel%
:download
rem Last resort: fetch this version's binary from the release channel into
@@ -166,7 +166,7 @@ exit /b 127
:run
"%run%" %*
exit /b
exit /b %errorlevel%
:probe
rem Sets probe_ok=1 when %1 answers the engine handshake: prints
+28
View File
@@ -174,6 +174,34 @@ test('launcher downloads and runs a verified executable', async t => {
assert.equal(result.requests.length, 2);
});
test('cmd launcher forwards engine exit code through cmd /c', { skip: WINDOWS ? false : 'Windows-only cmd /c exit-code forwarding' }, async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-launcher-exit-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const home = path.join(root, 'home');
fs.mkdirSync(home);
const launcher = path.join(root, 'impeccable.cmd');
fs.copyFileSync(path.join(ROOT, 'skill/scripts/impeccable.cmd'), launcher);
const env = {
PATH: `${process.env.SystemRoot}\\System32;${process.env.SystemRoot}`,
HOME: home, USERPROFILE: home, TEMP: root, TMP: root,
IMPECCABLE_HOME: path.join(root, 'cache'),
IMPECCABLE_BIN: COMSPEC,
SystemRoot: process.env.SystemRoot,
ComSpec: COMSPEC,
PROCESSOR_ARCHITECTURE: 'AMD64',
};
const run = (args) => new Promise((resolve, reject) => {
const child = spawn(COMSPEC, ['/d', '/s', '/c', `""${launcher}" ${args}"`], { env, cwd: root, windowsVerbatimArguments: true, timeout: 20000 });
child.on('error', reject);
child.on('close', (status, signal) => resolve({ status, signal }));
});
for (const [args, expected] of [['/c exit 2', 2], ['/c exit 1', 1], ['/c exit 0', 0]]) {
const result = await run(args);
assert.equal(result.signal, null, JSON.stringify(result));
assert.equal(result.status, expected, args);
}
});
for (const scenario of ['removed', 'emptied', 'empty-download', 'no-sidecar', 'empty-sidecar', 'mismatch', 'hash-failure', 'removed-during-hash', 'removed-before-move', 'removed-after-move', 'emptied-after-move', 'move-failure']) {
test(`launcher refuses ${scenario} with an accurate diagnostic`, async t => {
const result = await exercise(t, scenario);
@@ -16,6 +16,6 @@
"files": {
".impeccable/live/accept-receipts/ab12cd34.json": "{\n \"id\": \"ab12cd34\",\n \"operation\": \"discard\",\n \"variantId\": null,\n \"result\": {\n \"handled\": true,\n \"file\": \"index.html\",\n \"carbonize\": false\n },\n \"completedAt\": \"<ISO>\"\n}\n",
".impeccable/live/config.json": "{\n \"files\": [\"index.html\", \"public/**/*.html\"],\n \"insertBefore\": \"</body>\",\n \"commentSyntax\": \"html\"\n}\n",
"index.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Oracle Live HTML Fixture</title>\n <style>\n .hero-title { font-size: 2rem; }\n .hero-hook { color: #555; }\n </style>\n </head>\n <body>\n <main class=\"page\">\n\n <h1 id=\"hero\" class=\"hero-title\">Oracle Fixture</h1>\n\n <p class=\"hero-hook\">Minimal static page for oracle live-mode goldens.</p>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n <aside class=\"side-note\">\n <h2 class=\"note-title\">Aside</h2>\n <p>Nested content.</p>\n </aside>\n </main>\n </body>\n</html>\n"
"index.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Oracle Live HTML Fixture</title>\n <style>\n .hero-title { font-size: 2rem; }\n .hero-hook { color: #555; }\n </style>\n </head>\n <body>\n <main class=\"page\">\n <h1 id=\"hero\" class=\"hero-title\">Oracle Fixture</h1>\n <p class=\"hero-hook\">Minimal static page for oracle live-mode goldens.</p>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n <aside class=\"side-note\">\n <h2 class=\"note-title\">Aside</h2>\n <p>Nested content.</p>\n </aside>\n </main>\n </body>\n</html>\n"
}
}
@@ -6,6 +6,6 @@
"files": {
".impeccable/live/accept-receipts/ab12cd34.json": "{\n \"id\": \"ab12cd34\",\n \"operation\": \"discard\",\n \"variantId\": null,\n \"result\": {\n \"handled\": true,\n \"file\": \"index.html\",\n \"carbonize\": false\n },\n \"completedAt\": \"<ISO>\"\n}\n",
".impeccable/live/config.json": "{\n \"files\": [\"index.html\", \"public/**/*.html\"],\n \"insertBefore\": \"</body>\",\n \"commentSyntax\": \"html\"\n}\n",
"index.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Oracle Live HTML Fixture</title>\n <style>\n .hero-title { font-size: 2rem; }\n .hero-hook { color: #555; }\n </style>\n </head>\n <body>\n <main class=\"page\">\n\n <h1 id=\"hero\" class=\"hero-title\">Oracle Fixture</h1>\n\n <p class=\"hero-hook\">Minimal static page for oracle live-mode goldens.</p>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n <aside class=\"side-note\">\n <h2 class=\"note-title\">Aside</h2>\n <p>Nested content.</p>\n </aside>\n </main>\n </body>\n</html>\n"
"index.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Oracle Live HTML Fixture</title>\n <style>\n .hero-title { font-size: 2rem; }\n .hero-hook { color: #555; }\n </style>\n </head>\n <body>\n <main class=\"page\">\n <h1 id=\"hero\" class=\"hero-title\">Oracle Fixture</h1>\n <p class=\"hero-hook\">Minimal static page for oracle live-mode goldens.</p>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n <aside class=\"side-note\">\n <h2 class=\"note-title\">Aside</h2>\n <p>Nested content.</p>\n </aside>\n </main>\n </body>\n</html>\n"
}
}
@@ -6,7 +6,7 @@
"files": {
".impeccable/live/accept-receipts/ab12cd34.json": "{\n \"id\": \"ab12cd34\",\n \"operation\": \"discard\",\n \"variantId\": null,\n \"result\": {\n \"handled\": true,\n \"file\": \"src/App.jsx\",\n \"carbonize\": false\n },\n \"completedAt\": \"<ISO>\"\n}\n",
".impeccable/live/config.json": "{\n \"files\": [\"index.html\"],\n \"insertBefore\": \"</body>\",\n \"commentSyntax\": \"html\"\n}\n",
"src/App.jsx": "export default function App() {\n return (\n <main className=\"page\">\n\n <h1 className=\"hero-title\">Vite Fixture</h1>\n\n <p className=\"hero-hook\">Minimal React tree for oracle live-mode goldens.</p>\n <section id=\"features\" className=\"feature-grid\">\n <article className=\"feature-card\">One</article>\n <article className=\"feature-card\">Two</article>\n </section>\n <ul className=\"item-list\">\n {items.map((item) => (\n <li key={item.id} className=\"item-row\">{item.title}</li>\n ))}\n </ul>\n </main>\n );\n}\n\nconst items = [\n { id: 1, title: 'First' },\n { id: 2, title: 'Second' },\n];\n",
"src/App.jsx": "export default function App() {\n return (\n <main className=\"page\">\n <h1 className=\"hero-title\">Vite Fixture</h1>\n <p className=\"hero-hook\">Minimal React tree for oracle live-mode goldens.</p>\n <section id=\"features\" className=\"feature-grid\">\n <article className=\"feature-card\">One</article>\n <article className=\"feature-card\">Two</article>\n </section>\n <ul className=\"item-list\">\n {items.map((item) => (\n <li key={item.id} className=\"item-row\">{item.title}</li>\n ))}\n </ul>\n </main>\n );\n}\n\nconst items = [\n { id: 1, title: 'First' },\n { id: 2, title: 'Second' },\n];\n",
"src/main.jsx": "import { createRoot } from 'react-dom/client';\nimport App from './App.jsx';\n\ncreateRoot(document.getElementById('root')).render(<App />);\n",
"src/Panel.tsx": "type PanelProps = { title: string; children?: React.ReactNode };\n\nexport function Panel({ title, children }: PanelProps) {\n return (\n <section className=\"panel\">\n <header className=\"panel-header\">\n <h2 className=\"panel-title\">{title}</h2>\n </header>\n <div className=\"panel-body\">{children}</div>\n </section>\n );\n}\n"
}
@@ -17,6 +17,6 @@
".impeccable/live/accept-receipts/ab12cd34.json": "{\n \"id\": \"ab12cd34\",\n \"operation\": \"discard\",\n \"variantId\": null,\n \"result\": {\n \"handled\": true,\n \"file\": \"src/routes/+page.svelte\",\n \"carbonize\": false\n },\n \"completedAt\": \"<ISO>\"\n}\n",
".impeccable/live/config.json": "{\n \"files\": [\"src/app.html\"],\n \"insertBefore\": \"</body>\",\n \"commentSyntax\": \"html\"\n}\n",
"src/app.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>SvelteKit Fixture</title>\n %sveltekit.head%\n </head>\n <body data-sveltekit-preload-data=\"hover\">\n <div style=\"display: contents\">%sveltekit.body%</div>\n </body>\n</html>\n",
"src/routes/+page.svelte": "<script>\n let title = 'SvelteKit Fixture';\n let expenses = [\n { id: 1, label: 'Coffee', amount: 3 },\n { id: 2, label: 'Lunch', amount: 12 },\n ];\n</script>\n\n<main class=\"page\">\n\n <h1 class=\"hero-title\">{title}</h1>\n\n <p class=\"hero-hook\">Minimal SvelteKit route for oracle live-mode goldens.</p>\n <ul class=\"expense-list\">\n {#each expenses as expense (expense.id)}\n <li class=\"expense-row\">{expense.label}: {expense.amount}</li>\n {/each}\n </ul>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n</main>\n\n<style>\n .hero-title { font-size: 2rem; }\n .expense-list { list-style: none; padding: 0; }\n .expense-row { padding: 4px 0; }\n</style>\n"
"src/routes/+page.svelte": "<script>\n let title = 'SvelteKit Fixture';\n let expenses = [\n { id: 1, label: 'Coffee', amount: 3 },\n { id: 2, label: 'Lunch', amount: 12 },\n ];\n</script>\n\n<main class=\"page\">\n <h1 class=\"hero-title\">{title}</h1>\n <p class=\"hero-hook\">Minimal SvelteKit route for oracle live-mode goldens.</p>\n <ul class=\"expense-list\">\n {#each expenses as expense (expense.id)}\n <li class=\"expense-row\">{expense.label}: {expense.amount}</li>\n {/each}\n </ul>\n <section id=\"features\" class=\"feature-grid\">\n <article class=\"feature-card\">One</article>\n <article class=\"feature-card\">Two</article>\n </section>\n</main>\n\n<style>\n .hero-title { font-size: 2rem; }\n .expense-list { list-style: none; padding: 0; }\n .expense-row { padding: 4px 0; }\n</style>\n"
}
}