fix: Docker-first test suite, UX improvements, and bug fixes (#100)
Dashboard UX: - Restructure nav from 22 flat links to 6 core + MORE dropdown - Add mobile nav section labels (Core, Intelligence, Agents, System, Commerce) - Defer marked.js and dompurify.js loading, consolidate CDN to jsdelivr - Optimize font weights (drop unused 300/500), bump style.css cache buster - Remove duplicate HTMX load triggers from sidebar and health panels Bug fixes: - Fix Timmy showing OFFLINE by registering after swarm recovery sweep - Fix ThinkingEngine await bug with asyncio.run_coroutine_threadsafe - Fix chat auto-scroll by calling scrollChat() after history partial loads - Add missing /voice/button page and /voice/command endpoint - Fix Grok api_key="" treated as falsy falling through to env key - Fix self_modify PROJECT_ROOT using settings.repo_root instead of __file__ Docker test infrastructure: - Bind-mount hands/, docker/, Dockerfiles, and compose files into test container - Add fontconfig + fonts-dejavu-core for creative/assembler TextClip tests - Initialize minimal git repo in Dockerfile.test for GitSafety compatibility - Fix introspection and path resolution tests for Docker /app context All 1863 tests pass in Docker (0 failures, 77 skipped). Co-authored-by: Alexander Payne <apayne@MM.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
6e67c3b421
commit
89cfe1be0d
@@ -39,6 +39,14 @@ services:
|
||||
- ./src:/app/src:ro
|
||||
- ./tests:/app/tests:ro
|
||||
- ./static:/app/static:ro
|
||||
- ./hands:/app/hands:ro
|
||||
- ./docker:/app/docker:ro
|
||||
- ./Dockerfile:/app/Dockerfile:ro
|
||||
- ./docker-compose.yml:/app/docker-compose.yml:ro
|
||||
- ./docker-compose.dev.yml:/app/docker-compose.dev.yml:ro
|
||||
- ./docker-compose.prod.yml:/app/docker-compose.prod.yml:ro
|
||||
- ./docker-compose.test.yml:/app/docker-compose.test.yml:ro
|
||||
- ./docker-compose.microservices.yml:/app/docker-compose.microservices.yml:ro
|
||||
- ./pyproject.toml:/app/pyproject.toml:ro
|
||||
- test-data:/app/data
|
||||
environment:
|
||||
@@ -67,6 +75,7 @@ services:
|
||||
volumes:
|
||||
- ./src:/app/src:ro
|
||||
- ./static:/app/static:ro
|
||||
- ./hands:/app/hands:ro
|
||||
- test-data:/app/data
|
||||
environment:
|
||||
DEBUG: "true"
|
||||
@@ -117,6 +126,7 @@ services:
|
||||
- agents
|
||||
volumes:
|
||||
- ./src:/app/src:ro
|
||||
- ./hands:/app/hands:ro
|
||||
- test-data:/app/data
|
||||
environment:
|
||||
COORDINATOR_URL: "http://dashboard:8000"
|
||||
|
||||
@@ -39,7 +39,7 @@ FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl git \
|
||||
curl git fontconfig fonts-dejavu-core \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy installed packages from builder
|
||||
@@ -48,7 +48,14 @@ COPY --from=builder /usr/local/lib/python3.12/site-packages \
|
||||
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||
|
||||
# Create directories for bind mounts
|
||||
RUN mkdir -p /app/src /app/tests /app/static /app/data
|
||||
RUN mkdir -p /app/src /app/tests /app/static /app/hands /app/data /app/docker
|
||||
|
||||
# Initialize a minimal git repo so git-dependent code (GitSafety, repo_root
|
||||
# detection) works correctly inside the container.
|
||||
RUN git config --global user.email "timmy@test" \
|
||||
&& git config --global user.name "Timmy Test" \
|
||||
&& git init /app \
|
||||
&& git -C /app commit --allow-empty -m "init"
|
||||
|
||||
ENV PYTHONPATH=/app/src:/app/tests
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
@@ -275,7 +275,11 @@ async def _task_processor_loop() -> None:
|
||||
def handle_thought(task):
|
||||
from timmy.thinking import thinking_engine
|
||||
try:
|
||||
result = thinking_engine.think_once()
|
||||
loop = asyncio.get_event_loop()
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
thinking_engine.think_once(), loop
|
||||
)
|
||||
result = future.result(timeout=120)
|
||||
return str(result) if result else "Thought completed"
|
||||
except Exception as e:
|
||||
logger.error("Thought processing failed: %s", e)
|
||||
@@ -457,15 +461,7 @@ async def lifespan(app: FastAPI):
|
||||
# Create all background tasks without waiting for them
|
||||
briefing_task = asyncio.create_task(_briefing_scheduler())
|
||||
|
||||
# Register Timmy in swarm registry
|
||||
from swarm import registry as swarm_registry
|
||||
swarm_registry.register(
|
||||
name="Timmy",
|
||||
capabilities="chat,reasoning,research,planning",
|
||||
agent_id="timmy",
|
||||
)
|
||||
|
||||
# Run swarm recovery and log summary
|
||||
# Run swarm recovery first (offlines all stale agents)
|
||||
from swarm.coordinator import coordinator as swarm_coordinator
|
||||
swarm_coordinator.initialize()
|
||||
rec = swarm_coordinator._recovery_summary
|
||||
@@ -476,6 +472,14 @@ async def lifespan(app: FastAPI):
|
||||
rec["agents_offlined"],
|
||||
)
|
||||
|
||||
# Register Timmy AFTER recovery sweep so status sticks as "idle"
|
||||
from swarm import registry as swarm_registry
|
||||
swarm_registry.register(
|
||||
name="Timmy",
|
||||
capabilities="chat,reasoning,research,planning",
|
||||
agent_id="timmy",
|
||||
)
|
||||
|
||||
# Spawn persona agents in background
|
||||
persona_task = asyncio.create_task(_spawn_persona_agents_background())
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
"""Voice routes — /voice/* and /voice/enhanced/* endpoints.
|
||||
|
||||
Provides NLU intent detection, TTS control, and the full voice-to-action
|
||||
pipeline (detect intent → execute → optionally speak).
|
||||
Provides NLU intent detection, TTS control, the full voice-to-action
|
||||
pipeline (detect intent → execute → optionally speak), and the voice
|
||||
button UI page.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Form
|
||||
from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pathlib import Path
|
||||
|
||||
from integrations.voice.nlu import detect_intent, extract_command
|
||||
from timmy.agent import create_timmy
|
||||
@@ -14,6 +18,7 @@ from timmy.agent import create_timmy
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/voice", tags=["voice"])
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
|
||||
@router.post("/nlu")
|
||||
@@ -56,6 +61,31 @@ async def tts_speak(text: str = Form(...)):
|
||||
return {"spoken": False, "reason": str(exc)}
|
||||
|
||||
|
||||
# ── Voice button page ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/button", response_class=HTMLResponse)
|
||||
async def voice_button_page(request: Request):
|
||||
"""Render the voice button UI."""
|
||||
return templates.TemplateResponse(request, "voice_button.html")
|
||||
|
||||
|
||||
@router.post("/command")
|
||||
async def voice_command(text: str = Form(...)):
|
||||
"""Process a voice command (used by voice_button.html).
|
||||
|
||||
Wraps the enhanced pipeline and returns the result in the format
|
||||
the voice button template expects.
|
||||
"""
|
||||
result = await process_voice_input(text=text, speak_response=False)
|
||||
return {
|
||||
"command": {
|
||||
"intent": result["intent"],
|
||||
"response": result["response"] or result.get("error", "No response"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ── Enhanced voice pipeline ──────────────────────────────────────────────
|
||||
|
||||
@router.post("/enhanced/process")
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
<title>{% block title %}Timmy Time — Mission Control{% endblock %}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet" />
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="/static/style.css?v=4" />
|
||||
<link rel="stylesheet" href="/static/style.css?v=5" />
|
||||
{% block extra_styles %}{% endblock %}
|
||||
<script src="https://unpkg.com/htmx.org@2.0.3/dist/htmx.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.3/dist/htmx.min.js" crossorigin="anonymous"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="mc-header">
|
||||
@@ -28,26 +29,31 @@
|
||||
<div class="mc-header-right mc-desktop-nav">
|
||||
<a href="/tasks" class="mc-test-link">TASKS</a>
|
||||
<a href="/briefing" class="mc-test-link">BRIEFING</a>
|
||||
<a href="/thinking" class="mc-test-link" style="color:#c084fc;">THINKING</a>
|
||||
<a href="/swarm/mission-control" class="mc-test-link">MISSION CONTROL</a>
|
||||
<a href="/thinking" class="mc-test-link mc-link-thinking">THINKING</a>
|
||||
<a href="/swarm/mission-control" class="mc-test-link">MISSION CTRL</a>
|
||||
<a href="/swarm/live" class="mc-test-link">SWARM</a>
|
||||
<a href="/spark/ui" class="mc-test-link">SPARK</a>
|
||||
<a href="/marketplace/ui" class="mc-test-link">MARKET</a>
|
||||
<a href="/tools" class="mc-test-link">TOOLS</a>
|
||||
<a href="/swarm/events" class="mc-test-link">EVENTS</a>
|
||||
<a href="/bugs" class="mc-test-link" style="color:#ff6b6b;">BUGS</a>
|
||||
<a href="/lightning/ledger" class="mc-test-link">LEDGER</a>
|
||||
<a href="/memory" class="mc-test-link">MEMORY</a>
|
||||
<a href="/router/status" class="mc-test-link">ROUTER</a>
|
||||
<a href="/grok/status" class="mc-test-link" style="color:#00ff88;">GROK</a>
|
||||
<a href="/self-modify/queue" class="mc-test-link">UPGRADES</a>
|
||||
<a href="/self-coding" class="mc-test-link">SELF-CODING</a>
|
||||
<a href="/hands" class="mc-test-link">HANDS</a>
|
||||
<a href="/work-orders/queue" class="mc-test-link">WORK ORDERS</a>
|
||||
<a href="/creative/ui" class="mc-test-link">CREATIVE</a>
|
||||
<a href="/voice/button" class="mc-test-link">VOICE</a>
|
||||
<a href="/mobile" class="mc-test-link" title="Mobile-optimized view">MOBILE</a>
|
||||
<a href="/mobile/local" class="mc-test-link" title="Local AI on iPhone">LOCAL AI</a>
|
||||
<a href="/bugs" class="mc-test-link mc-link-bugs">BUGS</a>
|
||||
<div class="mc-nav-dropdown">
|
||||
<button class="mc-test-link mc-dropdown-toggle" aria-expanded="false">MORE ▾</button>
|
||||
<div class="mc-dropdown-menu">
|
||||
<a href="/spark/ui" class="mc-test-link">SPARK</a>
|
||||
<a href="/marketplace/ui" class="mc-test-link">MARKET</a>
|
||||
<a href="/tools" class="mc-test-link">TOOLS</a>
|
||||
<a href="/swarm/events" class="mc-test-link">EVENTS</a>
|
||||
<a href="/lightning/ledger" class="mc-test-link">LEDGER</a>
|
||||
<a href="/memory" class="mc-test-link">MEMORY</a>
|
||||
<a href="/router/status" class="mc-test-link">ROUTER</a>
|
||||
<a href="/grok/status" class="mc-test-link mc-link-grok">GROK</a>
|
||||
<a href="/self-modify/queue" class="mc-test-link">UPGRADES</a>
|
||||
<a href="/self-coding" class="mc-test-link">SELF-CODING</a>
|
||||
<a href="/hands" class="mc-test-link">HANDS</a>
|
||||
<a href="/work-orders/queue" class="mc-test-link">WORK ORDERS</a>
|
||||
<a href="/creative/ui" class="mc-test-link">CREATIVE</a>
|
||||
<a href="/voice/button" class="mc-test-link">VOICE</a>
|
||||
<a href="/mobile" class="mc-test-link" title="Mobile-optimized view">MOBILE</a>
|
||||
<a href="/mobile/local" class="mc-test-link" title="Local AI on iPhone">LOCAL AI</a>
|
||||
</div>
|
||||
</div>
|
||||
<button id="enable-notifications" class="mc-test-link" style="background:none;cursor:pointer;" title="Enable notifications">🔔</button>
|
||||
<span class="mc-time" id="clock"></span>
|
||||
</div>
|
||||
@@ -66,24 +72,29 @@
|
||||
<span class="mc-time" id="clock-mobile"></span>
|
||||
</div>
|
||||
<a href="/" class="mc-mobile-link">HOME</a>
|
||||
<div class="mc-mobile-section-label">CORE</div>
|
||||
<a href="/tasks" class="mc-mobile-link">TASKS</a>
|
||||
<a href="/briefing" class="mc-mobile-link">BRIEFING</a>
|
||||
<a href="/thinking" class="mc-mobile-link">THINKING</a>
|
||||
<a href="/swarm/mission-control" class="mc-mobile-link">MISSION CONTROL</a>
|
||||
<a href="/swarm/live" class="mc-mobile-link">SWARM</a>
|
||||
<a href="/spark/ui" class="mc-mobile-link">SPARK</a>
|
||||
<a href="/marketplace/ui" class="mc-mobile-link">MARKET</a>
|
||||
<a href="/tools" class="mc-mobile-link">TOOLS</a>
|
||||
<a href="/swarm/events" class="mc-mobile-link">EVENTS</a>
|
||||
<a href="/bugs" class="mc-mobile-link">BUGS</a>
|
||||
<a href="/lightning/ledger" class="mc-mobile-link">LEDGER</a>
|
||||
<div class="mc-mobile-section-label">INTELLIGENCE</div>
|
||||
<a href="/spark/ui" class="mc-mobile-link">SPARK</a>
|
||||
<a href="/memory" class="mc-mobile-link">MEMORY</a>
|
||||
<a href="/router/status" class="mc-mobile-link">ROUTER</a>
|
||||
<a href="/grok/status" class="mc-mobile-link">GROK</a>
|
||||
<a href="/self-modify/queue" class="mc-mobile-link">UPGRADES</a>
|
||||
<a href="/self-coding" class="mc-mobile-link">SELF-CODING</a>
|
||||
<a href="/marketplace/ui" class="mc-mobile-link">MARKET</a>
|
||||
<div class="mc-mobile-section-label">AGENTS</div>
|
||||
<a href="/hands" class="mc-mobile-link">HANDS</a>
|
||||
<a href="/work-orders/queue" class="mc-mobile-link">WORK ORDERS</a>
|
||||
<a href="/self-modify/queue" class="mc-mobile-link">UPGRADES</a>
|
||||
<a href="/self-coding" class="mc-mobile-link">SELF-CODING</a>
|
||||
<div class="mc-mobile-section-label">SYSTEM</div>
|
||||
<a href="/tools" class="mc-mobile-link">TOOLS</a>
|
||||
<a href="/swarm/events" class="mc-mobile-link">EVENTS</a>
|
||||
<a href="/router/status" class="mc-mobile-link">ROUTER</a>
|
||||
<a href="/grok/status" class="mc-mobile-link">GROK</a>
|
||||
<div class="mc-mobile-section-label">COMMERCE</div>
|
||||
<a href="/lightning/ledger" class="mc-mobile-link">LEDGER</a>
|
||||
<a href="/creative/ui" class="mc-mobile-link">CREATIVE</a>
|
||||
<a href="/voice/button" class="mc-mobile-link">VOICE</a>
|
||||
<a href="/mobile" class="mc-mobile-link">MOBILE</a>
|
||||
@@ -128,6 +139,25 @@
|
||||
a.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Desktop "More" dropdown toggle
|
||||
var dropdownToggle = document.querySelector('.mc-dropdown-toggle');
|
||||
if (dropdownToggle) {
|
||||
dropdownToggle.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
var dd = this.closest('.mc-nav-dropdown');
|
||||
var isOpen = dd.classList.toggle('open');
|
||||
this.setAttribute('aria-expanded', isOpen);
|
||||
});
|
||||
document.addEventListener('click', function() {
|
||||
var dd = document.querySelector('.mc-nav-dropdown');
|
||||
if (dd) {
|
||||
dd.classList.remove('open');
|
||||
var btn = dd.querySelector('.mc-dropdown-toggle');
|
||||
if (btn) btn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script src="/static/notifications.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<!-- Agents (HTMX-polled from registry) -->
|
||||
<div class="card mc-panel"
|
||||
hx-get="/swarm/agents/sidebar"
|
||||
hx-trigger="load, every 10s"
|
||||
hx-trigger="every 10s"
|
||||
hx-target="this"
|
||||
hx-swap="innerHTML">
|
||||
<div class="card-header mc-panel-header">// AGENTS</div>
|
||||
@@ -23,7 +23,7 @@
|
||||
<!-- System Health (HTMX polled) -->
|
||||
<div class="card mc-panel"
|
||||
hx-get="/health/status"
|
||||
hx-trigger="load, every 30s"
|
||||
hx-trigger="every 30s"
|
||||
hx-target="this"
|
||||
hx-swap="innerHTML">
|
||||
<div class="card-header mc-panel-header">// SYSTEM HEALTH</div>
|
||||
|
||||
@@ -23,3 +23,4 @@
|
||||
<div class="msg-body">Mission Control initialized. Timmy ready — awaiting input.</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<script>if(typeof scrollChat==='function'){setTimeout(scrollChat,50);}</script>
|
||||
|
||||
@@ -36,8 +36,8 @@ from config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Project root — two levels up from src/self_modify/
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
# Project root — use settings.repo_root (works in Docker and local dev)
|
||||
PROJECT_ROOT = Path(settings.repo_root)
|
||||
|
||||
# Reports directory
|
||||
REPORTS_DIR = PROJECT_ROOT / "data" / "self_modify_reports"
|
||||
|
||||
@@ -194,7 +194,7 @@ class GrokBackend:
|
||||
) -> None:
|
||||
from config import settings
|
||||
|
||||
self._api_key = api_key or settings.xai_api_key
|
||||
self._api_key = api_key if api_key is not None else settings.xai_api_key
|
||||
self._model = model or settings.grok_default_model
|
||||
self._history: list[dict[str, str]] = []
|
||||
self.stats = GrokUsageStats()
|
||||
|
||||
@@ -145,6 +145,61 @@ a:hover { color: var(--orange); }
|
||||
}
|
||||
.mc-test-link:hover { border-color: var(--purple); color: var(--purple); }
|
||||
|
||||
/* ── Named link colors ───────────────────────────── */
|
||||
.mc-link-thinking { color: #c084fc; }
|
||||
.mc-link-bugs { color: #ff6b6b; }
|
||||
.mc-link-grok { color: #00ff88; }
|
||||
|
||||
/* ── Desktop "More" dropdown ─────────────────────── */
|
||||
.mc-nav-dropdown { position: relative; }
|
||||
.mc-dropdown-toggle {
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mc-dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
right: 0;
|
||||
background: rgba(17, 8, 32, 0.96);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 8px 0;
|
||||
min-width: 160px;
|
||||
z-index: 150;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
.mc-nav-dropdown:hover .mc-dropdown-menu,
|
||||
.mc-nav-dropdown.open .mc-dropdown-menu {
|
||||
display: flex;
|
||||
}
|
||||
.mc-dropdown-menu .mc-test-link {
|
||||
display: block;
|
||||
padding: 6px 14px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mc-dropdown-menu .mc-test-link:hover {
|
||||
background: rgba(124, 58, 237, 0.12);
|
||||
}
|
||||
|
||||
/* ── Mobile section labels ───────────────────────── */
|
||||
.mc-mobile-section-label {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 0.2em;
|
||||
padding: 12px 20px 4px;
|
||||
text-transform: uppercase;
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ── Hamburger (mobile only) ───────────────────── */
|
||||
.mc-hamburger {
|
||||
display: none;
|
||||
|
||||
@@ -37,7 +37,8 @@ def test_get_system_info_contains_repo_root():
|
||||
|
||||
assert "repo_root" in info
|
||||
assert info["repo_root"] == settings.repo_root
|
||||
assert "Timmy-time-dashboard" in info["repo_root"]
|
||||
# In Docker the CWD is /app, so just verify it's a non-empty path
|
||||
assert len(info["repo_root"]) > 0
|
||||
|
||||
|
||||
def test_check_ollama_health_returns_dict():
|
||||
|
||||
@@ -20,8 +20,9 @@ def test_resolve_path_relative_to_repo():
|
||||
|
||||
result = _resolve_path("src/config.py")
|
||||
|
||||
assert "Timmy-time-dashboard" in str(result)
|
||||
# In Docker the repo root is /app; locally it contains Timmy-time-dashboard
|
||||
assert result.name == "config.py"
|
||||
assert "src" in str(result)
|
||||
|
||||
|
||||
def test_resolve_path_absolute():
|
||||
|
||||
Reference in New Issue
Block a user