Per-Project Integration Architecture

Problem

Organizations have multiple Jira projects with different configurations:

TestIntel currently has a single global config for each integration. This limits users to one Jira project and one integration type at a time.

Design

Extended Project Model

Each TestIntel project carries its own integration configuration:


class ProjectIntegration(BaseModel):
    type: str = ""                    # "zephyr" | "xray" | "jira" | ""
    jira_project_key: str = ""        # e.g. "HR", "SHOP"
    jira_board_id: str = ""           # optional: specific Jira board
    field_mapping: dict = {}          # Jira field ID -> TestIntel field
    status_mapping: dict = {}         # Jira status -> TestIntel test_status
    discovered_schema: dict = {}      # Raw schema from discover

class Project(BaseModel):
    name: str
    org: str = ""
    repos: list[Repository] = []
    workspace_path: str = ""          # local folder path
    jira_project_key: str = ""        # legacy — still works as fallback
    kb_scope: str = ""
    output_path: str = ""
    test_id: TestIdConfig = TestIdConfig()
    integration: ProjectIntegration = ProjectIntegration()

Config Resolution (backward compatible)

When a sync operation runs, config is resolved in this order:

  1. Default project's integration config — if project.integration.type is set
  1. Global config — falls back to config.yaml zephyr/xray/jira sections
  1. Error — if neither is configured

This means existing setups with global config keep working. New multi-project setups configure per-project.

Field Mapping

For plain Jira (no Xray/Zephyr), users configure which fields map to TestIntel fields:


# Stored in project integration config
field_mapping:
  issue_type: "Test Case"              # Jira issue type to pull/push
  filter_label: ""                     # Optional: only issues with this label
  filter_jql: ""                       # Optional: custom JQL override
  steps: "customfield_10042"           # or "description"
  expected_result: "customfield_10043" # or ""
  test_status: "status"                # standard Jira field
  priority: "priority"                 # standard Jira field
  tags: "labels"                       # standard Jira field

Status Mapping

Maps Jira workflow statuses to TestIntel lifecycle statuses:


status_mapping:
  "To Do": "Draft"
  "In Progress": "Draft"
  "In Review": "Draft"
  "Done": "Approved"
  "Closed": "Deprecated"

Discovery (per-project)

testintel discover is extended to:

  1. Read the default project's jira_project_key
  1. Detect which integration is available (Zephyr API responds? Xray API responds? Plain Jira?)
  1. Scan available issue types, custom fields, workflow statuses
  1. Write discovered schema to the project's integration config
  1. For Zephyr/Xray: same as current discover behavior
  1. For plain Jira: list issue types → user picks one, list fields → auto-map known patterns

Sync Endpoint Changes

Current:


# Global config passed at router init
sync_router(storage, cfg.zephyr, cfg.xray, api_key, ...)

New:


# Sync endpoints resolve config from default project
def _resolve_integration(project):
    proj = pm.get(project) or pm.active()
    if proj.integration.type:
        return proj.integration
    # Fallback to global config
    return global_config

UI Changes

Implementation Status

All 6 phases implemented and tested (2026-04-23). 64 tests in tests/test_project_integration.py.

Phase 1: Project Integration Model ✅

Phase 2: Config Resolution ✅

Phase 3: Discover Per-Project ✅

Phase 4: Jira Pull with Mapping ✅

Phase 5: Jira Push with Mapping ✅

Phase 6: Migrate Zephyr/Xray to Project Config ✅

Migration Path

Existing users:

  1. Global config continues to work — no action required
  1. To use multi-project: create projects with integration config
  1. testintel discover auto-detects and writes project config
  1. Per-project config overrides global when set

Future Enhancements


Per-Request Project Resolution (2026-06-03)

Problem

The original design used a single active project stored in projects.yaml as server state. The Chat route called pm.active() to resolve project settings (test preferences, repos, KB scope, inventory). This caused:

  1. Multi-user conflicts — User A selects Project X, User B selects Project Y → they overwrite each other's active field
  1. UI/backend mismatch — Frontend dropdown shows one project, backend uses a different one
  1. Stale inventory — The inventory singleton was created at startup from whatever project was active then; never updated on project switch

Solution: Per-Request Project Resolution

The frontend now sends the selected project name explicitly in every chat request:


class ChatRequest(BaseModel):
    message: str
    project: str | None = None  # Explicit project from UI dropdown
    kb_scope: str = ""          # Fallback resolution
    # ... other fields

Backend resolution order:

  1. project field — if provided, look up by name via pm.get(name)
  1. kb_scope match — match against project list by kb_scope or name
  1. pm.active() fallback — for backward compatibility (CLI, scripts, API callers without project field)

Per-Request Inventory

Previously, ChatService received a single Inventory instance at init (scoped to whichever project was active at startup). Now:


# In chat route — inventory created fresh per request
if project and req.include_inventory:
    request_inventory = Inventory(storage, scope=project.output_path, test_id_config=project.test_id)

chat_service.respond(..., inventory_override=request_inventory)

ChatService.respond() uses inventory_override when provided, falls back to the singleton for non-UI callers.

Session Isolation on Project Switch

When a user switches projects mid-conversation, the frontend:

  1. Checks if there are messages in the current chat
  1. If yes: shows confirmation modal ("Switching projects will start a new chat session")
  1. If confirmed: clears sessionId, empties message history, shows fresh welcome card
  1. If cancelled: reverts dropdown to previous project

This ensures one chat session = one project context. No stale KB docs, preferences, or uploads leak across projects.

Test Preferences Flow


User sets Test Format: BDD in Project settings
    → Saved to projects.yaml under project.test_preferences
    → Frontend sends project="testintel" in chat request
    → Backend resolves project, reads test_preferences
    → Context builder injects "## Team Test Preferences (AUTHORITATIVE)" section
    → If test_format == "bdd": prompt swapped to bdd_scenarios
    → AI generates Gherkin without asking about format

What "Active" Means Now

The active field in projects.yaml is now only used for:

It is NOT used for resolving project context in UI-initiated chat requests.