fix(stage-state): v0.18.2b — symmetric agent_role via in-class resolve_role seam
QA REJECT-P1 remediation (seq 29). start()/done()/is_done()/get_output()
were asymmetric: start() passed the mapped role but done()/is_done()/
get_output() passed '' via the default, addressing a different unique key
(run_id, stage, agent_role, item_key). On every normal run this orphaned
the start() row in 'running'; on resume is_done() always returned false
→ duplicate-post vector.
Fix: add resolve_role(stage, agent_role) private static seam in
Run_Stage_State. When the caller passes '' the role is derived from
Stage_Display_Map::default_agent_role(stage) before any DB operation.
Non-empty roles (Phase-2 fan-out) pass through unchanged. All four
write/lookup methods (start, done, fail, get) call resolve_role.
is_done() and get_output() go through get() so they benefit too.
Mid-run-upgrade compat (v0.18.1 → v0.18.2): get() falls back to a
legacy role='' row on miss so a run started under v0.18.1 (all rows
have agent_role='') can still resume under v0.18.2. One extra indexed
query, only on miss, only for known stages.
Tests: two new RunStageStateTest cases —
test_role_tagged_start_and_done_are_symmetric: start+done with
'editor', assert is_done('editor')=true and is_done('')=false.
test_legacy_empty_role_row_is_found_on_fallback: resolve to 'editor',
primary miss, legacy role='' hit → is_done=true.
ARCHITECTURE.md §22: agent_role column and #22 Phase 1 description
updated — no longer 'Phase-2 only / '' default'; now documents Phase 1
population from Stage_Display_Map.
Agent-Session: prab-v0182-maintenance-20260612
This commit is contained in:
@@ -137,4 +137,76 @@ class RunStageStateTest extends BaseTestCase {
|
||||
$this->assertFalse( \PRAutoBlogger_Run_Stage_State::is_done( 'run-1', 'draft' ) );
|
||||
$this->assertNull( \PRAutoBlogger_Run_Stage_State::get_output( 'run-1', 'draft' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* start(role) → done(role) → is_done(role) must be true; is_done('') must
|
||||
* be false — this catches the v0.18.2 regression where done() passed ''
|
||||
* while start() passed a real role, so done() addressed a different row.
|
||||
*
|
||||
* In this test the wpdb mock records inserts via query() and simulates
|
||||
* the SELECT via get_row():
|
||||
* is_done('review','editor') → get_row finds the done row → true.
|
||||
* is_done('review','') → resolve_role→'editor'; primary: miss;
|
||||
* legacy fallback (role=''): miss → false.
|
||||
*/
|
||||
public function test_role_tagged_start_and_done_are_symmetric(): void {
|
||||
$get_row_results = array(
|
||||
array( 'status' => 'done', 'meta_json' => null ), // primary hit for 'editor'.
|
||||
null, // primary miss for legacy check (role='editor' resolved, not '').
|
||||
null, // legacy fallback (role='') also not found.
|
||||
);
|
||||
$call_index = 0;
|
||||
$this->wpdb->method( 'get_var' )->willReturn( 'wp_prautoblogger_run_stages' );
|
||||
$this->wpdb->method( 'query' )->willReturn( 1 );
|
||||
$this->wpdb->method( 'get_row' )->willReturnCallback(
|
||||
static function () use ( &$get_row_results, &$call_index ) {
|
||||
$result = $get_row_results[ $call_index ] ?? null;
|
||||
++$call_index;
|
||||
return $result;
|
||||
}
|
||||
);
|
||||
|
||||
\PRAutoBlogger_Run_Stage_State::start( 'run-2', 'review', 'editor', 'idea:xyz' );
|
||||
\PRAutoBlogger_Run_Stage_State::done( 'run-2', 'review', 'editor', 'idea:xyz' );
|
||||
|
||||
// Explicit role finds the done row.
|
||||
$this->assertTrue(
|
||||
\PRAutoBlogger_Run_Stage_State::is_done( 'run-2', 'review', 'editor', 'idea:xyz' ),
|
||||
'is_done with explicit role should be true'
|
||||
);
|
||||
// '' resolves to 'editor'; primary misses; legacy '' fallback also misses.
|
||||
$this->assertFalse(
|
||||
\PRAutoBlogger_Run_Stage_State::is_done( 'run-2', 'review', '', 'idea:xyz' ),
|
||||
'is_done with legacy empty role should be false (no legacy row exists)'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mid-run-upgrade resume: a row created by v0.18.1 (agent_role='') must
|
||||
* still be found by is_done('') under v0.18.2 via the legacy fallback.
|
||||
* resolve_role('review','') → 'editor'; primary query misses; legacy
|
||||
* fallback (role='') hits → is_done returns true.
|
||||
*/
|
||||
public function test_legacy_empty_role_row_is_found_on_fallback(): void {
|
||||
$call_index = 0;
|
||||
$get_row_results = array(
|
||||
null, // primary query (role='editor'): miss.
|
||||
array( 'status' => 'done', 'meta_json' => null ), // legacy fallback (role=''): hit.
|
||||
);
|
||||
$this->wpdb->method( 'get_var' )->willReturn( 'wp_prautoblogger_run_stages' );
|
||||
$this->wpdb->method( 'get_row' )->willReturnCallback(
|
||||
static function () use ( &$get_row_results, &$call_index ) {
|
||||
$result = $get_row_results[ $call_index ] ?? null;
|
||||
++$call_index;
|
||||
return $result;
|
||||
}
|
||||
);
|
||||
|
||||
// is_done('') resolves to 'editor', misses, falls back to role='',
|
||||
// finds the v0.18.1 row → true.
|
||||
$this->assertTrue(
|
||||
\PRAutoBlogger_Run_Stage_State::is_done( 'run-3', 'review', '', 'idea:abc' ),
|
||||
'v0.18.1 empty-role row must be found via legacy fallback'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user