Files
locally-uncensored/scripts/remote-all-tools-e2e.mjs
David 2910d06a13 tech(AS-10): zwei Zonen, die gruen aussahen, weil sie niemand geprueft hat
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
2026-09-01 09:14:24 -07:00

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) })