diff --git a/docs/test-results/web-console-all-phases-20261021.md b/docs/test-results/web-console-all-phases-20261021.md new file mode 100644 index 0000000..3affd49 --- /dev/null +++ b/docs/test-results/web-console-all-phases-20261021.md @@ -0,0 +1,19 @@ +# Kreisdiagramm über alle Phasen – 2026-10-21 + +Ausgangspunkt `58fc006`. Die tatsächliche Phasenzuordnung unterstützte bereits den +gesamten Ablauf, aber die erste Browservorschau enthielt ausschließlich synthetische +Untersuchungs-/Korrekturaufrufe. Die Vorschau wurde deshalb um Fallaufbau, Planung, +Hypothesen, Diagnose und Codingauftrag ergänzt. Die Legende zeigt diese Phasen nun +auch dann an, wenn noch kein Verbrauch gemessen wurde; Nullwerte erhalten keinen +erfundenen Flächenanteil. + +Der neue Regressionstest prüft die Zuordnung aller sechs LLM-Phasenkategorien sowie +die zusätzliche, disjunkte Korrekturkategorie. Ein gemeinsamer Planungsaufruf wird +weiterhin nicht doppelt gezählt. Die Browserprüfung verlangt jede Phasenkategorie +in der Legende und prüft weiterhin Projektfilter, Mobilansicht und Abmeldung. + +Die neue Vorschau enthält 35.300 synthetische Tokens und 22,1 % Korrekturanteil. Ein +abgeschlossener Testfall steuert auch Diagnose und Codingauftrag bei; der noch +laufende Testfall enthält keine erfundenen künftigen Abschlussaufrufe. Dies sind +ausschließlich Testdaten, keine tatsächlichen Providerkosten. Der vorherige +Prüfbericht bleibt als eigener Snapshot erhalten. diff --git a/docs/web-console.md b/docs/web-console.md index 8e490a3..18340fb 100644 --- a/docs/web-console.md +++ b/docs/web-console.md @@ -38,6 +38,9 @@ Telemetrie-Heartbeat wird nicht als aktiver Worker dargestellt. - Kreisdiagramm: bekannte Eingabe- und Ausgabetokens je Phase; zusätzliche Korrektur-/Reparatur-/Wiederholungsaufrufe haben ein eigenes orangefarbenes Segment. + Die Legende zeigt Fallaufbau, Fallplanung, Hypothesen, Untersuchung, Diagnose und + Codingauftrag auch bei null gemessenen Tokens. Nullwerte erzeugen keinen + künstlichen Tortenanteil. Gemeinsame Planungsaufrufe bleiben einmal gezählt. - Balkendiagramm: Eingabe und Ausgabe einschließlich Denken je Kategorie. - Der erste reguläre Aufruf bleibt bei seiner Phase, auch wenn sein Ergebnis später zurückgewiesen wurde. Seine Korrektur wird ausschließlich dem Korrektursegment diff --git a/src/viano_bug_analysis/web/dashboard.js b/src/viano_bug_analysis/web/dashboard.js index d5a044f..ce679f1 100644 --- a/src/viano_bug_analysis/web/dashboard.js +++ b/src/viano_bug_analysis/web/dashboard.js @@ -57,7 +57,8 @@ function renderPie(buckets) { const legend=$('legend'); legend.replaceChildren(); let angle=-Math.PI/2; if(!total) {ctx.beginPath();ctx.arc(180,180,140,0,Math.PI*2);ctx.fillStyle='#edf0f7';ctx.fill();} buckets.forEach((b,i)=>{ - const count=b.input_tokens+b.output_tokens; if(!count&&!b.calls)return; + const count=b.input_tokens+b.output_tokens; + if(b.id==='unknown'&&!count&&!b.calls)return; const color=colors[i%colors.length]; if(total&&count) {const end=angle+count/total*Math.PI*2; ctx.beginPath();ctx.moveTo(180,180);ctx.arc(180,180,140,angle,end);ctx.closePath();ctx.fillStyle=color;ctx.fill();angle=end;} const item=document.createElement('li'), swatch=text('span','','swatch');swatch.style.background=color; diff --git a/tests/dashboard-smoke.mjs b/tests/dashboard-smoke.mjs index 4413690..3b87694 100644 --- a/tests/dashboard-smoke.mjs +++ b/tests/dashboard-smoke.mjs @@ -30,15 +30,19 @@ try { await send('Page.navigate',{url:`http://127.0.0.1:${port}/`}); await until(()=>evaluate("document.readyState === 'complete' && typeof render === 'function'")); await evaluate("document.getElementById('token').value='ui_fixture_"+'x'.repeat(40)+"';document.getElementById('login-form').dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));"); - await until(()=>evaluate("document.getElementById('token-count').textContent==='37.800'")); + await until(()=>evaluate("document.getElementById('token-count').textContent==='35.300'")); const state=await evaluate("({workers:document.querySelectorAll('.worker').length,cases:document.querySelectorAll('#case-rows tr').length,active:document.getElementById('active-count').textContent,correction:document.getElementById('correction-count').textContent,overflow:document.documentElement.scrollWidth>innerWidth,stored:localStorage.length,progress:document.querySelector('.worker [role=progressbar]').getAttribute('aria-valuenow')})"); if(state.workers!==1||state.cases!==1||state.active!=='1'||state.stored!==0||state.overflow||state.progress!=='57')throw new Error(JSON.stringify(state)); + const categories=await evaluate("Array.from(document.querySelectorAll('#legend li')).map(e=>e.textContent)"); + for(const name of ['Fallaufbau','Fallplanung','Hypothesen','Untersuchung','Diagnose','Codingauftrag','Korrekturen']) { + if(!categories.some(c=>c.includes(name)))throw new Error(`Missing phase in pie legend: ${name}`); + } await writeFile(path.join(output,'desktop.png'),Buffer.from((await send('Page.captureScreenshot',{format:'png'})).data,'base64')); await send('Emulation.setDeviceMetricsOverride',{width:390,height:1200,deviceScaleFactor:1,mobile:true});await pause(200); if(await evaluate('document.documentElement.scrollWidth>innerWidth'))throw new Error('Mobile horizontal overflow'); await writeFile(path.join(output,'mobile.png'),Buffer.from((await send('Page.captureScreenshot',{format:'png'})).data,'base64')); await evaluate("document.getElementById('project-filter').value='viano-core';document.getElementById('project-filter').dispatchEvent(new Event('change')); "); - if(await evaluate("document.getElementById('token-count').textContent")!=='37.800')throw new Error('Project aggregation differs'); + if(await evaluate("document.getElementById('token-count').textContent")!=='35.300')throw new Error('Project aggregation differs'); await evaluate("document.getElementById('kind').value='case_return';document.getElementById('kind').dispatchEvent(new Event('change'));"); if(!await evaluate("document.getElementById('new-files').hidden&&!document.getElementById('return-files').hidden"))throw new Error('Return form not selected'); await evaluate("document.getElementById('logout').click()"); diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index dc7a219..3761277 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -9,7 +9,7 @@ from starlette.testclient import TestClient from viano_bug_analysis.api import APIConfig, Client, Project from viano_bug_analysis.api.app import create_app -from viano_bug_analysis.api.dashboard import aggregate, case_view +from viano_bug_analysis.api.dashboard import aggregate, case_view, phase from viano_bug_analysis.llm.accounting import classification from viano_bug_analysis.lifecycle.storage import atomic_json from test_case_builder import case_setup @@ -75,6 +75,30 @@ def test_unknown_usage_is_not_reported_as_complete(): assert next(b for b in result['buckets'] if b['id']=='unknown')['output_tokens']==5 +def test_pie_accounts_for_whole_pipeline_and_separates_corrections(): + stages = [ + ('BUILDING_CASE', 'entity_resolution_plan_v4', 'building'), + ('INTERPRETING_CASE', 'case_plan_v2', 'planning'), + ('GENERATING_HYPOTHESES', 'hypotheses_v4', 'hypotheses'), + ('INVESTIGATING_HYPOTHESES', 'investigation_lean_v21', 'investigation'), + ('BUILDING_DIAGNOSIS', 'diagnosis_v8', 'diagnosis'), + ('BUILDING_DIAGNOSIS', 'change_request_v9', 'coding'), + ] + calls = [] + for state, prompt, expected in stages: + assert phase({'phase': state}, prompt) == expected + calls.append({'bucket': expected, 'category':'regular', 'phase':expected, + 'day':'2026-10-21', 'input_tokens':10, 'output_tokens':2, + 'reasoning_tokens':1, 'cached_input_tokens':0}) + calls.append(calls[3] | {'bucket':'correction','category':'correction'}) + buckets = {b['id']:b for b in aggregate([{'calls':calls}])['buckets']} + assert all(buckets[expected]['calls']==1 for _, _, expected in stages) + assert buckets['correction']['calls']==1 + assert sum(b['input_tokens']+b['output_tokens'] for b in buckets.values())==84 + empty = {b['id']:b for b in aggregate([])['buckets']} + assert all(expected in empty for _, _, expected in stages) + + @pytest.fixture(autouse=True) def project_manifest(manifest): manifest['project']={'id':'p','type':'wow','profile':'vcore'} diff --git a/tests/ui_dashboard_server.py b/tests/ui_dashboard_server.py index 8702821..0d3acf6 100644 --- a/tests/ui_dashboard_server.py +++ b/tests/ui_dashboard_server.py @@ -37,14 +37,26 @@ def seed(root, case_id, title, completed=False): ref=pointer+'.json';store.advance(*identity,state,step_id=state, artifacts={pointer:ref},additions={ref:json_bytes({'analysis_revision':1})}) store.advance(*identity,'INVESTIGATING_HYPOTHESES',step_id='investigate') - for i,(category,inp,out) in enumerate([('regular',12000,3000),('correction',3000,900)]): + measured = [ + ('BUILDING_CASE','entity_resolution_plan_v4','regular',2500,500), + ('INTERPRETING_CASE','case_interpretation_v1','regular',2000,500), + ('GENERATING_HYPOTHESES','hypotheses_v4','regular',1500,500), + ('INVESTIGATING_HYPOTHESES','investigation_lean_v21','regular',4000,1000), + ] + if completed: + measured += [ + ('BUILDING_DIAGNOSIS','diagnosis_v8','regular',1500,500), + ('BUILDING_DIAGNOSIS','change_request_v9','regular',500,0), + ] + measured.append(('INVESTIGATING_HYPOTHESES','investigation_format_repair_v1','correction',3000,900)) + for i,(phase,prompt,category,inp,out) in enumerate(measured): ref=f'call{i}.json';request=f'request{i}.json' - call={'rounds':1,'phase':'INVESTIGATING_HYPOTHESES','request_ref':request,'outcome':{ + call={'rounds':1,'phase':phase,'request_ref':request,'outcome':{ 'usage_classification':category,'metadata':{'input_tokens':inp,'output_tokens':out, 'reasoning_tokens':out//2,'cached_input_tokens':1000}}} store.advance(*identity,'INVESTIGATING_HYPOTHESES',step_id=f'call{i}', details={'operation':'llm-round','result_ref':ref}, - additions={ref:json_bytes(call),request:json_bytes({'prompt_version':'investigation_lean_v21'})}) + additions={ref:json_bytes(call),request:json_bytes({'prompt_version':prompt})}) if completed: store.advance(*identity,'BUILDING_DIAGNOSIS',step_id='diagnosis') store.advance(*identity,'OUTPUT_READY',step_id='done')