refactor(issues): remove dead trust level system — frontend progressive trust UI, backend L0-L4 escalation, and deprecated ai-triage store
Two unconnected trust systems were fully implemented but never wired up: the frontend crawl/walk/run progressive trust settings wrote to config that no backend code read, and the backend L0-L4 trust escalation module was never imported anywhere. Removes ~2200 lines of dead code across components, stores, IPC handlers, types, constants, i18n, and tests. Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1b34dec107
commit
72522dd815
@@ -53,7 +53,6 @@ class RepoConfig:
|
||||
relationship: Relationship to other repos
|
||||
upstream_repo: Upstream repo if this is a fork
|
||||
labels: Label configuration overrides
|
||||
trust_level: Trust level for this repo
|
||||
"""
|
||||
|
||||
repo: str # owner/repo format
|
||||
@@ -64,7 +63,6 @@ class RepoConfig:
|
||||
labels: dict[str, list[str]] = field(
|
||||
default_factory=dict
|
||||
) # e.g., {"auto_fix": ["fix-me"]}
|
||||
trust_level: int = 0 # 0-4 trust level
|
||||
display_name: str | None = None # Human-readable name
|
||||
|
||||
# Feature toggles per repo
|
||||
@@ -125,7 +123,6 @@ class RepoConfig:
|
||||
"relationship": self.relationship.value,
|
||||
"upstream_repo": self.upstream_repo,
|
||||
"labels": self.labels,
|
||||
"trust_level": self.trust_level,
|
||||
"display_name": self.display_name,
|
||||
"auto_fix_enabled": self.auto_fix_enabled,
|
||||
"pr_review_enabled": self.pr_review_enabled,
|
||||
@@ -141,7 +138,6 @@ class RepoConfig:
|
||||
relationship=RepoRelationship(data.get("relationship", "standalone")),
|
||||
upstream_repo=data.get("upstream_repo"),
|
||||
labels=data.get("labels", {}),
|
||||
trust_level=data.get("trust_level", 0),
|
||||
display_name=data.get("display_name"),
|
||||
auto_fix_enabled=data.get("auto_fix_enabled", True),
|
||||
pr_review_enabled=data.get("pr_review_enabled", True),
|
||||
|
||||
@@ -1,543 +0,0 @@
|
||||
"""
|
||||
Trust Escalation Model
|
||||
======================
|
||||
|
||||
Progressive trust system that unlocks more autonomous actions as accuracy improves:
|
||||
|
||||
- L0: Review-only (comment, no actions)
|
||||
- L1: Auto-apply labels based on triage
|
||||
- L2: Auto-close duplicates and spam
|
||||
- L3: Auto-merge trivial fixes (docs, typos)
|
||||
- L4: Full auto-fix with merge
|
||||
|
||||
Trust increases with accuracy, decreases with overrides.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
class TrustLevel(IntEnum):
|
||||
"""Trust levels with increasing autonomy."""
|
||||
|
||||
L0_REVIEW_ONLY = 0 # Comment only, no actions
|
||||
L1_LABEL = 1 # Auto-apply labels
|
||||
L2_CLOSE = 2 # Auto-close duplicates/spam
|
||||
L3_MERGE_TRIVIAL = 3 # Auto-merge trivial fixes
|
||||
L4_FULL_AUTO = 4 # Full autonomous operation
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
names = {
|
||||
0: "Review Only",
|
||||
1: "Auto-Label",
|
||||
2: "Auto-Close",
|
||||
3: "Auto-Merge Trivial",
|
||||
4: "Full Autonomous",
|
||||
}
|
||||
return names.get(self.value, "Unknown")
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
descriptions = {
|
||||
0: "AI can comment with suggestions but takes no actions",
|
||||
1: "AI can automatically apply labels based on triage",
|
||||
2: "AI can auto-close clear duplicates and spam",
|
||||
3: "AI can auto-merge trivial changes (docs, typos, formatting)",
|
||||
4: "AI can auto-fix issues and merge PRs autonomously",
|
||||
}
|
||||
return descriptions.get(self.value, "")
|
||||
|
||||
@property
|
||||
def allowed_actions(self) -> set[str]:
|
||||
"""Actions allowed at this trust level."""
|
||||
actions = {
|
||||
0: {"comment", "review"},
|
||||
1: {"comment", "review", "label", "triage"},
|
||||
2: {
|
||||
"comment",
|
||||
"review",
|
||||
"label",
|
||||
"triage",
|
||||
"close_duplicate",
|
||||
"close_spam",
|
||||
},
|
||||
3: {
|
||||
"comment",
|
||||
"review",
|
||||
"label",
|
||||
"triage",
|
||||
"close_duplicate",
|
||||
"close_spam",
|
||||
"merge_trivial",
|
||||
},
|
||||
4: {
|
||||
"comment",
|
||||
"review",
|
||||
"label",
|
||||
"triage",
|
||||
"close_duplicate",
|
||||
"close_spam",
|
||||
"merge_trivial",
|
||||
"auto_fix",
|
||||
"merge",
|
||||
},
|
||||
}
|
||||
return actions.get(self.value, set())
|
||||
|
||||
def can_perform(self, action: str) -> bool:
|
||||
"""Check if this trust level allows an action."""
|
||||
return action in self.allowed_actions
|
||||
|
||||
|
||||
# Thresholds for trust level upgrades
|
||||
TRUST_THRESHOLDS = {
|
||||
TrustLevel.L1_LABEL: {
|
||||
"min_actions": 20,
|
||||
"min_accuracy": 0.90,
|
||||
"min_days": 3,
|
||||
},
|
||||
TrustLevel.L2_CLOSE: {
|
||||
"min_actions": 50,
|
||||
"min_accuracy": 0.92,
|
||||
"min_days": 7,
|
||||
},
|
||||
TrustLevel.L3_MERGE_TRIVIAL: {
|
||||
"min_actions": 100,
|
||||
"min_accuracy": 0.95,
|
||||
"min_days": 14,
|
||||
},
|
||||
TrustLevel.L4_FULL_AUTO: {
|
||||
"min_actions": 200,
|
||||
"min_accuracy": 0.97,
|
||||
"min_days": 30,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class AccuracyMetrics:
|
||||
"""Tracks accuracy metrics for trust calculation."""
|
||||
|
||||
total_actions: int = 0
|
||||
correct_actions: int = 0
|
||||
overridden_actions: int = 0
|
||||
last_action_at: str | None = None
|
||||
first_action_at: str | None = None
|
||||
|
||||
# Per-action type metrics
|
||||
review_total: int = 0
|
||||
review_correct: int = 0
|
||||
label_total: int = 0
|
||||
label_correct: int = 0
|
||||
triage_total: int = 0
|
||||
triage_correct: int = 0
|
||||
close_total: int = 0
|
||||
close_correct: int = 0
|
||||
merge_total: int = 0
|
||||
merge_correct: int = 0
|
||||
fix_total: int = 0
|
||||
fix_correct: int = 0
|
||||
|
||||
@property
|
||||
def accuracy(self) -> float:
|
||||
"""Overall accuracy rate."""
|
||||
if self.total_actions == 0:
|
||||
return 0.0
|
||||
return self.correct_actions / self.total_actions
|
||||
|
||||
@property
|
||||
def override_rate(self) -> float:
|
||||
"""Rate of overridden actions."""
|
||||
if self.total_actions == 0:
|
||||
return 0.0
|
||||
return self.overridden_actions / self.total_actions
|
||||
|
||||
@property
|
||||
def days_active(self) -> int:
|
||||
"""Days since first action."""
|
||||
if not self.first_action_at:
|
||||
return 0
|
||||
first = datetime.fromisoformat(self.first_action_at)
|
||||
now = datetime.now(timezone.utc)
|
||||
return (now - first).days
|
||||
|
||||
def record_action(
|
||||
self,
|
||||
action_type: str,
|
||||
correct: bool,
|
||||
overridden: bool = False,
|
||||
) -> None:
|
||||
"""Record an action outcome."""
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
self.total_actions += 1
|
||||
if correct:
|
||||
self.correct_actions += 1
|
||||
if overridden:
|
||||
self.overridden_actions += 1
|
||||
|
||||
self.last_action_at = now
|
||||
if not self.first_action_at:
|
||||
self.first_action_at = now
|
||||
|
||||
# Update per-type metrics
|
||||
type_map = {
|
||||
"review": ("review_total", "review_correct"),
|
||||
"label": ("label_total", "label_correct"),
|
||||
"triage": ("triage_total", "triage_correct"),
|
||||
"close": ("close_total", "close_correct"),
|
||||
"merge": ("merge_total", "merge_correct"),
|
||||
"fix": ("fix_total", "fix_correct"),
|
||||
}
|
||||
|
||||
if action_type in type_map:
|
||||
total_attr, correct_attr = type_map[action_type]
|
||||
setattr(self, total_attr, getattr(self, total_attr) + 1)
|
||||
if correct:
|
||||
setattr(self, correct_attr, getattr(self, correct_attr) + 1)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"total_actions": self.total_actions,
|
||||
"correct_actions": self.correct_actions,
|
||||
"overridden_actions": self.overridden_actions,
|
||||
"last_action_at": self.last_action_at,
|
||||
"first_action_at": self.first_action_at,
|
||||
"review_total": self.review_total,
|
||||
"review_correct": self.review_correct,
|
||||
"label_total": self.label_total,
|
||||
"label_correct": self.label_correct,
|
||||
"triage_total": self.triage_total,
|
||||
"triage_correct": self.triage_correct,
|
||||
"close_total": self.close_total,
|
||||
"close_correct": self.close_correct,
|
||||
"merge_total": self.merge_total,
|
||||
"merge_correct": self.merge_correct,
|
||||
"fix_total": self.fix_total,
|
||||
"fix_correct": self.fix_correct,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> AccuracyMetrics:
|
||||
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrustState:
|
||||
"""Trust state for a repository."""
|
||||
|
||||
repo: str
|
||||
current_level: TrustLevel = TrustLevel.L0_REVIEW_ONLY
|
||||
metrics: AccuracyMetrics = field(default_factory=AccuracyMetrics)
|
||||
manual_override: TrustLevel | None = None # User-set override
|
||||
last_level_change: str | None = None
|
||||
level_history: list[dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def effective_level(self) -> TrustLevel:
|
||||
"""Get effective trust level (considers manual override)."""
|
||||
if self.manual_override is not None:
|
||||
return self.manual_override
|
||||
return self.current_level
|
||||
|
||||
def can_perform(self, action: str) -> bool:
|
||||
"""Check if current trust level allows an action."""
|
||||
return self.effective_level.can_perform(action)
|
||||
|
||||
def get_progress_to_next_level(self) -> dict[str, Any]:
|
||||
"""Get progress toward next trust level."""
|
||||
current = self.current_level
|
||||
if current >= TrustLevel.L4_FULL_AUTO:
|
||||
return {
|
||||
"next_level": None,
|
||||
"at_max": True,
|
||||
}
|
||||
|
||||
next_level = TrustLevel(current + 1)
|
||||
thresholds = TRUST_THRESHOLDS.get(next_level, {})
|
||||
|
||||
min_actions = thresholds.get("min_actions", 0)
|
||||
min_accuracy = thresholds.get("min_accuracy", 0)
|
||||
min_days = thresholds.get("min_days", 0)
|
||||
|
||||
return {
|
||||
"next_level": next_level.value,
|
||||
"next_level_name": next_level.display_name,
|
||||
"at_max": False,
|
||||
"actions": {
|
||||
"current": self.metrics.total_actions,
|
||||
"required": min_actions,
|
||||
"progress": min(1.0, self.metrics.total_actions / max(1, min_actions)),
|
||||
},
|
||||
"accuracy": {
|
||||
"current": self.metrics.accuracy,
|
||||
"required": min_accuracy,
|
||||
"progress": min(1.0, self.metrics.accuracy / max(0.01, min_accuracy)),
|
||||
},
|
||||
"days": {
|
||||
"current": self.metrics.days_active,
|
||||
"required": min_days,
|
||||
"progress": min(1.0, self.metrics.days_active / max(1, min_days)),
|
||||
},
|
||||
}
|
||||
|
||||
def check_upgrade(self) -> TrustLevel | None:
|
||||
"""Check if eligible for trust level upgrade."""
|
||||
current = self.current_level
|
||||
if current >= TrustLevel.L4_FULL_AUTO:
|
||||
return None
|
||||
|
||||
next_level = TrustLevel(current + 1)
|
||||
thresholds = TRUST_THRESHOLDS.get(next_level)
|
||||
if not thresholds:
|
||||
return None
|
||||
|
||||
if (
|
||||
self.metrics.total_actions >= thresholds["min_actions"]
|
||||
and self.metrics.accuracy >= thresholds["min_accuracy"]
|
||||
and self.metrics.days_active >= thresholds["min_days"]
|
||||
):
|
||||
return next_level
|
||||
|
||||
return None
|
||||
|
||||
def upgrade_level(self, new_level: TrustLevel, reason: str = "auto") -> None:
|
||||
"""Upgrade to a new trust level."""
|
||||
if new_level <= self.current_level:
|
||||
return
|
||||
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
self.level_history.append(
|
||||
{
|
||||
"from_level": self.current_level.value,
|
||||
"to_level": new_level.value,
|
||||
"reason": reason,
|
||||
"timestamp": now,
|
||||
"metrics_snapshot": self.metrics.to_dict(),
|
||||
}
|
||||
)
|
||||
self.current_level = new_level
|
||||
self.last_level_change = now
|
||||
|
||||
def downgrade_level(self, reason: str = "override") -> None:
|
||||
"""Downgrade trust level due to override or errors."""
|
||||
if self.current_level <= TrustLevel.L0_REVIEW_ONLY:
|
||||
return
|
||||
|
||||
new_level = TrustLevel(self.current_level - 1)
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
self.level_history.append(
|
||||
{
|
||||
"from_level": self.current_level.value,
|
||||
"to_level": new_level.value,
|
||||
"reason": reason,
|
||||
"timestamp": now,
|
||||
}
|
||||
)
|
||||
self.current_level = new_level
|
||||
self.last_level_change = now
|
||||
|
||||
def set_manual_override(self, level: TrustLevel | None) -> None:
|
||||
"""Set or clear manual trust level override."""
|
||||
self.manual_override = level
|
||||
if level is not None:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
self.level_history.append(
|
||||
{
|
||||
"from_level": self.current_level.value,
|
||||
"to_level": level.value,
|
||||
"reason": "manual_override",
|
||||
"timestamp": now,
|
||||
}
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"repo": self.repo,
|
||||
"current_level": self.current_level.value,
|
||||
"metrics": self.metrics.to_dict(),
|
||||
"manual_override": self.manual_override.value
|
||||
if self.manual_override
|
||||
else None,
|
||||
"last_level_change": self.last_level_change,
|
||||
"level_history": self.level_history[-20:], # Keep last 20 changes
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> TrustState:
|
||||
return cls(
|
||||
repo=data["repo"],
|
||||
current_level=TrustLevel(data.get("current_level", 0)),
|
||||
metrics=AccuracyMetrics.from_dict(data.get("metrics", {})),
|
||||
manual_override=TrustLevel(data["manual_override"])
|
||||
if data.get("manual_override") is not None
|
||||
else None,
|
||||
last_level_change=data.get("last_level_change"),
|
||||
level_history=data.get("level_history", []),
|
||||
)
|
||||
|
||||
|
||||
class TrustManager:
|
||||
"""
|
||||
Manages trust levels across repositories.
|
||||
|
||||
Usage:
|
||||
trust = TrustManager(state_dir=Path(".auto-claude/github"))
|
||||
|
||||
# Check if action is allowed
|
||||
if trust.can_perform("owner/repo", "auto_fix"):
|
||||
perform_auto_fix()
|
||||
|
||||
# Record action outcome
|
||||
trust.record_action("owner/repo", "review", correct=True)
|
||||
|
||||
# Check for upgrade
|
||||
if trust.check_and_upgrade("owner/repo"):
|
||||
print("Trust level upgraded!")
|
||||
"""
|
||||
|
||||
def __init__(self, state_dir: Path):
|
||||
self.state_dir = state_dir
|
||||
self.trust_dir = state_dir / "trust"
|
||||
self.trust_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._states: dict[str, TrustState] = {}
|
||||
|
||||
def _get_state_file(self, repo: str) -> Path:
|
||||
safe_name = repo.replace("/", "_")
|
||||
return self.trust_dir / f"{safe_name}.json"
|
||||
|
||||
def get_state(self, repo: str) -> TrustState:
|
||||
"""Get trust state for a repository."""
|
||||
if repo in self._states:
|
||||
return self._states[repo]
|
||||
|
||||
state_file = self._get_state_file(repo)
|
||||
if state_file.exists():
|
||||
try:
|
||||
with open(state_file, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
state = TrustState.from_dict(data)
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
# Return default state if file is corrupted
|
||||
state = TrustState(repo=repo)
|
||||
else:
|
||||
state = TrustState(repo=repo)
|
||||
|
||||
self._states[repo] = state
|
||||
return state
|
||||
|
||||
def save_state(self, repo: str) -> None:
|
||||
"""Save trust state for a repository with secure file permissions."""
|
||||
import os
|
||||
|
||||
state = self.get_state(repo)
|
||||
state_file = self._get_state_file(repo)
|
||||
|
||||
# Write with restrictive permissions (0o600 = owner read/write only)
|
||||
fd = os.open(str(state_file), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
# os.fdopen takes ownership of fd and will close it when the with block exits
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump(state.to_dict(), f, indent=2)
|
||||
|
||||
def get_trust_level(self, repo: str) -> TrustLevel:
|
||||
"""Get current trust level for a repository."""
|
||||
return self.get_state(repo).effective_level
|
||||
|
||||
def can_perform(self, repo: str, action: str) -> bool:
|
||||
"""Check if an action is allowed for a repository."""
|
||||
return self.get_state(repo).can_perform(action)
|
||||
|
||||
def record_action(
|
||||
self,
|
||||
repo: str,
|
||||
action_type: str,
|
||||
correct: bool,
|
||||
overridden: bool = False,
|
||||
) -> None:
|
||||
"""Record an action outcome."""
|
||||
state = self.get_state(repo)
|
||||
state.metrics.record_action(action_type, correct, overridden)
|
||||
|
||||
# Check for downgrade on override
|
||||
if overridden:
|
||||
# Downgrade if override rate exceeds 10%
|
||||
if state.metrics.override_rate > 0.10 and state.metrics.total_actions >= 10:
|
||||
state.downgrade_level(reason="high_override_rate")
|
||||
|
||||
self.save_state(repo)
|
||||
|
||||
def check_and_upgrade(self, repo: str) -> bool:
|
||||
"""Check for and apply trust level upgrade."""
|
||||
state = self.get_state(repo)
|
||||
new_level = state.check_upgrade()
|
||||
|
||||
if new_level:
|
||||
state.upgrade_level(new_level, reason="threshold_met")
|
||||
self.save_state(repo)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def set_manual_level(self, repo: str, level: TrustLevel) -> None:
|
||||
"""Manually set trust level for a repository."""
|
||||
state = self.get_state(repo)
|
||||
state.set_manual_override(level)
|
||||
self.save_state(repo)
|
||||
|
||||
def clear_manual_override(self, repo: str) -> None:
|
||||
"""Clear manual trust level override."""
|
||||
state = self.get_state(repo)
|
||||
state.set_manual_override(None)
|
||||
self.save_state(repo)
|
||||
|
||||
def get_progress(self, repo: str) -> dict[str, Any]:
|
||||
"""Get progress toward next trust level."""
|
||||
state = self.get_state(repo)
|
||||
return {
|
||||
"current_level": state.effective_level.value,
|
||||
"current_level_name": state.effective_level.display_name,
|
||||
"is_manual_override": state.manual_override is not None,
|
||||
"accuracy": state.metrics.accuracy,
|
||||
"total_actions": state.metrics.total_actions,
|
||||
"override_rate": state.metrics.override_rate,
|
||||
"days_active": state.metrics.days_active,
|
||||
"progress_to_next": state.get_progress_to_next_level(),
|
||||
}
|
||||
|
||||
def get_all_states(self) -> list[TrustState]:
|
||||
"""Get trust states for all repos."""
|
||||
states = []
|
||||
for file in self.trust_dir.glob("*.json"):
|
||||
try:
|
||||
with open(file, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
states.append(TrustState.from_dict(data))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
# Skip corrupted state files
|
||||
continue
|
||||
return states
|
||||
|
||||
def get_summary(self) -> dict[str, Any]:
|
||||
"""Get summary of trust across all repos."""
|
||||
states = self.get_all_states()
|
||||
by_level = {}
|
||||
for state in states:
|
||||
level = state.effective_level.value
|
||||
by_level[level] = by_level.get(level, 0) + 1
|
||||
|
||||
total_actions = sum(s.metrics.total_actions for s in states)
|
||||
total_correct = sum(s.metrics.correct_actions for s in states)
|
||||
|
||||
return {
|
||||
"total_repos": len(states),
|
||||
"by_level": by_level,
|
||||
"total_actions": total_actions,
|
||||
"overall_accuracy": total_correct / max(1, total_actions),
|
||||
}
|
||||
@@ -499,46 +499,3 @@ describe('applyTriageResults handler', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// saveProgressiveTrust
|
||||
// ============================================
|
||||
|
||||
describe('saveProgressiveTrust handler', () => {
|
||||
const trustConfig = {
|
||||
autoApply: {
|
||||
type: { enabled: true, threshold: 0.85 },
|
||||
priority: { enabled: false, threshold: 0.9 },
|
||||
labels: { enabled: true, threshold: 0.8 },
|
||||
duplicate: { enabled: false, threshold: 0.9 },
|
||||
},
|
||||
batchSize: 25,
|
||||
confirmAbove: 5,
|
||||
};
|
||||
|
||||
const call = (projectId: string, config: typeof trustConfig) =>
|
||||
handleHandlers['github:triage:saveTrust']({}, projectId, config);
|
||||
|
||||
it('saves config successfully', async () => {
|
||||
const { writeJsonWithRetry } = await import('../../../utils/atomic-file');
|
||||
const result = await call('test-project', trustConfig);
|
||||
|
||||
expect(writeJsonWithRetry).toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// getProgressiveTrust
|
||||
// ============================================
|
||||
|
||||
describe('getProgressiveTrust handler', () => {
|
||||
const call = (projectId: string) =>
|
||||
handleHandlers['github:triage:getTrust']({}, projectId);
|
||||
|
||||
it('returns default config when no file exists', async () => {
|
||||
const result = await call('test-project') as { batchSize: number };
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.batchSize).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
import { getToolPath } from '../../cli-tool-manager';
|
||||
import { killProcessGracefully } from '../../platform';
|
||||
import { MAX_SPLIT_SUB_ISSUES } from '../../../shared/constants/ai-triage';
|
||||
import { createDefaultProgressiveTrust } from '../../../shared/types/ai-triage';
|
||||
import { readEnrichmentFile, writeEnrichmentFile, withEnrichmentFileLock, appendTransition } from './enrichment-persistence';
|
||||
import { createDefaultEnrichment } from '../../../shared/types/enrichment';
|
||||
import type { TriageCategory } from '../../../shared/types/enrichment';
|
||||
@@ -44,7 +43,6 @@ import type {
|
||||
EnrichmentProgress,
|
||||
SplitProgress,
|
||||
ApplyResultsProgress,
|
||||
ProgressiveTrustConfig,
|
||||
} from '../../../shared/types/ai-triage';
|
||||
|
||||
const { debug: debugLog } = createContextLogger('AI Triage');
|
||||
@@ -442,63 +440,6 @@ export function registerAITriageHandlers(
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// Save progressive trust config
|
||||
// ============================================
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.GITHUB_TRIAGE_SAVE_TRUST,
|
||||
async (_, projectId: string, config: ProgressiveTrustConfig): Promise<boolean> => {
|
||||
debugLog('saveProgressiveTrust handler called', { projectId });
|
||||
const result = await withProjectOrNull(projectId, async (project) => {
|
||||
const githubDir = getGitHubDir(project.path);
|
||||
fs.mkdirSync(githubDir, { recursive: true });
|
||||
|
||||
const configPath = path.join(githubDir, 'config.json');
|
||||
let existingConfig: Record<string, unknown> = {};
|
||||
|
||||
try {
|
||||
existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
} catch {
|
||||
// Use empty config if file doesn't exist
|
||||
}
|
||||
|
||||
const updatedConfig = {
|
||||
...existingConfig,
|
||||
progressive_trust: config,
|
||||
};
|
||||
|
||||
await writeJsonWithRetry(configPath, updatedConfig, { indent: 2 });
|
||||
return true;
|
||||
});
|
||||
return result ?? false;
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// Get progressive trust config
|
||||
// ============================================
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.GITHUB_TRIAGE_GET_TRUST,
|
||||
async (_, projectId: string): Promise<ProgressiveTrustConfig> => {
|
||||
debugLog('getProgressiveTrust handler called', { projectId });
|
||||
const result = await withProjectOrNull(projectId, async (project) => {
|
||||
const configPath = path.join(getGitHubDir(project.path), 'config.json');
|
||||
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
if (data.progressive_trust) {
|
||||
return data.progressive_trust as ProgressiveTrustConfig;
|
||||
}
|
||||
} catch {
|
||||
// Return default
|
||||
}
|
||||
|
||||
return createDefaultProgressiveTrust();
|
||||
});
|
||||
return result ?? createDefaultProgressiveTrust();
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// Save pending review queue
|
||||
// ============================================
|
||||
|
||||
@@ -32,7 +32,6 @@ import type {
|
||||
EnrichmentProgress,
|
||||
SplitProgress,
|
||||
ApplyResultsProgress,
|
||||
ProgressiveTrustConfig,
|
||||
} from '../../../shared/types/ai-triage';
|
||||
import { createIpcListener, invokeIpc, sendIpc, IpcListenerCleanup } from './ipc-utils';
|
||||
|
||||
@@ -429,9 +428,6 @@ export interface GitHubAPI {
|
||||
|
||||
savePendingReview: (projectId: string, items: TriageReviewItem[]) => Promise<boolean>;
|
||||
loadPendingReview: (projectId: string) => Promise<TriageReviewItem[]>;
|
||||
saveProgressiveTrust: (projectId: string, config: ProgressiveTrustConfig) => Promise<boolean>;
|
||||
getProgressiveTrust: (projectId: string) => Promise<ProgressiveTrustConfig>;
|
||||
|
||||
// Label Sync (Phase 4)
|
||||
enableLabelSync: (projectId: string) => Promise<LabelSyncResult>;
|
||||
disableLabelSync: (projectId: string, cleanup: boolean) => Promise<{ success: boolean }>;
|
||||
@@ -1089,12 +1085,6 @@ export const createGitHubAPI = (): GitHubAPI => ({
|
||||
loadPendingReview: (projectId: string): Promise<TriageReviewItem[]> =>
|
||||
invokeIpc(IPC_CHANNELS.GITHUB_TRIAGE_LOAD_PENDING_REVIEW, projectId),
|
||||
|
||||
saveProgressiveTrust: (projectId: string, config: ProgressiveTrustConfig): Promise<boolean> =>
|
||||
invokeIpc(IPC_CHANNELS.GITHUB_TRIAGE_SAVE_TRUST, projectId, config),
|
||||
|
||||
getProgressiveTrust: (projectId: string): Promise<ProgressiveTrustConfig> =>
|
||||
invokeIpc(IPC_CHANNELS.GITHUB_TRIAGE_GET_TRUST, projectId),
|
||||
|
||||
// Label Sync (Phase 4)
|
||||
enableLabelSync: (projectId: string): Promise<LabelSyncResult> =>
|
||||
invokeIpc(IPC_CHANNELS.GITHUB_LABEL_SYNC_ENABLE, projectId),
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useAITriageStore } from '../stores/github/ai-triage-store';
|
||||
import type { TriageReviewItem } from '../../shared/types/ai-triage';
|
||||
|
||||
function createReviewItem(overrides: Partial<TriageReviewItem> = {}): TriageReviewItem {
|
||||
return {
|
||||
issueNumber: 1,
|
||||
issueTitle: 'Test Issue',
|
||||
result: {
|
||||
category: 'bug',
|
||||
confidence: 0.9,
|
||||
labelsToAdd: ['bug'],
|
||||
labelsToRemove: [],
|
||||
isDuplicate: false,
|
||||
isSpam: false,
|
||||
isFeatureCreep: false,
|
||||
suggestedBreakdown: [],
|
||||
priority: 'high',
|
||||
triagedAt: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
status: 'pending',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
useAITriageStore.setState({
|
||||
isTriaging: false,
|
||||
triageProgress: null,
|
||||
reviewItems: [],
|
||||
enrichmentProgress: null,
|
||||
splitSuggestion: null,
|
||||
splitProgress: null,
|
||||
});
|
||||
});
|
||||
|
||||
describe('initial state', () => {
|
||||
it('has no results, not triaging, no review items', () => {
|
||||
const state = useAITriageStore.getState();
|
||||
expect(state.isTriaging).toBe(false);
|
||||
expect(state.triageProgress).toBeNull();
|
||||
expect(state.reviewItems).toEqual([]);
|
||||
expect(state.enrichmentProgress).toBeNull();
|
||||
expect(state.splitSuggestion).toBeNull();
|
||||
expect(state.splitProgress).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('startTriage / endTriage', () => {
|
||||
it('startTriage sets isTriaging to true', () => {
|
||||
useAITriageStore.getState().startTriage();
|
||||
expect(useAITriageStore.getState().isTriaging).toBe(true);
|
||||
});
|
||||
|
||||
it('endTriage sets isTriaging to false', () => {
|
||||
useAITriageStore.getState().startTriage();
|
||||
useAITriageStore.getState().endTriage();
|
||||
expect(useAITriageStore.getState().isTriaging).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setTriageProgress', () => {
|
||||
it('updates progress', () => {
|
||||
const progress = { phase: 'analyzing' as const, progress: 50, message: 'Processing...' };
|
||||
useAITriageStore.getState().setTriageProgress(progress);
|
||||
expect(useAITriageStore.getState().triageProgress).toEqual(progress);
|
||||
});
|
||||
});
|
||||
|
||||
describe('review items', () => {
|
||||
it('addReviewItems adds items to review queue', () => {
|
||||
const items = [createReviewItem({ issueNumber: 1 }), createReviewItem({ issueNumber: 2 })];
|
||||
useAITriageStore.getState().addReviewItems(items);
|
||||
expect(useAITriageStore.getState().reviewItems).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('acceptReviewItem changes status to accepted', () => {
|
||||
useAITriageStore.getState().addReviewItems([createReviewItem({ issueNumber: 1 })]);
|
||||
useAITriageStore.getState().acceptReviewItem(1);
|
||||
expect(useAITriageStore.getState().reviewItems[0].status).toBe('accepted');
|
||||
});
|
||||
|
||||
it('rejectReviewItem changes status to rejected', () => {
|
||||
useAITriageStore.getState().addReviewItems([createReviewItem({ issueNumber: 1 })]);
|
||||
useAITriageStore.getState().rejectReviewItem(1);
|
||||
expect(useAITriageStore.getState().reviewItems[0].status).toBe('rejected');
|
||||
});
|
||||
|
||||
it('acceptAllRemaining accepts all pending items', () => {
|
||||
useAITriageStore.getState().addReviewItems([
|
||||
createReviewItem({ issueNumber: 1 }),
|
||||
createReviewItem({ issueNumber: 2, status: 'rejected' }),
|
||||
createReviewItem({ issueNumber: 3 }),
|
||||
]);
|
||||
useAITriageStore.getState().acceptAllRemaining();
|
||||
|
||||
const items = useAITriageStore.getState().reviewItems;
|
||||
expect(items[0].status).toBe('accepted');
|
||||
expect(items[1].status).toBe('rejected'); // Unchanged — already decided
|
||||
expect(items[2].status).toBe('accepted');
|
||||
});
|
||||
|
||||
it('dismissReview clears review queue', () => {
|
||||
useAITriageStore.getState().addReviewItems([createReviewItem()]);
|
||||
useAITriageStore.getState().dismissReview();
|
||||
expect(useAITriageStore.getState().reviewItems).toEqual([]);
|
||||
});
|
||||
|
||||
it('getUnreviewedCount returns count of pending items', () => {
|
||||
useAITriageStore.getState().addReviewItems([
|
||||
createReviewItem({ issueNumber: 1 }),
|
||||
createReviewItem({ issueNumber: 2, status: 'accepted' }),
|
||||
createReviewItem({ issueNumber: 3 }),
|
||||
]);
|
||||
expect(useAITriageStore.getState().getUnreviewedCount()).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enrichment progress', () => {
|
||||
it('setEnrichmentProgress sets progress', () => {
|
||||
const progress = { phase: 'analyzing' as const, progress: 30, message: 'Analyzing...' };
|
||||
useAITriageStore.getState().setEnrichmentProgress(progress);
|
||||
expect(useAITriageStore.getState().enrichmentProgress).toEqual(progress);
|
||||
});
|
||||
|
||||
it('clearEnrichmentProgress clears progress', () => {
|
||||
useAITriageStore.getState().setEnrichmentProgress({
|
||||
phase: 'analyzing',
|
||||
progress: 30,
|
||||
message: 'X',
|
||||
});
|
||||
useAITriageStore.getState().clearEnrichmentProgress();
|
||||
expect(useAITriageStore.getState().enrichmentProgress).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('split suggestion', () => {
|
||||
it('setSplitSuggestion sets suggestion', () => {
|
||||
const suggestion = {
|
||||
issueNumber: 42,
|
||||
subIssues: [{ title: 'Sub 1', body: 'Body', labels: [] }],
|
||||
rationale: 'Too broad',
|
||||
confidence: 0.9,
|
||||
};
|
||||
useAITriageStore.getState().setSplitSuggestion(suggestion);
|
||||
expect(useAITriageStore.getState().splitSuggestion).toEqual(suggestion);
|
||||
});
|
||||
|
||||
it('clearSplitSuggestion clears suggestion', () => {
|
||||
useAITriageStore.getState().setSplitSuggestion({
|
||||
issueNumber: 42,
|
||||
subIssues: [],
|
||||
rationale: 'X',
|
||||
confidence: 0.5,
|
||||
});
|
||||
useAITriageStore.getState().clearSplitSuggestion();
|
||||
expect(useAITriageStore.getState().splitSuggestion).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('split progress', () => {
|
||||
it('setSplitProgress sets progress', () => {
|
||||
const progress = { phase: 'creating' as const, progress: 60, message: 'Creating sub-issues...' };
|
||||
useAITriageStore.getState().setSplitProgress(progress);
|
||||
expect(useAITriageStore.getState().splitProgress).toEqual(progress);
|
||||
});
|
||||
|
||||
it('clearSplitProgress clears progress', () => {
|
||||
useAITriageStore.getState().setSplitProgress({
|
||||
phase: 'creating',
|
||||
progress: 60,
|
||||
message: 'X',
|
||||
});
|
||||
useAITriageStore.getState().clearSplitProgress();
|
||||
expect(useAITriageStore.getState().splitProgress).toBeNull();
|
||||
});
|
||||
});
|
||||
-252
@@ -1,252 +0,0 @@
|
||||
/**
|
||||
* Integration test verifying the AI triage flow from store → constants → types.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useAITriageStore } from '../../../stores/github/ai-triage-store';
|
||||
import {
|
||||
CONFIDENCE_HIGH,
|
||||
CONFIDENCE_MEDIUM,
|
||||
DEFAULT_BATCH_SIZE,
|
||||
DEFAULT_CONFIRM_ABOVE,
|
||||
MAX_SPLIT_SUB_ISSUES,
|
||||
THRESHOLD_MIN,
|
||||
THRESHOLD_MAX,
|
||||
getConfidenceLevel,
|
||||
isValidThreshold,
|
||||
clampThreshold,
|
||||
estimateBatchCost,
|
||||
} from '@shared/constants/ai-triage';
|
||||
import {
|
||||
createDefaultProgressiveTrust,
|
||||
mapTriageCategory,
|
||||
} from '@shared/types/ai-triage';
|
||||
import type { TriageReviewItem } from '@shared/types/ai-triage';
|
||||
|
||||
function createItem(
|
||||
issueNumber: number,
|
||||
status: TriageReviewItem['status'] = 'pending',
|
||||
): TriageReviewItem {
|
||||
return {
|
||||
issueNumber,
|
||||
issueTitle: `Issue #${issueNumber}`,
|
||||
result: {
|
||||
category: 'bug',
|
||||
confidence: 0.85,
|
||||
labelsToAdd: ['bug'],
|
||||
labelsToRemove: [],
|
||||
isDuplicate: false,
|
||||
isSpam: false,
|
||||
isFeatureCreep: false,
|
||||
suggestedBreakdown: [],
|
||||
priority: 'high',
|
||||
triagedAt: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
useAITriageStore.setState({
|
||||
isTriaging: false,
|
||||
triageProgress: null,
|
||||
reviewItems: [],
|
||||
enrichmentProgress: null,
|
||||
splitSuggestion: null,
|
||||
splitProgress: null,
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Store + constants consistency
|
||||
// ============================================
|
||||
|
||||
describe('store + constants consistency', () => {
|
||||
it('default progressive trust uses expected constant values', () => {
|
||||
const defaults = createDefaultProgressiveTrust();
|
||||
expect(defaults.batchSize).toBe(DEFAULT_BATCH_SIZE);
|
||||
expect(defaults.confirmAbove).toBe(DEFAULT_CONFIRM_ABOVE);
|
||||
});
|
||||
|
||||
it('all trust categories start disabled at 0.9 threshold', () => {
|
||||
const defaults = createDefaultProgressiveTrust();
|
||||
for (const category of ['type', 'priority', 'labels', 'duplicate'] as const) {
|
||||
expect(defaults.autoApply[category].enabled).toBe(false);
|
||||
expect(defaults.autoApply[category].threshold).toBe(0.9);
|
||||
expect(isValidThreshold(defaults.autoApply[category].threshold)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('category mapping covers all enrichment categories', () => {
|
||||
const pythonCategories = [
|
||||
'bug', 'feature', 'documentation', 'question',
|
||||
'enhancement', 'chore', 'security', 'performance',
|
||||
];
|
||||
for (const cat of pythonCategories) {
|
||||
expect(mapTriageCategory(cat)).toBe(cat);
|
||||
}
|
||||
});
|
||||
|
||||
it('category mapping maps special Python categories', () => {
|
||||
expect(mapTriageCategory('duplicate')).toBe('bug');
|
||||
expect(mapTriageCategory('spam')).toBe('chore');
|
||||
expect(mapTriageCategory('feature_creep')).toBe('enhancement');
|
||||
});
|
||||
|
||||
it('unknown categories map to chore', () => {
|
||||
expect(mapTriageCategory('unknown_type')).toBe('chore');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Progressive trust defaults
|
||||
// ============================================
|
||||
|
||||
describe('progressive trust defaults', () => {
|
||||
it('threshold bounds are correct', () => {
|
||||
expect(THRESHOLD_MIN).toBe(0.5);
|
||||
expect(THRESHOLD_MAX).toBe(1.0);
|
||||
expect(THRESHOLD_MIN).toBeLessThan(THRESHOLD_MAX);
|
||||
});
|
||||
|
||||
it('default thresholds are within valid range', () => {
|
||||
const defaults = createDefaultProgressiveTrust();
|
||||
for (const category of ['type', 'priority', 'labels', 'duplicate'] as const) {
|
||||
const threshold = defaults.autoApply[category].threshold;
|
||||
expect(threshold).toBeGreaterThanOrEqual(THRESHOLD_MIN);
|
||||
expect(threshold).toBeLessThanOrEqual(THRESHOLD_MAX);
|
||||
}
|
||||
});
|
||||
|
||||
it('clampThreshold enforces bounds', () => {
|
||||
expect(clampThreshold(0.3)).toBe(THRESHOLD_MIN);
|
||||
expect(clampThreshold(1.5)).toBe(THRESHOLD_MAX);
|
||||
expect(clampThreshold(0.75)).toBe(0.75);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Review item lifecycle
|
||||
// ============================================
|
||||
|
||||
describe('review item lifecycle', () => {
|
||||
it('pending → accepted → applied flow', () => {
|
||||
const store = useAITriageStore.getState();
|
||||
|
||||
// Add pending items
|
||||
store.addReviewItems([createItem(1), createItem(2), createItem(3)]);
|
||||
expect(useAITriageStore.getState().reviewItems).toHaveLength(3);
|
||||
expect(useAITriageStore.getState().getUnreviewedCount()).toBe(3);
|
||||
|
||||
// Accept one
|
||||
useAITriageStore.getState().acceptReviewItem(1);
|
||||
const item1 = useAITriageStore.getState().reviewItems.find((i) => i.issueNumber === 1);
|
||||
expect(item1?.status).toBe('accepted');
|
||||
expect(useAITriageStore.getState().getUnreviewedCount()).toBe(2);
|
||||
|
||||
// Reject one
|
||||
useAITriageStore.getState().rejectReviewItem(2);
|
||||
const item2 = useAITriageStore.getState().reviewItems.find((i) => i.issueNumber === 2);
|
||||
expect(item2?.status).toBe('rejected');
|
||||
expect(useAITriageStore.getState().getUnreviewedCount()).toBe(1);
|
||||
});
|
||||
|
||||
it('acceptAllRemaining accepts only pending items', () => {
|
||||
const store = useAITriageStore.getState();
|
||||
store.addReviewItems([
|
||||
createItem(1, 'rejected'),
|
||||
createItem(2, 'pending'),
|
||||
createItem(3, 'pending'),
|
||||
]);
|
||||
|
||||
useAITriageStore.getState().acceptAllRemaining();
|
||||
|
||||
const items = useAITriageStore.getState().reviewItems;
|
||||
expect(items.find((i) => i.issueNumber === 1)?.status).toBe('rejected');
|
||||
expect(items.find((i) => i.issueNumber === 2)?.status).toBe('accepted');
|
||||
expect(items.find((i) => i.issueNumber === 3)?.status).toBe('accepted');
|
||||
});
|
||||
|
||||
it('dismissReview clears all items', () => {
|
||||
const store = useAITriageStore.getState();
|
||||
store.addReviewItems([createItem(1), createItem(2)]);
|
||||
expect(useAITriageStore.getState().reviewItems).toHaveLength(2);
|
||||
|
||||
useAITriageStore.getState().dismissReview();
|
||||
expect(useAITriageStore.getState().reviewItems).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Split suggestion capping
|
||||
// ============================================
|
||||
|
||||
describe('split suggestion capping', () => {
|
||||
it('MAX_SPLIT_SUB_ISSUES is 5', () => {
|
||||
expect(MAX_SPLIT_SUB_ISSUES).toBe(5);
|
||||
});
|
||||
|
||||
it('store accepts split suggestion with sub-issues at max', () => {
|
||||
const subIssues = Array.from({ length: MAX_SPLIT_SUB_ISSUES }, (_, i) => ({
|
||||
title: `Sub ${i + 1}`,
|
||||
body: `Body ${i + 1}`,
|
||||
labels: [],
|
||||
}));
|
||||
|
||||
useAITriageStore.getState().setSplitSuggestion({
|
||||
issueNumber: 42,
|
||||
subIssues,
|
||||
rationale: 'Too complex',
|
||||
confidence: 0.9,
|
||||
});
|
||||
|
||||
const suggestion = useAITriageStore.getState().splitSuggestion;
|
||||
expect(suggestion?.subIssues).toHaveLength(MAX_SPLIT_SUB_ISSUES);
|
||||
});
|
||||
|
||||
it('clearSplitSuggestion removes the suggestion', () => {
|
||||
useAITriageStore.getState().setSplitSuggestion({
|
||||
issueNumber: 42,
|
||||
subIssues: [{ title: 'Sub', body: 'Body', labels: [] }],
|
||||
rationale: 'Test',
|
||||
confidence: 0.8,
|
||||
});
|
||||
|
||||
useAITriageStore.getState().clearSplitSuggestion();
|
||||
expect(useAITriageStore.getState().splitSuggestion).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Confidence level helper at boundaries
|
||||
// ============================================
|
||||
|
||||
describe('confidence level helper at boundaries', () => {
|
||||
it('exactly at CONFIDENCE_HIGH returns high', () => {
|
||||
expect(getConfidenceLevel(CONFIDENCE_HIGH)).toBe('high');
|
||||
});
|
||||
|
||||
it('just below CONFIDENCE_HIGH returns medium', () => {
|
||||
expect(getConfidenceLevel(CONFIDENCE_HIGH - 0.001)).toBe('medium');
|
||||
});
|
||||
|
||||
it('exactly at CONFIDENCE_MEDIUM returns medium', () => {
|
||||
expect(getConfidenceLevel(CONFIDENCE_MEDIUM)).toBe('medium');
|
||||
});
|
||||
|
||||
it('just below CONFIDENCE_MEDIUM returns low', () => {
|
||||
expect(getConfidenceLevel(CONFIDENCE_MEDIUM - 0.001)).toBe('low');
|
||||
});
|
||||
|
||||
it('0 returns low', () => {
|
||||
expect(getConfidenceLevel(0)).toBe('low');
|
||||
});
|
||||
|
||||
it('1 returns high', () => {
|
||||
expect(getConfidenceLevel(1)).toBe('high');
|
||||
});
|
||||
|
||||
it('estimateBatchCost returns formatted string', () => {
|
||||
const cost = estimateBatchCost(10, 'sonnet');
|
||||
expect(cost).toMatch(/~\$\d+\.\d{2}/);
|
||||
});
|
||||
});
|
||||
+4
-2
@@ -14,7 +14,7 @@ vi.mock('react-i18next', () => ({
|
||||
|
||||
describe('AI Triage integration', () => {
|
||||
it('TriageProgressOverlay renders progress bar and message', () => {
|
||||
const progress = { progress: 60, message: 'Enriching issue...' };
|
||||
const progress = { phase: 'generating' as const, progress: 60, message: 'Enriching issue...' };
|
||||
render(<TriageProgressOverlay progress={progress} onCancel={vi.fn()} />);
|
||||
expect(screen.getByText('Enriching issue...')).toBeDefined();
|
||||
expect(screen.getByRole('progressbar')).toBeDefined();
|
||||
@@ -22,7 +22,7 @@ describe('AI Triage integration', () => {
|
||||
|
||||
it('TriageProgressOverlay cancel button calls onCancel', () => {
|
||||
const onCancel = vi.fn();
|
||||
const progress = { progress: 30, message: 'Working...' };
|
||||
const progress = { phase: 'analyzing' as const, progress: 30, message: 'Working...' };
|
||||
render(<TriageProgressOverlay progress={progress} onCancel={onCancel} />);
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
expect(onCancel).toHaveBeenCalledOnce();
|
||||
@@ -32,6 +32,7 @@ describe('AI Triage integration', () => {
|
||||
const suggestion = {
|
||||
issueNumber: 42,
|
||||
rationale: 'This issue should be split',
|
||||
confidence: 0.9,
|
||||
subIssues: [
|
||||
{ title: 'Sub-issue A', body: 'Body A', labels: ['bug'] },
|
||||
{ title: 'Sub-issue B', body: 'Body B', labels: [] },
|
||||
@@ -55,6 +56,7 @@ describe('AI Triage integration', () => {
|
||||
const suggestion = {
|
||||
issueNumber: 42,
|
||||
rationale: 'Split needed',
|
||||
confidence: 0.85,
|
||||
subIssues: [{ title: 'Sub A', body: 'Body A', labels: [] }],
|
||||
};
|
||||
render(
|
||||
|
||||
@@ -41,8 +41,6 @@ describe('Phase 5 Barrel Exports', () => {
|
||||
expect(exportNames).toContain('BatchTriageReview');
|
||||
expect(exportNames).toContain('IssueSplitDialog');
|
||||
expect(exportNames).toContain('TriageProgressOverlay');
|
||||
expect(exportNames).toContain('ProgressiveTrustSettings');
|
||||
|
||||
// Phase 4
|
||||
expect(exportNames).toContain('LabelSyncSettings');
|
||||
expect(exportNames).toContain('DependencyList');
|
||||
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
/**
|
||||
* Progressive Trust Settings — configure auto-apply thresholds per category.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ProgressiveTrustConfig } from '@shared/types/ai-triage';
|
||||
import { THRESHOLD_MIN, THRESHOLD_MAX, THRESHOLD_STEP } from '@shared/constants/ai-triage';
|
||||
import type { TrustLevel } from '@shared/constants/ai-triage';
|
||||
|
||||
const CATEGORIES = ['type', 'priority', 'labels', 'duplicate'] as const;
|
||||
type Category = (typeof CATEGORIES)[number];
|
||||
|
||||
const TRUST_LEVELS: TrustLevel[] = ['crawl', 'walk', 'run'];
|
||||
|
||||
const TRUST_LEVEL_PRESETS: Record<TrustLevel, Record<Category, boolean>> = {
|
||||
crawl: { type: false, priority: false, labels: false, duplicate: false },
|
||||
walk: { type: false, priority: false, labels: true, duplicate: true },
|
||||
run: { type: true, priority: true, labels: true, duplicate: true },
|
||||
};
|
||||
|
||||
function deriveTrustLevel(config: ProgressiveTrustConfig): TrustLevel {
|
||||
const allEnabled = CATEGORIES.every((c) => config.autoApply[c].enabled);
|
||||
const noneEnabled = CATEGORIES.every((c) => !config.autoApply[c].enabled);
|
||||
if (allEnabled) return 'run';
|
||||
if (noneEnabled) return 'crawl';
|
||||
return 'walk';
|
||||
}
|
||||
|
||||
interface ProgressiveTrustSettingsProps {
|
||||
config: ProgressiveTrustConfig;
|
||||
onSave: (config: ProgressiveTrustConfig) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function ProgressiveTrustSettings({ config: initialConfig, onSave, onCancel }: ProgressiveTrustSettingsProps) {
|
||||
const { t } = useTranslation(['common']);
|
||||
const [config, setConfig] = useState<ProgressiveTrustConfig>(() =>
|
||||
JSON.parse(JSON.stringify(initialConfig)),
|
||||
);
|
||||
|
||||
const toggleCategory = (category: Category) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
autoApply: {
|
||||
...prev.autoApply,
|
||||
[category]: {
|
||||
...prev.autoApply[category],
|
||||
enabled: !prev.autoApply[category].enabled,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const setTrustLevel = (level: TrustLevel) => {
|
||||
const preset = TRUST_LEVEL_PRESETS[level];
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
autoApply: {
|
||||
type: { ...prev.autoApply.type, enabled: preset.type },
|
||||
priority: { ...prev.autoApply.priority, enabled: preset.priority },
|
||||
labels: { ...prev.autoApply.labels, enabled: preset.labels },
|
||||
duplicate: { ...prev.autoApply.duplicate, enabled: preset.duplicate },
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const currentLevel = deriveTrustLevel(config);
|
||||
|
||||
const setThreshold = (category: Category, threshold: number) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
autoApply: {
|
||||
...prev.autoApply,
|
||||
[category]: {
|
||||
...prev.autoApply[category],
|
||||
threshold,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="space-y-4" aria-label={t('common:progressiveTrust.title')}>
|
||||
{/* Trust level radio group */}
|
||||
<fieldset className="space-y-2">
|
||||
<legend className="text-sm font-medium text-foreground">{t('common:progressiveTrust.trustLevel')}</legend>
|
||||
<div className="flex gap-4">
|
||||
{TRUST_LEVELS.map((level) => (
|
||||
<label key={level} className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="trust-level"
|
||||
value={level}
|
||||
checked={currentLevel === level}
|
||||
onChange={() => setTrustLevel(level)}
|
||||
/>
|
||||
<span className="text-sm">{t(`common:progressiveTrust.${level}`)}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{currentLevel === 'run' && (
|
||||
<p role="alert" className="text-xs text-destructive">
|
||||
{t('common:progressiveTrust.runWarning')}
|
||||
</p>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
{/* Category rows */}
|
||||
{CATEGORIES.map((category) => (
|
||||
<div key={category} className="flex items-center gap-3">
|
||||
<label className="flex items-center gap-2 min-w-[120px]">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.autoApply[category].enabled}
|
||||
onChange={() => toggleCategory(category)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm capitalize">{t(`common:progressiveTrust.${category}`)}</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min={THRESHOLD_MIN}
|
||||
max={THRESHOLD_MAX}
|
||||
step={THRESHOLD_STEP}
|
||||
value={config.autoApply[category].threshold}
|
||||
onChange={(e) => setThreshold(category, Number.parseFloat(e.target.value))}
|
||||
disabled={!config.autoApply[category].enabled}
|
||||
className="flex-1"
|
||||
aria-label={t('common:progressiveTrust.threshold', { category })}
|
||||
/>
|
||||
<span className="text-xs text-foreground/50 w-10 text-right">
|
||||
{Math.round(config.autoApply[category].threshold * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Batch size */}
|
||||
<div className="flex items-center gap-3">
|
||||
<label htmlFor="trust-batch-size" className="text-sm min-w-[120px]">{t('common:progressiveTrust.batchSize')}</label>
|
||||
<input
|
||||
id="trust-batch-size"
|
||||
type="number"
|
||||
value={config.batchSize}
|
||||
onChange={(e) => setConfig((prev) => ({ ...prev, batchSize: Number.parseInt(e.target.value, 10) || 0 }))}
|
||||
className="w-20 bg-transparent text-sm border border-border/50 rounded px-2 py-1 outline-none"
|
||||
min={1}
|
||||
max={200}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Confirm above */}
|
||||
<div className="flex items-center gap-3">
|
||||
<label htmlFor="trust-confirm-above" className="text-sm min-w-[120px]">{t('common:progressiveTrust.confirmAbove')}</label>
|
||||
<input
|
||||
id="trust-confirm-above"
|
||||
type="number"
|
||||
value={config.confirmAbove}
|
||||
onChange={(e) => setConfig((prev) => ({ ...prev, confirmAbove: Number.parseInt(e.target.value, 10) || 0 }))}
|
||||
className="w-20 bg-transparent text-sm border border-border/50 rounded px-2 py-1 outline-none"
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 justify-end pt-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('common:progressiveTrust.cancel')}
|
||||
className="text-xs px-3 py-1.5 rounded bg-foreground/10 hover:bg-foreground/20 text-foreground/70 transition-colors"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common:progressiveTrust.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('common:progressiveTrust.save')}
|
||||
className="text-xs px-3 py-1.5 rounded bg-blue-600 hover:bg-blue-500 text-white transition-colors"
|
||||
onClick={() => onSave(config)}
|
||||
>
|
||||
{t('common:progressiveTrust.save')}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* Connected wrapper for ProgressiveTrustSettings that loads config via IPC.
|
||||
* Used in SectionRouter where hooks can't be called conditionally.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { ProgressiveTrustSettings } from './ProgressiveTrustSettings';
|
||||
import { useProjectStore } from '../../../stores/project-store';
|
||||
import { createDefaultProgressiveTrust } from '@shared/types/ai-triage';
|
||||
import type { ProgressiveTrustConfig } from '@shared/types/ai-triage';
|
||||
|
||||
export function ProgressiveTrustSettingsConnected() {
|
||||
const projectId = useProjectStore((s) => s.activeProjectId);
|
||||
const [config, setConfig] = useState<ProgressiveTrustConfig>(createDefaultProgressiveTrust);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectId) return;
|
||||
window.electronAPI.github
|
||||
.getProgressiveTrust(projectId)
|
||||
.then((c) => {
|
||||
setConfig(c);
|
||||
setVisible(true);
|
||||
})
|
||||
.catch(() => {
|
||||
setVisible(true);
|
||||
});
|
||||
}, [projectId]);
|
||||
|
||||
const handleSave = useCallback(
|
||||
async (updated: ProgressiveTrustConfig) => {
|
||||
if (!projectId) return;
|
||||
await window.electronAPI.github.saveProgressiveTrust(projectId, updated);
|
||||
setConfig(updated);
|
||||
},
|
||||
[projectId],
|
||||
);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
// Reset to last saved config by re-fetching
|
||||
if (!projectId) return;
|
||||
window.electronAPI.github
|
||||
.getProgressiveTrust(projectId)
|
||||
.then(setConfig)
|
||||
.catch(() => { /* config not available yet — use defaults */ });
|
||||
}, [projectId]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<ProgressiveTrustSettings
|
||||
config={config}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { ProgressiveTrustSettings } from '../ProgressiveTrustSettings';
|
||||
import { createDefaultProgressiveTrust } from '@shared/types/ai-triage';
|
||||
|
||||
// Mock i18n
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
const defaultProps = {
|
||||
config: createDefaultProgressiveTrust(),
|
||||
onSave: vi.fn(),
|
||||
onCancel: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('ProgressiveTrustSettings', () => {
|
||||
it('renders four category rows', () => {
|
||||
render(<ProgressiveTrustSettings {...defaultProps} />);
|
||||
expect(screen.getByText(/type/i)).toBeDefined();
|
||||
expect(screen.getByText(/priority/i)).toBeDefined();
|
||||
expect(screen.getByText(/labels/i)).toBeDefined();
|
||||
expect(screen.getByText(/duplicate/i)).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders toggle for each category', () => {
|
||||
render(<ProgressiveTrustSettings {...defaultProps} />);
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
expect(checkboxes.length).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
|
||||
it('calls onSave when save button clicked', () => {
|
||||
render(<ProgressiveTrustSettings {...defaultProps} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /save/i }));
|
||||
expect(defaultProps.onSave).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onCancel when cancel button clicked', () => {
|
||||
render(<ProgressiveTrustSettings {...defaultProps} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
|
||||
expect(defaultProps.onCancel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows batch size input', () => {
|
||||
render(<ProgressiveTrustSettings {...defaultProps} />);
|
||||
const batchInput = screen.getByDisplayValue('50');
|
||||
expect(batchInput).toBeDefined();
|
||||
});
|
||||
|
||||
it('shows confirm-above input', () => {
|
||||
render(<ProgressiveTrustSettings {...defaultProps} />);
|
||||
const confirmInput = screen.getByDisplayValue('10');
|
||||
expect(confirmInput).toBeDefined();
|
||||
});
|
||||
|
||||
it('toggles category enabled state', () => {
|
||||
render(<ProgressiveTrustSettings {...defaultProps} />);
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
fireEvent.click(checkboxes[0]);
|
||||
// After save, the config should reflect the change
|
||||
fireEvent.click(screen.getByRole('button', { name: /save/i }));
|
||||
const savedConfig = defaultProps.onSave.mock.calls[0][0];
|
||||
expect(savedConfig.autoApply.type.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('renders trust level radio group with Crawl/Walk/Run', () => {
|
||||
render(<ProgressiveTrustSettings {...defaultProps} />);
|
||||
const radios = screen.getAllByRole('radio');
|
||||
expect(radios).toHaveLength(3);
|
||||
expect(screen.getByText('common:progressiveTrust.crawl')).toBeDefined();
|
||||
expect(screen.getByText('common:progressiveTrust.walk')).toBeDefined();
|
||||
expect(screen.getByText('common:progressiveTrust.run')).toBeDefined();
|
||||
});
|
||||
|
||||
it('defaults to Crawl when all categories disabled', () => {
|
||||
render(<ProgressiveTrustSettings {...defaultProps} />);
|
||||
const radios = screen.getAllByRole('radio');
|
||||
// Default config has all disabled → crawl selected
|
||||
expect((radios[0] as HTMLInputElement).checked).toBe(true);
|
||||
});
|
||||
|
||||
it('selecting Run enables all categories and shows warning', () => {
|
||||
render(<ProgressiveTrustSettings {...defaultProps} />);
|
||||
const radios = screen.getAllByRole('radio');
|
||||
// Click Run (third radio)
|
||||
fireEvent.click(radios[2]);
|
||||
// All checkboxes should be checked
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
for (const checkbox of checkboxes) {
|
||||
expect((checkbox as HTMLInputElement).checked).toBe(true);
|
||||
}
|
||||
// Warning should be shown
|
||||
expect(screen.getByRole('alert')).toBeDefined();
|
||||
expect(screen.getByText('common:progressiveTrust.runWarning')).toBeDefined();
|
||||
});
|
||||
|
||||
it('selecting Crawl disables all categories', () => {
|
||||
// Start with all enabled (run-like config)
|
||||
const runConfig = createDefaultProgressiveTrust();
|
||||
runConfig.autoApply.type.enabled = true;
|
||||
runConfig.autoApply.priority.enabled = true;
|
||||
runConfig.autoApply.labels.enabled = true;
|
||||
runConfig.autoApply.duplicate.enabled = true;
|
||||
|
||||
render(<ProgressiveTrustSettings {...defaultProps} config={runConfig} />);
|
||||
const radios = screen.getAllByRole('radio');
|
||||
// Click Crawl (first radio)
|
||||
fireEvent.click(radios[0]);
|
||||
// All checkboxes should be unchecked
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
for (const checkbox of checkboxes) {
|
||||
expect((checkbox as HTMLInputElement).checked).toBe(false);
|
||||
}
|
||||
// No warning
|
||||
expect(screen.queryByRole('alert')).toBeNull();
|
||||
});
|
||||
|
||||
it('selecting Walk enables labels and duplicate only', () => {
|
||||
render(<ProgressiveTrustSettings {...defaultProps} />);
|
||||
const radios = screen.getAllByRole('radio');
|
||||
// Click Walk (second radio)
|
||||
fireEvent.click(radios[1]);
|
||||
|
||||
// Save and check config
|
||||
fireEvent.click(screen.getByRole('button', { name: /save/i }));
|
||||
const savedConfig = defaultProps.onSave.mock.calls[0][0];
|
||||
expect(savedConfig.autoApply.type.enabled).toBe(false);
|
||||
expect(savedConfig.autoApply.priority.enabled).toBe(false);
|
||||
expect(savedConfig.autoApply.labels.enabled).toBe(true);
|
||||
expect(savedConfig.autoApply.duplicate.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import { ProgressiveTrustSettingsConnected } from '../ProgressiveTrustSettingsConnected';
|
||||
import { createDefaultProgressiveTrust } from '@shared/types/ai-triage';
|
||||
|
||||
// Mock i18n
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock project store
|
||||
vi.mock('../../../../stores/project-store', () => ({
|
||||
useProjectStore: vi.fn((selector: (s: { activeProjectId: string | null }) => unknown) =>
|
||||
selector({ activeProjectId: 'proj-1' }),
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock electronAPI
|
||||
const mockGithub = {
|
||||
getProgressiveTrust: vi.fn(),
|
||||
saveProgressiveTrust: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
(window as unknown as Record<string, unknown>).electronAPI = { github: mockGithub };
|
||||
mockGithub.getProgressiveTrust.mockResolvedValue(createDefaultProgressiveTrust());
|
||||
mockGithub.saveProgressiveTrust.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
describe('ProgressiveTrustSettingsConnected', () => {
|
||||
it('renders ProgressiveTrustSettings after config loaded', async () => {
|
||||
render(<ProgressiveTrustSettingsConnected />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('common:progressiveTrust.crawl')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('loads config from IPC on mount', async () => {
|
||||
render(<ProgressiveTrustSettingsConnected />);
|
||||
await waitFor(() => {
|
||||
expect(mockGithub.getProgressiveTrust).toHaveBeenCalledWith('proj-1');
|
||||
});
|
||||
});
|
||||
|
||||
it('saves config via IPC on save', async () => {
|
||||
render(<ProgressiveTrustSettingsConnected />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeDefined();
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /save/i }));
|
||||
await waitFor(() => {
|
||||
expect(mockGithub.saveProgressiveTrust).toHaveBeenCalledWith('proj-1', expect.any(Object));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,6 @@ export { TriageResultCard } from './TriageResultCard';
|
||||
export { BatchTriageReview } from './BatchTriageReview';
|
||||
export { IssueSplitDialog } from './IssueSplitDialog';
|
||||
export { TriageProgressOverlay } from './TriageProgressOverlay';
|
||||
export { ProgressiveTrustSettings } from './ProgressiveTrustSettings';
|
||||
|
||||
// Phase 4 — Polish
|
||||
export { LabelSyncSettings } from './LabelSyncSettings';
|
||||
|
||||
@@ -8,7 +8,6 @@ import { GitHubIntegration } from '../integrations/GitHubIntegration';
|
||||
import { GitLabIntegration } from '../integrations/GitLabIntegration';
|
||||
import { InitializationGuard } from '../common/InitializationGuard';
|
||||
import { LabelSyncSettingsConnected } from '../../github-issues/components/LabelSyncSettingsConnected';
|
||||
import { ProgressiveTrustSettingsConnected } from '../../github-issues/components/ProgressiveTrustSettingsConnected';
|
||||
import { InvestigationSettings } from '../../github-issues/components/InvestigationSettings';
|
||||
import type { ProjectSettingsSection } from '../ProjectSettingsContent';
|
||||
|
||||
@@ -143,7 +142,6 @@ export function SectionRouter({
|
||||
setSettings={setSettings}
|
||||
/>
|
||||
<LabelSyncSettingsConnected />
|
||||
<ProgressiveTrustSettingsConnected />
|
||||
<InvestigationSettings projectId={project.id} />
|
||||
</InitializationGuard>
|
||||
</SettingsSection>
|
||||
|
||||
@@ -312,9 +312,6 @@ const browserMockAPI: ElectronAPI = {
|
||||
|
||||
savePendingReview: async () => true,
|
||||
loadPendingReview: async () => [],
|
||||
saveProgressiveTrust: async () => true,
|
||||
getProgressiveTrust: async () => ({ autoApply: { type: { enabled: false, threshold: 0.9 }, priority: { enabled: false, threshold: 0.9 }, labels: { enabled: false, threshold: 0.9 }, duplicate: { enabled: false, threshold: 0.9 } }, batchSize: 50, confirmAbove: 10 }),
|
||||
|
||||
// Label Sync (Phase 4)
|
||||
enableLabelSync: async () => ({ created: 0, updated: 0, removed: 0, errors: [] }),
|
||||
disableLabelSync: async () => ({ success: true }),
|
||||
|
||||
@@ -1,369 +0,0 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useAITriageStore } from '../ai-triage-store';
|
||||
import type { TriageReviewItem, ProgressiveTrustConfig } from '@shared/types/ai-triage';
|
||||
|
||||
function makeReviewItem(
|
||||
overrides: Partial<TriageReviewItem> & { issueNumber: number },
|
||||
): TriageReviewItem {
|
||||
return {
|
||||
issueTitle: `Issue #${overrides.issueNumber}`,
|
||||
status: 'pending',
|
||||
result: {
|
||||
category: 'bug',
|
||||
confidence: 0.8,
|
||||
labelsToAdd: [],
|
||||
labelsToRemove: [],
|
||||
isDuplicate: false,
|
||||
isSpam: false,
|
||||
isFeatureCreep: false,
|
||||
suggestedBreakdown: [],
|
||||
priority: 'medium',
|
||||
triagedAt: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeTrustConfig(
|
||||
overrides?: Partial<ProgressiveTrustConfig>,
|
||||
): ProgressiveTrustConfig {
|
||||
return {
|
||||
autoApply: {
|
||||
type: { enabled: false, threshold: 0.9 },
|
||||
priority: { enabled: false, threshold: 0.9 },
|
||||
labels: { enabled: false, threshold: 0.9 },
|
||||
duplicate: { enabled: false, threshold: 0.9 },
|
||||
},
|
||||
batchSize: 50,
|
||||
confirmAbove: 10,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('useAITriageStore', () => {
|
||||
beforeEach(() => {
|
||||
useAITriageStore.setState({
|
||||
isTriaging: false,
|
||||
triageProgress: null,
|
||||
reviewItems: [],
|
||||
enrichmentProgress: null,
|
||||
enrichmentResult: null,
|
||||
splitSuggestion: null,
|
||||
splitProgress: null,
|
||||
lastError: null,
|
||||
lastBatchSnapshot: null,
|
||||
});
|
||||
});
|
||||
|
||||
describe('autoApplyByTrust', () => {
|
||||
it('auto-applies items above labels threshold when labels enabled', () => {
|
||||
useAITriageStore.getState().addReviewItems([
|
||||
makeReviewItem({
|
||||
issueNumber: 1,
|
||||
result: {
|
||||
category: 'bug',
|
||||
confidence: 0.95,
|
||||
labelsToAdd: ['bug'],
|
||||
labelsToRemove: [],
|
||||
isDuplicate: false,
|
||||
isSpam: false,
|
||||
isFeatureCreep: false,
|
||||
suggestedBreakdown: [],
|
||||
priority: 'high',
|
||||
triagedAt: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const config = makeTrustConfig({
|
||||
autoApply: {
|
||||
type: { enabled: false, threshold: 0.9 },
|
||||
priority: { enabled: false, threshold: 0.9 },
|
||||
labels: { enabled: true, threshold: 0.9 },
|
||||
duplicate: { enabled: false, threshold: 0.9 },
|
||||
},
|
||||
});
|
||||
|
||||
useAITriageStore.getState().autoApplyByTrust(config);
|
||||
|
||||
expect(useAITriageStore.getState().reviewItems[0].status).toBe('auto-applied');
|
||||
});
|
||||
|
||||
it('leaves items below threshold as pending', () => {
|
||||
useAITriageStore.getState().addReviewItems([
|
||||
makeReviewItem({
|
||||
issueNumber: 1,
|
||||
result: {
|
||||
category: 'bug',
|
||||
confidence: 0.7,
|
||||
labelsToAdd: ['bug'],
|
||||
labelsToRemove: [],
|
||||
isDuplicate: false,
|
||||
isSpam: false,
|
||||
isFeatureCreep: false,
|
||||
suggestedBreakdown: [],
|
||||
priority: 'medium',
|
||||
triagedAt: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const config = makeTrustConfig({
|
||||
autoApply: {
|
||||
type: { enabled: false, threshold: 0.9 },
|
||||
priority: { enabled: false, threshold: 0.9 },
|
||||
labels: { enabled: true, threshold: 0.9 },
|
||||
duplicate: { enabled: false, threshold: 0.9 },
|
||||
},
|
||||
});
|
||||
|
||||
useAITriageStore.getState().autoApplyByTrust(config);
|
||||
|
||||
expect(useAITriageStore.getState().reviewItems[0].status).toBe('pending');
|
||||
});
|
||||
|
||||
it('auto-applies duplicate items above threshold when duplicate enabled', () => {
|
||||
useAITriageStore.getState().addReviewItems([
|
||||
makeReviewItem({
|
||||
issueNumber: 2,
|
||||
result: {
|
||||
category: 'bug',
|
||||
confidence: 0.95,
|
||||
labelsToAdd: [],
|
||||
labelsToRemove: [],
|
||||
isDuplicate: true,
|
||||
duplicateOf: 1,
|
||||
isSpam: false,
|
||||
isFeatureCreep: false,
|
||||
suggestedBreakdown: [],
|
||||
priority: 'medium',
|
||||
triagedAt: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const config = makeTrustConfig({
|
||||
autoApply: {
|
||||
type: { enabled: false, threshold: 0.9 },
|
||||
priority: { enabled: false, threshold: 0.9 },
|
||||
labels: { enabled: false, threshold: 0.9 },
|
||||
duplicate: { enabled: true, threshold: 0.9 },
|
||||
},
|
||||
});
|
||||
|
||||
useAITriageStore.getState().autoApplyByTrust(config);
|
||||
|
||||
expect(useAITriageStore.getState().reviewItems[0].status).toBe('auto-applied');
|
||||
});
|
||||
|
||||
it('skips disabled categories even when confidence is high', () => {
|
||||
useAITriageStore.getState().addReviewItems([
|
||||
makeReviewItem({
|
||||
issueNumber: 1,
|
||||
result: {
|
||||
category: 'bug',
|
||||
confidence: 0.99,
|
||||
labelsToAdd: ['bug'],
|
||||
labelsToRemove: [],
|
||||
isDuplicate: false,
|
||||
isSpam: false,
|
||||
isFeatureCreep: false,
|
||||
suggestedBreakdown: [],
|
||||
priority: 'high',
|
||||
triagedAt: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// All categories disabled
|
||||
const config = makeTrustConfig();
|
||||
|
||||
useAITriageStore.getState().autoApplyByTrust(config);
|
||||
|
||||
expect(useAITriageStore.getState().reviewItems[0].status).toBe('pending');
|
||||
});
|
||||
|
||||
it('does not touch already-accepted or rejected items', () => {
|
||||
useAITriageStore.getState().addReviewItems([
|
||||
makeReviewItem({
|
||||
issueNumber: 1,
|
||||
status: 'accepted',
|
||||
result: {
|
||||
category: 'bug',
|
||||
confidence: 0.95,
|
||||
labelsToAdd: ['bug'],
|
||||
labelsToRemove: [],
|
||||
isDuplicate: false,
|
||||
isSpam: false,
|
||||
isFeatureCreep: false,
|
||||
suggestedBreakdown: [],
|
||||
priority: 'high',
|
||||
triagedAt: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
}),
|
||||
makeReviewItem({
|
||||
issueNumber: 2,
|
||||
status: 'rejected',
|
||||
result: {
|
||||
category: 'bug',
|
||||
confidence: 0.95,
|
||||
labelsToAdd: ['bug'],
|
||||
labelsToRemove: [],
|
||||
isDuplicate: false,
|
||||
isSpam: false,
|
||||
isFeatureCreep: false,
|
||||
suggestedBreakdown: [],
|
||||
priority: 'high',
|
||||
triagedAt: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const config = makeTrustConfig({
|
||||
autoApply: {
|
||||
type: { enabled: false, threshold: 0.9 },
|
||||
priority: { enabled: false, threshold: 0.9 },
|
||||
labels: { enabled: true, threshold: 0.9 },
|
||||
duplicate: { enabled: false, threshold: 0.9 },
|
||||
},
|
||||
});
|
||||
|
||||
useAITriageStore.getState().autoApplyByTrust(config);
|
||||
|
||||
const items = useAITriageStore.getState().reviewItems;
|
||||
expect(items[0].status).toBe('accepted');
|
||||
expect(items[1].status).toBe('rejected');
|
||||
});
|
||||
|
||||
it('labels category requires labelsToAdd to be non-empty', () => {
|
||||
useAITriageStore.getState().addReviewItems([
|
||||
makeReviewItem({
|
||||
issueNumber: 1,
|
||||
result: {
|
||||
category: 'bug',
|
||||
confidence: 0.95,
|
||||
labelsToAdd: [], // empty — no labels to apply
|
||||
labelsToRemove: [],
|
||||
isDuplicate: false,
|
||||
isSpam: false,
|
||||
isFeatureCreep: false,
|
||||
suggestedBreakdown: [],
|
||||
priority: 'high',
|
||||
triagedAt: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const config = makeTrustConfig({
|
||||
autoApply: {
|
||||
type: { enabled: false, threshold: 0.9 },
|
||||
priority: { enabled: false, threshold: 0.9 },
|
||||
labels: { enabled: true, threshold: 0.9 },
|
||||
duplicate: { enabled: false, threshold: 0.9 },
|
||||
},
|
||||
});
|
||||
|
||||
useAITriageStore.getState().autoApplyByTrust(config);
|
||||
|
||||
// Should remain pending since no labels to add
|
||||
expect(useAITriageStore.getState().reviewItems[0].status).toBe('pending');
|
||||
});
|
||||
});
|
||||
|
||||
describe('enrichmentResult', () => {
|
||||
it('setEnrichmentResult stores the result', () => {
|
||||
const result = {
|
||||
issueNumber: 42,
|
||||
problem: 'Login fails',
|
||||
goal: 'Fix login',
|
||||
scopeIn: ['auth'],
|
||||
scopeOut: ['signup'],
|
||||
acceptanceCriteria: ['User can log in'],
|
||||
technicalContext: 'React app',
|
||||
risksEdgeCases: ['Token expiry'],
|
||||
confidence: 0.9,
|
||||
};
|
||||
useAITriageStore.getState().setEnrichmentResult(result);
|
||||
expect(useAITriageStore.getState().enrichmentResult).toEqual(result);
|
||||
});
|
||||
|
||||
it('clearEnrichmentResult resets to null', () => {
|
||||
useAITriageStore.getState().setEnrichmentResult({
|
||||
issueNumber: 1,
|
||||
problem: 'test',
|
||||
goal: 'test',
|
||||
scopeIn: [],
|
||||
scopeOut: [],
|
||||
acceptanceCriteria: [],
|
||||
technicalContext: '',
|
||||
risksEdgeCases: [],
|
||||
confidence: 0.5,
|
||||
});
|
||||
useAITriageStore.getState().clearEnrichmentResult();
|
||||
expect(useAITriageStore.getState().enrichmentResult).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('undoBatch', () => {
|
||||
it('snapshotBeforeApply saves current review items', () => {
|
||||
const items = [
|
||||
makeReviewItem({ issueNumber: 1, status: 'accepted' }),
|
||||
makeReviewItem({ issueNumber: 2, status: 'rejected' }),
|
||||
];
|
||||
useAITriageStore.getState().addReviewItems(items);
|
||||
useAITriageStore.getState().snapshotBeforeApply();
|
||||
|
||||
const snapshot = useAITriageStore.getState().lastBatchSnapshot;
|
||||
expect(snapshot).toHaveLength(2);
|
||||
expect(snapshot?.[0].status).toBe('accepted');
|
||||
});
|
||||
|
||||
it('undoLastBatch restores snapshot and clears it', () => {
|
||||
const items = [
|
||||
makeReviewItem({ issueNumber: 1 }),
|
||||
makeReviewItem({ issueNumber: 2 }),
|
||||
];
|
||||
useAITriageStore.getState().addReviewItems(items);
|
||||
useAITriageStore.getState().snapshotBeforeApply();
|
||||
|
||||
// Simulate apply — accept all
|
||||
useAITriageStore.getState().acceptAllRemaining();
|
||||
expect(useAITriageStore.getState().reviewItems[0].status).toBe('accepted');
|
||||
|
||||
// Undo
|
||||
useAITriageStore.getState().undoLastBatch();
|
||||
const restored = useAITriageStore.getState().reviewItems;
|
||||
expect(restored[0].status).toBe('pending');
|
||||
expect(restored[1].status).toBe('pending');
|
||||
expect(useAITriageStore.getState().lastBatchSnapshot).toBeNull();
|
||||
});
|
||||
|
||||
it('undoLastBatch does nothing when no snapshot', () => {
|
||||
useAITriageStore.getState().addReviewItems([
|
||||
makeReviewItem({ issueNumber: 1, status: 'accepted' }),
|
||||
]);
|
||||
useAITriageStore.getState().undoLastBatch();
|
||||
// Items unchanged
|
||||
expect(useAITriageStore.getState().reviewItems[0].status).toBe('accepted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('lastError', () => {
|
||||
it('setLastError stores error message', () => {
|
||||
useAITriageStore.getState().setLastError('API timeout');
|
||||
expect(useAITriageStore.getState().lastError).toBe('API timeout');
|
||||
});
|
||||
|
||||
it('clearLastError resets error to null', () => {
|
||||
useAITriageStore.getState().setLastError('Some error');
|
||||
useAITriageStore.getState().clearLastError();
|
||||
expect(useAITriageStore.getState().lastError).toBeNull();
|
||||
});
|
||||
|
||||
it('startTriage clears lastError', () => {
|
||||
useAITriageStore.getState().setLastError('Previous error');
|
||||
useAITriageStore.getState().startTriage();
|
||||
expect(useAITriageStore.getState().lastError).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,159 +0,0 @@
|
||||
/**
|
||||
* @deprecated Legacy AI triage store — replaced by investigation-store.ts.
|
||||
* Kept for backwards compatibility. Will be removed in a future cleanup pass.
|
||||
*
|
||||
* AI Triage Zustand store (Phase 3).
|
||||
*
|
||||
* Manages triage operation state, review queue, enrichment progress,
|
||||
* and split suggestion state.
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import type {
|
||||
TriageReviewItem,
|
||||
EnrichmentProgress,
|
||||
SplitSuggestion,
|
||||
SplitProgress,
|
||||
ProgressiveTrustConfig,
|
||||
AIEnrichmentResult,
|
||||
} from '@shared/types/ai-triage';
|
||||
|
||||
// Re-use the enrichment progress type for triage progress display
|
||||
type TriageProgress = EnrichmentProgress;
|
||||
|
||||
interface AITriageState {
|
||||
// Triage operation
|
||||
isTriaging: boolean;
|
||||
triageProgress: TriageProgress | null;
|
||||
|
||||
// Review queue
|
||||
reviewItems: TriageReviewItem[];
|
||||
|
||||
// Enrichment operation
|
||||
enrichmentProgress: EnrichmentProgress | null;
|
||||
enrichmentResult: AIEnrichmentResult | null;
|
||||
|
||||
// Split operation
|
||||
splitSuggestion: SplitSuggestion | null;
|
||||
splitProgress: SplitProgress | null;
|
||||
|
||||
// Error state
|
||||
lastError: string | null;
|
||||
|
||||
// Undo state
|
||||
lastBatchSnapshot: TriageReviewItem[] | null;
|
||||
|
||||
// Actions
|
||||
startTriage: () => void;
|
||||
endTriage: () => void;
|
||||
setTriageProgress: (progress: TriageProgress) => void;
|
||||
addReviewItems: (items: TriageReviewItem[]) => void;
|
||||
acceptReviewItem: (issueNumber: number) => void;
|
||||
rejectReviewItem: (issueNumber: number) => void;
|
||||
acceptAllRemaining: () => void;
|
||||
dismissReview: () => void;
|
||||
getUnreviewedCount: () => number;
|
||||
setEnrichmentProgress: (progress: EnrichmentProgress) => void;
|
||||
clearEnrichmentProgress: () => void;
|
||||
setEnrichmentResult: (result: AIEnrichmentResult) => void;
|
||||
clearEnrichmentResult: () => void;
|
||||
setSplitSuggestion: (suggestion: SplitSuggestion) => void;
|
||||
clearSplitSuggestion: () => void;
|
||||
setSplitProgress: (progress: SplitProgress) => void;
|
||||
clearSplitProgress: () => void;
|
||||
autoApplyByTrust: (config: ProgressiveTrustConfig) => void;
|
||||
snapshotBeforeApply: () => void;
|
||||
undoLastBatch: () => void;
|
||||
setLastError: (error: string) => void;
|
||||
clearLastError: () => void;
|
||||
}
|
||||
|
||||
export const useAITriageStore = create<AITriageState>((set, get) => ({
|
||||
// Initial state
|
||||
isTriaging: false,
|
||||
triageProgress: null,
|
||||
reviewItems: [],
|
||||
enrichmentProgress: null,
|
||||
enrichmentResult: null,
|
||||
splitSuggestion: null,
|
||||
splitProgress: null,
|
||||
lastError: null,
|
||||
lastBatchSnapshot: null,
|
||||
|
||||
// Triage operation
|
||||
startTriage: () => set({ isTriaging: true, lastError: null }),
|
||||
endTriage: () => set({ isTriaging: false, triageProgress: null }),
|
||||
setTriageProgress: (progress) => set({ triageProgress: progress }),
|
||||
|
||||
// Review queue
|
||||
addReviewItems: (items) =>
|
||||
set((state) => ({ reviewItems: [...state.reviewItems, ...items] })),
|
||||
|
||||
acceptReviewItem: (issueNumber) =>
|
||||
set((state) => ({
|
||||
reviewItems: state.reviewItems.map((item) =>
|
||||
item.issueNumber === issueNumber ? { ...item, status: 'accepted' as const } : item,
|
||||
),
|
||||
})),
|
||||
|
||||
rejectReviewItem: (issueNumber) =>
|
||||
set((state) => ({
|
||||
reviewItems: state.reviewItems.map((item) =>
|
||||
item.issueNumber === issueNumber ? { ...item, status: 'rejected' as const } : item,
|
||||
),
|
||||
})),
|
||||
|
||||
acceptAllRemaining: () =>
|
||||
set((state) => ({
|
||||
reviewItems: state.reviewItems.map((item) =>
|
||||
item.status === 'pending' ? { ...item, status: 'accepted' as const } : item,
|
||||
),
|
||||
})),
|
||||
|
||||
dismissReview: () => set({ reviewItems: [] }),
|
||||
|
||||
getUnreviewedCount: () =>
|
||||
get().reviewItems.filter((item) => item.status === 'pending').length,
|
||||
|
||||
// Enrichment
|
||||
setEnrichmentProgress: (progress) => set({ enrichmentProgress: progress }),
|
||||
clearEnrichmentProgress: () => set({ enrichmentProgress: null }),
|
||||
setEnrichmentResult: (result) => set({ enrichmentResult: result }),
|
||||
clearEnrichmentResult: () => set({ enrichmentResult: null }),
|
||||
|
||||
// Split
|
||||
setSplitSuggestion: (suggestion) => set({ splitSuggestion: suggestion }),
|
||||
clearSplitSuggestion: () => set({ splitSuggestion: null }),
|
||||
setSplitProgress: (progress) => set({ splitProgress: progress }),
|
||||
clearSplitProgress: () => set({ splitProgress: null }),
|
||||
|
||||
snapshotBeforeApply: () =>
|
||||
set((state) => ({
|
||||
lastBatchSnapshot: state.reviewItems.map((item) => ({ ...item })),
|
||||
})),
|
||||
|
||||
undoLastBatch: () =>
|
||||
set((state) => {
|
||||
if (!state.lastBatchSnapshot) return state;
|
||||
return { reviewItems: state.lastBatchSnapshot, lastBatchSnapshot: null };
|
||||
}),
|
||||
|
||||
setLastError: (error) => set({ lastError: error }),
|
||||
clearLastError: () => set({ lastError: null }),
|
||||
|
||||
autoApplyByTrust: (config) =>
|
||||
set((state) => ({
|
||||
reviewItems: state.reviewItems.map((item) => {
|
||||
if (item.status !== 'pending') return item;
|
||||
const conf = item.result.confidence;
|
||||
// Check each enabled trust category
|
||||
if (config.autoApply.labels.enabled && conf >= config.autoApply.labels.threshold && item.result.labelsToAdd.length > 0) {
|
||||
return { ...item, status: 'auto-applied' as const };
|
||||
}
|
||||
if (config.autoApply.duplicate.enabled && conf >= config.autoApply.duplicate.threshold && item.result.isDuplicate) {
|
||||
return { ...item, status: 'auto-applied' as const };
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
@@ -12,14 +12,12 @@ import {
|
||||
THRESHOLD_MIN,
|
||||
THRESHOLD_MAX,
|
||||
THRESHOLD_STEP,
|
||||
TRUST_LEVEL_LABELS,
|
||||
ENRICHMENT_COMMENT_FOOTER,
|
||||
getConfidenceLevel,
|
||||
isValidThreshold,
|
||||
clampThreshold,
|
||||
estimateBatchCost,
|
||||
} from '../constants/ai-triage';
|
||||
import type { TrustLevel } from '../constants/ai-triage';
|
||||
|
||||
describe('confidence constants', () => {
|
||||
it('CONFIDENCE_HIGH is 0.8', () => {
|
||||
@@ -63,16 +61,6 @@ describe('threshold constants', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('TRUST_LEVEL_LABELS', () => {
|
||||
it('has entry for every trust level', () => {
|
||||
const allLevels: TrustLevel[] = ['crawl', 'walk', 'run'];
|
||||
for (const level of allLevels) {
|
||||
expect(TRUST_LEVEL_LABELS).toHaveProperty(level);
|
||||
expect(typeof TRUST_LEVEL_LABELS[level]).toBe('string');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ENRICHMENT_COMMENT_FOOTER', () => {
|
||||
it('contains Auto-Claude marker', () => {
|
||||
expect(ENRICHMENT_COMMENT_FOOTER).toContain('Auto-Claude');
|
||||
|
||||
@@ -3,45 +3,8 @@
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
createDefaultProgressiveTrust,
|
||||
mapTriageCategory,
|
||||
} from '../types/ai-triage';
|
||||
import type { ProgressiveTrustConfig } from '../types/ai-triage';
|
||||
|
||||
describe('createDefaultProgressiveTrust', () => {
|
||||
it('returns all categories disabled', () => {
|
||||
const config = createDefaultProgressiveTrust();
|
||||
expect(config.autoApply.type.enabled).toBe(false);
|
||||
expect(config.autoApply.priority.enabled).toBe(false);
|
||||
expect(config.autoApply.labels.enabled).toBe(false);
|
||||
expect(config.autoApply.duplicate.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('returns 0.9 threshold for all categories', () => {
|
||||
const config = createDefaultProgressiveTrust();
|
||||
expect(config.autoApply.type.threshold).toBe(0.9);
|
||||
expect(config.autoApply.priority.threshold).toBe(0.9);
|
||||
expect(config.autoApply.labels.threshold).toBe(0.9);
|
||||
expect(config.autoApply.duplicate.threshold).toBe(0.9);
|
||||
});
|
||||
|
||||
it('returns default batch size of 50', () => {
|
||||
const config = createDefaultProgressiveTrust();
|
||||
expect(config.batchSize).toBe(50);
|
||||
});
|
||||
|
||||
it('returns default confirmAbove of 10', () => {
|
||||
const config = createDefaultProgressiveTrust();
|
||||
expect(config.confirmAbove).toBe(10);
|
||||
});
|
||||
|
||||
it('returns a fresh object each time (no shared references)', () => {
|
||||
const a = createDefaultProgressiveTrust();
|
||||
const b = createDefaultProgressiveTrust();
|
||||
expect(a).not.toBe(b);
|
||||
expect(a.autoApply).not.toBe(b.autoApply);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapTriageCategory', () => {
|
||||
it('maps bug to bug', () => {
|
||||
|
||||
@@ -26,18 +26,6 @@ export const THRESHOLD_MIN = 0.5;
|
||||
export const THRESHOLD_MAX = 1.0;
|
||||
export const THRESHOLD_STEP = 0.05;
|
||||
|
||||
// ============================================
|
||||
// Trust Levels
|
||||
// ============================================
|
||||
|
||||
export type TrustLevel = 'crawl' | 'walk' | 'run';
|
||||
|
||||
export const TRUST_LEVEL_LABELS: Record<TrustLevel, string> = {
|
||||
crawl: 'Suggestions Only',
|
||||
walk: 'Auto-Apply Above Threshold',
|
||||
run: 'Fully Automated',
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Enrichment Comment
|
||||
// ============================================
|
||||
|
||||
@@ -475,8 +475,6 @@ export const IPC_CHANNELS = {
|
||||
GITHUB_TRIAGE_CANCEL: 'github:triage:cancel',
|
||||
GITHUB_TRIAGE_SAVE_PENDING_REVIEW: 'github:triage:savePendingReview',
|
||||
GITHUB_TRIAGE_LOAD_PENDING_REVIEW: 'github:triage:loadPendingReview',
|
||||
GITHUB_TRIAGE_SAVE_TRUST: 'github:triage:saveTrust',
|
||||
GITHUB_TRIAGE_GET_TRUST: 'github:triage:getTrust',
|
||||
|
||||
// GitHub Enrichment operations
|
||||
GITHUB_ENRICHMENT_GET_ALL: 'github:enrichment:getAll',
|
||||
|
||||
@@ -643,23 +643,6 @@
|
||||
"cancel": "Cancel",
|
||||
"duplicateWarning": "An AI enrichment comment already exists on this issue. Posting will create a duplicate."
|
||||
},
|
||||
"progressiveTrust": {
|
||||
"title": "Progressive Trust Settings",
|
||||
"type": "Type",
|
||||
"priority": "Priority",
|
||||
"labels": "Labels",
|
||||
"duplicate": "Duplicate",
|
||||
"threshold": "{{category}} threshold",
|
||||
"batchSize": "Batch size",
|
||||
"confirmAbove": "Confirm above",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"trustLevel": "Trust Level",
|
||||
"crawl": "Suggestions Only",
|
||||
"walk": "Auto-Apply Above Threshold",
|
||||
"run": "Fully Automated",
|
||||
"runWarning": "All triage results will be applied automatically without review."
|
||||
},
|
||||
"labelSync": {
|
||||
"title": "Label Synchronization",
|
||||
"description": "Sync workflow states as ac: labels on GitHub",
|
||||
|
||||
@@ -643,23 +643,6 @@
|
||||
"cancel": "Annuler",
|
||||
"duplicateWarning": "Un commentaire d'enrichissement IA existe déjà sur cette issue. La publication créera un doublon."
|
||||
},
|
||||
"progressiveTrust": {
|
||||
"title": "Paramètres de confiance progressive",
|
||||
"type": "Type",
|
||||
"priority": "Priorité",
|
||||
"labels": "Labels",
|
||||
"duplicate": "Doublon",
|
||||
"threshold": "Seuil de {{category}}",
|
||||
"batchSize": "Taille du lot",
|
||||
"confirmAbove": "Confirmer au-dessus de",
|
||||
"save": "Enregistrer",
|
||||
"cancel": "Annuler",
|
||||
"trustLevel": "Niveau de confiance",
|
||||
"crawl": "Suggestions uniquement",
|
||||
"walk": "Auto-application au-dessus du seuil",
|
||||
"run": "Entièrement automatisé",
|
||||
"runWarning": "Tous les résultats de triage seront appliqués automatiquement sans révision."
|
||||
},
|
||||
"labelSync": {
|
||||
"title": "Synchronisation des labels",
|
||||
"description": "Synchroniser les états du workflow en tant que labels ac: sur GitHub",
|
||||
|
||||
@@ -8,21 +8,6 @@ import type { TriageCategory as EnrichmentTriageCategory } from './enrichment';
|
||||
// (cannot import directly due to main/renderer boundary — consumers
|
||||
// in renderer use IPC; only main-process code imports triage-handlers)
|
||||
|
||||
// ============================================
|
||||
// Progressive Trust
|
||||
// ============================================
|
||||
|
||||
export interface ProgressiveTrustConfig {
|
||||
autoApply: {
|
||||
type: { enabled: boolean; threshold: number };
|
||||
priority: { enabled: boolean; threshold: number };
|
||||
labels: { enabled: boolean; threshold: number };
|
||||
duplicate: { enabled: boolean; threshold: number };
|
||||
};
|
||||
batchSize: number;
|
||||
confirmAbove: number;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// AI Enrichment
|
||||
// ============================================
|
||||
@@ -116,23 +101,6 @@ export interface ApplyResultsProgress {
|
||||
currentIssueNumber?: number;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Factory Functions
|
||||
// ============================================
|
||||
|
||||
export function createDefaultProgressiveTrust(): ProgressiveTrustConfig {
|
||||
return {
|
||||
autoApply: {
|
||||
type: { enabled: false, threshold: 0.9 },
|
||||
priority: { enabled: false, threshold: 0.9 },
|
||||
labels: { enabled: false, threshold: 0.9 },
|
||||
duplicate: { enabled: false, threshold: 0.9 },
|
||||
},
|
||||
batchSize: 50,
|
||||
confirmAbove: 10,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Category Mapping
|
||||
// ============================================
|
||||
|
||||
Reference in New Issue
Block a user