honcho/tests/unified
adavyas c73f6a0b7a
feat: Add workspace-level chat (#931)
* Add workspace-level chat (DEV-1326)

POST /v3/workspaces/{workspace_id}/chat: agentic dialectic over the whole
workspace instead of a single (observer, observed) pair. Salvaged from
plastic-labs/honcho#373 and re-grown on today's DialecticAgent:

- WorkspaceDialecticAgent subclasses DialecticAgent via four new seams
  (_get_tools, _create_tool_executor, _prefetch_intro, _trace_name) instead
  of a base-class extraction; observer/observed use empty-string sentinels.
- Routing-accelerated prefetch: workspace stats + top-5 active peers with
  their self peer-cards (pure DB, ~7ms measured) so routing-obvious queries
  resolve without a discovery tool round.
- Observation search stays pair-scoped (matches per-pair vector namespaces;
  avoids workspace-flat top-k dilution): search_memory/get_peer_card take
  observer/observed as tool arguments, with pair attribution in results.
- workspace_chat / workspace_chat_stream orchestrators, WorkspaceChatOptions
  schema (scope param seam left for the #897 scopes facade), SSE streaming,
  structured output via response_format.
- crud: get_workspace_stats, get_active_peers; format_documents_with_attribution.
- SDKs: Python Honcho.chat/chat_stream + HonchoAio mirrors; TypeScript
  honcho.chat/chatStream.
- 46 tests (route, orchestrator preflight, tool handlers, executor routing,
  attribution formatting) + unified test cases + docs.

Co-Authored-By: doria <93405247+dr-frmr@users.noreply.github.com>
Co-Authored-By: Benjamin McCormick <docterformer@protonmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: type SSE stream wrapper as AsyncIterator (basedpyright)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: silence unused db_session fixture warnings (basedpyright failOnWarnings)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: drop docs changes from this PR (defer to follow-up)

Restores docs/v3/documentation/features/chat.mdx to main's version. This
also puts back the peer-chat Structured Outputs section (#896) that the
workspace-chat commit removed as a rebase artifact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: workspace message tools deny-all under rebased session scoping

The #882 rebase changed the unscoped-observer contract from falsy to
'observer is None': resolve_session_scope looked up the workspace
executor's observer='' sentinel as a real peer with no session
memberships and denied every workspace-flat message read (search, grep,
date-range, temporal, observation context) whenever no session was
pinned — the primary workspace-chat shape. Normalize the sentinel to
None at the five read-handler crud boundaries and add regression tests
that run the tools unpinned (verified to fail without the fix).

Also from review:
- wrap the workspace prefetch in the same degrade-to-None protection
  the base agent has (an overview query error no longer 500s the
  request or kills the SSE stream after headers)
- thread session_allowlist through create_workspace_tool_executor so
  the agent-level allowlist seam is honored end to end when scopes
  (#897) wire it up; allowlisted grep is covered by a test
- deterministic name tie-break in get_active_peers ordering

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* review: SDK response_format parity, shared query sanitizer, annotations

- TS SDK: WorkspaceChatParams gains response_format; _workspaceChat/
  _workspaceChatStream consume the shared interface instead of inline
  duplicates; chat/chatStream expose responseFormat.
- Consolidate the three identical sanitize_query validators into one
  NulStripped annotation.
- workspace_chat_stream: return annotation + full docstring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* review: fold active peers into workspace stats; trace + query bounds

- Merge get_active_peers into get_workspace_stats (one discovery round
  instead of two); minimal loadout keeps a discovery tool via the merged
  stats tool. Fixed top-10 by recent activity; deeper discovery routes
  through search_messages.
- get_active_peers CRUD now aggregates over a trailing 90-day window so
  the chat-path prefetch never scans a workspace's full message history.
- Workspace agent inherits the "dialectic_chat" trace name; scope stays
  distinguished by agent_type/track_name (workspace name was already in
  telemetry context).
- Prefetch failure logs carry workspace + traceback; prompt no longer
  contrasts against a peer-level agent the model has no concept of;
  drop ticket identifiers from comments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: add `scope` to workspace chat and exclude scope peers from stats

Workspace chat is peer-unanchored, so `scope` is always a session-union
allowlist (single name or list), fail-closed when empty. Stats and
active-peer prefetch drop scope-kind peers and honor the same allowlist.

* test: teach the unified runner `workspace_chat` and parse every case

QueryAction now accepts target=workspace_chat (SDK path, including
scope). A pytest over tests/unified/test_cases/*.json keeps the four
existing workspace-chat cases — and a new scoped one — from rotting
against the schema again.

* docs: tighten workspace-chat scope docs and judge prompt

Scoped workspace_chat uses the SDK, not raw HTTP. The scope fixture's
judge now requires the in-scope tea fact, not merely the absence of the
leak. format_sse_stream matches the peer-chat one-liner.

* fix(dialectic): restore the empty-memory fallback for workspace chat

`search_memory` auto-searches messages when a pair has no observations,
but the gate only admitted `agent_type == "dialectic"`. The workspace
executor passes `workspace_dialectic`, so workspace chat got a bare
"No observations found" and answered that it knew nothing rather than
falling through to message search.

Also fixes the two unified cases that never ran: `deriver` is not a
field on `WorkspaceConfiguration`, so both aborted at load with
`extra_forbidden`. `workspace_chat_scope` additionally enables reasoning,
since it asserts scope isolation and has no reason to depend on the
fallback path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(tests/unified): fail CI when unified tests fail

`runner.run()` tallied failures into `failed_count` and printed them, but
returned nothing, and both entrypoints ignored the result. The workflow
invokes `python -m tests.unified.run` bare, so the job has gone green on
failing and unrunnable cases since it was wired up in #291.

Return the count and exit non-zero on it. `INVALID SCHEMA` already counts
toward the tally, so a malformed case now fails the job instead of being
skipped silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(unified): assert scope peers stay out of workspace chat answers

Scope peers are real peer rows, so a regression in the `scope_peer_clause`
exclusion would surface `scope.therapy` through workspace stats or the
routing prefetch. Nothing asserted against that.

Adds the check to the existing scoped query and a new unscoped one, since
the two exercise different `get_active_peers` branches. Verified by
removing the exclusion, which fails the unscoped query.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: Remove dead code references

---------

Co-authored-by: doria <93405247+dr-frmr@users.noreply.github.com>
Co-authored-by: Benjamin McCormick <docterformer@protonmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Aakash Kattelu <aakash@plasticlabs.ai>
Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
2026-08-24 15:54:23 -04:00
..
test_cases feat: Add workspace-level chat (#931) 2026-08-24 15:54:23 -04:00
README.md feat: Add workspace-level chat (#931) 2026-08-24 15:54:23 -04:00
run.py feat: Add workspace-level chat (#931) 2026-08-24 15:54:23 -04:00
runner.py feat: Add workspace-level chat (#931) 2026-08-24 15:54:23 -04:00
schema.py feat: Add workspace-level chat (#931) 2026-08-24 15:54:23 -04:00
test_schema.py feat: Add workspace-level chat (#931) 2026-08-24 15:54:23 -04:00

README.md

Unified Honcho Test System

This system allows for defining comprehensive, step-based tests for Honcho in a unified JSON format. It supports testing configuration hierarchy, multi-turn interactions, and complex assertions including LLM-as-a-judge.

Running Tests

# Run all tests in the test_cases directory
python -m tests.unified.run

# Run a specific test file
python -m tests.unified.run --test-dir tests/unified/test_cases

Test Schema

Tests are defined in JSON files. A test definition consists of a name, optional description, and a list of steps.

Structure

{
  "name": "my_test",
  "workspace_config": { ... },
  "steps": [
    { "step_type": "..." },
    ...
  ]
}

Actions

  1. Configuration:

    • set_workspace_config: Update workspace settings.
    • set_session_config: Update session settings.
  2. Interaction:

    • create_session: Create a new session, optionally with peers and config.
    • add_message: Add a single message.
    • add_messages: Add multiple messages.
    • create_scope: Create a scope and optionally add member sessions. Add the sessions before the messages you want in scope — membership only affects messages ingested after a session joins.
  3. Waiting:

    • wait: Wait for duration or "queue_empty".
  4. Querying & Assertions:

    • query: Perform an action and assert on the result.
      • target: "chat", "get_context", "get_peer_card", "get_representation", "workspace_chat"
      • scope: confine the read to a scope (or, for chat/representation/ workspace_chat, to the union of several). Valid for "chat", "get_representation", "get_context", and "workspace_chat"; get_context takes a single scope and requires observed_peer_id.

Raw HTTP vs the SDK

Most steps drive the Honcho Python SDK. create_scope and scoped chat / get_representation / get_context queries go over raw HTTP instead, because the published SDK trails the API and exposes neither. Scoped workspace_chat uses the SDK scope argument. Calling the API directly also tests the contract the SDK is generated from, so a wrong status code or response shape surfaces here rather than being masked by client-side validation.

Assertions

  • llm_judge: Use Claude to evaluate the result against a natural language prompt.
  • contains / not_contains: Substring matching.
  • exact_match: Strict equality.
  • json_match: specific key-value checks.

Example

{
  "name": "demo_config_flow",
  "steps": [
    {
      "step_type": "create_session",
      "session_id": "s1",
      "peer_configs": {
        "user": { "observe_me": true },
        "agent": { "observe_others": true }
      }
    },
    {
      "step_type": "add_message",
      "session_id": "s1",
      "peer_id": "user",
      "content": "My name is Alice."
    },
    {
      "step_type": "wait",
      "target": "queue_empty"
    },
    {
      "step_type": "query",
      "target": "chat",
      "peer_id": "agent",
      "session_id": "s1",
      "input": "Who am I?",
      "assertions": [
        {
          "assertion_type": "contains",
          "text": "Alice"
        }
      ]
    }
  ]
}