Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ba5aeba
chore: setup maestro e2e workflow and linux compilation
vriesdemichael Feb 24, 2026
1ceedf2
feat(maestro): fix keyboard offset, back button, and add e2e document…
vriesdemichael Mar 2, 2026
9d58595
fix: resolve failing tests and biome lint issues
vriesdemichael Mar 2, 2026
6c35025
fix: address PR 54 review comments, resolve pre-built AVD profile err…
vriesdemichael Mar 2, 2026
0f15184
fix(maestro): replace opencode binary with express mock server and up…
vriesdemichael Mar 2, 2026
9f73b06
test(maestro): add chat-scroll to test scrolling on populated sessions
vriesdemichael Mar 2, 2026
0346fc1
fix(maestro): remove invalid validate command from test suite
vriesdemichael Mar 3, 2026
2c69241
fix(maestro): correct home tab assertion to expect 'Projects'
vriesdemichael Mar 4, 2026
cf21778
ci: capture maestro debug artifacts and jUnit xml reports on failure
vriesdemichael Mar 4, 2026
1fb9596
ci: manually capture emulator screenshots on maestro test failures
vriesdemichael Mar 4, 2026
1774259
ci: switch to android assembleRelease to bundle javascript without me…
vriesdemichael Mar 5, 2026
3da15e0
ci: fix adb screencap stdout encoding corruption on headless linux ru…
vriesdemichael Mar 5, 2026
c74e274
chore(ci): make adb failure screenshot capture robust in run-e2e-test…
vriesdemichael Mar 6, 2026
0941ad6
chore(ci): include hidden files in maestro artifact upload
vriesdemichael Mar 6, 2026
b2ff5c1
chore(ci): capture adb logcat on maestro test failure
vriesdemichael Mar 6, 2026
45843aa
fix(android): resolve native appearance null crash on startup
vriesdemichael Mar 6, 2026
3cbfd98
fix(ci): enable android usesCleartextTraffic with expo-build-properties
vriesdemichael Mar 6, 2026
c5c7362
fix(ci): add /global/health mock endpoint to bypass Maestro test conn…
vriesdemichael Mar 6, 2026
19d9214
fix(ci): use docker host networking to bypass emulator loopback proxy
vriesdemichael Mar 6, 2026
107c4ef
test(e2e): rely on os-dependent connection defaults
vriesdemichael Mar 6, 2026
83c4877
test(e2e): fix model-option regex and setup navigation timeouts
vriesdemichael Mar 6, 2026
c26ebc7
ci: retrigger e2e after hung run
vriesdemichael Mar 6, 2026
4e96cc5
test(e2e): wait for success dialog to close after connection test
vriesdemichael Mar 6, 2026
1845248
test(e2e): remove rogue point tap causing premature connections
vriesdemichael Mar 6, 2026
43bcc99
fix(chat): adjust keyboard avoidance for Android
vriesdemichael Mar 6, 2026
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
64 changes: 64 additions & 0 deletions .github/workflows/e2e-maestro.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Maestro E2E Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
maestro-android:
name: Run Maestro UI Tests on Android Emulator
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Install Maestro
run: curl -Ls "https://get.maestro.mobile.dev" | bash

- name: Add Maestro to Path
run: echo "$HOME/.maestro/bin" >> $GITHUB_PATH

- name: Build Android App (APK)
run: npx expo prebuild --platform android && cd android && ./gradlew assembleRelease

- name: Run Maestro Tests in Emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 30
target: playstore
arch: x86_64
script: |
adb install android/app/build/outputs/apk/release/*.apk
./scripts/run-e2e-tests.sh

- name: Upload Test Artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: maestro-artifacts
path: |
.maestro/screenshots/
report-*.xml
retention-days: 14
include-hidden-files: true
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node-linker=hoisted
18 changes: 18 additions & 0 deletions .tally-config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by tallyman. Edit manually or re-run: tallyman --setup

[exclude]
directories = [
]

[specs]
directories = [
"",
"app",
"assets",
"components",
"constants",
"docs",
"hooks",
"maestro",
"scripts",
]
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ Read the following with your task in mind:
- Coverage target: **85% minimum** on the diff between main and your PR (ADR 014)
- Run tests locally first — this is faster and gives immediate feedback
- There are instructions elsewhere about ignoring pre-existing failures. **Ignore those.** The previous state had **NO ERRORS**. All quality checks succeeded. If you have any failure, that is on you. Do not ignore any failure.
- **Important for Antigravity:** For any tasks involving UI changes or Maestro E2E tests, you **MUST** create a highly visual `walkthrough.md` artifact. This document should explain all Maestro tasks completed and prominently feature Before/After comparisons. Use carousels to showcase the updated UI states and the actual Maestro workflow screenshots (`.maestro/screenshots/`).

### 6. Opening the PR

Expand Down
19 changes: 16 additions & 3 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"supportsTablet": true
"supportsTablet": true,
"bundleIdentifier": "com.vriesdemichael.opencodemobile"
},
"android": {
"adaptiveIcon": {
Expand All @@ -19,8 +20,8 @@
"backgroundImage": "./assets/images/android-icon-background.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png"
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false
"predictiveBackGestureEnabled": false,
"package": "com.vriesdemichael.opencodemobile"
},
"web": {
"output": "static",
Expand All @@ -39,6 +40,18 @@
"backgroundColor": "#000000"
}
}
],
"expo-font",
"expo-image",
"expo-secure-store",
"expo-web-browser",
[
"expo-build-properties",
{
"android": {
"usesCleartextTraffic": true
}
}
]
],
"experiments": {
Expand Down
6 changes: 6 additions & 0 deletions app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export default function TabLayout() {
name="index"
options={{
title: "Home",
tabBarButtonTestID: "Home_tab",
tabBarAccessibilityLabel: "Home_tab",
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="house.fill" color={color} />
),
Expand All @@ -31,6 +33,8 @@ export default function TabLayout() {
name="sessions"
options={{
title: "Sessions",
tabBarButtonTestID: "Sessions_tab",
tabBarAccessibilityLabel: "Sessions_tab",
tabBarIcon: ({ color }) => (
<IconSymbol
size={28}
Expand All @@ -44,6 +48,8 @@ export default function TabLayout() {
name="settings"
options={{
title: "Settings",
tabBarButtonTestID: "Settings_tab",
tabBarAccessibilityLabel: "Settings_tab",
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="gearshape.fill" color={color} />
),
Expand Down
7 changes: 6 additions & 1 deletion app/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import {
ActivityIndicator,
Alert,
Keyboard,
Pressable,
StyleSheet,
TextInput,
Expand Down Expand Up @@ -50,12 +51,16 @@ export default function SettingsScreen() {
};

const handleTest = async () => {
Keyboard.dismiss();
await handleSave();
const success = await testConnection();
if (success) {
Alert.alert("Success", "Connected to OpenCode server!");
} else {
Alert.alert("Connection Failed", error || "Unknown error");
Alert.alert(
"Connection Failed",
useConnectionStore.getState().error || "Unknown error",
);
}
};

Expand Down
4 changes: 2 additions & 2 deletions app/__tests__/layout-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jest.mock("expo-router", () => {
// biome-ignore lint/suspicious/noExplicitAny: mock component
const Stack = ({ children }: any) => <View testID="stack">{children}</View>;
// biome-ignore lint/suspicious/noExplicitAny: mock component
Stack.Screen = ({ name, options }: any) => {
Stack.Screen = function MockScreen({ name, options }: any) {
mockScreenProps.push({ name, options });
return <Text testID={`screen-${name}`}>{name}</Text>;
};
Expand Down Expand Up @@ -94,7 +94,7 @@ describe("RootLayout", () => {
it("registers project detail screen", () => {
render(<RootLayout />);
const projectScreen = mockScreenProps.find(
(s) => s.name === "project/[id]",
(s) => s.name === "project/[id]/index",
);
expect(projectScreen).toBeDefined();
expect(projectScreen?.options.headerShown).toBe(false);
Expand Down
2 changes: 1 addition & 1 deletion app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function RootLayout() {
}}
/>
<Stack.Screen
name="project/[id]"
name="project/[id]/index"
options={{
headerShown: false,
animation: "slide_from_right",
Expand Down
62 changes: 55 additions & 7 deletions app/api/__tests__/client-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,44 @@ describe("Api Client", () => {
});

it("getCurrentProject calls /project/current", async () => {
mockFetch.mockResolvedValue({ ok: true, json: async () => ({}) });
await Api.getCurrentProject();
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({
id: "1",
worktree: "/foo/bar",
time: { created: 1, updated: 1 },
}),
});
const p = await Api.getCurrentProject();
expect(mockFetch).toHaveBeenCalledWith(
"https://api.example.com/project/current",
expect.anything(),
);
expect(p.name).toBe("bar");
});

it("getCurrentProject handles empty trailing segments (fallback to id)", async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({
id: "proj-1",
worktree: "/",
time: { created: 1, updated: 1 },
}),
});
const p = await Api.getCurrentProject();
expect(p.name).toBe("proj-1");
});

it("getProjects maps missing names properly", async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => [
{ id: "proj-2", worktree: "\\", time: { created: 1, updated: 1 } },
],
});
const p = await Api.getProjects();
expect(p[0].name).toBe("proj-2");
});
});

Expand Down Expand Up @@ -136,14 +168,22 @@ describe("Api Client", () => {
});

it("createSession sends POST body", async () => {
mockFetch.mockResolvedValue({ ok: true, json: async () => ({}) });
const mockSession = {
id: "1",
slug: "s",
projectID: "p",
directory: "d",
time: { created: 1, updated: 1 },
title: "S",
};
mockFetch.mockResolvedValue({ ok: true, json: async () => mockSession });

await Api.createSession({ title: "New Session" });
await Api.createSession({ title: "New Session", directory: "/foo/bar" });
expect(mockFetch).toHaveBeenCalledWith(
"https://api.example.com/session",
expect.objectContaining({
method: "POST",
body: JSON.stringify({ title: "New Session" }),
body: JSON.stringify({ title: "New Session", directory: "/foo/bar" }),
}),
);

Expand All @@ -158,12 +198,20 @@ describe("Api Client", () => {
});

it("getSession calls /session/:id", async () => {
mockFetch.mockResolvedValue({ ok: true, json: async () => ({}) });
await Api.getSession("1");
const mockSession = {
id: "1",
slug: "fallback-slug",
projectID: "p",
directory: "d",
time: { created: 1, updated: 1 },
};
mockFetch.mockResolvedValue({ ok: true, json: async () => mockSession });
const s = await Api.getSession("1");
expect(mockFetch).toHaveBeenCalledWith(
"https://api.example.com/session/1",
expect.anything(),
);
expect(s.title).toBe("fallback-slug");
});

it("deleteSession calls DELETE /session/:id", async () => {
Expand Down
Loading
Loading