Skip to content

Commit 4a1c580

Browse files
committed
ci: run Midscene inside Omarchy VM
1 parent dc1bf4b commit 4a1c580

4 files changed

Lines changed: 249 additions & 4 deletions

File tree

‎.github/workflows/midscene-experimental.yml‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,17 @@ concurrency:
1818

1919
jobs:
2020
omarchy-vm:
21-
name: Install real Omarchy in a headless VM
21+
name: Run Midscene in a real Omarchy VM
2222
runs-on: ubuntu-latest
2323
timeout-minutes: 70
24+
env:
25+
MIDSCENE_MODEL_API_KEY: ${{ secrets.MIDSCENE_MODEL_API_KEY }}
26+
MIDSCENE_MODEL_NAME: ${{ secrets.MIDSCENE_MODEL_NAME }}
27+
MIDSCENE_MODEL_BASE_URL: ${{ secrets.MIDSCENE_MODEL_BASE_URL }}
28+
MIDSCENE_MODEL_FAMILY: ${{ secrets.MIDSCENE_MODEL_FAMILY }}
29+
MIDSCENE_MODEL_REASONING_ENABLED: ${{ secrets.MIDSCENE_MODEL_REASONING_ENABLED }}
30+
MIDSCENE_COMPUTER_HEADLESS_LINUX: "true"
31+
MIDSCENE_REPLANNING_CYCLE_LIMIT: "40"
2432
steps:
2533
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
2634
with:
@@ -29,6 +37,28 @@ jobs:
2937
- name: Install Omarchy VM
3038
run: tests/midscene/install-omarchy-vm.sh
3139

40+
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
41+
with:
42+
node-version: "22"
43+
cache: npm
44+
cache-dependency-path: tests/midscene/package-lock.json
45+
46+
- name: Require Midscene model configuration
47+
run: |
48+
test -n "$MIDSCENE_MODEL_API_KEY"
49+
test -n "$MIDSCENE_MODEL_NAME"
50+
test -n "$MIDSCENE_MODEL_BASE_URL"
51+
test -n "$MIDSCENE_MODEL_FAMILY"
52+
53+
- name: Install Midscene and VNC bridge
54+
run: |
55+
sudo apt-get install -y --no-install-recommends \
56+
fluxbox tigervnc-viewer x11-xserver-utils xvfb
57+
npm --prefix tests/midscene ci --include=optional --ignore-scripts
58+
59+
- name: Run real Omarchy Midscene E2E
60+
run: tests/midscene/run-omarchy-midscene.sh
61+
3262
- name: Upload installer evidence
3363
if: always()
3464
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -37,3 +67,12 @@ jobs:
3767
path: .midscene-omarchy/omarchy-iso/test-runs/**/runs/**
3868
if-no-files-found: warn
3969
retention-days: 3
70+
71+
- name: Upload Omarchy Midscene report
72+
if: always()
73+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
74+
with:
75+
name: omarchy-midscene-e2e-report
76+
path: tests/midscene/midscene_run/
77+
if-no-files-found: warn
78+
retention-days: 7

‎tests/midscene/README.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ Omarchy 4.0.3 was installed successfully on a GitHub-hosted runner in
2121
The run took 13 minutes 41 seconds, including about 55 seconds to transfer the
2222
ISO.
2323

24-
After that probe is stable, the next stages are:
24+
The manual experimental workflow now continues from that installed base image:
2525

2626
1. Boot a throwaway overlay from the installed base image.
27-
2. install and enable this checkout as `md.lifeos.doubao-say` in the guest.
27+
2. Copy this checkout to `md.lifeos.doubao-say` in the guest and run Omarchy's
28+
plugin validator.
2829
3. Run deterministic GTK UI fixtures with `@midscene/computer` on ordinary
2930
Ubuntu for every trusted change.
30-
4. Run a smaller Omarchy/Hyprland Midscene suite manually in the guest.
31+
4. Run the same focused Midscene onboarding suite through VNC in the real
32+
Omarchy/Hyprland guest session.
3133

3234
The ordinary Ubuntu stage maps the real GTK onboarding window inside the
3335
headless Midscene desktop. Its synthetic fixture performs no login, recording,
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { spawn, type ChildProcess } from 'node:child_process';
2+
import { afterAll, beforeAll, describe, it, vi } from 'vitest';
3+
import {
4+
type ComputerAgent,
5+
agentFromComputer,
6+
} from '@midscene/computer';
7+
8+
vi.setConfig({ testTimeout: 8 * 60 * 1000, hookTimeout: 2 * 60 * 1000 });
9+
10+
const sleep = (milliseconds: number) =>
11+
new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
12+
13+
const stopProcessGroup = (child?: ChildProcess) => {
14+
if (!child?.pid) return;
15+
try {
16+
process.kill(-child.pid, 'SIGTERM');
17+
} catch {
18+
// The viewer may already have exited after a test failure.
19+
}
20+
};
21+
22+
describe.skipIf(process.env.OMARCHY_E2E !== 'true')(
23+
'Doubao Say onboarding on Omarchy',
24+
() => {
25+
let agent: ComputerAgent;
26+
let fluxbox: ChildProcess;
27+
let viewer: ChildProcess;
28+
29+
beforeAll(async () => {
30+
agent = await agentFromComputer({
31+
aiContexts: {
32+
aiAct:
33+
'You are testing the English Doubao Say GTK onboarding window ' +
34+
'inside a real Omarchy Hyprland virtual machine shown through a VNC viewer. ' +
35+
'Interact only with the Doubao Say window and use visible labels.',
36+
},
37+
xvfbResolution: '1280x960x24',
38+
});
39+
40+
fluxbox = spawn('fluxbox', [], {
41+
detached: true,
42+
stdio: 'ignore',
43+
env: process.env,
44+
});
45+
await sleep(1000);
46+
47+
viewer = spawn(
48+
'vncviewer',
49+
['-FullScreen=1', '-RemoteResize=0', '-ViewOnly=0', '127.0.0.1:5905'],
50+
{
51+
detached: true,
52+
stdio: 'ignore',
53+
env: process.env,
54+
},
55+
);
56+
await sleep(4000);
57+
});
58+
59+
afterAll(() => {
60+
stopProcessGroup(viewer);
61+
stopProcessGroup(fluxbox);
62+
});
63+
64+
it('runs the onboarding flow in the real Omarchy desktop session', async () => {
65+
await agent.aiAct(
66+
'Verify the Omarchy desktop visibly contains the Doubao Say window on the Microphone step with a Next button in its fixed top navigation.',
67+
);
68+
await agent.aiAct(
69+
'Click Next in the Doubao Say fixed top navigation and wait until the current step is Trigger key.',
70+
);
71+
await agent.aiAct(
72+
'Stay on the Trigger key step and do not click Previous or Next. Scroll down inside the light-gray central content panel until the Voice polishing heading and its controls are visible.',
73+
);
74+
await agent.aiAct(
75+
'On the current Trigger key step, click Test endpoint in the visible Voice polishing section and wait for the endpoint result.',
76+
);
77+
await agent.aiAct(
78+
'Verify a visible message says the endpoint works and includes Synthetic endpoint response.',
79+
);
80+
await agent.aiAct(
81+
'Click Next in the fixed top navigation and wait for the Voice test step.',
82+
);
83+
await agent.aiAct(
84+
'Verify the Voice test heading and introductory text are visible near the top of the content, proving that navigation reset the previous scroll position.',
85+
);
86+
await agent.aiAct(
87+
'Scroll within the content if needed and click Finish setup.',
88+
);
89+
await agent.aiAct(
90+
'Verify the Finish setup button changed to a disabled visible status saying Setup completed by the synthetic E2E fixture.',
91+
);
92+
});
93+
},
94+
);
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
readonly ROOT_DIR="$PWD"
5+
readonly WORK_DIR="$ROOT_DIR/.midscene-omarchy"
6+
readonly HARNESS_DIR="$WORK_DIR/omarchy-iso"
7+
readonly ISO_PATH="$WORK_DIR/omarchy-4.0.3.iso"
8+
readonly BASE_DIR="$HARNESS_DIR/test-runs/omarchy-4.0.3"
9+
readonly SSH_KEY="$BASE_DIR/id_ed25519"
10+
readonly SSH_PORT=2222
11+
readonly PLUGIN_DIR="/home/omarchy/.config/omarchy/plugins/md.lifeos.doubao-say"
12+
readonly SHIM_DIR="$(mktemp -d)"
13+
14+
VM_PID=""
15+
16+
cleanup() {
17+
if [[ -n $VM_PID ]]; then
18+
kill "$VM_PID" 2>/dev/null || true
19+
fi
20+
rm -rf "$SHIM_DIR"
21+
}
22+
trap cleanup EXIT
23+
24+
ssh_guest() {
25+
ssh -i "$SSH_KEY" -p "$SSH_PORT" \
26+
-o BatchMode=yes \
27+
-o IdentitiesOnly=yes \
28+
-o StrictHostKeyChecking=no \
29+
-o UserKnownHostsFile=/dev/null \
30+
-o ConnectTimeout=10 \
31+
-o LogLevel=ERROR \
32+
omarchy@127.0.0.1 "$@"
33+
}
34+
35+
ssh_session() {
36+
local command="$1"
37+
ssh_guest "export XDG_RUNTIME_DIR=/run/user/\$(id -u); \
38+
export DBUS_SESSION_BUS_ADDRESS=unix:path=\$XDG_RUNTIME_DIR/bus; \
39+
export HYPRLAND_INSTANCE_SIGNATURE=\$(ls -t \$XDG_RUNTIME_DIR/hypr | head -1); \
40+
export WAYLAND_DISPLAY=\$(find \$XDG_RUNTIME_DIR -maxdepth 1 -name 'wayland-*' ! -name '*.lock' -printf '%f\\n' | head -1); \
41+
export OMARCHY_PATH=/usr/share/omarchy; \
42+
export PATH=\$OMARCHY_PATH/bin:\$PATH; \
43+
$command" </dev/null
44+
}
45+
46+
test -s "$ISO_PATH"
47+
test -s "$BASE_DIR/base.qcow2"
48+
test -s "$SSH_KEY"
49+
50+
# Reuse the pinned official harness's VM, login and session routines. Replace
51+
# only its post-login acceptance body so this job can hand the live desktop to
52+
# Midscene instead of running Omarchy's unrelated upstream acceptance suite.
53+
readonly SESSION_HARNESS="$HARNESS_DIR/bin/omarchy-midscene-session"
54+
cp "$HARNESS_DIR/bin/omarchy-iso-test" "$SESSION_HARNESS"
55+
sed -i '/^acceptance_phase() {/,/^}/c\
56+
acceptance_phase() {\
57+
log "Booting Omarchy session for Doubao Say Midscene E2E"\
58+
qemu-img create -f qcow2 -b "$BASE_DISK" -F qcow2 "$RUN_DIR/run.qcow2" >/dev/null\
59+
start_vm "$RUN_DIR/run.qcow2" "$RUN_DIR/serial.log"\
60+
establish_session\
61+
capture_console "success-session-ready-for-midscene"\
62+
}' "$SESSION_HARNESS"
63+
64+
printf '#!/bin/sh\nexit 0\n' >"$SHIM_DIR/omarchy-pkg-add"
65+
printf '#!/bin/sh\nexec convert "$@"\n' >"$SHIM_DIR/magick"
66+
chmod 0755 "$SHIM_DIR/omarchy-pkg-add" "$SHIM_DIR/magick"
67+
68+
PATH="$SHIM_DIR:$PATH" "$SESSION_HARNESS" "$ISO_PATH" \
69+
--reuse-base \
70+
--keep-running \
71+
--memory 4096 \
72+
--no-preview
73+
74+
readonly RUN_DIR="$(find "$BASE_DIR/runs" -mindepth 1 -maxdepth 1 -type d | sort | tail -1)"
75+
VM_PID="$(cat "$RUN_DIR/qemu.pid")"
76+
kill -0 "$VM_PID"
77+
78+
# Put this exact checkout at its real Omarchy plugin location, validate the
79+
# manifest with Omarchy, then launch the deterministic GTK fixture in the
80+
# guest's actual Hyprland session.
81+
tar -C "$ROOT_DIR" --exclude='__pycache__' -cf - \
82+
LICENSE README.md manifest.json install.sh install-user.sh setup-omarchy.sh start.sh \
83+
omarchy src tests/midscene/gtk_fixture.py | \
84+
ssh_guest "rm -rf '$PLUGIN_DIR' && mkdir -p '$PLUGIN_DIR' && tar -C '$PLUGIN_DIR' -xf -"
85+
86+
ssh_session "omarchy plugin validate '$PLUGIN_DIR'"
87+
ssh_session "rm -rf /tmp/doubao-midscene-config; \
88+
mkdir -p /tmp/doubao-midscene-config; \
89+
export PYTHONPATH='$PLUGIN_DIR/src'; \
90+
export XDG_CONFIG_HOME=/tmp/doubao-midscene-config; \
91+
export PYTHONDONTWRITEBYTECODE=1; \
92+
setsid -f python3 '$PLUGIN_DIR/tests/midscene/gtk_fixture.py' \
93+
>/tmp/doubao-midscene-fixture.log 2>&1"
94+
95+
for _attempt in $(seq 1 30); do
96+
if ssh_session "grep -q 'READY: synthetic Doubao Say GTK fixture' /tmp/doubao-midscene-fixture.log && \
97+
hyprctl -j clients | jq -e '[.[] | select(.title == \"Doubao Say\")] | length == 1'" \
98+
>/dev/null 2>&1; then
99+
break
100+
fi
101+
sleep 2
102+
done
103+
104+
ssh_session "grep -q 'READY: synthetic Doubao Say GTK fixture' /tmp/doubao-midscene-fixture.log"
105+
ssh_session "hyprctl -j clients | jq -e '[.[] | select(.title == \"Doubao Say\")] | length == 1'"
106+
107+
OMARCHY_E2E=true npm --prefix tests/midscene test -- \
108+
e2e/omarchy-onboarding.test.ts
109+
110+
ssh_session "hyprctl -j clients | jq -e '[.[] | select(.title == \"Doubao Say\")] | length == 1'"

0 commit comments

Comments
 (0)