import json import subprocess from pathlib import Path from src.frontend_bundle import build_frontend VOICE_CAPTURE = Path(__file__).parents[1] / "frontend" / "voice-issue-capture.js" VOICE_STORE = Path(__file__).parents[1] / "frontend" / "voice-transcript-store.js" FRONTEND = VOICE_CAPTURE.parent def run_node(script: str): completed = subprocess.run( ["node", "-e", script], capture_output=True, text=True ) assert completed.returncode == 0, completed.stderr return json.loads(completed.stdout) def test_transcript_maps_first_sentence_to_title_and_remainder_to_note_with_field_limits(): script = f""" const {{mapVoiceTranscript}} = require({json.dumps(str(VOICE_CAPTURE))}); const mapped = mapVoiceTranscript(' Lift outage on level four. Elevator B is trapped. '); const bounded = mapVoiceTranscript('x'.repeat(300) + '. ' + 'y'.repeat(10100)); process.stdout.write(JSON.stringify({{mapped, titleLength:bounded.title.length, bodyLength:bounded.body.length}})); """ assert run_node(script) == { "mapped": { "title": "Lift outage on level four.", "body": "Elevator B is trapped.", }, "titleLength": 255, "bodyLength": 10000, } def test_explicit_tap_starts_recognition_and_final_transcript_waits_for_review(): script = f""" const {{createVoiceIssueCapture}} = require({json.dumps(str(VOICE_CAPTURE))}); class Element {{ constructor() {{ this.hidden=false; this.value=''; this.textContent=''; this.listeners={{}}; }} addEventListener(name, callback) {{ this.listeners[name]=callback; }} click() {{ this.listeners.click?.({{currentTarget:this}}); }} dispatchEvent() {{}} }} let instance; class Recognition {{ constructor() {{ instance=this; this.started=0; }} start() {{ this.started += 1; }} stop() {{}} abort() {{}} }} const elements = Object.fromEntries( ['root','start','stop','review','transcript','append','replace','status','title','body'].map(key=>[key,new Element()]) ); elements.review.hidden=true; const controller = createVoiceIssueCapture({{Recognition,elements}}); const before = {{started:Boolean(instance), rootHidden:elements.root.hidden}}; elements.start.click(); instance.onresult({{results:[Object.assign([{{transcript:'Lift outage. Elevator B is trapped.'}}], {{isFinal:true}})]}}); process.stdout.write(JSON.stringify({{ supported:controller.supported, before, started:instance.started, listening:elements.status.textContent, transcript:elements.transcript.value, reviewHidden:elements.review.hidden, title:elements.title.value, body:elements.body.value, appendLabel:elements.append.textContent, replaceHidden:elements.replace.hidden }})); """ assert run_node(script) == { "supported": True, "before": {"started": False, "rootHidden": False}, "started": 1, "listening": "Transcript ready. Review it before using it.", "transcript": "Lift outage. Elevator B is trapped.", "reviewHidden": False, "title": "", "body": "", "appendLabel": "Use transcript", "replaceHidden": True, } def test_final_transcript_is_checkpointed_for_the_confirmed_account(): script = f""" const {{createVoiceIssueCapture}} = require({json.dumps(str(VOICE_CAPTURE))}); class Element {{ constructor() {{ this.hidden=false; this.value=''; this.textContent=''; this.listeners={{}}; }} addEventListener(name, callback) {{ this.listeners[name]=callback; }} click() {{ this.listeners.click?.({{currentTarget:this}}); }} dispatchEvent() {{}} }} let recognition; class Recognition {{ constructor() {{ recognition=this; }} start() {{}} stop() {{}} abort() {{}} }} const elements=Object.fromEntries( ['root','start','stop','review','transcript','append','replace','discard','status','title','body'].map(key=>[key,new Element()]) ); elements.review.hidden=true; const saved=[]; const transcriptStore={{ async load() {{ return ''; }}, async save(login, transcript) {{ saved.push({{login, transcript}}); }}, async clear() {{}}, }}; (async () => {{ const controller=createVoiceIssueCapture({{ Recognition,elements,transcriptStore,getLogin:()=> 'timmy' }}); await controller.ready; elements.start.click(); recognition.onresult({{results:[Object.assign([{{transcript:'Lift outage.'}}], {{isFinal:true}})]}}); await Promise.resolve(); process.stdout.write(JSON.stringify({{saved, transcript:elements.transcript.value}})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ assert run_node(script) == { "saved": [{"login": "timmy", "transcript": "Lift outage."}], "transcript": "Lift outage.", } def test_pending_transcript_recovers_only_for_the_confirmed_account(): script = f""" const {{createVoiceIssueCapture}} = require({json.dumps(str(VOICE_CAPTURE))}); class Element {{ constructor() {{ this.hidden=false; this.value=''; this.textContent=''; this.listeners={{}}; }} addEventListener(name, callback) {{ this.listeners[name]=callback; }} dispatchEvent() {{}} }} class Recognition {{ start() {{}} stop() {{}} abort() {{}} }} const elements=Object.fromEntries( ['root','start','stop','review','transcript','append','replace','discard','status','title','body'].map(key=>[key,new Element()]) ); elements.review.hidden=true; const loads=[]; (async () => {{ const controller=createVoiceIssueCapture({{ Recognition,elements,getLogin:()=> 'timmy', transcriptStore:{{ async load(login) {{ loads.push(login); return 'Recovered lift outage.'; }}, async save() {{}}, async clear() {{}}, }}, }}); await controller.ready; process.stdout.write(JSON.stringify({{ loads, transcript:elements.transcript.value, reviewHidden:elements.review.hidden, status:elements.status.textContent }})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ assert run_node(script) == { "loads": ["timmy"], "transcript": "Recovered lift outage.", "reviewHidden": False, "status": "Recovered transcript. Review it before using or discarding it.", } def test_cumulative_recognition_events_append_each_new_final_segment_once(): script = f""" const {{createVoiceIssueCapture}} = require({json.dumps(str(VOICE_CAPTURE))}); class Element {{ constructor() {{ this.hidden=false; this.value=''; this.textContent=''; this.listeners={{}}; }} addEventListener(name, callback) {{ this.listeners[name]=callback; }} click() {{ this.listeners.click?.(); }} dispatchEvent() {{}} }} const result=(transcript,isFinal)=>Object.assign([{{transcript}}],{{isFinal}}); let recognition; class Recognition {{ constructor() {{ recognition=this; }} start() {{}} stop() {{}} abort() {{}} }} const elements=Object.fromEntries( ['root','start','stop','review','transcript','append','replace','discard','status','title','body'].map(key=>[key,new Element()]) ); const saved=[]; (async () => {{ const controller=createVoiceIssueCapture({{Recognition,elements,getLogin:()=> 'timmy',transcriptStore:{{ async load() {{ return ''; }}, async save(login,text) {{ saved.push(text); }}, async clear() {{}}, }}}}); await controller.ready; elements.start.click(); const first=result('Lift outage.',true); recognition.onresult({{resultIndex:0,results:[first]}}); recognition.onresult({{ resultIndex:1, results:[first,result('Elevator B',false),result('Elevator B is trapped.',true)], }}); await Promise.resolve(); process.stdout.write(JSON.stringify({{transcript:elements.transcript.value,saved}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ assert run_node(script) == { "transcript": "Lift outage. Elevator B is trapped.", "saved": ["Lift outage.", "Lift outage. Elevator B is trapped."], } def test_apply_and_discard_each_clear_the_consumed_checkpoint(): script = f""" const {{createVoiceIssueCapture}} = require({json.dumps(str(VOICE_CAPTURE))}); class Element {{ constructor() {{ this.hidden=false;this.value='';this.textContent='';this.listeners={{}}; }} addEventListener(name,callback) {{ this.listeners[name]=callback; }} click() {{ this.listeners.click?.(); }} dispatchEvent() {{}} }} class Recognition {{ start(){{}} stop(){{}} abort(){{}} }} async function setup() {{ const elements=Object.fromEntries( ['root','start','stop','review','transcript','append','replace','discard','status','title','body'].map(key=>[key,new Element()]) ); const cleared=[]; const controller=createVoiceIssueCapture({{Recognition,elements,getLogin:()=> 'timmy',transcriptStore:{{ async load() {{ return 'Recovered lift outage.'; }}, async save() {{}}, async clear(login) {{ cleared.push(login); }}, }}}}); await controller.ready; return {{elements,cleared}}; }} (async () => {{ const applied=await setup(); applied.elements.append.click(); await Promise.resolve(); const discarded=await setup(); discarded.elements.discard.click(); await Promise.resolve(); process.stdout.write(JSON.stringify({{ applied:{{cleared:applied.cleared,reviewHidden:applied.elements.review.hidden,title:applied.elements.title.value}}, discarded:{{cleared:discarded.cleared,reviewHidden:discarded.elements.review.hidden,transcript:discarded.elements.transcript.value,status:discarded.elements.status.textContent}} }})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ assert run_node(script) == { "applied": { "cleared": ["timmy"], "reviewHidden": True, "title": "Recovered lift outage.", }, "discarded": { "cleared": ["timmy"], "reviewHidden": True, "transcript": "", "status": "Recovered transcript discarded.", }, } def test_review_requires_explicit_append_or_replace_when_typed_content_exists(): script = f""" const {{createVoiceIssueCapture}} = require({json.dumps(str(VOICE_CAPTURE))}); class Element {{ constructor() {{ this.hidden=false; this.value=''; this.textContent=''; this.listeners={{}}; this.events=[]; }} addEventListener(name, callback) {{ this.listeners[name]=callback; }} click() {{ this.listeners.click?.({{currentTarget:this}}); }} dispatchEvent(event) {{ this.events.push(event.type); }} }} function setup() {{ let instance; class Recognition {{ constructor() {{ instance=this; }} start() {{}} stop() {{}} abort() {{}} }} const elements=Object.fromEntries( ['root','start','stop','review','transcript','append','replace','status','title','body'].map(key=>[key,new Element()]) ); elements.title.value='Existing title'; elements.body.value='Existing note'; elements.review.hidden=true; createVoiceIssueCapture({{Recognition,elements, createEvent:name=>({{type:name}})}}); elements.start.click(); instance.onresult({{results:[Object.assign([{{transcript:'Lift outage. Elevator B is trapped.'}}], {{isFinal:true}})]}}); return elements; }} const appended=setup(); appended.append.click(); const replaced=setup(); replaced.replace.click(); process.stdout.write(JSON.stringify({{ appended:{{title:appended.title.value,body:appended.body.value,events:[appended.title.events,appended.body.events]}}, replaced:{{title:replaced.title.value,body:replaced.body.value,events:[replaced.title.events,replaced.body.events]}} }})); """ assert run_node(script) == { "appended": { "title": "Existing title", "body": "Existing note\n\nLift outage.\n\nElevator B is trapped.", "events": [["input"], ["input"]], }, "replaced": { "title": "Lift outage.", "body": "Elevator B is trapped.", "events": [["input"], ["input"]], }, } def test_unsupported_browser_keeps_keyboard_flow_and_exposes_no_voice_control(): script = f""" const {{createVoiceIssueCapture}} = require({json.dumps(str(VOICE_CAPTURE))}); const element=()=>({{hidden:false,textContent:'',addEventListener(){{}}}}); const elements=Object.fromEntries( ['root','start','stop','review','transcript','append','replace','status','title','body'].map(key=>[key,element()]) ); const controller=createVoiceIssueCapture({{Recognition:null,elements}}); process.stdout.write(JSON.stringify({{ supported:controller.supported, hidden:elements.root.hidden, guidance:elements.status.textContent }})); """ assert run_node(script) == { "supported": False, "hidden": True, "guidance": "Voice capture is unavailable; type the title and note instead.", } def test_recognition_start_rejection_restores_controls_and_preserves_draft(): script = f""" const {{createVoiceIssueCapture}} = require({json.dumps(str(VOICE_CAPTURE))}); class Element {{ constructor() {{ this.hidden=false; this.value=''; this.textContent=''; this.listeners={{}}; }} addEventListener(name, callback) {{ this.listeners[name]=callback; }} click() {{ this.listeners.click?.({{currentTarget:this}}); }} dispatchEvent() {{}} }} class Recognition {{ start() {{ throw new Error('microphone blocked by browser policy'); }} stop() {{}} abort() {{}} }} const elements=Object.fromEntries( ['root','start','stop','review','transcript','append','replace','status','title','body'].map(key=>[key,new Element()]) ); elements.title.value='Existing title'; elements.body.value='Existing note'; elements.transcript.value='Recovered transcript.'; elements.start.hidden=false; elements.stop.hidden=true; createVoiceIssueCapture({{Recognition,elements}}); elements.start.click(); process.stdout.write(JSON.stringify({{ startHidden:elements.start.hidden, stopHidden:elements.stop.hidden, status:elements.status.textContent, title:elements.title.value, body:elements.body.value, transcript:elements.transcript.value }})); """ assert run_node(script) == { "startHidden": False, "stopHidden": True, "status": "Voice capture could not start. Your draft and transcript are unchanged.", "title": "Existing title", "body": "Existing note", "transcript": "Recovered transcript.", } def test_stop_error_and_sheet_close_release_recognition_without_losing_transcript(): script = f""" const {{createVoiceIssueCapture}} = require({json.dumps(str(VOICE_CAPTURE))}); class Element {{ constructor() {{ this.hidden=false; this.value=''; this.textContent=''; this.listeners={{}}; }} addEventListener(name, callback) {{ this.listeners[name]=callback; }} click() {{ this.listeners.click?.({{currentTarget:this}}); }} dispatchEvent() {{}} }} let instance; class Recognition {{ constructor() {{ instance=this; this.stops=0; this.aborts=0; }} start() {{}} stop() {{ this.stops+=1; }} abort() {{ this.aborts+=1; }} }} const elements=Object.fromEntries( ['root','start','stop','review','transcript','append','replace','status','title','body'].map(key=>[key,new Element()]) ); const controller=createVoiceIssueCapture({{Recognition,elements}}); elements.start.click(); instance.onresult({{results:[Object.assign([{{transcript:'Partial final transcript.'}}], {{isFinal:true}})]}}); elements.stop.click(); const afterStop=elements.status.textContent; instance.onerror({{error:'not-allowed'}}); const afterError=elements.status.textContent; controller.cancel(); process.stdout.write(JSON.stringify({{ stops:instance.stops, aborts:instance.aborts, afterStop, afterError, transcript:elements.transcript.value, reviewHidden:elements.review.hidden }})); """ assert run_node(script) == { "stops": 1, "aborts": 1, "afterStop": "Finishing transcript…", "afterError": "Microphone permission was denied. Your draft and transcript are unchanged.", "transcript": "Partial final transcript.", "reviewHidden": False, } def test_private_store_isolates_bounded_transcripts_by_normalized_account(): script = f""" const createVoiceTranscriptStore=require({json.dumps(str(VOICE_STORE))}); const records=new Map(); const transaction=async work=>work({{ get:async key=>records.get(key), put:async value=>records.set(value.login,{{...value}}), delete:async key=>records.delete(key), }}); (async()=>{{ const store=createVoiceTranscriptStore({{transaction}}); await store.save(' Timmy ', ' Lift outage. ' + 'x'.repeat(11000)); await store.save('alexander', 'Private second account'); const before={{timmy:await store.load('timmy'),alexander:await store.load('alexander')}}; await store.clear('TIMMY'); process.stdout.write(JSON.stringify({{ before,after:await store.load('timmy'),keys:[...records.keys()],length:before.timmy.length }})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ assert run_node(script) == { "before": { "timmy": "Lift outage. " + "x" * 10242, "alexander": "Private second account", }, "after": "", "keys": ["alexander"], "length": 10255, } def test_mobile_new_sheet_ships_accessible_voice_review_inside_issue_capture_feature(): build = build_frontend(FRONTEND) html = build.dashboard_html capture_bundle = build.feature_bundles["issue-capture"].runtime_bytes dashboard = (FRONTEND / "dashboard.js").read_text() assert 'id="voice-issue-capture"' in html assert 'id="start-voice-issue-capture"' in html assert 'id="stop-voice-issue-capture"' in html assert 'id="voice-issue-transcript"' in html assert 'id="voice-issue-status" class="small" aria-live="polite"' in html assert 'id="append-voice-issue-transcript"' in html assert 'id="replace-with-voice-issue-transcript"' in html assert 'id="discard-voice-issue-transcript"' in html assert b"function createVoiceIssueCapture" in capture_bundle assert b"function createVoiceTranscriptStore" in capture_bundle assert b"function createVoiceIssueCapture" not in build.runtime_bytes assert "window.SpeechRecognition || window.webkitSpeechRecognition" in dashboard assert "transcriptStore:createVoiceTranscriptStore()" in dashboard assert "getLogin:()=>confirmedOwnerLogin" in dashboard assert "voiceIssueCapture.cancel();" in dashboard registry = run_node( f"process.stdout.write(JSON.stringify(require({json.dumps(str(FRONTEND / 'private-data-registry.js'))})))" ) assert "stackchain-voice-transcripts-v1" in registry