[AI] fix
This commit is contained in:
89
.gitignore
vendored
Normal file
89
.gitignore
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
Icon?
|
||||
|
||||
# Configuration and data files generated by the application
|
||||
config.json
|
||||
tasks/
|
||||
*.json
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Time Tracker package."""
|
||||
"""Entry point for running the time tracker as a module."""
|
||||
|
||||
from .main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -51,16 +51,28 @@ except ImportError:
|
||||
return 0
|
||||
|
||||
|
||||
class ConfigWrapper:
|
||||
"""Wrapper to allow attribute access to config dict"""
|
||||
|
||||
def __init__(self, config_dict: Dict):
|
||||
self.__dict__.update(config_dict)
|
||||
|
||||
def update(self, new_config: Dict):
|
||||
"""Update the config and underlying dict"""
|
||||
self.__dict__.update(new_config)
|
||||
|
||||
|
||||
class WorkTracker:
|
||||
def __init__(self):
|
||||
self.config = load_config()
|
||||
raw_config = load_config()
|
||||
self.config = ConfigWrapper(raw_config)
|
||||
self.session_manager = SessionManager("tasks")
|
||||
self.active_window = ""
|
||||
self.window_classification = "neutral"
|
||||
self.last_activity = time.time()
|
||||
self.session_locked = False
|
||||
self.is_tracking = self.config.get("tracking_enabled", True)
|
||||
self.current_task_id = self.config.get("current_task_id")
|
||||
self.is_tracking = self.config.tracking_enabled
|
||||
self.current_task_id = self.config.current_task_id
|
||||
|
||||
# Start monitoring in background
|
||||
self.start_monitoring()
|
||||
@@ -119,7 +131,7 @@ class WorkTracker:
|
||||
def classify_window(self, title: str) -> str:
|
||||
"""Classify window as work, distraction, or neutral"""
|
||||
title_lower = title.lower()
|
||||
rules = self.config.get("window_rules", {})
|
||||
rules = self.config.window_rules # Access through wrapper
|
||||
|
||||
# Check distractions first
|
||||
for keyword in rules.get("distraction", []):
|
||||
@@ -150,7 +162,6 @@ class WorkTracker:
|
||||
# Check if tracking is enabled
|
||||
if not self.is_tracking:
|
||||
return False
|
||||
|
||||
# Check if session is locked
|
||||
if self.session_locked:
|
||||
return False
|
||||
@@ -186,16 +197,19 @@ class WorkTracker:
|
||||
def toggle_tracking(self):
|
||||
"""Toggle tracking on/off"""
|
||||
self.is_tracking = not self.is_tracking
|
||||
self.config["tracking_enabled"] = self.is_tracking
|
||||
save_config(self.config)
|
||||
self.config.tracking_enabled = self.is_tracking
|
||||
# Update raw config too
|
||||
raw_config = {key: value for key, value in vars(self.config).items() if not key.startswith('_')}
|
||||
save_config(raw_config)
|
||||
|
||||
def create_task(self, task_id: str):
|
||||
"""Create a new task and start tracking it"""
|
||||
project_id = task_id.split('-')[0] if '-' in task_id else task_id
|
||||
self.session_manager.create_session(task_id, project_id)
|
||||
self.current_task_id = task_id
|
||||
self.config["current_task_id"] = task_id
|
||||
save_config(self.config)
|
||||
self.config.current_task_id = task_id
|
||||
raw_config = {key: value for key, value in vars(self.config).items() if not key.startswith('_')}
|
||||
save_config(raw_config)
|
||||
|
||||
def switch_task(self, task_id: str):
|
||||
"""Switch to an existing task"""
|
||||
@@ -207,8 +221,9 @@ class WorkTracker:
|
||||
project_id = task_id.split('-')[0] if '-' in task_id else task_id
|
||||
self.session_manager.create_session(task_id, project_id)
|
||||
self.current_task_id = task_id
|
||||
self.config["current_task_id"] = task_id
|
||||
save_config(self.config)
|
||||
self.config.current_task_id = task_id
|
||||
raw_config = {key: value for key, value in vars(self.config).items() if not key.startswith('_')}
|
||||
save_config(raw_config)
|
||||
|
||||
def get_available_tasks(self) -> List[str]:
|
||||
"""Get list of all available tasks"""
|
||||
|
||||
@@ -18,11 +18,26 @@ def register_session_listener(callback: Callable[[str], None]) -> None:
|
||||
"""Register a callback for Windows session lock/unlock events"""
|
||||
|
||||
def session_event_handler(hwnd, msg, wparam, lparam):
|
||||
# Check if the constant exists before using it
|
||||
try:
|
||||
if msg == win32con.WM_WTSSESSION_CHANGE:
|
||||
if wparam == win32con.WTS_SESSION_LOCK:
|
||||
if wparam == getattr(win32con, 'WTS_SESSION_LOCK', 0x1):
|
||||
callback("locked")
|
||||
elif wparam == win32con.WTS_SESSION_UNLOCK:
|
||||
elif wparam == getattr(win32con, 'WTS_SESSION_UNLOCK', 0x2):
|
||||
callback("unlocked")
|
||||
except AttributeError:
|
||||
# Some versions of pywin32 might not have these constants
|
||||
# Use numeric values as fallback
|
||||
WM_WTSSESSION_CHANGE = 0x02B1
|
||||
WTS_SESSION_LOCK = 0x1
|
||||
WTS_SESSION_UNLOCK = 0x2
|
||||
|
||||
if msg == WM_WTSSESSION_CHANGE:
|
||||
if wparam == WTS_SESSION_LOCK:
|
||||
callback("locked")
|
||||
elif wparam == WTS_SESSION_UNLOCK:
|
||||
callback("unlocked")
|
||||
|
||||
return win32gui.DefWindowProc(hwnd, msg, wparam, lparam)
|
||||
|
||||
def run_message_loop():
|
||||
|
||||
133
tests/test_main_integration.py
Normal file
133
tests/test_main_integration.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from unittest.mock import patch, MagicMock
|
||||
import tkinter as tk
|
||||
from time_tracker.main import WorkTracker, ConfigWrapper
|
||||
|
||||
|
||||
def test_tracker_initialization():
|
||||
"""Test that WorkTracker initializes without errors"""
|
||||
# Create a temporary config file to avoid creating one in the current directory
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp:
|
||||
json.dump({
|
||||
"tracking_enabled": True,
|
||||
"current_task_id": None,
|
||||
"window_rules": {
|
||||
"work": ["PyCharm", "VS Code"],
|
||||
"distraction": ["YouTube", "Twitter"]
|
||||
}
|
||||
}, tmp)
|
||||
tmp_path = tmp.name
|
||||
|
||||
original_config_path = "config.json"
|
||||
|
||||
# Temporarily replace the config file
|
||||
if os.path.exists(original_config_path):
|
||||
os.rename(original_config_path, f"{original_config_path}.backup")
|
||||
|
||||
os.rename(tmp_path, original_config_path)
|
||||
|
||||
try:
|
||||
# Initialize the tracker
|
||||
tracker = WorkTracker()
|
||||
|
||||
# Verify that config is properly loaded as a ConfigWrapper object
|
||||
assert hasattr(tracker, 'config')
|
||||
assert isinstance(tracker.config, ConfigWrapper)
|
||||
assert hasattr(tracker.config, 'tracking_enabled')
|
||||
assert hasattr(tracker.config, 'current_task_id')
|
||||
assert hasattr(tracker.config, 'window_rules')
|
||||
assert tracker.config.tracking_enabled is True
|
||||
assert tracker.config.current_task_id is None
|
||||
|
||||
finally:
|
||||
# Restore original config file
|
||||
os.remove(original_config_path)
|
||||
if os.path.exists(f"{original_config_path}.backup"):
|
||||
os.rename(f"{original_config_path}.backup", original_config_path)
|
||||
|
||||
|
||||
def test_config_wrapper_functionality():
|
||||
"""Test that ConfigWrapper provides attribute access to config values"""
|
||||
config_dict = {
|
||||
"tracking_enabled": True,
|
||||
"current_task_id": "TEST-123",
|
||||
"window_rules": {
|
||||
"work": ["PyCharm", "VS Code"],
|
||||
"distraction": ["YouTube", "Twitter"]
|
||||
}
|
||||
}
|
||||
|
||||
wrapper = ConfigWrapper(config_dict)
|
||||
|
||||
# Test attribute access
|
||||
assert wrapper.tracking_enabled is True
|
||||
assert wrapper.current_task_id == "TEST-123"
|
||||
assert "PyCharm" in wrapper.window_rules["work"]
|
||||
|
||||
# Test updating config
|
||||
wrapper.tracking_enabled = False
|
||||
assert wrapper.tracking_enabled is False
|
||||
# Note: Direct assignment doesn't update the original dict, but the wrapper still works
|
||||
|
||||
|
||||
def test_tracker_config_attribute_access():
|
||||
"""Test that config can be accessed as an object with attributes"""
|
||||
|
||||
# Create a mock config object that behaves like the real config
|
||||
class MockConfig:
|
||||
def __init__(self):
|
||||
self.tracking_enabled = True
|
||||
self.current_task_id = "TEST-123"
|
||||
|
||||
config = MockConfig()
|
||||
assert hasattr(config, 'tracking_enabled')
|
||||
assert config.tracking_enabled is True
|
||||
assert config.current_task_id == "TEST-123"
|
||||
|
||||
|
||||
def test_overlay_compatible_config():
|
||||
"""Test that the config structure is compatible with overlay expectations"""
|
||||
from time_tracker.core.config import load_config
|
||||
|
||||
# Create a temporary config file
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp:
|
||||
json.dump({
|
||||
"tracking_enabled": True,
|
||||
"current_task_id": "TEST-123",
|
||||
"window_rules": {
|
||||
"work": ["PyCharm", "VS Code"],
|
||||
"distraction": ["YouTube", "Twitter"]
|
||||
}
|
||||
}, tmp)
|
||||
tmp_path = tmp.name
|
||||
|
||||
original_config_path = "config.json"
|
||||
|
||||
# Temporarily replace the config file
|
||||
if os.path.exists(original_config_path):
|
||||
os.rename(original_config_path, f"{original_config_path}.backup")
|
||||
|
||||
os.rename(tmp_path, original_config_path)
|
||||
|
||||
try:
|
||||
# Load config using the actual function
|
||||
config = load_config()
|
||||
|
||||
# Verify structure
|
||||
assert isinstance(config, dict)
|
||||
assert config.get("tracking_enabled") is True
|
||||
assert config.get("current_task_id") == "TEST-123"
|
||||
|
||||
# Create a compatible object that supports attribute access
|
||||
wrapped_config = ConfigWrapper(config)
|
||||
assert hasattr(wrapped_config, 'tracking_enabled')
|
||||
assert wrapped_config.tracking_enabled is True
|
||||
assert wrapped_config.current_task_id == "TEST-123"
|
||||
|
||||
finally:
|
||||
# Restore original config file
|
||||
os.remove(original_config_path)
|
||||
if os.path.exists(f"{original_config_path}.backup"):
|
||||
os.rename(f"{original_config_path}.backup", original_config_path)
|
||||
Reference in New Issue
Block a user