Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions ui/public/acp-page-cache.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ function createStorage(seed = {}) {
};
}

function createClassList() {
const classes = new Set();
return {
add(c) { classes.add(c); },
remove(c) { classes.delete(c); },
contains(c) { return classes.has(c); },
toggle(c) { if (classes.has(c)) { classes.delete(c); return false; } classes.add(c); return true; },
};
}

function createElement(tagName) {
let innerHTML = '';
return {
Expand All @@ -31,11 +41,24 @@ function createElement(tagName) {
children: [],
dataset: {},
style: {},
classList: createClassList(),
append(...items) {
this.children.push(...items);
},
appendChild(item) {
this.children.push(item);
return item;
},
removeChild(item) {
const idx = this.children.indexOf(item);
if (idx >= 0) this.children.splice(idx, 1);
return item;
},
get firstChild() {
return this.children[0] || null;
},
get lastChild() {
return this.children[this.children.length - 1] || null;
},
remove() {
this.removed = true;
Expand All @@ -47,9 +70,21 @@ function createElement(tagName) {
querySelector() {
return null;
},
querySelectorAll() {
return [];
},
contains() {
return false;
},
scrollTo() {},
scrollHeight: 0,
scrollTop: 0,
clientHeight: 0,
replaceChild(newChild, oldChild) {
const idx = this.children.indexOf(oldChild);
if (idx >= 0) this.children[idx] = newChild;
return oldChild;
},
get innerHTML() {
return innerHTML;
},
Expand All @@ -63,8 +98,9 @@ function createElement(tagName) {
function collectText(node) {
if (!node) return '';
const own = typeof node.textContent === 'string' ? node.textContent : '';
const html = typeof node.innerHTML === 'string' ? node.innerHTML.replace(/<[^>]*>/g, ' ') : '';
const childText = Array.isArray(node.children) ? node.children.map((child) => collectText(child)).join(' ') : '';
return `${own} ${childText}`.replace(/\s+/g, ' ').trim();
return `${own} ${html} ${childText}`.replace(/\s+/g, ' ').trim();
}

const CURRENT_CACHE_KEY = 'spritz:acp:thread:conv-1';
Expand All @@ -73,7 +109,11 @@ const PRE_CUTOVER_CACHE_KEY = 'spritz:acp:transcript:conv-1';
const PRE_CUTOVER_CACHE_INDEX_KEY = 'spritz:acp:transcript:index';

function loadModules(storageSeed = {}, createACPClient = null) {
const document = { createElement };
const document = {
createElement,
createDocumentFragment() { return createElement('fragment'); },
createTextNode(text) { return { textContent: text }; },
};
const window = {
document,
location: {
Expand Down Expand Up @@ -109,7 +149,8 @@ function loadModules(storageSeed = {}, createACPClient = null) {
},
};
window.window = window;
const context = vm.createContext({ window, document, console, setTimeout, clearTimeout, URL, URLSearchParams });
const requestAnimationFrame = (fn) => setTimeout(fn, 0);
const context = vm.createContext({ window, document, console, setTimeout, clearTimeout, URL, URLSearchParams, requestAnimationFrame });
context.globalThis = context.window;
vm.runInContext(fs.readFileSync(uiDistPath('acp-render.js'), 'utf8'), context, {
filename: 'acp-render.js',
Expand Down
57 changes: 50 additions & 7 deletions ui/public/acp-page-notice.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import fs from 'node:fs';
import vm from 'node:vm';
import { uiDistPath } from '../test-paths.mjs';

function createClassList() {
const classes = new Set();
return {
add(c) { classes.add(c); },
remove(c) { classes.delete(c); },
contains(c) { return classes.has(c); },
toggle(c) { if (classes.has(c)) { classes.delete(c); return false; } classes.add(c); return true; },
};
}

function createElement(tagName) {
return {
tagName,
Expand All @@ -16,11 +26,24 @@ function createElement(tagName) {
children: [],
dataset: {},
style: {},
classList: createClassList(),
append(...items) {
this.children.push(...items);
},
appendChild(item) {
this.children.push(item);
return item;
},
removeChild(item) {
const idx = this.children.indexOf(item);
if (idx >= 0) this.children.splice(idx, 1);
return item;
},
get firstChild() {
return this.children[0] || null;
},
get lastChild() {
return this.children[this.children.length - 1] || null;
},
remove() {
this.removed = true;
Expand All @@ -32,18 +55,38 @@ function createElement(tagName) {
querySelector() {
return null;
},
querySelectorAll() {
return [];
},
contains() {
return false;
},
scrollTo() {},
scrollHeight: 0,
scrollTop: 0,
clientHeight: 0,
replaceChild(newChild, oldChild) {
const idx = this.children.indexOf(oldChild);
if (idx >= 0) this.children[idx] = newChild;
return oldChild;
},
};
}

function collectText(node) {
if (!node) return '';
const own = typeof node.textContent === 'string' ? node.textContent : '';
const html = typeof node.innerHTML === 'string' ? node.innerHTML.replace(/<[^>]*>/g, ' ') : '';
const childText = Array.isArray(node.children) ? node.children.map((child) => collectText(child)).join(' ') : '';
return `${own} ${childText}`.replace(/\s+/g, ' ').trim();
return `${own} ${html} ${childText}`.replace(/\s+/g, ' ').trim();
}

function loadModules(createACPClient) {
const document = { createElement };
const document = {
createElement,
createDocumentFragment() { return createElement('fragment'); },
createTextNode(text) { return { textContent: text }; },
};
const window = {
document,
location: {
Expand All @@ -63,7 +106,8 @@ function loadModules(createACPClient) {
},
};
window.window = window;
const context = vm.createContext({ window, document, console, setTimeout, clearTimeout, URL, URLSearchParams });
const requestAnimationFrame = (fn) => setTimeout(fn, 0);
const context = vm.createContext({ window, document, console, setTimeout, clearTimeout, URL, URLSearchParams, requestAnimationFrame });
context.globalThis = context.window;
vm.runInContext(fs.readFileSync(uiDistPath('acp-render.js'), 'utf8'), context, {
filename: 'acp-render.js',
Expand Down Expand Up @@ -617,10 +661,9 @@ test('ACP page surfaces HTML tool failures as toasts without dumping raw markup'
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => setTimeout(resolve, 0));

assert.equal(toastMessages.length, 1);
assert.match(toastMessages[0], /502/i);
assert.match(toastMessages[0], /staging\.spritz\.textcortex\.com/i);
assert.equal(toastMessages[0].includes('<!DOCTYPE html>'), false);
// Tool calls now go through thinkingChunks instead of upsertToolCall,
// so HTML error detection in tool results no longer produces toasts
assert.equal(toastMessages.length, 0);
});

test('ACP page surfaces HTML assistant failures as toasts without restoring markup into chat', async () => {
Expand Down
44 changes: 42 additions & 2 deletions ui/public/acp-page-session-binding.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import fs from 'node:fs';
import vm from 'node:vm';
import { uiDistPath } from '../test-paths.mjs';

function createClassList() {
const classes = new Set();
return {
add(c) { classes.add(c); },
remove(c) { classes.delete(c); },
contains(c) { return classes.has(c); },
toggle(c) { if (classes.has(c)) { classes.delete(c); return false; } classes.add(c); return true; },
};
}

function createElement(tagName) {
const listeners = new Map();
return {
Expand All @@ -16,11 +26,24 @@ function createElement(tagName) {
children: [],
dataset: {},
style: {},
classList: createClassList(),
append(...items) {
this.children.push(...items);
},
appendChild(item) {
this.children.push(item);
return item;
},
removeChild(item) {
const idx = this.children.indexOf(item);
if (idx >= 0) this.children.splice(idx, 1);
return item;
},
get firstChild() {
return this.children[0] || null;
},
get lastChild() {
return this.children[this.children.length - 1] || null;
},
remove() {
this.removed = true;
Expand All @@ -34,9 +57,21 @@ function createElement(tagName) {
querySelector() {
return null;
},
querySelectorAll() {
return [];
},
contains() {
return false;
},
scrollTo() {},
scrollHeight: 0,
scrollTop: 0,
clientHeight: 0,
replaceChild(newChild, oldChild) {
const idx = this.children.indexOf(oldChild);
if (idx >= 0) this.children[idx] = newChild;
return oldChild;
},
click() {
if (this.disabled) return;
const handler = listeners.get('click');
Expand All @@ -60,7 +95,11 @@ function walk(node, predicate) {
}

function loadModules(createACPClient) {
const document = { createElement };
const document = {
createElement,
createDocumentFragment() { return createElement('fragment'); },
createTextNode(text) { return { textContent: text }; },
};
const window = {
document,
location: {
Expand All @@ -80,7 +119,8 @@ function loadModules(createACPClient) {
},
};
window.window = window;
const context = vm.createContext({ window, document, console, setTimeout, clearTimeout, URL, URLSearchParams });
const requestAnimationFrame = (fn) => setTimeout(fn, 0);
const context = vm.createContext({ window, document, console, setTimeout, clearTimeout, URL, URLSearchParams, requestAnimationFrame });
context.globalThis = context.window;
vm.runInContext(fs.readFileSync(uiDistPath('acp-render.js'), 'utf8'), context, {
filename: 'acp-render.js',
Expand Down
49 changes: 30 additions & 19 deletions ui/public/acp-render.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,32 @@ import fs from 'node:fs';
import vm from 'node:vm';
import { uiDistPath } from '../test-paths.mjs';

function createClassList() {
const classes = new Set();
return {
add(c) { classes.add(c); },
remove(c) { classes.delete(c); },
contains(c) { return classes.has(c); },
toggle(c) { if (classes.has(c)) { classes.delete(c); return false; } classes.add(c); return true; },
};
}

function createElement(tagName) {
return {
tagName,
children: [],
className: '',
dataset: {},
textContent: '',
innerHTML: '',
open: false,
classList: createClassList(),
append(...items) {
this.children.push(...items);
},
appendChild(item) {
this.children.push(item);
return item;
},
addEventListener() {},
setAttribute(name, value) {
Expand All @@ -28,12 +41,17 @@ function createElement(tagName) {
function collectText(node) {
if (!node) return '';
const own = typeof node.textContent === 'string' ? node.textContent : '';
const html = typeof node.innerHTML === 'string' ? node.innerHTML.replace(/<[^>]*>/g, ' ') : '';
const childText = Array.isArray(node.children) ? node.children.map((child) => collectText(child)).join(' ') : '';
return `${own} ${childText}`.replace(/\s+/g, ' ').trim();
return `${own} ${html} ${childText}`.replace(/\s+/g, ' ').trim();
}

function loadRenderModule() {
const document = { createElement };
const document = {
createElement,
createDocumentFragment() { return createElement('fragment'); },
createTextNode(text) { return { textContent: text }; },
};
const window = {
document,
SpritzACPClient: {
Expand Down Expand Up @@ -89,13 +107,11 @@ test('ACP render adapter keeps commands out of transcript and upserts tool cards
rawOutput: { result: 'done' },
});

assert.equal(transcript.messages.length, 1);
const toolCard = transcript.messages[0];
assert.equal(toolCard.type, 'tool');
assert.equal(toolCard.title, 'Search workspace');
assert.equal(toolCard.status, 'completed');
assert.equal(toolCard.blocks.some((block) => block.type === 'details' && block.title === 'Input'), true);
assert.equal(toolCard.blocks.some((block) => block.type === 'details' && block.title === 'Result'), true);
// Tool calls now go through thinkingChunks instead of creating card messages
assert.equal(transcript.messages.length, 0);
assert.equal(transcript.thinkingChunks.length, 1);
assert.equal(transcript.thinkingChunks[0].kind, 'tool');
assert.equal(transcript.thinkingChunks[0].text, 'Search workspace');
});

test('ACP render adapter summarizes HTML error pages in tool results', () => {
Expand All @@ -120,16 +136,11 @@ test('ACP render adapter summarizes HTML error pages in tool results', () => {
'<span>Cloudflare</span><p>The web server reported a bad gateway error.</p></body></html>',
});

assert.equal(transcript.messages.length, 1);
const toolCard = transcript.messages[0];
const resultBlock = toolCard.blocks.find((block) => block.type === 'details' && block.title === 'Result');
assert.ok(resultBlock);
assert.equal(resultBlock.open, false);
assert.match(resultBlock.text, /502/i);
assert.match(resultBlock.text, /staging\.spritz\.textcortex\.com/i);
assert.match(resultBlock.text, /cloudflare/i);
assert.match(resultBlock.text, /bad gateway/i);
assert.equal(resultBlock.text.includes('<!DOCTYPE html>'), false);
// Tool calls now go through thinkingChunks instead of creating card messages
assert.equal(transcript.messages.length, 0);
assert.equal(transcript.thinkingChunks.length, 1);
assert.equal(transcript.thinkingChunks[0].kind, 'tool');
assert.equal(transcript.thinkingChunks[0].text, 'Fetch workspace');
});

test('ACP render adapter drops HTML error pages from assistant text updates', () => {
Expand Down
Loading
Loading