Lint-Schulden ausserhalb von src/components — und der Auftrag hat zwei
blinde Flecken freigelegt, die groesser sind als die Schulden selbst.
`scripts/**` hatte NULL Regeln. Der TypeScript-Block greift *.ts/tsx, der
mobile-client-Block greift mobile-client/**/*.js, und .mjs faellt zwischen
beide. `npx eslint scripts` listete zehn Dateien und meldete nichts — ein
gruener Lauf ueber eine ungepruefte Zone, was schlimmer ist als ein roter:
er behauptet Deckung. Mit Regeln kamen 42 Meldungen zum Vorschein, davon
11 `no-undef` — in einem Node-Modul kein Stil, sondern ein ReferenceError
zur Laufzeit. Alle 42 bezahlt.
`e2e/` stand in KEINEM tsconfig-Projekt. 17 Playwright-Dateien plus
playwright.config.ts hat `npm run typecheck` nie gelesen. Neues
tsconfig.e2e.json, referenziert aus tsconfig.json: neu sichtbare Fehler 0
bei 18 zusaetzlich geprueften Dateien. Die Zone war nicht kaputt, sie war
unbeobachtet.
Ein echter Fehler im ganzen Haufen, und er stand hinter der Konvention:
`builtin-tools.ts:506` faengt `fallbackErr`, wirft ihn weg und meldet `e`,
den Fehler des ERSTEN Weges. Im Browser-/Dev-Modus ist `web_fetch` dort
grundsaetzlich unerreichbar, `e` also auf jedem Fehlschlag derselbe Satz —
DNS, CORS, 404, Timeout sehen fuer das Modell identisch aus. Dreissig
Zeilen hoeher schreibt dieselbe Datei die Regel selbst hin. Mit Test.
Die "10 Parse-Fehler" waren keine: es sind tote eslint-disable-Direktiven,
die eslints JSON mit `ruleId: null` kodiert wie einen Parse-Fehler. Sechs
davon unterdruecken Regeln, die in keiner Config je eingeschaltet waren —
jemand glaubte an ein Gate, das nie existierte. Jetzt `error` statt `warn`.
Regel-Lockerungen, jede an BEIDEN Seiten der Grenze gemessen (der Test
laesst die echte eslint.config.js ueber die Node-API laufen, kein Nachbau):
`no-fallthrough` allowEmptyCase (ein leeres case-Label kann nirgends
hineinfallen — der Kommentar kostet den Fehler, nicht der Code),
`no-unused-vars` mit `^_` (von 43 Meldungen ueberlebt genau EINE, und die
ist der echte Fehler oben), `react-refresh/only-export-components` aus
(23 der 29 Exporte werden von Tests importiert — das ist die Naht, an der
dieses Projekt Logik aus Komponenten zieht; die Alternative waere eine
Namensliste, also genau der zweite Pflegeweg, der hier der Grundfehler ist).
eslint . : 161 Fehler + 10 Warnungen -> 60 Fehler + 0 Warnungen.
Offen fuer das scharfe Gate: 45 in src/components (zweiter Durchgang),
15 in vite.config.ts (ZB-7).
Geprueft im isolierten Worktree auf 70489a23 (nur diese 33 Dateien, und
node_modules diesmal KOPIERT statt verlinkt — Lehre aus dem Zwischenfall):
tsc -b --force 0 (jetzt inkl. e2e) · vite build 0 · madge 0 Zyklen ·
vitest 7851 gruen / 0 rot (= Basis 7809 + 42 aus dem Token-Paket) ·
eslint 60/0, Verteilung nachgezaehlt und deckungsgleich.
Zwei eigene Sonden auf die Kernbehauptung, dass die Zonen jetzt echt
gedeckt sind:
undefinierter Bezeichner in scripts/ => `no-undef` schlaegt an
frische tote Unterdrueckung in src/lib => als FEHLER gemeldet, nicht als
Warnung durchgewinkt
240 lines
11 KiB
JavaScript
240 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* All-tools-in-one-run E2E. Mimics what an agent does over the course of
|
|
* one multi-step task — exercising every Remote tool sequentially in a
|
|
* single authenticated session, the same way `runToolLoop` would dispatch
|
|
* them. Proves that nothing leaks paths into ~/agent-workspace/__remote__/
|
|
* and that the tool chain works end-to-end with the user-picked workspace.
|
|
*
|
|
* Task being simulated:
|
|
* "Build a tiny static site (index.html, style.css), grep for a tag,
|
|
* list the tree, run a shell + python diagnostic, capture system info,
|
|
* and write a final summary file."
|
|
*
|
|
* Usage:
|
|
* node scripts/remote-all-tools-e2e.mjs --base http://<ip>:<port> --code <6-digit>
|
|
*/
|
|
|
|
import { argv, exit } from 'node:process'
|
|
// Siehe scripts/app-identity.mjs — der Sandkasten-Name trägt auf diesem Branch
|
|
// einen Suffix, ein festes Literal würde hier nichts mehr fangen.
|
|
import { REMOTE_FALLBACK_DIR } from './app-identity.mjs'
|
|
|
|
function parseArgs() {
|
|
const out = { base: null, code: null, chatId: 'all-tools-e2e' }
|
|
for (let i = 2; i < argv.length; i++) {
|
|
const a = argv[i]
|
|
if (a === '--base') out.base = argv[++i]
|
|
else if (a === '--code') out.code = argv[++i]
|
|
else if (a === '--chat-id') out.chatId = argv[++i]
|
|
}
|
|
if (!out.base || !out.code) { console.error('Usage: --base <url> --code <6-digit>'); exit(2) }
|
|
return out
|
|
}
|
|
|
|
async function authenticate(base, code) {
|
|
const r = await fetch(`${base}/remote-api/auth`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ passcode: code }),
|
|
})
|
|
if (!r.ok) throw new Error(`auth ${r.status}: ${await r.text()}`)
|
|
const data = await r.json()
|
|
return data.token
|
|
}
|
|
|
|
async function tool(base, token, chatId, name, args) {
|
|
const r = await fetch(`${base}/remote-api/agent-tool`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
|
body: JSON.stringify({ tool: name, args, chatId }),
|
|
})
|
|
return await r.json()
|
|
}
|
|
|
|
let pass = 0, fail = 0
|
|
function check(label, ok, detail) {
|
|
if (ok) { console.log(` ✓ ${label}`); pass++ }
|
|
else { console.error(` ✗ ${label}\n ${detail}`); fail++ }
|
|
}
|
|
|
|
const NOT_REMOTE = (s) => !String(s || '').replaceAll('\\', '/').includes(REMOTE_FALLBACK_DIR)
|
|
|
|
async function step(label, fn) {
|
|
console.log(`\n━━ ${label} ━━`)
|
|
return await fn()
|
|
}
|
|
|
|
async function main() {
|
|
const { base, code, chatId } = parseArgs()
|
|
const token = await authenticate(base, code)
|
|
console.log(`[e2e] auth ok, chatId=${chatId}`)
|
|
|
|
const startedAt = Date.now()
|
|
const toolsHit = new Set()
|
|
const T = (n) => { toolsHit.add(n); return n }
|
|
|
|
// ── 1. Time check (no permissions) ──
|
|
await step('1. get_current_time — kick off the run', async () => {
|
|
const t = await tool(base, token, chatId, T('get_current_time'), {})
|
|
check('time tool returned', !!t && (t.iso || t.iso_local || t.unix), JSON.stringify(t))
|
|
})
|
|
|
|
// ── 2. System info ──
|
|
await step('2. system_info — capture host facts', async () => {
|
|
const s = await tool(base, token, chatId, T('system_info'), {})
|
|
check('system_info returned object', !!s && typeof s === 'object', JSON.stringify(s))
|
|
check('system_info had at least one of os/platform/cpu/osName',
|
|
s && (s.os || s.platform || s.cpu || s.osName), JSON.stringify(s))
|
|
})
|
|
|
|
// ── 3. file_write — index.html ──
|
|
// (Hier stand `let writePath`, ausserhalb des Schritts gehoben, damit ein
|
|
// spaeterer Schritt den Pfad lesen kann. Den Schritt gibt es nicht; die
|
|
// Zusicherung ueber den Pfad steht schon in Schritt 3 selbst.)
|
|
await step('3. file_write index.html — start the site build', async () => {
|
|
const w = await tool(base, token, chatId, T('file_write'), {
|
|
path: 'site/index.html',
|
|
content: '<!doctype html><html><head><title>LU E2E</title>' +
|
|
'<link rel="stylesheet" href="style.css"></head>' +
|
|
'<body><h1 id="hero">Hello LU E2E</h1></body></html>\n',
|
|
})
|
|
check('file_write status=saved', w?.status === 'saved', JSON.stringify(w))
|
|
check('file_write path NOT in __remote__', NOT_REMOTE(w?.path), w?.path)
|
|
})
|
|
|
|
// ── 4. file_write — style.css ──
|
|
await step('4. file_write style.css — second artefact', async () => {
|
|
const w = await tool(base, token, chatId, T('file_write'), {
|
|
path: 'site/style.css',
|
|
content: 'body{font-family:sans-serif;background:#111;color:#eee}\n#hero{font-size:32px}\n',
|
|
})
|
|
check('css saved', w?.status === 'saved', JSON.stringify(w))
|
|
check('css path NOT in __remote__', NOT_REMOTE(w?.path), w?.path)
|
|
})
|
|
|
|
// ── 5. file_list — confirm both files exist ──
|
|
await step('5. file_list site/ — verify the layout', async () => {
|
|
const l = await tool(base, token, chatId, T('file_list'), { path: 'site' })
|
|
const names = (l?.entries || []).map(e => String(e.name || ''))
|
|
check('file_list returned array', Array.isArray(l?.entries), JSON.stringify(l))
|
|
check('found index.html', names.some(n => /index\.html$/.test(n)), names.join('|'))
|
|
check('found style.css', names.some(n => /style\.css$/.test(n)), names.join('|'))
|
|
})
|
|
|
|
// ── 6. file_list recursive — full tree ──
|
|
await step('6. file_list recursive — full tree', async () => {
|
|
const l = await tool(base, token, chatId, T('file_list'), { path: '.', recursive: true })
|
|
check('recursive list returned entries',
|
|
Array.isArray(l?.entries) && l.entries.length >= 2, JSON.stringify(l).slice(0,200))
|
|
})
|
|
|
|
// ── 7. file_read — read the index back ──
|
|
await step('7. file_read index.html — round-trip content', async () => {
|
|
const r = await tool(base, token, chatId, T('file_read'), { path: 'site/index.html' })
|
|
check('file_read returned content', typeof r?.content === 'string', JSON.stringify(r))
|
|
check('file_read content includes "<h1 id=\\"hero\\">"',
|
|
typeof r?.content === 'string' && r.content.includes('<h1 id="hero">'),
|
|
r?.content?.slice?.(0, 200))
|
|
})
|
|
|
|
// ── 8. file_search — grep for the hero id ──
|
|
await step('8. file_search "hero" — grep regex', async () => {
|
|
const s = await tool(base, token, chatId, T('file_search'), {
|
|
path: 'site', query: 'hero',
|
|
})
|
|
check('file_search returned results array', Array.isArray(s?.results), JSON.stringify(s))
|
|
const hit = (s?.results || []).find(r => /index\.html$/.test(r.file || ''))
|
|
check('grep hit index.html', !!hit, JSON.stringify(s?.results))
|
|
check('hit file path NOT in __remote__',
|
|
hit ? NOT_REMOTE(hit.file) : false, JSON.stringify(hit))
|
|
})
|
|
|
|
// ── 9. shell_execute — list files via a real shell command ──
|
|
await step('9. shell_execute — sanity diagnostic', async () => {
|
|
const cmd = process.platform === 'win32'
|
|
? 'Get-ChildItem -Recurse | Select-Object -First 10 | ForEach-Object { $_.FullName }'
|
|
: 'find . -maxdepth 2 | head -20'
|
|
const sh = await tool(base, token, chatId, T('shell_execute'), { command: cmd })
|
|
check('shell_execute had stdout', typeof sh?.stdout === 'string', JSON.stringify(sh))
|
|
check('shell_execute exitCode 0', sh?.exitCode === 0, JSON.stringify(sh))
|
|
check('shell saw site/index.html in cwd',
|
|
typeof sh?.stdout === 'string' && /site/i.test(sh.stdout),
|
|
sh?.stdout?.slice?.(0, 300))
|
|
check('shell stdout NOT mentioning __remote__',
|
|
typeof sh?.stdout === 'string' && NOT_REMOTE(sh.stdout),
|
|
sh?.stdout?.slice?.(0, 300))
|
|
})
|
|
|
|
// ── 10. code_execute — Python writes a JSON manifest ──
|
|
await step('10. code_execute Python — emit manifest.json', async () => {
|
|
const code = `
|
|
import os, json
|
|
files = []
|
|
for root, dirs, names in os.walk('.'):
|
|
for n in names:
|
|
p = os.path.join(root, n)
|
|
files.append({'path': p, 'size': os.path.getsize(p)})
|
|
with open('manifest.json','w',encoding='utf-8') as f:
|
|
json.dump({'cwd': os.getcwd(), 'files': files}, f, indent=2)
|
|
print('manifest written, file count =', len(files))
|
|
`.trim()
|
|
const py = await tool(base, token, chatId, T('code_execute'), { code, timeout: 30000 })
|
|
check('python exitCode 0', py?.exitCode === 0, JSON.stringify(py))
|
|
check('python printed file count',
|
|
typeof py?.stdout === 'string' && /file count = \d+/.test(py.stdout),
|
|
py?.stdout)
|
|
})
|
|
|
|
// ── 11. file_read manifest.json — round trip ──
|
|
await step('11. file_read manifest.json — verify python output', async () => {
|
|
const r = await tool(base, token, chatId, T('file_read'), { path: 'manifest.json' })
|
|
check('manifest read', typeof r?.content === 'string', JSON.stringify(r))
|
|
let parsed = null
|
|
try { parsed = JSON.parse(r.content) } catch { /* die naechste Zeile prueft genau das */ }
|
|
check('manifest is valid JSON', !!parsed, r?.content?.slice?.(0, 200))
|
|
check('manifest cwd is the override folder',
|
|
parsed && typeof parsed.cwd === 'string' && NOT_REMOTE(parsed.cwd),
|
|
parsed?.cwd)
|
|
check('manifest lists ≥ 3 files (index, style, manifest)',
|
|
parsed && Array.isArray(parsed.files) && parsed.files.length >= 3,
|
|
JSON.stringify(parsed?.files?.length))
|
|
})
|
|
|
|
// ── 12. shell_execute with explicit relative cwd ──
|
|
await step('12. shell_execute cwd=site — relative cwd resolves', async () => {
|
|
const cmd = process.platform === 'win32' ? '$pwd.Path' : 'pwd'
|
|
const sh = await tool(base, token, chatId, T('shell_execute'), { command: cmd, cwd: 'site' })
|
|
check('shell saw site as cwd',
|
|
typeof sh?.stdout === 'string' && /site/i.test(sh.stdout) && NOT_REMOTE(sh.stdout),
|
|
sh?.stdout?.slice?.(0, 300))
|
|
})
|
|
|
|
// ── 13. file_write — final summary ──
|
|
await step('13. file_write summary.md — wrap-up artefact', async () => {
|
|
const w = await tool(base, token, chatId, T('file_write'), {
|
|
path: 'summary.md',
|
|
content: `# LU Remote Multi-Tool E2E\n\nRun completed at ${new Date().toISOString()}.\nTools exercised: ${[...toolsHit].join(', ')}\n`,
|
|
})
|
|
check('summary saved', w?.status === 'saved', JSON.stringify(w))
|
|
})
|
|
|
|
// ── 14. image_generate — graceful unsupported ──
|
|
await step('14. image_generate — graceful "desktop only" error', async () => {
|
|
const i = await tool(base, token, chatId, T('image_generate'), { prompt: 'cat' })
|
|
check('image_generate returned graceful error',
|
|
typeof i?.error === 'string' && /desktop|create tab/i.test(i.error),
|
|
JSON.stringify(i))
|
|
})
|
|
|
|
// ── Summary ──
|
|
const elapsed = Date.now() - startedAt
|
|
console.log(`\n━━ Run summary ━━`)
|
|
console.log(`Tools exercised in this single run: ${[...toolsHit].sort().join(', ')}`)
|
|
console.log(`Total duration: ${elapsed} ms`)
|
|
console.log(`\n${pass} passed, ${fail} failed.`)
|
|
exit(fail === 0 ? 0 : 1)
|
|
}
|
|
|
|
main().catch(e => { console.error('[e2e] fatal:', e); exit(2) })
|