feat(core): consolidate config hub, unify daemon scheduling across all entrypoints, and fix step replay
- Unified configuration and paths under artemis.config as single source of truth - Consolidated execution across CLI, SDK, MCP, and Admin Console to default to Artemis Daemon - Fixed frontend step replay to preserve failure steps with proper sequential numbering - Implemented task deduplication, debouncing, and multi-device lock isolation - Added comprehensive end-to-end multi-port integration tests for live phone and emulator
This commit is contained in:
1 parent
e984d17635
commit
5f1ee18bc1
38 files changed
+1426
-203
No files matched your search
@@ -211,4 +211,67 @@ describe('extractStepReplayFrames', () => {
|
||||
expect(frames[0].imageUrl).toBe('/images/img_start');
|
||||
expect(frames[0].stepNumber).toBe(1);
|
||||
});
|
||||
|
||||
it('should deduplicate steps with identical step_number and prefer real physical actions', () => {
|
||||
const logs = [
|
||||
{
|
||||
step_id: 'step-click',
|
||||
step_number: 1,
|
||||
pre_image_name: 'img_screen_1',
|
||||
post_image_name: 'img_screen_2',
|
||||
action_taken: { action: 'click', coordinates: [106, 101] },
|
||||
summary: 'In Step 1, I tapped the back arrow',
|
||||
timestamp: 100
|
||||
},
|
||||
{
|
||||
step_id: 'step-failed-report',
|
||||
step_number: 1,
|
||||
pre_image_name: 'img_screen_2',
|
||||
post_image_name: null,
|
||||
action_taken: { action: 'report_task_status', args: { status: 'failed' } },
|
||||
timestamp: 200
|
||||
},
|
||||
{
|
||||
step_id: 'step-completed-report',
|
||||
step_number: 2,
|
||||
pre_image_name: 'img_screen_2',
|
||||
post_image_name: null,
|
||||
action_taken: { action: 'report_task_status', args: { status: 'completed' } },
|
||||
timestamp: 150
|
||||
}
|
||||
];
|
||||
|
||||
const frames = extractStepReplayFrames(logs);
|
||||
// Duplicate step_number: 1 merges into Step 1, while Step 2 report is faithfully displayed
|
||||
expect(frames.length).toBe(2);
|
||||
expect(frames[0].stepNumber).toBe(1);
|
||||
expect(frames[0].action?.action).toBe('click');
|
||||
expect(frames[0].actionText).toContain('Tapping Element');
|
||||
expect(frames[1].stepNumber).toBe(2);
|
||||
expect(frames[1].title).toContain('Report Task Status');
|
||||
});
|
||||
|
||||
it('should enforce strictly sequential stepNumber without duplicates', () => {
|
||||
const logs = [
|
||||
{
|
||||
step_id: 's-1',
|
||||
step_number: 1,
|
||||
pre_image_name: 'img_1',
|
||||
action_taken: { action: 'click', coordinates: [100, 100] },
|
||||
timestamp: 100
|
||||
},
|
||||
{
|
||||
step_id: 's-2',
|
||||
step_number: 2,
|
||||
pre_image_name: 'img_2',
|
||||
action_taken: { action: 'swipe', direction: 'up' },
|
||||
timestamp: 200
|
||||
}
|
||||
];
|
||||
|
||||
const frames = extractStepReplayFrames(logs);
|
||||
expect(frames.length).toBe(2);
|
||||
expect(frames[0].stepNumber).toBe(1);
|
||||
expect(frames[1].stepNumber).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -33,9 +33,15 @@ export function getActionObject(action: any): any {
|
||||
* Check if the action is a user interaction action on Android
|
||||
*/
|
||||
export function isAndroidAction(action: any): boolean {
|
||||
const act = getActionObject(action);
|
||||
if (!act) return false;
|
||||
const name = (act.name || act.action || '').toLowerCase();
|
||||
if (!action) return false;
|
||||
let name = '';
|
||||
if (typeof action === 'string') {
|
||||
name = action.toLowerCase();
|
||||
} else {
|
||||
const act = getActionObject(action);
|
||||
if (!act) return false;
|
||||
name = (act.name || act.action || '').toLowerCase();
|
||||
}
|
||||
return [
|
||||
'tap', 'click', 'click_sequence', 'input', 'input_text', 'swipe', 'scroll', 'press_key',
|
||||
'press_home', 'press_back', 'launch_app', 'manage_app', 'open_app',
|
||||
@@ -48,9 +54,16 @@ export function isAndroidAction(action: any): boolean {
|
||||
* Check if the action or tool is a task completion / submission report
|
||||
*/
|
||||
export function isReportStatusAction(action: any): boolean {
|
||||
const act = getActionObject(action);
|
||||
if (!act) return false;
|
||||
const name = (act.name || act.action || '').toLowerCase().replace(/^(_)?(self\.)?exec_/, '');
|
||||
if (!action) return false;
|
||||
let name = '';
|
||||
if (typeof action === 'string') {
|
||||
name = action.toLowerCase();
|
||||
} else {
|
||||
const act = getActionObject(action);
|
||||
if (!act) return false;
|
||||
name = (act.name || act.action || '').toLowerCase();
|
||||
}
|
||||
name = name.replace(/^(_)?(self\.)?exec_/, '');
|
||||
return name === 'report_task_status' || name === 'report_status' || name === 'submit_task_status';
|
||||
}
|
||||
|
||||
@@ -785,21 +798,31 @@ export function extractStepReplayFrames(logsOrSteps: any[]): StepReplayFrame[] {
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate and merge by step_id or step_number
|
||||
// Deduplicate and merge by step_number (fallback to step_id if step_number missing)
|
||||
const stepMap = new Map<any, any>();
|
||||
for (const step of rawSteps) {
|
||||
const key = step.step_id || step.step_number;
|
||||
const key = (step.step_number !== undefined && step.step_number !== null)
|
||||
? `step_${step.step_number}`
|
||||
: (step.step_id || Math.random());
|
||||
if (key !== undefined) {
|
||||
const existing = stepMap.get(key);
|
||||
if (existing) {
|
||||
// When merging duplicate records with the same step_number:
|
||||
// Prefer real actions over report_task_status
|
||||
const existingAct = existing.action_taken;
|
||||
const newAct = step.action_taken;
|
||||
const preferExistingAction = existingAct && isAndroidAction(existingAct) && isReportStatusAction(newAct);
|
||||
const preferNewAction = newAct && isAndroidAction(newAct) && isReportStatusAction(existingAct);
|
||||
|
||||
stepMap.set(key, {
|
||||
...existing,
|
||||
...step,
|
||||
step_number: step.step_number !== undefined ? step.step_number : existing.step_number,
|
||||
pre_image_name: step.pre_image_name || existing.pre_image_name,
|
||||
post_image_name: step.post_image_name || existing.post_image_name,
|
||||
action_taken: step.action_taken || existing.action_taken,
|
||||
pre_image_name: (preferExistingAction ? existing.pre_image_name : (step.pre_image_name || existing.pre_image_name)),
|
||||
post_image_name: (preferExistingAction ? (existing.post_image_name || step.post_image_name) : (step.post_image_name || existing.post_image_name)),
|
||||
action_taken: preferExistingAction ? existing.action_taken : (preferNewAction ? newAct : (step.action_taken || existing.action_taken)),
|
||||
last_execution_result: step.last_execution_result || existing.last_execution_result,
|
||||
summary: (preferExistingAction ? (existing.summary || step.summary) : (step.summary || existing.summary)),
|
||||
generic_tools: step.generic_tools || existing.generic_tools
|
||||
});
|
||||
} else {
|
||||
@@ -820,7 +843,8 @@ export function extractStepReplayFrames(logsOrSteps: any[]): StepReplayFrame[] {
|
||||
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const stepData = steps[i];
|
||||
const stepNum = Number(stepData.step_number ?? (i + 1));
|
||||
// Guarantee 1-indexed sequential step numbering so frames never duplicate numbers
|
||||
const stepNum = i + 1;
|
||||
const act = stepData.action_taken;
|
||||
const title = act ? (getActionTitle(act) || 'Action') : `Step ${stepNum}`;
|
||||
const coords = act ? getActionCoords(act) : '';
|
||||
|
||||
Reference in new issue
Block a user