Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions __tests__/e2e/device/visionMmprojMultiModel.e2e.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# On-device e2e: multi-model mmproj invariants (Android=adb, iOS=devicectl log pull).
#
# Two properties the A1 fix guarantees, proven on a REAL device via disk state + the app's debug log
# (un-fakeable — no reliance on UI assertions):
#
# A) QUANT DEDUP — downloading a 2nd quantization of the SAME model must NOT re-download the mmproj.
# One projector serves every quant (mmProjLocalName is quant-independent), and checkMmProjExists
# skips the sidecar when it is already on disk. ASSERT: after the 2nd quant, the model still has
# exactly ONE mmproj file on disk, and the log shows the mmproj was reused, not re-fetched.
#
# B) FAMILY SEPARATION — two different models of the same family (Gemma 4 E2B vs E4B) must each keep
# their OWN mmproj and never mispair. ASSERT: two DISTINCT mmproj files on disk, and each model
# loads with `[WIRE-VISION] … initialized:true` naming ITS OWN mmproj, with no
# "Multimodal support not enabled" anywhere.
#
# PLATFORM: pass PLATFORM=android (adb) or PLATFORM=ios (devicectl). The DOWNLOADS + model LOADS are
# driven by the operator/harness (coordinate taps drift — screenshot-verify); this script owns the
# deterministic ASSERTIONS. Run each phase with PHASE=baseline|dedup|family.
#
# PLATFORM=android bash visionMmprojMultiModel.e2e.sh <phase>
set -uo pipefail
PLATFORM="${PLATFORM:-android}"
IOS_UDID="${IOS_UDID:-00008150-000225103CD8C01C}"
PKG=ai.offgridmobile.dev

models_ls() {

Check warning on line 27 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCV&open=AZ-V9kWD9vEWfpfxNsCV&pullRequest=611
if [ "$PLATFORM" = ios ]; then

Check failure on line 28 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCW&open=AZ-V9kWD9vEWfpfxNsCW&pullRequest=611
xcrun devicectl device info files --device "$IOS_UDID" --domain-type appDataContainer \
--domain-identifier "$PKG" --subdirectory Documents/models 2>/dev/null | grep -oE '[^ /]+\.gguf' || true
else
export PATH="$PATH:$HOME/Library/Android/sdk/platform-tools"
adb exec-out run-as "$PKG" sh -c 'ls -1 files/models 2>/dev/null'
fi
}

read_log() {

Check warning on line 37 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCX&open=AZ-V9kWD9vEWfpfxNsCX&pullRequest=611
if [ "$PLATFORM" = ios ]; then

Check failure on line 38 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCY&open=AZ-V9kWD9vEWfpfxNsCY&pullRequest=611
xcrun devicectl device copy from --device "$IOS_UDID" --domain-type appDataContainer \
--domain-identifier "$PKG" --source Documents/offgrid-debug.log --destination /tmp/mm-e2e-ios.log >/dev/null 2>&1
cat /tmp/mm-e2e-ios.log 2>/dev/null
Comment on lines +39 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid reading a stale iOS log after a failed pull.

copy from failures are suppressed, then the script reads a persistent /tmp/mm-e2e-ios.log. A failed current pull can therefore reuse a prior run’s pairing/initialization evidence and falsely pass. Use a unique mktemp file, fail the phase when the copy fails, and remove it afterward; this also removes the predictable-path symlink risk.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 39-39: Writing to or reading from a hardcoded, predictable path under /tmp is vulnerable to symlink and TOCTOU attacks: a local attacker can pre-create the file (or a symlink pointing elsewhere) and hijack or corrupt the contents. Generate a unique, unpredictable temporary file with mktemp instead, e.g. tmpfile="$(mktemp)" (or mktemp -d for directories) and reference "$tmpfile".
Context: /tmp/mm-e2e-ios.log
Note: [CWE-377] Insecure Temporary File.

(predictable-tmp-file-bash)


[warning] 40-40: Writing to or reading from a hardcoded, predictable path under /tmp is vulnerable to symlink and TOCTOU attacks: a local attacker can pre-create the file (or a symlink pointing elsewhere) and hijack or corrupt the contents. Generate a unique, unpredictable temporary file with mktemp instead, e.g. tmpfile="$(mktemp)" (or mktemp -d for directories) and reference "$tmpfile".
Context: /tmp/mm-e2e-ios.log
Note: [CWE-377] Insecure Temporary File.

(predictable-tmp-file-bash)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/e2e/device/visionMmprojMultiModel.e2e.sh` around lines 39 - 41,
Update the iOS log retrieval flow around the devicectl copy command to use a
unique mktemp output file instead of /tmp/mm-e2e-ios.log, stop the phase
immediately when the copy fails, and remove the temporary file afterward. Keep
the log-reading behavior for successful pulls while ensuring no stale or
symlinked file can influence the result.

Source: Linters/SAST tools

else
export PATH="$PATH:$HOME/Library/Android/sdk/platform-tools"
adb exec-out run-as "$PKG" sh -c 'cat files/offgrid-debug.log 2>/dev/null'
fi
}

# count a model-family's mmproj files on disk (e.g. STEM=gemma-4-e2b)
mmproj_count() { models_ls | grep -i mmproj | grep -ic "$1" || true; }

Check warning on line 49 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCa&open=AZ-V9kWD9vEWfpfxNsCa&pullRequest=611

Check warning on line 49 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCZ&open=AZ-V9kWD9vEWfpfxNsCZ&pullRequest=611

case "${1:?phase: baseline|dedup|family}" in

Check failure on line 51 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a default case (*) to handle unexpected values.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCb&open=AZ-V9kWD9vEWfpfxNsCb&pullRequest=611
baseline)
echo "[$PLATFORM] gemma-4-e2b mmproj files: $(mmproj_count gemma-4-e2b)"
echo "[$PLATFORM] gemma-4-e4b mmproj files: $(mmproj_count gemma-4-e4b)"
models_ls | tr '\n' ' '; echo ;;

dedup) # run AFTER downloading a 2nd quant of gemma-4-E2B
N=$(mmproj_count gemma-4-e2b)
WEIGHTS=$(models_ls | grep -vi mmproj | grep -ic gemma-4-e2b)
echo "gemma-4-e2b: weights=$WEIGHTS mmproj=$N"
# the 2nd quant's finalization must report the mmproj already present (not re-downloaded)
REUSE=$(read_log | grep -aiE "mmproj already on disk|mmProjFileExists\":true.*gemma-4-e2b|mmproj.*skip" | tail -1)
if [ "$N" -eq 1 ] && [ "$WEIGHTS" -ge 2 ]; then

Check failure on line 63 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCd&open=AZ-V9kWD9vEWfpfxNsCd&pullRequest=611

Check failure on line 63 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCc&open=AZ-V9kWD9vEWfpfxNsCc&pullRequest=611
Comment on lines +62 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require the reuse log for the dedup assertion.

REUSE is only printed. The phase passes whenever disk state is 1 mmproj and >=2 weights, even if the second quant re-downloaded or replaced the projector. Include a non-empty, model-specific reuse record in the predicate.

Proposed fix
-    if [ "$N" -eq 1 ] && [ "$WEIGHTS" -ge 2 ]; then
+    if [[ "$N" -eq 1 && "$WEIGHTS" -ge 2 && -n "$REUSE" ]]; then
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
REUSE=$(read_log | grep -aiE "mmproj already on disk|mmProjFileExists\":true.*gemma-4-e2b|mmproj.*skip" | tail -1)
if [ "$N" -eq 1 ] && [ "$WEIGHTS" -ge 2 ]; then
REUSE=$(read_log | grep -aiE "mmproj already on disk|mmProjFileExists\":true.*gemma-4-e2b|mmproj.*skip" | tail -1)
if [[ "$N" -eq 1 && "$WEIGHTS" -ge 2 && -n "$REUSE" ]]; then
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 63-63: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCd&open=AZ-V9kWD9vEWfpfxNsCd&pullRequest=611


[failure] 63-63: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCc&open=AZ-V9kWD9vEWfpfxNsCc&pullRequest=611

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/e2e/device/visionMmprojMultiModel.e2e.sh` around lines 62 - 63,
Update the deduplication assertion using REUSE so the phase only passes when the
reuse log is non-empty and matches the expected model-specific projector reuse
record. Keep the existing N and WEIGHTS checks, but require REUSE in the
predicate to reject cases where the projector was re-downloaded or replaced.

echo "PASS(A): 2 gemma-4-E2B quants share ONE mmproj (no re-download). reuse-log: ${REUSE:0:80}"
else
echo "FAIL(A): expected 1 mmproj + >=2 weights, got mmproj=$N weights=$WEIGHTS"; exit 1
fi ;;

family) # run AFTER downloading gemma-4-E4B alongside E2B
E2B=$(mmproj_count gemma-4-e2b); E4B=$(mmproj_count gemma-4-e4b)
echo "mmproj files: e2b=$E2B e4b=$E4B"
DUMP=$(read_log)
ERR=$(echo "$DUMP" | grep -aic "multimodal support not enabled")

# PAIRING proof — the un-fakeable signal is the app's own pairing decision, logged two ways:
# 1. [linkOrphanMmProj] (fires at boot / finalization) prints "<weights> — linking <mmproj>".
# 2. [WIRE-VISION] (fires at model LOAD) prints the model + mmproj it initialized with.
# Family separation holds iff E2B weights pair the e2b mmproj AND E4B weights pair the e4b mmproj
# (NEVER the other family's). A load-triggered WIRE-VISION is nice-to-have but needs a manual
# message-send; the boot-time linkOrphanMmProj line is deterministic, so accept EITHER.
pair_ok() { # $1=weights-family stem $2=required mmproj stem

Check warning on line 81 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCe&open=AZ-V9kWD9vEWfpfxNsCe&pullRequest=611
echo "$DUMP" | grep -aiE "linkOrphanMmProj|WIRE-VISION" | grep -i "$1" | grep -i "$2" | tail -1

Check warning on line 82 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCg&open=AZ-V9kWD9vEWfpfxNsCg&pullRequest=611

Check warning on line 82 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCf&open=AZ-V9kWD9vEWfpfxNsCf&pullRequest=611
}
mispair() { # $1=weights-family stem $2=FORBIDDEN mmproj stem (the other family's)

Check warning on line 84 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCh&open=AZ-V9kWD9vEWfpfxNsCh&pullRequest=611
echo "$DUMP" | grep -aiE "linkOrphanMmProj|WIRE-VISION" | grep -i "$1" | grep -ic "$2"

Check warning on line 85 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCj&open=AZ-V9kWD9vEWfpfxNsCj&pullRequest=611

Check warning on line 85 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCi&open=AZ-V9kWD9vEWfpfxNsCi&pullRequest=611
}
P_E2B=$(pair_ok "gemma-4-e2b-it-q" "gemma-4-e2b-it-mmproj")
P_E4B=$(pair_ok "gemma-4-e4b-it-q" "gemma-4-e4b-it-mmproj")
X_E2B=$(mispair "gemma-4-e2b-it-q" "gemma-4-e4b-it-mmproj") # E2B must NOT pair e4b mmproj
X_E4B=$(mispair "gemma-4-e4b-it-q" "gemma-4-e2b-it-mmproj") # E4B must NOT pair e2b mmproj
echo "pair e2b->e2b: ${P_E2B:+yes} | pair e4b->e4b: ${P_E4B:+yes} | mispairs: e2b->e4b=$X_E2B e4b->e2b=$X_E4B"
if [ "$E2B" -ge 1 ] && [ "$E4B" -ge 1 ] && [ -n "$P_E2B" ] && [ -n "$P_E4B" ] \

Check failure on line 92 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCl&open=AZ-V9kWD9vEWfpfxNsCl&pullRequest=611

Check failure on line 92 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCn&open=AZ-V9kWD9vEWfpfxNsCn&pullRequest=611

Check failure on line 92 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCm&open=AZ-V9kWD9vEWfpfxNsCm&pullRequest=611

Check failure on line 92 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCk&open=AZ-V9kWD9vEWfpfxNsCk&pullRequest=611
&& [ "$X_E2B" -eq 0 ] && [ "$X_E4B" -eq 0 ] && [ "$ERR" -eq 0 ]; then

Check failure on line 93 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCo&open=AZ-V9kWD9vEWfpfxNsCo&pullRequest=611

Check failure on line 93 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCp&open=AZ-V9kWD9vEWfpfxNsCp&pullRequest=611

Check failure on line 93 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCq&open=AZ-V9kWD9vEWfpfxNsCq&pullRequest=611
Comment on lines +75 to +93

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require successful vision initialization for both families.

linkOrphanMmProj only proves an association decision; it does not prove either model loaded or vision initialized. The fallback lets this phase pass with no [WIRE-VISION] event—or with initialized:false—despite the stated E2E invariant. Require a correct-family [WIRE-VISION] entry with initialized:true for E2B and E4B; retain orphan-link logs only as diagnostics.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 92-92: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCl&open=AZ-V9kWD9vEWfpfxNsCl&pullRequest=611


[failure] 93-93: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCo&open=AZ-V9kWD9vEWfpfxNsCo&pullRequest=611


[failure] 93-93: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCp&open=AZ-V9kWD9vEWfpfxNsCp&pullRequest=611


[failure] 92-92: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCn&open=AZ-V9kWD9vEWfpfxNsCn&pullRequest=611


[failure] 93-93: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCq&open=AZ-V9kWD9vEWfpfxNsCq&pullRequest=611


[warning] 82-82: Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCg&open=AZ-V9kWD9vEWfpfxNsCg&pullRequest=611


[failure] 92-92: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCm&open=AZ-V9kWD9vEWfpfxNsCm&pullRequest=611


[warning] 84-84: Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCh&open=AZ-V9kWD9vEWfpfxNsCh&pullRequest=611


[failure] 92-92: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCk&open=AZ-V9kWD9vEWfpfxNsCk&pullRequest=611


[warning] 82-82: Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCf&open=AZ-V9kWD9vEWfpfxNsCf&pullRequest=611


[warning] 85-85: Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCj&open=AZ-V9kWD9vEWfpfxNsCj&pullRequest=611


[warning] 85-85: Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCi&open=AZ-V9kWD9vEWfpfxNsCi&pullRequest=611


[warning] 81-81: Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCe&open=AZ-V9kWD9vEWfpfxNsCe&pullRequest=611

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/e2e/device/visionMmprojMultiModel.e2e.sh` around lines 75 - 93,
Update pair_ok and the final pairing assertions so success requires a
correct-family [WIRE-VISION] entry containing initialized:true for both E2B and
E4B. Do not treat linkOrphanMmProj output as proof of initialization; retain
those logs only for diagnostic output, and preserve the existing
forbidden-family mispair checks and error conditions.

echo "PASS(B): distinct mmproj on disk; E2B pairs its OWN mmproj and E4B pairs its OWN mmproj; no cross-pairing, no error"

Check warning on line 94 in __tests__/e2e/device/visionMmprojMultiModel.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCr&open=AZ-V9kWD9vEWfpfxNsCr&pullRequest=611
else
echo "FAIL(B): e2b_files=$E2B e4b_files=$E4B e2b_paired=${P_E2B:+1} e4b_paired=${P_E4B:+1} mispair_e2b=$X_E2B mispair_e4b=$X_E4B errors=$ERR"; exit 1
fi ;;
esac
Comment on lines +51 to +98

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject unknown phases explicitly.

An invalid phase falls through the case and exits successfully, allowing a typo in CI to report a false green result.

Proposed fix
   family)
     # ...
     fi ;;
+  *)
+    echo "Unknown phase: $1 (expected baseline, dedup, or family)" >&2
+    exit 2 ;;
 esac
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case "${1:?phase: baseline|dedup|family}" in
baseline)
echo "[$PLATFORM] gemma-4-e2b mmproj files: $(mmproj_count gemma-4-e2b)"
echo "[$PLATFORM] gemma-4-e4b mmproj files: $(mmproj_count gemma-4-e4b)"
models_ls | tr '\n' ' '; echo ;;
dedup) # run AFTER downloading a 2nd quant of gemma-4-E2B
N=$(mmproj_count gemma-4-e2b)
WEIGHTS=$(models_ls | grep -vi mmproj | grep -ic gemma-4-e2b)
echo "gemma-4-e2b: weights=$WEIGHTS mmproj=$N"
# the 2nd quant's finalization must report the mmproj already present (not re-downloaded)
REUSE=$(read_log | grep -aiE "mmproj already on disk|mmProjFileExists\":true.*gemma-4-e2b|mmproj.*skip" | tail -1)
if [ "$N" -eq 1 ] && [ "$WEIGHTS" -ge 2 ]; then
echo "PASS(A): 2 gemma-4-E2B quants share ONE mmproj (no re-download). reuse-log: ${REUSE:0:80}"
else
echo "FAIL(A): expected 1 mmproj + >=2 weights, got mmproj=$N weights=$WEIGHTS"; exit 1
fi ;;
family) # run AFTER downloading gemma-4-E4B alongside E2B
E2B=$(mmproj_count gemma-4-e2b); E4B=$(mmproj_count gemma-4-e4b)
echo "mmproj files: e2b=$E2B e4b=$E4B"
DUMP=$(read_log)
ERR=$(echo "$DUMP" | grep -aic "multimodal support not enabled")
# PAIRING proof — the un-fakeable signal is the app's own pairing decision, logged two ways:
# 1. [linkOrphanMmProj] (fires at boot / finalization) prints "<weights> — linking <mmproj>".
# 2. [WIRE-VISION] (fires at model LOAD) prints the model + mmproj it initialized with.
# Family separation holds iff E2B weights pair the e2b mmproj AND E4B weights pair the e4b mmproj
# (NEVER the other family's). A load-triggered WIRE-VISION is nice-to-have but needs a manual
# message-send; the boot-time linkOrphanMmProj line is deterministic, so accept EITHER.
pair_ok() { # $1=weights-family stem $2=required mmproj stem
echo "$DUMP" | grep -aiE "linkOrphanMmProj|WIRE-VISION" | grep -i "$1" | grep -i "$2" | tail -1
}
mispair() { # $1=weights-family stem $2=FORBIDDEN mmproj stem (the other family's)
echo "$DUMP" | grep -aiE "linkOrphanMmProj|WIRE-VISION" | grep -i "$1" | grep -ic "$2"
}
P_E2B=$(pair_ok "gemma-4-e2b-it-q" "gemma-4-e2b-it-mmproj")
P_E4B=$(pair_ok "gemma-4-e4b-it-q" "gemma-4-e4b-it-mmproj")
X_E2B=$(mispair "gemma-4-e2b-it-q" "gemma-4-e4b-it-mmproj") # E2B must NOT pair e4b mmproj
X_E4B=$(mispair "gemma-4-e4b-it-q" "gemma-4-e2b-it-mmproj") # E4B must NOT pair e2b mmproj
echo "pair e2b->e2b: ${P_E2B:+yes} | pair e4b->e4b: ${P_E4B:+yes} | mispairs: e2b->e4b=$X_E2B e4b->e2b=$X_E4B"
if [ "$E2B" -ge 1 ] && [ "$E4B" -ge 1 ] && [ -n "$P_E2B" ] && [ -n "$P_E4B" ] \
&& [ "$X_E2B" -eq 0 ] && [ "$X_E4B" -eq 0 ] && [ "$ERR" -eq 0 ]; then
echo "PASS(B): distinct mmproj on disk; E2B pairs its OWN mmproj and E4B pairs its OWN mmproj; no cross-pairing, no error"
else
echo "FAIL(B): e2b_files=$E2B e4b_files=$E4B e2b_paired=${P_E2B:+1} e4b_paired=${P_E4B:+1} mispair_e2b=$X_E2B mispair_e4b=$X_E4B errors=$ERR"; exit 1
fi ;;
esac
case "${1:?phase: baseline|dedup|family}" in
baseline)
echo "[$PLATFORM] gemma-4-e2b mmproj files: $(mmproj_count gemma-4-e2b)"
echo "[$PLATFORM] gemma-4-e4b mmproj files: $(mmproj_count gemma-4-e4b)"
models_ls | tr '\n' ' '; echo ;;
dedup) # run AFTER downloading a 2nd quant of gemma-4-E2B
N=$(mmproj_count gemma-4-e2b)
WEIGHTS=$(models_ls | grep -vi mmproj | grep -ic gemma-4-e2b)
echo "gemma-4-e2b: weights=$WEIGHTS mmproj=$N"
# the 2nd quant's finalization must report the mmproj already present (not re-downloaded)
REUSE=$(read_log | grep -aiE "mmproj already on disk|mmProjFileExists\":true.*gemma-4-e2b|mmproj.*skip" | tail -1)
if [ "$N" -eq 1 ] && [ "$WEIGHTS" -ge 2 ]; then
echo "PASS(A): 2 gemma-4-E2B quants share ONE mmproj (no re-download). reuse-log: ${REUSE:0:80}"
else
echo "FAIL(A): expected 1 mmproj + >=2 weights, got mmproj=$N weights=$WEIGHTS"; exit 1
fi ;;
family) # run AFTER downloading gemma-4-E4B alongside E2B
E2B=$(mmproj_count gemma-4-e2b); E4B=$(mmproj_count gemma-4-e4b)
echo "mmproj files: e2b=$E2B e4b=$E4B"
DUMP=$(read_log)
ERR=$(echo "$DUMP" | grep -aic "multimodal support not enabled")
# PAIRING proof — the un-fakeable signal is the app's own pairing decision, logged two ways:
# 1. [linkOrphanMmProj] (fires at boot / finalization) prints "<weights> — linking <mmproj>".
# 2. [WIRE-VISION] (fires at model LOAD) prints the model + mmproj it initialized with.
# Family separation holds iff E2B weights pair the e2b mmproj AND E4B weights pair the e4b mmproj
# (NEVER the other family's). A load-triggered WIRE-VISION is nice-to-have but needs a manual
# message-send; the boot-time linkOrphanMmProj line is deterministic, so accept EITHER.
pair_ok() { # $1=weights-family stem $2=required mmproj stem
echo "$DUMP" | grep -aiE "linkOrphanMmProj|WIRE-VISION" | grep -i "$1" | grep -i "$2" | tail -1
}
mispair() { # $1=weights-family stem $2=FORBIDDEN mmproj stem (the other family's)
echo "$DUMP" | grep -aiE "linkOrphanMmProj|WIRE-VISION" | grep -i "$1" | grep -ic "$2"
}
P_E2B=$(pair_ok "gemma-4-e2b-it-q" "gemma-4-e2b-it-mmproj")
P_E4B=$(pair_ok "gemma-4-e4b-it-q" "gemma-4-e4b-it-mmproj")
X_E2B=$(mispair "gemma-4-e2b-it-q" "gemma-4-e2b-it-mmproj") # E2B must NOT pair e4b mmproj
X_E4B=$(mispair "gemma-4-e4b-it-q" "gemma-4-e2b-it-mmproj") # E4B must NOT pair e2b mmproj
echo "pair e2b->e2b: ${P_E2B:+yes} | pair e4b->e4b: ${P_E4B:+yes} | mispairs: e2b->e4b=$X_E2B e4b->e2b=$X_E4B"
if [ "$E2B" -ge 1 ] && [ "$E4B" -ge 1 ] && [ -n "$P_E2B" ] && [ -n "$P_E4B" ] \
&& [ "$X_E2B" -eq 0 ] && [ "$X_E4B" -eq 0 ] && [ "$ERR" -eq 0 ]; then
echo "PASS(B): distinct mmproj on disk; E2B pairs its OWN mmproj and E4B pairs its OWN mmproj; no cross-pairing, no error"
else
echo "FAIL(B): e2b_files=$E2B e4b_files=$E4B e2b_paired=${P_E2B:+1} e4b_paired=${P_E4B:+1} mispair_e2b=$X_E2B mispair_e4b=$X_E4B errors=$ERR"; exit 1
fi ;;
*)
echo "Unknown phase: $1 (expected baseline, dedup, or family)" >&2
exit 2 ;;
esac
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 92-92: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCl&open=AZ-V9kWD9vEWfpfxNsCl&pullRequest=611


[failure] 93-93: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCo&open=AZ-V9kWD9vEWfpfxNsCo&pullRequest=611


[failure] 93-93: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCp&open=AZ-V9kWD9vEWfpfxNsCp&pullRequest=611


[failure] 51-51: Add a default case (*) to handle unexpected values.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCb&open=AZ-V9kWD9vEWfpfxNsCb&pullRequest=611


[failure] 63-63: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCd&open=AZ-V9kWD9vEWfpfxNsCd&pullRequest=611


[failure] 92-92: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCn&open=AZ-V9kWD9vEWfpfxNsCn&pullRequest=611


[failure] 93-93: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCq&open=AZ-V9kWD9vEWfpfxNsCq&pullRequest=611


[warning] 82-82: Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCg&open=AZ-V9kWD9vEWfpfxNsCg&pullRequest=611


[failure] 92-92: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCm&open=AZ-V9kWD9vEWfpfxNsCm&pullRequest=611


[warning] 84-84: Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCh&open=AZ-V9kWD9vEWfpfxNsCh&pullRequest=611


[failure] 92-92: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCk&open=AZ-V9kWD9vEWfpfxNsCk&pullRequest=611


[failure] 63-63: Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCc&open=AZ-V9kWD9vEWfpfxNsCc&pullRequest=611


[warning] 82-82: Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCf&open=AZ-V9kWD9vEWfpfxNsCf&pullRequest=611


[warning] 94-94: Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCr&open=AZ-V9kWD9vEWfpfxNsCr&pullRequest=611


[warning] 85-85: Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCj&open=AZ-V9kWD9vEWfpfxNsCj&pullRequest=611


[warning] 85-85: Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCi&open=AZ-V9kWD9vEWfpfxNsCi&pullRequest=611


[warning] 81-81: Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kWD9vEWfpfxNsCe&open=AZ-V9kWD9vEWfpfxNsCe&pullRequest=611

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/e2e/device/visionMmprojMultiModel.e2e.sh` around lines 51 - 98,
Update the phase dispatch case around baseline, dedup, and family to add an
explicit wildcard branch that reports the invalid phase and exits nonzero.
Preserve the existing behavior and success status for the three supported phases
while ensuring unknown arguments cannot fall through successfully.

Source: Linters/SAST tools

86 changes: 86 additions & 0 deletions __tests__/e2e/device/visionModelOnDevice.android.e2e.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# On-device e2e (Android, adb): download a vision GGUF, load it, and PROVE multimodal initialised.
#
# This is a REAL device test — no Provit journey engine, just adb driving + the app's own debug log.
# It codifies the flow we hand-drove during the A1 (mmproj) verification so it is repeatable.
#
# Assertion (the mmproj-fix proof): after the model loads, offgrid-debug.log must contain
# [WIRE-VISION] {... "initialized":true, "support":{"vision":true ...}}
# and MUST NOT contain "Multimodal support not enabled". The load-time init is the exact thing the
# fix restores; attaching an image is a bonus, not required to prove the fix.
Comment on lines +7 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Wait for a fresh initialization event instead of sampling the historical log once.

initMultimodal is asynchronous and [WIRE-VISION] is logged only afterward (src/services/llm.ts:218-229), so a fixed 20-second sleep can miss slow devices. Conversely, DUMP=$(LOG) includes previous runs, allowing a stale successful line to pass. Capture/truncate a baseline, poll only newly written log content until a deadline, and assert that the fresh window contains no "Multimodal support not enabled" error—the documented negative assertion is currently missing.

Also applies to: 74-74, 76-86

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/e2e/device/visionModelOnDevice.android.e2e.sh` around lines 7 - 10,
Update the vision model E2E assertion to truncate or baseline offgrid-debug.log
before loading, then poll only newly appended content until a deadline for the
fresh “[WIRE-VISION]” initialized event, accommodating slow devices instead of
relying on a fixed sleep. Assert the fresh log window contains vision support
and does not contain “Multimodal support not enabled,” using the existing test
flow and log variables.

#
# ⚠️ NAVIGATION RELIABILITY: the taps below are COORDINATE-based (calibrated for 1080x2378). The RN app
# does NOT expose its tab-bar/search/testIDs to `uiautomator`, so text/element-based taps aren't available
# on Android, and blind coordinate sequences DRIFT (a tap can hit a promo link → Chrome, or a permission
# dialog). Treat this script as a RECORDED reference: run it with a human watching (or screenshot-verify
# each step). The ASSERTIONS are hardened (they check the model's own GGUF on disk + a fresh model-named
# [WIRE-VISION] initialized:true), so it can never false-pass even if navigation drifts — it will FAIL
# loudly instead. iOS (WDA) exposes accessibility ids (home-tab/models-tab/…) and is more reliable there.
#
# Prereqs (see rules.md → On-device testing playbook):
# - Debuggable Debug build installed (run-as works → log file readable).
# - Metro running on the branch + `adb reverse tcp:8081 tcp:8081` (Android Debug loads JS from Metro).
# - Device unlocked, Auto-Lock long. Coords below are calibrated for 1080x2378 (OnePlus CPH2707).
#
# Usage: MODEL_QUERY="Qwen3.5-0.8B" bash visionModelOnDevice.android.e2e.sh
set -uo pipefail
export PATH="$PATH:$HOME/Library/Android/sdk/platform-tools"
PKG=ai.offgridmobile.dev
QUERY="${MODEL_QUERY:?set MODEL_QUERY, e.g. Qwen3.5-0.8B or gemma-4-E2B}"
LOG() { adb exec-out run-as "$PKG" cat files/offgrid-debug.log 2>/dev/null; }

Check warning on line 30 in __tests__/e2e/device/visionModelOnDevice.android.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Function 'LOG' should be named in snake case.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kRt9vEWfpfxNsCK&open=AZ-V9kRt9vEWfpfxNsCK&pullRequest=611

Check warning on line 30 in __tests__/e2e/device/visionModelOnDevice.android.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kRt9vEWfpfxNsCL&open=AZ-V9kRt9vEWfpfxNsCL&pullRequest=611
TAP() { adb shell input tap "$1" "$2"; sleep "${3:-2}"; }

Check warning on line 31 in __tests__/e2e/device/visionModelOnDevice.android.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kRt9vEWfpfxNsCN&open=AZ-V9kRt9vEWfpfxNsCN&pullRequest=611

Check warning on line 31 in __tests__/e2e/device/visionModelOnDevice.android.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kRt9vEWfpfxNsCO&open=AZ-V9kRt9vEWfpfxNsCO&pullRequest=611

Check warning on line 31 in __tests__/e2e/device/visionModelOnDevice.android.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Function 'TAP' should be named in snake case.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kRt9vEWfpfxNsCM&open=AZ-V9kRt9vEWfpfxNsCM&pullRequest=611

Check warning on line 31 in __tests__/e2e/device/visionModelOnDevice.android.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kRt9vEWfpfxNsCP&open=AZ-V9kRt9vEWfpfxNsCP&pullRequest=611
SHOT() { adb exec-out screencap -p > /tmp/e2e-android.png 2>/dev/null; }

Check warning on line 32 in __tests__/e2e/device/visionModelOnDevice.android.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Function 'SHOT' should be named in snake case.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kRt9vEWfpfxNsCQ&open=AZ-V9kRt9vEWfpfxNsCQ&pullRequest=611

Check warning on line 32 in __tests__/e2e/device/visionModelOnDevice.android.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kRt9vEWfpfxNsCR&open=AZ-V9kRt9vEWfpfxNsCR&pullRequest=611

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Use a unique temporary screenshot path.

SHOT writes to predictable /tmp/e2e-android.png; a local process can pre-create a symlink and redirect the screenshot contents to another file. Use mktemp and clean it up with a trap.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 32-32: Function 'SHOT' should be named in snake case.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kRt9vEWfpfxNsCQ&open=AZ-V9kRt9vEWfpfxNsCQ&pullRequest=611


[warning] 32-32: Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kRt9vEWfpfxNsCR&open=AZ-V9kRt9vEWfpfxNsCR&pullRequest=611

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/e2e/device/visionModelOnDevice.android.e2e.sh` at line 32, Update
the SHOT function to capture screenshots at a uniquely generated temporary path
using mktemp, rather than the predictable /tmp/e2e-android.png path. Add cleanup
for the generated file through an appropriate shell trap while preserving the
existing adb screencap behavior.

Source: Linters/SAST tools


echo "== reset app to Home =="
adb shell input keyevent 3; sleep 1
adb shell monkey -p "$PKG" -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1; sleep 4
# skip onboarding if present (optional taps)
TAP 949 215 2 # Skip (intro) — no-op if absent
TAP 538 2299 2 # Skip for Now — no-op if absent

echo "== Models tab -> search '$QUERY' =="
TAP 757 2245 2 # Models tab (bottom nav, 4th)
TAP 402 740 1 # search field
adb shell input text "$QUERY"; sleep 4
TAP 538 1000 1 # dismiss keyboard (first tap on result)
TAP 538 1000 3 # open top result detail

echo "== download the smallest (topmost) file =="
SHOT
TAP 949 975 4 # download icon on the first (smallest) file row
Comment on lines +48 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Carry the exact downloaded GGUF identity through the test.

The script only proves that some filename contains the query stem, then loads the first local model and accepts any [WIRE-VISION] line containing that stem. Multiple local quantizations can therefore make this test validate the wrong artifact. Require a completed .gguf, preserve its exact basename, select or verify that model in the picker, and match the exact filename in the model field logged by src/services/llm.ts:218-229. Also reject a query whose normalized stem is empty.

Also applies to: 54-60, 69-70, 79-82

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/e2e/device/visionModelOnDevice.android.e2e.sh` around lines 48 -
50, Update the vision model E2E flow around the download, picker selection, and
[WIRE-VISION] assertion to require a completed .gguf file, preserve its exact
basename, and verify/select that same model before inference; match the exact
basename in the logged model field rather than only the query stem. Validate
that the normalized query stem is non-empty before proceeding.

TAP 538 2217 2 # dismiss any memory-warning "Download Anyway"/OK — no-op if absent

echo "== wait for download to finalize — assert the model's OWN GGUF lands on disk (not a log grep) =="
STEM=$(echo "$QUERY" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9') # e.g. qwen3508b
DL_OK=0
for i in $(seq 1 90); do
ON_DISK=$(adb exec-out run-as "$PKG" sh -c 'ls files/models 2>/dev/null')
# a matching WEIGHTS file (not the mmproj) whose name reduces to our query stem
if echo "$ON_DISK" | grep -vi mmproj | tr -d '[:upper:] .-_' | grep -qi "$STEM"; then
echo "download complete — files: $(echo "$ON_DISK" | tr '\n' ' ')"; DL_OK=1; break
fi
echo " [$i] downloading… (on disk: $(echo "$ON_DISK" | tr '\n' ' '))"; sleep 20
done
[ "$DL_OK" = 1 ] || { echo "FAIL: $QUERY never landed on disk — navigation/download did not start"; exit 1; }

Check failure on line 64 in __tests__/e2e/device/visionModelOnDevice.android.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kRt9vEWfpfxNsCS&open=AZ-V9kRt9vEWfpfxNsCS&pullRequest=611

echo "== load the model (Home -> Select Model -> first local -> New Chat -> send text) =="
adb shell input keyevent 3; sleep 1
adb shell monkey -p "$PKG" -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1; sleep 3
TAP 750 878 2 # Select Model (Home card)
TAP 538 1374 8 # first local model (the one just downloaded, top of LOCAL MODELS)
TAP 538 743 3 # New Chat
TAP 400 2266 1 # message input
adb shell input text "hello"; sleep 1
TAP 978 1451 20 # send (keyboard-up position) -> triggers load + [WIRE-VISION]

echo "== ASSERT: a FRESH [WIRE-VISION] for THIS model, initialised, no error =="

Check warning on line 76 in __tests__/e2e/device/visionModelOnDevice.android.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kRt9vEWfpfxNsCT&open=AZ-V9kRt9vEWfpfxNsCT&pullRequest=611
# Un-fakeable: the WIRE-VISION line must (a) name THIS model's on-disk file, (b) be initialized:true,
# (c) vision:true — a stale line for a different model (e.g. SmolVLM) can never satisfy this.
DUMP=$(LOG)
VISION=$(echo "$DUMP" | grep -aE "WIRE-VISION" | grep -i "$STEM" | tail -1)
echo " $VISION"
if [ -n "$VISION" ] && echo "$VISION" | grep -q '"initialized":true' && echo "$VISION" | grep -q '"vision":true'; then

Check failure on line 82 in __tests__/e2e/device/visionModelOnDevice.android.e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ-V9kRt9vEWfpfxNsCU&open=AZ-V9kRt9vEWfpfxNsCU&pullRequest=611
echo "PASS: $QUERY vision initialised on Android"; exit 0
else
echo "FAIL: no fresh initialised [WIRE-VISION] for $QUERY (model may not have loaded / wrong model loaded)"; exit 1
fi
Loading