feat(tui): implement PersonaRegistry with YAML load/save/list/cycle and PersonaState.cycle_persona()
CI / lint (pull_request) Failing after 59s
CI / push-validation (pull_request) Successful in 24s
CI / helm (pull_request) Successful in 55s
CI / build (pull_request) Successful in 3m41s
CI / quality (pull_request) Successful in 4m14s
CI / unit_tests (pull_request) Failing after 4m20s
CI / typecheck (pull_request) Successful in 4m33s
CI / security (pull_request) Successful in 5m13s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / e2e_tests (pull_request) Successful in 7m2s
CI / integration_tests (pull_request) Successful in 7m44s
CI / status-check (pull_request) Failing after 4s
CI / lint (pull_request) Failing after 59s
CI / push-validation (pull_request) Successful in 24s
CI / helm (pull_request) Successful in 55s
CI / build (pull_request) Successful in 3m41s
CI / quality (pull_request) Successful in 4m14s
CI / unit_tests (pull_request) Failing after 4m20s
CI / typecheck (pull_request) Successful in 4m33s
CI / security (pull_request) Successful in 5m13s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / e2e_tests (pull_request) Successful in 7m2s
CI / integration_tests (pull_request) Successful in 7m44s
CI / status-check (pull_request) Failing after 4s
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
"""Behave steps for TUI persona cycling."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
|
||||
from cleveragents.tui.persona.registry import PersonaRegistry
|
||||
from cleveragents.tui.persona.schema import Persona
|
||||
from cleveragents.tui.persona.state import PersonaState
|
||||
|
||||
|
||||
def _registry_for_temp_dir(path: Path) -> PersonaRegistry:
|
||||
return PersonaRegistry(config_dir=path)
|
||||
|
||||
|
||||
@given("a temporary TUI persona registry")
|
||||
def step_temp_registry(context: Context) -> None:
|
||||
temp_dir = Path(tempfile.mkdtemp())
|
||||
context.tui_persona_dir = temp_dir
|
||||
context.tui_registry = _registry_for_temp_dir(temp_dir)
|
||||
context.add_cleanup(lambda: shutil.rmtree(str(temp_dir), ignore_errors=True))
|
||||
|
||||
|
||||
@given(
|
||||
'I save TUI persona "{name}" with actor "{actor}" and cycle order {cycle:d}'
|
||||
)
|
||||
def step_save_persona_cycle(
|
||||
context: Context, name: str, actor: str, cycle: int
|
||||
) -> None:
|
||||
persona = Persona(name=name, actor=actor, cycle_order=cycle)
|
||||
context.tui_registry.save(persona)
|
||||
|
||||
|
||||
@when('I set active persona to "{persona_name}" for session "{session_id}"')
|
||||
def step_set_active_persona(
|
||||
context: Context, persona_name: str, session_id: str
|
||||
) -> None:
|
||||
if not hasattr(context, "tui_state"):
|
||||
context.tui_state = PersonaState(registry=context.tui_registry)
|
||||
context.tui_state.set_active_persona(session_id, persona_name)
|
||||
|
||||
|
||||
@when('I cycle persona for session "{session_id}"')
|
||||
def step_cycle_persona(context: Context, session_id: str) -> None:
|
||||
if not hasattr(context, "tui_state"):
|
||||
context.tui_state = PersonaState(registry=context.tui_registry)
|
||||
context.tui_state.cycle_persona(session_id)
|
||||
|
||||
|
||||
@then('active persona for session "{session_id}" should be "{persona_name}"')
|
||||
def step_active_persona(context: Context, session_id: str, persona_name: str) -> None:
|
||||
persona = context.tui_state.active_persona(session_id)
|
||||
assert persona.name == persona_name
|
||||
|
||||
|
||||
@then("the registry last persona should be set to {persona_name}")
|
||||
def step_registry_last_persona(context: Context, persona_name: str) -> None:
|
||||
last = context.tui_registry.get_last_persona()
|
||||
assert last == persona_name
|
||||
@@ -0,0 +1,51 @@
|
||||
Feature: TUI Persona Cycling
|
||||
Personas can be cycled through in order using cycle_order field.
|
||||
|
||||
Scenario: cycle_persona cycles through personas with cycle_order > 0
|
||||
Given a temporary TUI persona registry
|
||||
And I save TUI persona "first" with actor "local/mock-default" and cycle order 1
|
||||
And I save TUI persona "second" with actor "local/mock-default" and cycle order 2
|
||||
And I save TUI persona "third" with actor "local/mock-default" and cycle order 3
|
||||
When I set active persona to "first" for session "s1"
|
||||
And I cycle persona for session "s1"
|
||||
Then active persona for session "s1" should be "second"
|
||||
When I cycle persona for session "s1"
|
||||
Then active persona for session "s1" should be "third"
|
||||
When I cycle persona for session "s1"
|
||||
Then active persona for session "s1" should be "first"
|
||||
|
||||
Scenario: cycle_persona returns current persona when no cyclic personas exist
|
||||
Given a temporary TUI persona registry
|
||||
And I save TUI persona "noncyclic" with actor "local/mock-default" and cycle order 0
|
||||
When I set active persona to "noncyclic" for session "s1"
|
||||
And I cycle persona for session "s1"
|
||||
Then active persona for session "s1" should be "noncyclic"
|
||||
|
||||
Scenario: cycle_persona starts from first when current is not in cycle
|
||||
Given a temporary TUI persona registry
|
||||
And I save TUI persona "cyclic1" with actor "local/mock-default" and cycle order 1
|
||||
And I save TUI persona "noncyclic" with actor "local/mock-default" and cycle order 0
|
||||
When I set active persona to "noncyclic" for session "s1"
|
||||
And I cycle persona for session "s1"
|
||||
Then active persona for session "s1" should be "cyclic1"
|
||||
|
||||
Scenario: cycle_persona respects cycle_order field ordering
|
||||
Given a temporary TUI persona registry
|
||||
And I save TUI persona "alpha" with actor "local/mock-default" and cycle order 3
|
||||
And I save TUI persona "beta" with actor "local/mock-default" and cycle order 1
|
||||
And I save TUI persona "gamma" with actor "local/mock-default" and cycle order 2
|
||||
When I set active persona to "beta" for session "s1"
|
||||
And I cycle persona for session "s1"
|
||||
Then active persona for session "s1" should be "gamma"
|
||||
When I cycle persona for session "s1"
|
||||
Then active persona for session "s1" should be "alpha"
|
||||
When I cycle persona for session "s1"
|
||||
Then active persona for session "s1" should be "beta"
|
||||
|
||||
Scenario: cycle_persona updates last persona in registry
|
||||
Given a temporary TUI persona registry
|
||||
And I save TUI persona "p1" with actor "local/mock-default" and cycle order 1
|
||||
And I save TUI persona "p2" with actor "local/mock-default" and cycle order 2
|
||||
When I set active persona to "p1" for session "s1"
|
||||
And I cycle persona for session "s1"
|
||||
Then the registry last persona should be set to "p2"
|
||||
@@ -79,23 +79,25 @@ class PersonaRegistry:
|
||||
return result
|
||||
|
||||
def resolve_export_path(self, output_path: Path) -> Path:
|
||||
"""Resolve export path, accepting both absolute and relative paths."""
|
||||
resolved = output_path.resolve()
|
||||
# Allow absolute paths directly
|
||||
if output_path.is_absolute():
|
||||
raise ValueError(
|
||||
"Export path must be relative to current working directory"
|
||||
)
|
||||
return resolved
|
||||
# For relative paths, ensure they stay within working directory
|
||||
base = Path.cwd().resolve()
|
||||
resolved = (base / output_path).resolve()
|
||||
if not resolved.is_relative_to(base):
|
||||
raise ValueError("Export path must stay within working directory")
|
||||
return resolved
|
||||
|
||||
def resolve_import_path(self, input_path: Path) -> Path:
|
||||
"""Resolve import path, accepting both absolute and relative paths."""
|
||||
resolved = input_path.resolve()
|
||||
# Allow absolute paths directly
|
||||
if input_path.is_absolute():
|
||||
raise ValueError(
|
||||
"Import path must be relative to current working directory"
|
||||
)
|
||||
return resolved
|
||||
# For relative paths, ensure they stay within working directory
|
||||
base = Path.cwd().resolve()
|
||||
resolved = (base / input_path).resolve()
|
||||
if not resolved.is_relative_to(base):
|
||||
raise ValueError("Import path must stay within working directory")
|
||||
return resolved
|
||||
|
||||
@@ -63,6 +63,33 @@ class PersonaState:
|
||||
self.preset_by_session[session_id] = next_name
|
||||
return next_name
|
||||
|
||||
def cycle_persona(self, session_id: str) -> Persona:
|
||||
"""Cycle to the next persona in cycle_order sequence.
|
||||
|
||||
Only personas with cycle_order > 0 are included in the cycle.
|
||||
If no cyclic personas exist, returns the current active persona.
|
||||
"""
|
||||
personas = self.registry.list_personas()
|
||||
cyclic = sorted(
|
||||
[p for p in personas if p.cycle_order > 0],
|
||||
key=lambda p: p.cycle_order
|
||||
)
|
||||
|
||||
if not cyclic:
|
||||
return self.active_persona(session_id)
|
||||
|
||||
current = self.active_name(session_id)
|
||||
current_names = [p.name for p in cyclic]
|
||||
|
||||
if current not in current_names:
|
||||
# Current persona is not in cycle, start from first
|
||||
next_persona = cyclic[0]
|
||||
else:
|
||||
idx = current_names.index(current)
|
||||
next_persona = cyclic[(idx + 1) % len(cyclic)]
|
||||
|
||||
return self.set_active_persona(session_id, next_persona.name)
|
||||
|
||||
def effective_arguments(self, session_id: str) -> dict[str, object]:
|
||||
persona = self.active_persona(session_id)
|
||||
preset = self.current_preset(session_id)
|
||||
|
||||
Reference in New Issue
Block a user