alpiecode 10.6.4__tar.gz → 10.6.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {alpiecode-10.6.4 → alpiecode-10.6.6}/PKG-INFO +1 -1
  2. {alpiecode-10.6.4 → alpiecode-10.6.6}/pyproject.toml +1 -1
  3. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/__init__.py +1 -1
  5. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/backends/local_backend.py +243 -9
  6. {alpiecode-10.6.4 → alpiecode-10.6.6}/test/test_local_model_download.py +97 -0
  7. {alpiecode-10.6.4 → alpiecode-10.6.6}/README.md +0 -0
  8. {alpiecode-10.6.4 → alpiecode-10.6.6}/setup.cfg +0 -0
  9. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/alpiecode/__init__.py +0 -0
  10. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/alpiecode.egg-info/SOURCES.txt +0 -0
  11. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  12. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/alpiecode.egg-info/entry_points.txt +0 -0
  13. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/alpiecode.egg-info/requires.txt +0 -0
  14. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/alpiecode.egg-info/top_level.txt +0 -0
  15. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/agent.py +0 -0
  16. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/assessment.py +0 -0
  17. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/backends/__init__.py +0 -0
  18. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/backends/base.py +0 -0
  19. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/backends/openai_backend.py +0 -0
  20. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/cache.py +0 -0
  21. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/cli.py +0 -0
  22. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/client.py +0 -0
  23. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/compaction.py +0 -0
  24. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/config.py +0 -0
  25. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/context.py +0 -0
  26. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/discovery.py +0 -0
  27. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/doctor.py +0 -0
  28. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/dream_rsi.py +0 -0
  29. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/executor.py +0 -0
  30. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/extension/alpiecode.vsix +0 -0
  31. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/git_ops.py +0 -0
  32. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/github.py +0 -0
  33. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/guardian.py +0 -0
  34. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/guardrails.py +0 -0
  35. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/ipython_ext.py +0 -0
  36. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/local_model.py +0 -0
  37. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/media.py +0 -0
  38. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/memory.py +0 -0
  39. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/metrics.py +0 -0
  40. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/orchestrator.py +0 -0
  41. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/paths.py +0 -0
  42. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/progress.py +0 -0
  43. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/prompt.py +0 -0
  44. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/rephraser.py +0 -0
  45. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/server.py +0 -0
  46. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/session.py +0 -0
  47. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/system1.py +0 -0
  48. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/tools.py +0 -0
  49. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/updater.py +0 -0
  50. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/verifier.py +0 -0
  51. {alpiecode-10.6.4 → alpiecode-10.6.6}/src/codeagent/vscode_installer.py +0 -0
  52. {alpiecode-10.6.4 → alpiecode-10.6.6}/test/test_adaptive_resilience.py +0 -0
  53. {alpiecode-10.6.4 → alpiecode-10.6.6}/test/test_cli_ui.py +0 -0
  54. {alpiecode-10.6.4 → alpiecode-10.6.6}/test/test_context_and_trajectory.py +0 -0
  55. {alpiecode-10.6.4 → alpiecode-10.6.6}/test/test_dream_rsi.py +0 -0
  56. {alpiecode-10.6.4 → alpiecode-10.6.6}/test/test_task_assessment.py +0 -0
  57. {alpiecode-10.6.4 → alpiecode-10.6.6}/test/test_verifier.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 10.6.4
3
+ Version: 10.6.6
4
4
  Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "alpiecode"
7
- version = "10.6.4"
7
+ version = "10.6.6"
8
8
  description = "AlpieCode"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 10.6.4
3
+ Version: 10.6.6
4
4
  Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -17,7 +17,7 @@ Supports:
17
17
  from pathlib import Path
18
18
  from typing import Optional, Any, Dict
19
19
 
20
- __version__ = "10.6.4"
20
+ __version__ = "10.6.6"
21
21
 
22
22
  def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
23
23
  """Run an autonomous coding task programmatically."""
@@ -1,11 +1,14 @@
1
1
  import os
2
2
  import shutil
3
+ import subprocess
3
4
  import sys
4
- from typing import Any, List, Optional
5
+ import tempfile
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional, Tuple
5
8
 
6
9
  from ..config import Config
7
10
  from ..local_model import LocalModel
8
- from ..paths import is_android, is_ios
11
+ from ..paths import ensure_dir, get_alpiecode_dir, is_android, is_ios
9
12
  from .base import ChatResponse, ToolCall
10
13
 
11
14
 
@@ -425,7 +428,7 @@ class iOSBackend(BasePlatformBackend):
425
428
  import llama_cpp
426
429
  return True
427
430
  except ImportError:
428
- return self._is_bridge_online()
431
+ return self._is_bridge_online() or self._is_wasm_available()
429
432
 
430
433
  def _get_bridge_url(self) -> str:
431
434
  return os.environ.get("IOS_LLAMA_BRIDGE", "http://127.0.0.1:8080").rstrip("/")
@@ -438,6 +441,224 @@ class iOSBackend(BasePlatformBackend):
438
441
  except Exception:
439
442
  return False
440
443
 
444
+ def _get_wasm_binary_path(self) -> Optional[Path]:
445
+ """Locate the llama.wasm binary for a-Shell / WASI execution."""
446
+ custom = os.environ.get("ALPIECODE_WASM_PATH") or os.environ.get("LLAMA_WASM_PATH")
447
+ if custom and Path(custom).exists():
448
+ return Path(custom)
449
+ candidates = [
450
+ get_alpiecode_dir() / "bin" / "llama.wasm",
451
+ get_alpiecode_dir() / "bin" / "llama-cli.wasm",
452
+ Path.home() / "Documents" / ".alpiecode" / "bin" / "llama.wasm",
453
+ Path.home() / ".alpiecode" / "bin" / "llama.wasm",
454
+ Path(sys.prefix) / "bin" / "llama.wasm",
455
+ Path.cwd() / "llama.wasm",
456
+ Path.cwd() / ".alpiecode" / "bin" / "llama.wasm",
457
+ ]
458
+ for c in candidates:
459
+ if c.is_file():
460
+ return c
461
+ w_bin = shutil.which("llama.wasm") or shutil.which("llama-cli.wasm")
462
+ if w_bin:
463
+ return Path(w_bin)
464
+ return None
465
+
466
+ def _get_model_path_safe(self) -> Path:
467
+ """Resolve model path whether _ensure_model returns LocalModel or Path."""
468
+ model_obj = self._ensure_model()
469
+ if hasattr(model_obj, "ensure_model"):
470
+ return model_obj.ensure_model()
471
+ if hasattr(model_obj, "model_path") and model_obj.model_path:
472
+ return Path(model_obj.model_path)
473
+ return Path(model_obj)
474
+
475
+ def _is_wasm_available(self) -> bool:
476
+ """Check if WASM runner (wasm command or executable .wasm) is available in a-Shell."""
477
+ has_wasm_cmd = shutil.which("wasm") is not None
478
+ wasm_bin = self._get_wasm_binary_path()
479
+ return bool(has_wasm_cmd or wasm_bin)
480
+
481
+ def _ensure_wasm_runner(self) -> Tuple[str, Path]:
482
+ """Verify WASM binary and return (wasm_command, wasm_binary_path)."""
483
+ wasm_bin = self._get_wasm_binary_path()
484
+ bin_dir = ensure_dir(get_alpiecode_dir() / "bin")
485
+ default_wasm = bin_dir / "llama.wasm"
486
+
487
+ if not wasm_bin:
488
+ # Attempt automatic download from HuggingFace so user doesn't have to manually transfer files
489
+ try:
490
+ from huggingface_hub import hf_hub_download
491
+ repo = getattr(self._cfg, "model_repo", "169Pi/alpie_learn_gguf")
492
+ downloaded = hf_hub_download(
493
+ repo_id=repo,
494
+ filename="llama.wasm",
495
+ local_dir=str(bin_dir),
496
+ )
497
+ if downloaded and Path(downloaded).exists():
498
+ wasm_bin = Path(downloaded)
499
+ except Exception:
500
+ pass
501
+
502
+ if not wasm_bin:
503
+ raise RuntimeError(
504
+ f"\n╭────────────────────────────────────────────────────────────╮\n"
505
+ f"│ a-Shell WebAssembly Inference: │\n"
506
+ f"│ llama.wasm binary not found. │\n"
507
+ f"│ │\n"
508
+ f"│ To enable offline inference in a-Shell: │\n"
509
+ f"│ 1. Place 'llama.wasm' into: │\n"
510
+ f"│ {str(default_wasm)[:55]:<55}│\n"
511
+ f"│ 2. Or set: export ALPIECODE_WASM_PATH=/path/to/llama.wasm │\n"
512
+ f"╰────────────────────────────────────────────────────────────╯"
513
+ )
514
+
515
+ runner = shutil.which("wasm") or "wasm"
516
+ return runner, wasm_bin
517
+
518
+ def _format_messages_to_prompt(self, messages: List[dict]) -> str:
519
+ """Format OpenAI-style message dictionary list into ChatML prompt string."""
520
+ sanitized = self._sanitize_messages_for_gguf(messages)
521
+ parts = []
522
+ for msg in sanitized:
523
+ role = msg.get("role", "user")
524
+ content = msg.get("content", "")
525
+ parts.append(f"<|im_start|>{role}\n{content}<|im_end|>")
526
+ parts.append("<|im_start|>assistant\n")
527
+ return "\n".join(parts)
528
+
529
+ def _wasm_chat_completion(
530
+ self,
531
+ messages: List[dict],
532
+ tools: Optional[List[dict]] = None,
533
+ temperature: float = 0.2,
534
+ max_tokens: int = 2048,
535
+ enable_thinking: bool = True,
536
+ ) -> ChatResponse:
537
+ runner, wasm_path = self._ensure_wasm_runner()
538
+ model_path = self._get_model_path_safe()
539
+ prompt = self._format_messages_to_prompt(messages)
540
+
541
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False, suffix=".txt") as tf:
542
+ tf.write(prompt)
543
+ prompt_file = tf.name
544
+
545
+ try:
546
+ n_ctx = str(getattr(self._cfg, "n_ctx", 4096))
547
+ base_args = [
548
+ "-m", str(model_path),
549
+ "-f", prompt_file,
550
+ "-n", str(max_tokens),
551
+ "--temp", str(temperature),
552
+ "-c", n_ctx,
553
+ "--no-mmap",
554
+ "--simple-io",
555
+ "--no-display-prompt",
556
+ "-t", "4",
557
+ ]
558
+ if shutil.which(runner):
559
+ cmd = [runner, str(wasm_path)] + base_args
560
+ else:
561
+ cmd = [str(wasm_path)] + base_args
562
+
563
+ res = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
564
+ if res.returncode != 0:
565
+ err = res.stderr.strip() or res.stdout.strip()
566
+ if "memory" in err.lower() or "bounds" in err.lower() or "alloc" in err.lower():
567
+ raise RuntimeError(
568
+ f"\n❌ WASM Memory Allocation Failed for '{model_path.name}'.\n"
569
+ f" The model size ({model_path.stat().st_size / (1024**3):.2f} GB) exceeded the 32-bit WASM limit (~2-4 GB).\n"
570
+ f" Details: {err[:200]}"
571
+ )
572
+ raise RuntimeError(f"WASM llama execution failed (code {res.returncode}): {err[:250]}")
573
+
574
+ raw_output = res.stdout.strip()
575
+ content = raw_output
576
+ reasoning = None
577
+ if "<think>" in raw_output and "</think>" in raw_output:
578
+ p1, p2 = raw_output.split("</think>", 1)
579
+ reasoning = p1.replace("<think>", "").strip()
580
+ content = p2.strip()
581
+ elif "<think>" in raw_output:
582
+ content = raw_output.replace("<think>", "").strip()
583
+
584
+ return ChatResponse(content=content, reasoning=reasoning, tool_calls=None, raw={"output": raw_output})
585
+ finally:
586
+ try:
587
+ os.remove(prompt_file)
588
+ except Exception:
589
+ pass
590
+
591
+ def _wasm_chat_completion_stream(
592
+ self,
593
+ messages: List[dict],
594
+ tools: Optional[List[dict]] = None,
595
+ temperature: Optional[float] = None,
596
+ max_tokens: Optional[int] = None,
597
+ enable_thinking: bool = True,
598
+ ):
599
+ runner, wasm_path = self._ensure_wasm_runner()
600
+ model_path = self._get_model_path_safe()
601
+ prompt = self._format_messages_to_prompt(messages)
602
+
603
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False, suffix=".txt") as tf:
604
+ tf.write(prompt)
605
+ prompt_file = tf.name
606
+
607
+ proc = None
608
+ try:
609
+ n_ctx = str(getattr(self._cfg, "n_ctx", 4096))
610
+ t_val = str(temperature if temperature is not None else 0.2)
611
+ m_val = str(max_tokens if max_tokens is not None else 2048)
612
+
613
+ base_args = [
614
+ "-m", str(model_path),
615
+ "-f", prompt_file,
616
+ "-n", m_val,
617
+ "--temp", t_val,
618
+ "-c", n_ctx,
619
+ "--no-mmap",
620
+ "--simple-io",
621
+ "--no-display-prompt",
622
+ "-t", "4",
623
+ ]
624
+ if shutil.which(runner):
625
+ cmd = [runner, str(wasm_path)] + base_args
626
+ else:
627
+ cmd = [str(wasm_path)] + base_args
628
+
629
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
630
+ full_content = []
631
+ for line in proc.stdout:
632
+ if line:
633
+ full_content.append(line)
634
+ yield ("token", {"delta": line})
635
+ proc.wait()
636
+ if proc.returncode != 0:
637
+ err = proc.stderr.read().strip() if proc.stderr else ""
638
+ if "memory" in err.lower() or "bounds" in err.lower():
639
+ raise RuntimeError(
640
+ f"\n❌ WASM Memory Allocation Failed for '{model_path.name}'.\n"
641
+ f" Details: {err[:200]}"
642
+ )
643
+ if not full_content:
644
+ raise RuntimeError(f"WASM execution exited with code {proc.returncode}: {err[:200]}")
645
+
646
+ yield ("done", ChatResponse(
647
+ content="".join(full_content).strip(),
648
+ reasoning=None,
649
+ tool_calls=None,
650
+ ))
651
+ finally:
652
+ if proc and proc.poll() is None:
653
+ try:
654
+ proc.kill()
655
+ except Exception:
656
+ pass
657
+ try:
658
+ os.remove(prompt_file)
659
+ except Exception:
660
+ pass
661
+
441
662
  def chat_completion(
442
663
  self,
443
664
  messages: List[dict],
@@ -455,12 +676,18 @@ class iOSBackend(BasePlatformBackend):
455
676
  if self._is_bridge_online():
456
677
  return self._bridge_chat_completion(messages, tools, temperature, max_tokens, enable_thinking)
457
678
 
679
+ if self._is_wasm_available():
680
+ return self._wasm_chat_completion(messages, tools, temperature, max_tokens, enable_thinking)
681
+
458
682
  raise RuntimeError(
459
683
  "\n╭────────────────────────────────────────────────────────────╮\n"
460
- "│ iOS Native Inference: │\n"
684
+ "│ iOS Native Inference (a-Shell / Container): │\n"
461
685
  "│ Dynamic compilation via pip is disabled on iOS. │\n"
462
- "│ llama.cpp must be pre-bundled with the iOS app container │\n"
463
- "│ or served via a local bridge (e.g. http://127.0.0.1:8080). │\n"
686
+ "│ │\n"
687
+ "│ Choose one of the following to run offline on iOS: │\n"
688
+ "│ 1. a-Shell WASM: Place llama.wasm into ~/.alpiecode/bin/ │\n"
689
+ "│ 2. Local Bridge: Run local server on 127.0.0.1:8080 │\n"
690
+ "│ 3. Pre-bundled: llama.cpp inside iOS app container │\n"
464
691
  "╰────────────────────────────────────────────────────────────╯"
465
692
  )
466
693
 
@@ -527,12 +754,19 @@ class iOSBackend(BasePlatformBackend):
527
754
  yield from self._bridge_chat_completion_stream(messages, tools, temperature, max_tokens, enable_thinking)
528
755
  return
529
756
 
757
+ if self._is_wasm_available():
758
+ yield from self._wasm_chat_completion_stream(messages, tools, temperature, max_tokens, enable_thinking)
759
+ return
760
+
530
761
  raise RuntimeError(
531
762
  "\n╭────────────────────────────────────────────────────────────╮\n"
532
- "│ iOS Native Inference: │\n"
763
+ "│ iOS Native Inference (a-Shell / Container): │\n"
533
764
  "│ Dynamic compilation via pip is disabled on iOS. │\n"
534
- "│ llama.cpp must be pre-bundled with the iOS app container │\n"
535
- "│ or served via a local bridge (e.g. http://127.0.0.1:8080). │\n"
765
+ "│ │\n"
766
+ "│ Choose one of the following to run offline on iOS: │\n"
767
+ "│ 1. a-Shell WASM: Place llama.wasm into ~/.alpiecode/bin/ │\n"
768
+ "│ 2. Local Bridge: Run local server on 127.0.0.1:8080 │\n"
769
+ "│ 3. Pre-bundled: llama.cpp inside iOS app container │\n"
536
770
  "╰────────────────────────────────────────────────────────────╯"
537
771
  )
538
772
 
@@ -286,3 +286,100 @@ def test_embedded_hf_token_used_when_no_env(monkeypatch):
286
286
  from codeagent.local_model import EMBEDDED_HF_TOKEN
287
287
  assert EMBEDDED_HF_TOKEN in captured_tokens
288
288
 
289
+
290
+ def test_ios_backend_wasm_dispatch(tmp_path, monkeypatch):
291
+ """When llama_cpp is absent and bridge is offline, iOSBackend dispatches to WASM runner."""
292
+ from codeagent.backends.local_backend import iOSBackend
293
+ cfg = load_config()
294
+ b = iOSBackend(cfg)
295
+
296
+ wasm_bin = tmp_path / "llama.wasm"
297
+ wasm_bin.touch()
298
+ fake_model = tmp_path / "alpie-vl-text-q3_k_m.gguf"
299
+ fake_model.touch()
300
+
301
+ monkeypatch.setenv("ALPIECODE_WASM_PATH", str(wasm_bin))
302
+
303
+ mock_run = MagicMock()
304
+ mock_run.returncode = 0
305
+ mock_run.stdout = "<think>Analyzing code...</think>\nHello from WASM offline!"
306
+ mock_run.stderr = ""
307
+
308
+ with patch.dict("sys.modules", {"llama_cpp": None}), \
309
+ patch.object(b, "_is_bridge_online", return_value=False), \
310
+ patch.object(b, "_ensure_model", return_value=fake_model), \
311
+ patch("shutil.which", return_value="/usr/bin/wasm"), \
312
+ patch("subprocess.run", return_value=mock_run) as run_spy:
313
+ res = b.chat_completion([{"role": "user", "content": "hello"}])
314
+ assert res.content == "Hello from WASM offline!"
315
+ assert res.reasoning == "Analyzing code..."
316
+
317
+ # Verify command arguments
318
+ assert run_spy.called
319
+ cmd = run_spy.call_args[0][0]
320
+ assert cmd[0] == "/usr/bin/wasm"
321
+ assert cmd[1] == str(wasm_bin)
322
+ assert "-m" in cmd
323
+ assert str(fake_model) in cmd
324
+ assert "-f" in cmd
325
+
326
+
327
+ def test_ios_backend_wasm_streaming(tmp_path, monkeypatch):
328
+ """iOSBackend streams tokens line-by-line via WASM runner."""
329
+ from codeagent.backends.local_backend import iOSBackend
330
+ cfg = load_config()
331
+ b = iOSBackend(cfg)
332
+
333
+ wasm_bin = tmp_path / "llama.wasm"
334
+ wasm_bin.touch()
335
+ fake_model = tmp_path / "alpie-vl-text-q3_k_m.gguf"
336
+ fake_model.touch()
337
+
338
+ monkeypatch.setenv("ALPIECODE_WASM_PATH", str(wasm_bin))
339
+
340
+ mock_proc = MagicMock()
341
+ mock_proc.stdout = ["Hello ", "from ", "stream!"]
342
+ mock_proc.returncode = 0
343
+ mock_proc.poll.return_value = 0
344
+
345
+ with patch.dict("sys.modules", {"llama_cpp": None}), \
346
+ patch.object(b, "_is_bridge_online", return_value=False), \
347
+ patch.object(b, "_ensure_model", return_value=fake_model), \
348
+ patch("shutil.which", return_value="/usr/bin/wasm"), \
349
+ patch("subprocess.Popen", return_value=mock_proc):
350
+ stream = list(b.chat_completion_stream([{"role": "user", "content": "hi"}]))
351
+ tokens = [payload["delta"] for tag, payload in stream if tag == "token"]
352
+ assert "".join(tokens) == "Hello from stream!"
353
+ assert stream[-1][0] == "done"
354
+ assert stream[-1][1].content == "Hello from stream!"
355
+
356
+
357
+ def test_ios_backend_wasm_memory_error(tmp_path, monkeypatch):
358
+ """WASM runtime memory allocation failure throws actionable diagnostic."""
359
+ from codeagent.backends.local_backend import iOSBackend
360
+ cfg = load_config()
361
+ b = iOSBackend(cfg)
362
+
363
+ wasm_bin = tmp_path / "llama.wasm"
364
+ wasm_bin.touch()
365
+ fake_model = tmp_path / "alpie-vl-text-q3_k_m.gguf"
366
+ fake_model.touch()
367
+
368
+ monkeypatch.setenv("ALPIECODE_WASM_PATH", str(wasm_bin))
369
+
370
+ mock_run = MagicMock()
371
+ mock_run.returncode = 1
372
+ mock_run.stdout = ""
373
+ mock_run.stderr = "RuntimeError: out of bounds memory access / memory allocation failed"
374
+
375
+ with patch.dict("sys.modules", {"llama_cpp": None}), \
376
+ patch.object(b, "_is_bridge_online", return_value=False), \
377
+ patch.object(b, "_ensure_model", return_value=fake_model), \
378
+ patch("shutil.which", return_value="/usr/bin/wasm"), \
379
+ patch("subprocess.run", return_value=mock_run):
380
+ with pytest.raises(RuntimeError) as exc_info:
381
+ b.chat_completion([{"role": "user", "content": "hello"}])
382
+ assert "WASM Memory Allocation Failed" in str(exc_info.value)
383
+ assert "32-bit WASM limit" in str(exc_info.value)
384
+
385
+
File without changes
File without changes