test: request_context tests
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
# Iteration 8 — Test Implementation Notes
|
||||||
|
|
||||||
|
## Plan file addressed
|
||||||
|
|
||||||
|
`docs/testing/plans/08-request-context.md`
|
||||||
|
|
||||||
|
## Tests created
|
||||||
|
|
||||||
|
### src/config/request_context.rs (22 new tests, 51 total in file)
|
||||||
|
|
||||||
|
| Test name | What it verifies |
|
||||||
|
|---|---|
|
||||||
|
| `state_empty_context` | Empty context → empty StateFlags |
|
||||||
|
| `state_with_role_only` | Role set → ROLE flag |
|
||||||
|
| `state_with_empty_session` | Empty session → SESSION_EMPTY flag |
|
||||||
|
| `state_flags_combine_role_and_session` | Multiple flags combine correctly |
|
||||||
|
| `role_info_errors_when_no_role` | No role → error |
|
||||||
|
| `role_info_succeeds_with_role` | Role present → exports prompt |
|
||||||
|
| `agent_info_errors_when_no_agent` | No agent → error |
|
||||||
|
| `rag_info_errors_when_no_rag` | No RAG → error |
|
||||||
|
| `use_role_obj_errors_when_agent_active` | Agent blocks role assignment |
|
||||||
|
| `exit_rag_clears_rag` | exit_rag() sets rag to None |
|
||||||
|
| `discontinuous_last_message_sets_continuous_false` | Marks last message non-continuous |
|
||||||
|
| `discontinuous_last_message_noop_when_none` | No last message → no-op |
|
||||||
|
| `before_chat_completion_sets_last_message` | Creates LastMessage with empty output |
|
||||||
|
| `role_like_mut_returns_none_when_empty` | No active scope → None |
|
||||||
|
| `role_like_mut_returns_role_when_only_role` | Role only → returns role |
|
||||||
|
| `role_like_mut_prefers_session_over_role` | Session takes priority |
|
||||||
|
| `working_mode_cmd` | CMD mode flags correct |
|
||||||
|
| `working_mode_repl` | REPL mode flags correct |
|
||||||
|
| `session_file_returns_yaml_path` | Correct .yaml suffix |
|
||||||
|
| `session_file_with_subdir` | subdir/name → nested path |
|
||||||
|
| `is_compressing_session_false_when_no_session` | No session → false |
|
||||||
|
| `is_compressing_session_false_with_default_session` | Default session → false |
|
||||||
|
|
||||||
|
**Total: 22 new tests (272 total in suite)**
|
||||||
|
|
||||||
|
## Bugs discovered
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Observations for future iterations
|
||||||
|
|
||||||
|
1. **Rag struct has no Default**: Rag requires an AppConfig, name,
|
||||||
|
embedding model, and HNSW index. Can't create test instances
|
||||||
|
without heavy setup. RAG-related state tests (state with RAG,
|
||||||
|
exit_rag with actual RAG) deferred.
|
||||||
|
|
||||||
|
2. **role_like_mut priority is session > agent > role > None**:
|
||||||
|
The session-over-role priority is verified. Agent priority
|
||||||
|
can't be easily tested without agent init (filesystem).
|
||||||
|
|
||||||
|
3. **StateFlags is a bitflags type**: Tested empty, individual
|
||||||
|
flags (ROLE, SESSION_EMPTY), and combinations. The SESSION
|
||||||
|
flag (non-empty session) requires adding messages to a session
|
||||||
|
which needs more setup — deferred.
|
||||||
|
|
||||||
|
4. **info() and sysinfo() require model provider config**: These
|
||||||
|
format system info strings that include model details. Testing
|
||||||
|
requires a valid model provider configuration.
|
||||||
|
|
||||||
|
5. **The RequestContext test file now has 51 tests** spanning
|
||||||
|
iterations 1, 4, 5, 7, and 8. It's the most heavily tested
|
||||||
|
module, which matches its role as the central state container.
|
||||||
|
|
||||||
|
## Next iteration
|
||||||
|
|
||||||
|
Plan file 09: REPL Commands — REPL command handlers, state
|
||||||
|
assertions, argument parsing.
|
||||||
@@ -10,59 +10,77 @@ and chat completion lifecycle.
|
|||||||
## Behaviors to test
|
## Behaviors to test
|
||||||
|
|
||||||
### State management
|
### State management
|
||||||
- [ ] info() returns formatted system info
|
- [ ] info() returns formatted system info (requires model provider config)
|
||||||
- [ ] state() returns correct StateFlags combination
|
- [x] state() returns correct StateFlags combination
|
||||||
- [ ] current_model() returns active model
|
- [ ] current_model() returns active model (tested implicitly via extract_role)
|
||||||
- [ ] role_info(), session_info(), rag_info(), agent_info() format correctly
|
- [x] role_info() errors when no role, succeeds with role
|
||||||
- [ ] sysinfo() returns system details
|
- [ ] session_info() format (requires filesystem for sessions)
|
||||||
- [ ] working_mode correctly distinguishes Repl vs Cmd
|
- [x] rag_info() errors when no rag
|
||||||
|
- [x] agent_info() errors when no agent
|
||||||
|
- [ ] sysinfo() returns system details (requires model provider config)
|
||||||
|
- [x] working_mode correctly distinguishes Repl vs Cmd
|
||||||
|
|
||||||
### Scope transitions
|
### Scope transitions
|
||||||
- [ ] use_role changes role, rebuilds tool scope
|
- [x] use_role changes role (via use_role_obj)
|
||||||
- [ ] use_session creates/loads session, rebuilds tool scope
|
- [ ] use_session creates/loads session, rebuilds tool scope (async + filesystem)
|
||||||
- [ ] use_agent initializes agent with all subsystems
|
- [x] use_agent initializes agent with all subsystems (via exit_agent test)
|
||||||
- [ ] exit_role clears role
|
- [x] exit_role clears role
|
||||||
- [ ] exit_session saves and clears session
|
- [x] exit_session saves and clears session
|
||||||
- [ ] exit_agent clears agent, supervisor, rag, session
|
- [x] exit_agent clears agent, supervisor, rag, session
|
||||||
- [ ] exit_rag clears rag
|
- [x] exit_rag clears rag
|
||||||
- [ ] bootstrap_tools rebuilds tool scope with global MCP
|
- [ ] bootstrap_tools rebuilds tool scope with global MCP (async + MCP servers)
|
||||||
|
|
||||||
### Chat completion lifecycle
|
### Chat completion lifecycle
|
||||||
- [ ] before_chat_completion sets up for API call
|
- [x] before_chat_completion sets up for API call
|
||||||
- [ ] after_chat_completion saves messages, updates state
|
- [ ] after_chat_completion saves messages, updates state (async + client)
|
||||||
- [ ] discontinuous_last_message marks last message as non-continuous
|
- [x] discontinuous_last_message marks last message as non-continuous
|
||||||
|
|
||||||
### ToolScope management
|
### ToolScope management
|
||||||
- [ ] rebuild_tool_scope creates fresh Functions
|
- [x] rebuild_tool_scope creates fresh Functions
|
||||||
- [ ] rebuild_tool_scope acquires MCP servers via factory
|
- [ ] rebuild_tool_scope acquires MCP servers via factory (requires live MCP)
|
||||||
- [ ] rebuild_tool_scope appends user interaction functions in REPL mode
|
- [x] rebuild_tool_scope appends user interaction functions in REPL mode
|
||||||
- [ ] rebuild_tool_scope appends MCP meta functions for started servers
|
- [ ] rebuild_tool_scope appends MCP meta functions for started servers (requires live MCP)
|
||||||
- [ ] Tool tracker preserved across scope rebuilds
|
- [x] Tool tracker preserved across scope rebuilds
|
||||||
|
|
||||||
### AgentRuntime management
|
### AgentRuntime management
|
||||||
- [ ] agent_runtime populated by use_agent
|
- [x] agent_runtime populated by use_agent (via exit_agent test)
|
||||||
- [ ] agent_runtime cleared by exit_agent
|
- [x] agent_runtime cleared by exit_agent
|
||||||
- [ ] Accessor methods (current_depth, supervisor, inbox, etc.) return
|
- [x] Accessor methods (current_depth, supervisor, inbox, etc.) return
|
||||||
correct values when agent active
|
correct values when agent active
|
||||||
- [ ] Accessor methods return defaults when no agent
|
- [x] Accessor methods return defaults when no agent
|
||||||
|
|
||||||
### Settings update
|
### Settings update
|
||||||
- [ ] update() handles all .set keys correctly
|
- [ ] update() handles all .set keys correctly (requires REPL command infra)
|
||||||
- [ ] update_app_config() clones and replaces Arc properly
|
- [x] update_app_config() clones and replaces Arc properly
|
||||||
- [ ] delete() handles all delete subcommands
|
- [ ] delete() handles all delete subcommands (requires REPL command infra)
|
||||||
|
|
||||||
### Session helpers
|
### Session helpers
|
||||||
- [ ] list_sessions() returns session names
|
- [ ] list_sessions() returns session names (requires filesystem)
|
||||||
- [ ] list_autoname_sessions() returns auto-named sessions
|
- [ ] list_autoname_sessions() returns auto-named sessions (requires filesystem)
|
||||||
- [ ] session_file() returns correct path
|
- [x] session_file() returns correct path
|
||||||
- [ ] save_session() persists session
|
- [ ] save_session() persists session (requires filesystem)
|
||||||
- [ ] empty_session() clears messages
|
- [x] empty_session() clears messages
|
||||||
|
|
||||||
## Context switching scenarios
|
## Context switching scenarios
|
||||||
- [ ] No state → use_role → exit_role → no state
|
- [x] No state → use_role → exit_role → no state
|
||||||
- [ ] No state → use_agent → exit_agent → no state
|
- [x] No state → use_agent → exit_agent → no state
|
||||||
- [ ] Role → use_agent (error: agent requires exiting role first)
|
- [x] Agent active → use_role_obj errors
|
||||||
- [ ] Agent → exit_agent → use_role (clean transition)
|
- [ ] Agent → exit_agent → use_role (clean transition) (async)
|
||||||
|
|
||||||
|
## Additional behaviors tested (not in original plan)
|
||||||
|
|
||||||
|
- [x] state() empty context returns empty flags
|
||||||
|
- [x] state() role only → ROLE flag
|
||||||
|
- [x] state() empty session → SESSION_EMPTY flag
|
||||||
|
- [x] state() role + session flags combine
|
||||||
|
- [x] discontinuous_last_message noop when no last_message
|
||||||
|
- [x] before_chat_completion creates LastMessage with empty output and continuous=true
|
||||||
|
- [x] role_like_mut returns None when no active scope
|
||||||
|
- [x] role_like_mut returns role when only role active
|
||||||
|
- [x] role_like_mut prefers session over role
|
||||||
|
- [x] session_file handles subdir/name format
|
||||||
|
- [x] is_compressing_session false with no session
|
||||||
|
- [x] is_compressing_session false with default session
|
||||||
|
|
||||||
## Old code reference
|
## Old code reference
|
||||||
- `src/config/request_context.rs` — all methods
|
- `src/config/request_context.rs` — all methods
|
||||||
|
|||||||
@@ -2879,4 +2879,199 @@ mod tests {
|
|||||||
assert!(names.contains(&"mcp_invoke_github"));
|
assert!(names.contains(&"mcp_invoke_github"));
|
||||||
assert!(!names.contains(&"mcp_invoke_slack"));
|
assert!(!names.contains(&"mcp_invoke_slack"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_empty_context() {
|
||||||
|
let ctx = create_test_ctx();
|
||||||
|
assert_eq!(ctx.state(), StateFlags::empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_with_role_only() {
|
||||||
|
let mut ctx = create_test_ctx();
|
||||||
|
ctx.role = Some(Role::new("r", "p"));
|
||||||
|
assert!(ctx.state().contains(StateFlags::ROLE));
|
||||||
|
assert!(!ctx.state().contains(StateFlags::SESSION));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_with_empty_session() {
|
||||||
|
let mut ctx = create_test_ctx();
|
||||||
|
ctx.session = Some(Session::default());
|
||||||
|
assert!(ctx.state().contains(StateFlags::SESSION_EMPTY));
|
||||||
|
assert!(!ctx.state().contains(StateFlags::SESSION));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_flags_combine_role_and_session() {
|
||||||
|
let mut ctx = create_test_ctx();
|
||||||
|
ctx.session = Some(Session::default());
|
||||||
|
ctx.role = Some(Role::new("r", "p"));
|
||||||
|
let state = ctx.state();
|
||||||
|
assert!(state.contains(StateFlags::SESSION_EMPTY));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn role_info_errors_when_no_role() {
|
||||||
|
let ctx = create_test_ctx();
|
||||||
|
assert!(ctx.role_info().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn role_info_succeeds_with_role() {
|
||||||
|
let mut ctx = create_test_ctx();
|
||||||
|
ctx.role = Some(Role::new("test", "be helpful"));
|
||||||
|
let info = ctx.role_info().unwrap();
|
||||||
|
assert!(info.contains("be helpful"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agent_info_errors_when_no_agent() {
|
||||||
|
let ctx = create_test_ctx();
|
||||||
|
assert!(ctx.agent_info().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rag_info_errors_when_no_rag() {
|
||||||
|
let ctx = create_test_ctx();
|
||||||
|
assert!(ctx.rag_info().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn use_role_obj_errors_when_agent_active() {
|
||||||
|
let _guard = TestConfigDirGuard::new();
|
||||||
|
let mut ctx = create_test_ctx();
|
||||||
|
let app = ctx.app.config.clone();
|
||||||
|
let agent_name = format!(
|
||||||
|
"test_agent_{}",
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos()
|
||||||
|
);
|
||||||
|
let agent_dir = paths::agent_data_dir(&agent_name);
|
||||||
|
create_dir_all(&agent_dir).unwrap();
|
||||||
|
write(
|
||||||
|
agent_dir.join("config.yaml"),
|
||||||
|
format!("name: {agent_name}\ninstructions: hi\n"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
ctx.use_agent(&app, &agent_name, None, crate::utils::create_abort_signal())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = ctx.use_role_obj(Role::new("r", "p"));
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().to_string().contains("using a agent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exit_rag_clears_rag() {
|
||||||
|
let mut ctx = create_test_ctx();
|
||||||
|
assert!(ctx.rag.is_none());
|
||||||
|
ctx.exit_rag().unwrap();
|
||||||
|
assert!(ctx.rag.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discontinuous_last_message_sets_continuous_false() {
|
||||||
|
let mut ctx = create_test_ctx();
|
||||||
|
let input = Input::from_str(&ctx, "test", None);
|
||||||
|
ctx.last_message = Some(LastMessage::new(input, "reply".to_string()));
|
||||||
|
assert!(ctx.last_message.as_ref().unwrap().continuous);
|
||||||
|
ctx.discontinuous_last_message();
|
||||||
|
assert!(!ctx.last_message.as_ref().unwrap().continuous);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discontinuous_last_message_noop_when_none() {
|
||||||
|
let mut ctx = create_test_ctx();
|
||||||
|
assert!(ctx.last_message.is_none());
|
||||||
|
ctx.discontinuous_last_message();
|
||||||
|
assert!(ctx.last_message.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn before_chat_completion_sets_last_message() {
|
||||||
|
let mut ctx = create_test_ctx();
|
||||||
|
let input = Input::from_str(&ctx, "hello", None);
|
||||||
|
ctx.before_chat_completion(&input).unwrap();
|
||||||
|
assert!(ctx.last_message.is_some());
|
||||||
|
let lm = ctx.last_message.as_ref().unwrap();
|
||||||
|
assert_eq!(lm.output, "");
|
||||||
|
assert!(lm.continuous);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn role_like_mut_returns_none_when_empty() {
|
||||||
|
let mut ctx = create_test_ctx();
|
||||||
|
assert!(ctx.role_like_mut().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn role_like_mut_returns_role_when_only_role() {
|
||||||
|
let mut ctx = create_test_ctx();
|
||||||
|
ctx.role = Some(Role::new("r", "p"));
|
||||||
|
assert!(ctx.role_like_mut().is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn role_like_mut_prefers_session_over_role() {
|
||||||
|
let mut ctx = create_test_ctx();
|
||||||
|
ctx.role = Some(Role::new("r", "p"));
|
||||||
|
ctx.session = Some(Session::default());
|
||||||
|
let rl = ctx.role_like_mut().unwrap();
|
||||||
|
rl.set_temperature(Some(0.5));
|
||||||
|
assert_eq!(ctx.session.as_ref().unwrap().temperature(), Some(0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn working_mode_cmd() {
|
||||||
|
let ctx = RequestContext::new(default_app_state(), WorkingMode::Cmd);
|
||||||
|
assert!(ctx.working_mode.is_cmd());
|
||||||
|
assert!(!ctx.working_mode.is_repl());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn working_mode_repl() {
|
||||||
|
let ctx = RequestContext::new(default_app_state(), WorkingMode::Repl);
|
||||||
|
assert!(ctx.working_mode.is_repl());
|
||||||
|
assert!(!ctx.working_mode.is_cmd());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_file_returns_yaml_path() {
|
||||||
|
let ctx = create_test_ctx();
|
||||||
|
let path = ctx.session_file("my-session");
|
||||||
|
assert!(path.to_string_lossy().ends_with("my-session.yaml"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_file_with_subdir() {
|
||||||
|
let ctx = create_test_ctx();
|
||||||
|
let path = ctx.session_file("subdir/my-session");
|
||||||
|
let path_str = path.to_string_lossy();
|
||||||
|
assert!(path_str.contains("subdir"));
|
||||||
|
assert!(path_str.ends_with("my-session.yaml"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_compressing_session_false_when_no_session() {
|
||||||
|
let ctx = create_test_ctx();
|
||||||
|
assert!(!ctx.is_compressing_session());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_compressing_session_false_with_default_session() {
|
||||||
|
let mut ctx = create_test_ctx();
|
||||||
|
ctx.session = Some(Session::default());
|
||||||
|
assert!(!ctx.is_compressing_session());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user