mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-27 00:02:21 +01:00
Merge branch 'sync/pr-6741-dev-20261026' into review/7011-with-dev-20261026
This commit is contained in:
@@ -32,10 +32,10 @@ export const ACTION_COMMANDS = Object.freeze([
|
||||
|
||||
const STORAGE_KEY = 'ge-actions';
|
||||
|
||||
function el(tag, className, html) {
|
||||
function el(tag, className, text) {
|
||||
const node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (html != null) node.innerHTML = html;
|
||||
if (text != null) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,11 @@ export function createActionsPanel({ showToast = () => {}, getLayers = null, sel
|
||||
}
|
||||
steps.forEach((step, index) => {
|
||||
const command = commandById(step);
|
||||
const row = el('div', 'ge-actions-step', `<span class="ge-actions-step-index">${index + 1}</span><span class="ge-actions-step-label">${command ? command[1] : step}</span>`);
|
||||
const row = el('div', 'ge-actions-step');
|
||||
row.append(
|
||||
el('span', 'ge-actions-step-index', String(index + 1)),
|
||||
el('span', 'ge-actions-step-label', command ? command[1] : step),
|
||||
);
|
||||
const remove = el('button', 'ge-action-remove', '✕');
|
||||
remove.type = 'button';
|
||||
remove.addEventListener('click', () => { steps.splice(index, 1); renderList(); });
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_action_names_and_unknown_steps_are_literal_text():
|
||||
script = r'''
|
||||
const fs = require('fs');
|
||||
const { chromium } = require('playwright');
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.route('http://actions.test/**', route => route.fulfill({
|
||||
status: 200, contentType: 'text/html', body: '<html><body></body></html>',
|
||||
}));
|
||||
await page.goto('http://actions.test/');
|
||||
const source = fs.readFileSync(process.argv[1], 'utf8');
|
||||
const moduleUrl = 'data:text/javascript;base64,' + Buffer.from(source).toString('base64');
|
||||
const result = await page.evaluate(async url => {
|
||||
const attack = '<img src=x onerror="window.__actionsXss=true">';
|
||||
localStorage.setItem('ge-actions', JSON.stringify({ [attack]: ['flip-h'] }));
|
||||
const { createActionsPanel } = await import(url);
|
||||
const panel = createActionsPanel();
|
||||
document.body.append(panel.element);
|
||||
panel.setSteps([attack]);
|
||||
return {
|
||||
images: panel.element.querySelectorAll('img').length,
|
||||
saved: panel.element.querySelector('.ge-actions-saved-row button').textContent,
|
||||
step: panel.element.querySelector('.ge-actions-step-label').textContent,
|
||||
executed: Boolean(window.__actionsXss),
|
||||
attack,
|
||||
};
|
||||
}, moduleUrl);
|
||||
console.log(JSON.stringify(result));
|
||||
} finally { await browser.close(); }
|
||||
})().catch(error => { console.error(error); process.exit(1); });
|
||||
'''
|
||||
result = subprocess.run(
|
||||
['node', '-e', script, str(ROOT / 'static/js/editor/actions-panel.js')],
|
||||
cwd=ROOT, capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
observed = json.loads(result.stdout)
|
||||
assert observed['images'] == 0
|
||||
assert observed['saved'] == observed['step'] == observed['attack']
|
||||
assert not observed['executed']
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Source checks for the gallery Settings-tab image-model picker.
|
||||
|
||||
The Quick Edit model picker lives in the editor; HARNESS mirrors it into the
|
||||
gallery Settings tab, fed by GET /api/editor/image-models and sharing the
|
||||
same localStorage key (`ge-ai-command-model`) so the two stay in sync.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
GALLERY_JS = (ROOT / "static/js/gallery.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_settings_tab_has_image_model_picker():
|
||||
assert 'id="gallery-image-model"' in GALLERY_JS
|
||||
assert "/api/editor/image-models" in GALLERY_JS
|
||||
assert "ge-ai-command-model" in GALLERY_JS
|
||||
|
||||
|
||||
def test_image_model_picker_lives_in_the_settings_container():
|
||||
settings_idx = GALLERY_JS.index('id="gallery-settings-container"')
|
||||
picker_idx = GALLERY_JS.index('id="gallery-image-model"')
|
||||
assert picker_idx > settings_idx
|
||||
|
||||
|
||||
def test_picker_persists_with_the_shared_localstorage_key():
|
||||
assert "localStorage.setItem(_GE_MODEL_KEY" in GALLERY_JS
|
||||
assert "localStorage.getItem(_GE_MODEL_KEY)" in GALLERY_JS
|
||||
|
||||
|
||||
def test_cloud_image_models_are_listed_alongside_the_local_ones():
|
||||
"""Gateway endpoints (OpenRouter) register as llm but carry image models.
|
||||
|
||||
The picker must surface those ids too — otherwise the only selectable
|
||||
edit model is the local Qwen server.
|
||||
"""
|
||||
routes = (ROOT / "routes/editor_agent_routes.py").read_text(encoding="utf-8")
|
||||
assert "from src.image_model_ids import looks_like_image_generation_model" in routes
|
||||
assert 'image_endpoint = getattr(ep, "model_type", None) == "image"' in routes
|
||||
assert "if not image_endpoint and not looks_like_image_generation_model(name):" in routes
|
||||
Reference in New Issue
Block a user