Introduce complete test coverage for the ARTEMIS platform across multiple layers: Unit tests: Add tests for agent nodes (Planner, Operator, Validator, Diagnoser, FlashRunner, Explorer), tools, utils (OCR/XML fusion, UI hierarchy), and data engine. Integration tests: Add live tool verification, reactive runner, IME, and standalone workflow tests. E2E tests: Add end-to-end scenario tests for media playback and failure recovery flows. Fixtures & mocks: Add mock inputs (audio, video, screenshot) and test utilities.
186 lines
6.1 KiB
Python
186 lines
6.1 KiB
Python
# Copyright 2026 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from unittest.mock import MagicMock
|
|
from artemis.context import ArtemisContext
|
|
from artemis.data_engine.engine import DataEngine
|
|
|
|
|
|
def test_get_or_create_image_updates_missing_data(tmp_path):
|
|
# Setup mock context
|
|
mock_ctx = MagicMock(spec=ArtemisContext)
|
|
mock_execution_setup = MagicMock()
|
|
mock_execution_setup.traces_path = str(tmp_path)
|
|
mock_ctx.execution_setup = mock_execution_setup
|
|
mock_ctx.device = None
|
|
|
|
# Initialize DataEngine
|
|
engine = DataEngine(mock_ctx)
|
|
|
|
image_bytes = b"fake_image_bytes"
|
|
|
|
# 1. Save image without OCR/UI tree
|
|
image_name1 = engine.get_or_create_image(image_bytes)
|
|
|
|
# Verify it was saved with None for OCR/UI tree
|
|
record1 = engine.storage.get_image(image_name1)
|
|
assert record1 is not None
|
|
assert record1.ocr_result is None
|
|
assert record1.ui_tree is None
|
|
|
|
# 2. Save same image with OCR/UI tree
|
|
fake_ocr = [{"text": "hello", "box": [0, 0, 10, 10]}]
|
|
fake_ui = [{"resource-id": "button1", "text": "click me"}]
|
|
|
|
image_name2 = engine.get_or_create_image(image_bytes, ui_tree=fake_ui, ocr_result=fake_ocr)
|
|
|
|
assert image_name1 == image_name2
|
|
|
|
# Verify it now has OCR/UI tree
|
|
record2 = engine.storage.get_image(image_name2)
|
|
assert record2 is not None
|
|
assert record2.ocr_result == fake_ocr
|
|
assert record2.ui_tree == fake_ui
|
|
|
|
|
|
def test_create_image_duplicate_handling(tmp_path):
|
|
from artemis.data_engine.storage import StorageManager
|
|
from artemis.data_engine.models import ImageRecord
|
|
|
|
storage = StorageManager(tmp_path / "data_engine.db", tmp_path)
|
|
image_record = ImageRecord(
|
|
image_name="test_image_hash",
|
|
timestamp=123.456,
|
|
ocr_result=None,
|
|
ui_tree=None,
|
|
extra_metadata={},
|
|
)
|
|
|
|
# First insert
|
|
storage.create_image(image_record)
|
|
|
|
# Second insert with same image_name should not raise IntegrityError due to INSERT OR IGNORE
|
|
storage.create_image(image_record)
|
|
|
|
record = storage.get_image("test_image_hash")
|
|
assert record is not None
|
|
assert record.image_name == "test_image_hash"
|
|
|
|
|
|
def test_video_recording_persistence(tmp_path):
|
|
from uuid import uuid4
|
|
from artemis.data_engine.storage import StorageManager
|
|
from artemis.data_engine.models import VideoRecordingRecord
|
|
|
|
storage = StorageManager(tmp_path / "data_engine.db", tmp_path)
|
|
video_id = uuid4()
|
|
session_id = uuid4()
|
|
|
|
record = VideoRecordingRecord(
|
|
video_id=video_id,
|
|
session_id=session_id,
|
|
device_id="emulator-5554",
|
|
start_time=100.0,
|
|
end_time=None,
|
|
local_video_path="/tmp/recording.mp4",
|
|
)
|
|
|
|
# Test insert
|
|
storage.create_video_recording(record)
|
|
|
|
persisted = storage.get_video_recording(video_id)
|
|
assert persisted is not None
|
|
assert persisted.video_id == video_id
|
|
assert persisted.session_id == session_id
|
|
assert persisted.device_id == "emulator-5554"
|
|
assert persisted.start_time == 100.0
|
|
assert persisted.end_time is None
|
|
assert persisted.local_video_path == "/tmp/recording.mp4"
|
|
|
|
# Test update
|
|
record.end_time = 150.0
|
|
record.local_video_path = "/tmp/recording_final.mp4"
|
|
storage.update_video_recording(record)
|
|
|
|
updated = storage.get_video_recording(video_id)
|
|
assert updated is not None
|
|
assert updated.end_time == 150.0
|
|
assert updated.local_video_path == "/tmp/recording_final.mp4"
|
|
|
|
|
|
def test_record_step_suppresses_identical_post_screenshot(tmp_path):
|
|
mock_ctx = MagicMock(spec=ArtemisContext)
|
|
mock_execution_setup = MagicMock()
|
|
mock_execution_setup.traces_path = str(tmp_path)
|
|
mock_ctx.execution_setup = mock_execution_setup
|
|
mock_ctx.device = None
|
|
|
|
engine = DataEngine(mock_ctx)
|
|
engine.start_session("test_goal")
|
|
same_image_bytes = b"identical_screen_bytes_123"
|
|
|
|
step_id = engine.record_step(
|
|
pre_screenshot_bytes=same_image_bytes,
|
|
post_screenshot_bytes=same_image_bytes,
|
|
summary="Test step with identical screenshot",
|
|
)
|
|
for t in list(engine._pending_threads):
|
|
t.join()
|
|
|
|
step_record = engine.storage.get_steps(engine.current_session_id)[0]
|
|
assert step_record is not None
|
|
assert step_record.pre_image_name is not None
|
|
# post_image_name must be None when no visual transition occurred
|
|
assert step_record.post_image_name is None
|
|
|
|
|
|
def test_update_step_execution_result_suppresses_identical_post_screenshot(tmp_path):
|
|
mock_ctx = MagicMock(spec=ArtemisContext)
|
|
mock_execution_setup = MagicMock()
|
|
mock_execution_setup.traces_path = str(tmp_path)
|
|
mock_ctx.execution_setup = mock_execution_setup
|
|
mock_ctx.device = None
|
|
|
|
engine = DataEngine(mock_ctx)
|
|
engine.start_session("test_goal")
|
|
image_bytes = b"pre_screen_bytes_456"
|
|
|
|
step_id = engine.record_step(
|
|
pre_screenshot_bytes=image_bytes,
|
|
summary="Initial step",
|
|
)
|
|
|
|
step_record = engine.storage.get_steps(engine.current_session_id)[0]
|
|
assert step_record.post_image_name is None
|
|
|
|
# Update with post_image_name matching pre_image_name
|
|
engine.storage.update_step_execution_result(
|
|
step_id,
|
|
{"status": "success"},
|
|
post_image_name=step_record.pre_image_name,
|
|
)
|
|
|
|
updated_step = engine.storage.get_steps(engine.current_session_id)[0]
|
|
assert updated_step.post_image_name is None
|
|
|
|
# Update with distinct post_image_name
|
|
engine.storage.update_step_execution_result(
|
|
step_id,
|
|
{"status": "success"},
|
|
post_image_name="distinct_new_hash_789",
|
|
)
|
|
|
|
updated_step_distinct = engine.storage.get_steps(engine.current_session_id)[0]
|
|
assert updated_step_distinct.post_image_name == "distinct_new_hash_789"
|