Compare commits
12
Commits
f3e9201afd
...
next
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed6bb95451 | ||
|
|
db42f81780 | ||
|
|
f54833e642 | ||
|
|
59adf22d42 | ||
|
|
4f9b01ce38 | ||
|
|
d69d2226a3 | ||
|
|
0b89833c32 | ||
|
|
95dee37b60 | ||
|
|
7e4a08d415 | ||
|
|
0f903f57bb | ||
|
|
7e71446cc7 | ||
|
|
c9cc424dda |
@@ -151,9 +151,12 @@ jobs:
|
|||||||
ADMIN_PASSWORD=${{ secrets.ADMIN_PASSWORD }}
|
ADMIN_PASSWORD=${{ secrets.ADMIN_PASSWORD }}
|
||||||
ADMIN_FIRST_NAME=${{ secrets.ADMIN_FIRST_NAME }}
|
ADMIN_FIRST_NAME=${{ secrets.ADMIN_FIRST_NAME }}
|
||||||
ADMIN_LAST_NAME=${{ secrets.ADMIN_LAST_NAME }}
|
ADMIN_LAST_NAME=${{ secrets.ADMIN_LAST_NAME }}
|
||||||
|
MONGO_URI=${{ secrets.MONGO_URI }}
|
||||||
|
USE_MONGODB=true
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "SECRET_KEY is set: $(grep -q 'SECRET_KEY=' .env && echo YES || echo NO)"
|
echo "SECRET_KEY is set: $(grep -q 'SECRET_KEY=' .env && echo YES || echo NO)"
|
||||||
|
echo "MONGO_URI is set: $(grep -q 'MONGO_URI=' .env && echo YES || echo NO)"
|
||||||
|
|
||||||
echo "Bringing down previous test environment..."
|
echo "Bringing down previous test environment..."
|
||||||
docker-compose -f docker-compose.test.yml down --volumes --remove-orphans || true
|
docker-compose -f docker-compose.test.yml down --volumes --remove-orphans || true
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
name: Developer
|
name: Developer
|
||||||
description: Implements core application features across Python backends and Vue frontends.
|
description: Implements core application features across Python backends and Vue frontends.
|
||||||
mode: subagent
|
mode: subagent
|
||||||
model: moonshotai/kimi-k2.7-code
|
model: "deepseek/deepseek-v4-pro"
|
||||||
temperature: 0.2
|
|
||||||
maxSteps: 50
|
maxSteps: 50
|
||||||
permission:
|
permission:
|
||||||
edit: allow
|
edit: allow
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
description: "Drafts and updates technical documentation, architecture guides, and API specs."
|
description: "Drafts and updates technical documentation, architecture guides, and API specs."
|
||||||
mode: "subagent"
|
mode: "subagent"
|
||||||
model: "deepseek/deepseek/deepseek-v4-flash"
|
model: "deepseek/deepseek-v4-flash"
|
||||||
permission:
|
permission:
|
||||||
edit: allow
|
edit: allow
|
||||||
bash: deny
|
bash: deny
|
||||||
|
|||||||
+1
-1
@@ -15,4 +15,4 @@ ENV PYTHONIOENCODING=utf-8
|
|||||||
VOLUME ["/app/data"]
|
VOLUME ["/app/data"]
|
||||||
|
|
||||||
# Use Gunicorn instead of python main.py
|
# Use Gunicorn instead of python main.py
|
||||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "-k", "gevent", "--workers", "1", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "--log-level", "info", "-c", "gunicorn.conf.py", "main:app"]
|
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "-k", "gthread", "--threads", "8", "--workers", "1", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "--log-level", "info", "-c", "gunicorn.conf.py", "main:app"]
|
||||||
@@ -212,10 +212,10 @@ def create_test_digest_token():
|
|||||||
def send_test_digest():
|
def send_test_digest():
|
||||||
"""Trigger a digest email for a specific user by email address.
|
"""Trigger a digest email for a specific user by email address.
|
||||||
|
|
||||||
Only active when DB_ENV is not 'production'. Requires admin authentication.
|
Only active when DB_ENV is not 'prod'. Requires admin authentication.
|
||||||
Note: actual email delivery is skipped in e2e mode by email_sender.
|
Note: actual email delivery is skipped in e2e mode by email_sender.
|
||||||
"""
|
"""
|
||||||
if os.environ.get('DB_ENV') == 'production':
|
if os.environ.get('DB_ENV') == 'prod':
|
||||||
return jsonify({'error': 'Not found', 'code': 'NOT_FOUND'}), 404
|
return jsonify({'error': 'Not found', 'code': 'NOT_FOUND'}), 404
|
||||||
|
|
||||||
user_id = get_validated_user_id()
|
user_id = get_validated_user_id()
|
||||||
@@ -253,10 +253,10 @@ def send_test_digest():
|
|||||||
def trigger_test_chore_expiry():
|
def trigger_test_chore_expiry():
|
||||||
"""Trigger the chore expiry notification check for a specific user by email address.
|
"""Trigger the chore expiry notification check for a specific user by email address.
|
||||||
|
|
||||||
Only active when DB_ENV is not 'production'. Requires admin authentication.
|
Only active when DB_ENV is not 'prod'. Requires admin authentication.
|
||||||
Note: actual push delivery requires VAPID keys to be configured.
|
Note: actual push delivery requires VAPID keys to be configured.
|
||||||
"""
|
"""
|
||||||
if os.environ.get('DB_ENV') == 'production':
|
if os.environ.get('DB_ENV') == 'prod':
|
||||||
return jsonify({'error': 'Not found', 'code': 'NOT_FOUND'}), 404
|
return jsonify({'error': 'Not found', 'code': 'NOT_FOUND'}), 404
|
||||||
|
|
||||||
user_id = get_validated_user_id()
|
user_id = get_validated_user_id()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# file: config/version.py
|
# file: config/version.py
|
||||||
import os
|
import os
|
||||||
|
|
||||||
BASE_VERSION = "1.0.16" # update manually when releasing features
|
BASE_VERSION = "1.1.0" # update manually when releasing features
|
||||||
|
|
||||||
def get_full_version() -> str:
|
def get_full_version() -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
+16
-1
@@ -580,7 +580,8 @@ def ensure_mongodb_indexes(client=None, db_name=None):
|
|||||||
|
|
||||||
|
|
||||||
# Clear test collections at import time so tests start with a clean slate.
|
# Clear test collections at import time so tests start with a clean slate.
|
||||||
if os.environ.get('DB_ENV', 'prod') == 'test':
|
# Only TinyDB path — MongoDB cleanup is handled explicitly via reset_mongo_db().
|
||||||
|
if os.environ.get('DB_ENV', 'prod') == 'test' and not USE_MONGODB:
|
||||||
child_db.truncate()
|
child_db.truncate()
|
||||||
task_db.truncate()
|
task_db.truncate()
|
||||||
routine_db.truncate()
|
routine_db.truncate()
|
||||||
@@ -599,3 +600,17 @@ if os.environ.get('DB_ENV', 'prod') == 'test':
|
|||||||
refresh_tokens_db.truncate()
|
refresh_tokens_db.truncate()
|
||||||
push_subscriptions_db.truncate()
|
push_subscriptions_db.truncate()
|
||||||
digest_action_tokens_db.truncate()
|
digest_action_tokens_db.truncate()
|
||||||
|
|
||||||
|
|
||||||
|
def reset_mongo_db():
|
||||||
|
"""Drop the MongoDB test/e2e database for a totally fresh slate.
|
||||||
|
|
||||||
|
Only acts on database names ending with ``_test`` or ``_e2e`` as a
|
||||||
|
safety guard against accidentally dropping production data. Call this
|
||||||
|
once at the start of a test session or deployment seed step.
|
||||||
|
"""
|
||||||
|
if not USE_MONGODB:
|
||||||
|
return
|
||||||
|
if not _mongo_db_name or not _mongo_db_name.endswith(('_test', '_e2e')):
|
||||||
|
return
|
||||||
|
get_mongo_client().drop_database(_mongo_db_name)
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ _mongo_client = None
|
|||||||
def _create_mongo_client():
|
def _create_mongo_client():
|
||||||
"""Build a fail-fast MongoClient from environment variables."""
|
"""Build a fail-fast MongoClient from environment variables."""
|
||||||
uri = os.environ.get('MONGO_URI')
|
uri = os.environ.get('MONGO_URI')
|
||||||
|
|
||||||
|
|
||||||
if not uri:
|
if not uri:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
'MONGO_URI environment variable is required when USE_MONGODB=true.'
|
'MONGO_URI environment variable is required when USE_MONGODB=true.'
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ def set_test_db_env():
|
|||||||
os.environ['MONGO_URI'] = 'mongomock'
|
os.environ['MONGO_URI'] = 'mongomock'
|
||||||
os.environ['SECRET_KEY'] = TEST_SECRET_KEY
|
os.environ['SECRET_KEY'] = TEST_SECRET_KEY
|
||||||
os.environ['REFRESH_TOKEN_EXPIRY_DAYS'] = str(TEST_REFRESH_TOKEN_EXPIRY_DAYS)
|
os.environ['REFRESH_TOKEN_EXPIRY_DAYS'] = str(TEST_REFRESH_TOKEN_EXPIRY_DAYS)
|
||||||
# Ensure indexes are created once for the test session. This is safe to
|
# Drop the MongoDB test database once at the start of the session so every
|
||||||
# call repeatedly because MongoDB treats index creation as idempotent.
|
# test begins from a clean slate. Indexes are recreated afterward.
|
||||||
from db.db import ensure_mongodb_indexes
|
from db.db import reset_mongo_db, ensure_mongodb_indexes
|
||||||
|
reset_mongo_db()
|
||||||
ensure_mongodb_indexes()
|
ensure_mongodb_indexes()
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ services:
|
|||||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
||||||
- ADMIN_FIRST_NAME=${ADMIN_FIRST_NAME}
|
- ADMIN_FIRST_NAME=${ADMIN_FIRST_NAME}
|
||||||
- ADMIN_LAST_NAME=${ADMIN_LAST_NAME}
|
- ADMIN_LAST_NAME=${ADMIN_LAST_NAME}
|
||||||
|
- MONGO_URI=${MONGO_URI}
|
||||||
|
- USE_MONGODB=true
|
||||||
|
- DATA_ENV=test
|
||||||
|
- DB_ENV=test
|
||||||
volumes:
|
volumes:
|
||||||
- chores-test-app-backend-data:/app/data
|
- chores-test-app-backend-data:/app/data
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ services:
|
|||||||
- DIGEST_TOKEN_SECRET=${DIGEST_TOKEN_SECRET}
|
- DIGEST_TOKEN_SECRET=${DIGEST_TOKEN_SECRET}
|
||||||
- VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY}
|
- VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY}
|
||||||
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
|
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
|
||||||
|
- MONGO_URI=${MONGO_URI}
|
||||||
|
- DATA_ENV=prod
|
||||||
|
- DB_ENV=prod
|
||||||
volumes:
|
volumes:
|
||||||
- chores-app-backend-data:/app/data # Assuming backend data storage; adjust path as needed
|
- chores-app-backend-data:/app/data # Assuming backend data storage; adjust path as needed
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "5gEK_iLdGl1BAGfetT8hEf6SJK2lQVbYxQjH0BdKWwU",
|
"value": "WGHWfbkdu5a4GEsBQ4exR8DG4W39iOm1b71weDg6RWU",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1792813397.168935,
|
"expires": 1793338810.330124,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS10dXRvcmlhbEB0ZXN0LmNvbSIsInVzZXJfaWQiOiJkODk0ZjA0OS0xODU3LTQwM2UtYjgzZC1jMTY2NTNmZDU2MmEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODUwNDgxOTd9.S7frWOrZwXC5xVgtG2RDioXGok04qK_jLCYWvfedD7s",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS10dXRvcmlhbEB0ZXN0LmNvbSIsInVzZXJfaWQiOiJlMzIxNzNmNy1lODM5LTQxNjMtOGUyYS04YmRhZGY5ZTZiZjEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODU1NzM2MTB9.W08NeVvTAftfz4VQVopd8U38mOEiqUW9qb-EhbttWJk",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1785048197.167997,
|
"expires": 1785573610.329316,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1785037396813}"
|
"value": "{\"type\":\"logout\",\"at\":1785562810109}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1785210197388}"
|
"value": "{\"expiresAt\":1785735610529}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "Ke6qDj_0RYyrMSI1Z9C69DPtvYo26oUUiLZr_pDXZv4",
|
"value": "nT_T0nZ7ZXthfVaOIvMOa-9Z8IJAkT8hByHcv9yxyno",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1792813392.716776,
|
"expires": 1793338807.10011,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiI3NTYwZDBlNy1iMmVlLTRiMjktYTYwNi1lZTM3NmIzN2Y3ODciLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODUwNDgxOTJ9.QB4bXo88KC7jr94QSjJI3Y1eVWeU4KNKdHRi1_7zcD0",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiJkZmQ4ZjI1NS0yMzkwLTRiOTgtODJmYi1jZGRmOTBjODYwYWMiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODU1NzM2MDd9.Wn8RdLZGFifPe3LwAjljmHIoiFOOIzqeDCfQWvvy-_I",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1785048192.71579,
|
"expires": 1785573607.099308,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1785037392502}"
|
"value": "{\"type\":\"logout\",\"at\":1785562806880}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1785210192904}"
|
"value": "{\"expiresAt\":1785735607328}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -445,16 +445,8 @@ function openRoutineMenu(routineId: string, e: MouseEvent) {
|
|||||||
'routine-kebab-menu',
|
'routine-kebab-menu',
|
||||||
() => document.querySelector('.kebab-menu') as HTMLElement | null,
|
() => document.querySelector('.kebab-menu') as HTMLElement | null,
|
||||||
)
|
)
|
||||||
tutorialMaybeShow(
|
|
||||||
'kebab-edit-points-cost',
|
|
||||||
() => document.querySelector('[data-tutorial~="kebab-edit-points-cost"]') as HTMLElement | null,
|
|
||||||
)
|
|
||||||
tutorialMaybeShow(
|
|
||||||
'routine-schedule',
|
|
||||||
() => document.querySelector('[data-tutorial="routine-schedule"]') as HTMLElement | null,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
const items = childRoutineListRef.value?.items ?? []
|
const items: ChildRoutine[] = childRoutineListRef.value?.items ?? []
|
||||||
const routine = items.find((r) => r.id === routineId)
|
const routine = items.find((r) => r.id === routineId)
|
||||||
if (routine) {
|
if (routine) {
|
||||||
if (isRoutineExpired(routine)) {
|
if (isRoutineExpired(routine)) {
|
||||||
@@ -740,14 +732,6 @@ function openChoreMenu(taskId: string, e: MouseEvent) {
|
|||||||
'chore-kebab-menu',
|
'chore-kebab-menu',
|
||||||
() => document.querySelector('.kebab-menu') as HTMLElement | null,
|
() => document.querySelector('.kebab-menu') as HTMLElement | null,
|
||||||
)
|
)
|
||||||
tutorialMaybeShow(
|
|
||||||
'kebab-edit-points-cost',
|
|
||||||
() => document.querySelector('[data-tutorial~="kebab-edit-points-cost"]') as HTMLElement | null,
|
|
||||||
)
|
|
||||||
tutorialMaybeShow(
|
|
||||||
'chore-schedule',
|
|
||||||
() => document.querySelector('[data-tutorial="chore-schedule"]') as HTMLElement | null,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
const items: ChildTask[] = childChoreListRef.value?.items ?? []
|
const items: ChildTask[] = childChoreListRef.value?.items ?? []
|
||||||
const task = items.find((t) => t.id === taskId)
|
const task = items.find((t) => t.id === taskId)
|
||||||
|
|||||||
@@ -1,17 +1,37 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
import {
|
import {
|
||||||
shouldShowStep,
|
shouldShowStep,
|
||||||
clearChainProgress,
|
clearChainProgress,
|
||||||
tutorialEnabled,
|
tutorialEnabled,
|
||||||
tutorialProgress,
|
tutorialProgress,
|
||||||
sessionSkipped,
|
sessionSkipped,
|
||||||
|
maybeShow,
|
||||||
|
dismissActive,
|
||||||
|
activeStep,
|
||||||
} from '../controller'
|
} from '../controller'
|
||||||
|
|
||||||
|
function setKebabAnchorHtml() {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div class="kebab-menu"></div>
|
||||||
|
<button data-tutorial="chore-edit-points kebab-edit-points-cost">Edit Points</button>
|
||||||
|
<button data-tutorial="chore-schedule">Schedule</button>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
describe('tutorial controller - shouldShowStep', () => {
|
describe('tutorial controller - shouldShowStep', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tutorialEnabled.value = true
|
tutorialEnabled.value = true
|
||||||
tutorialProgress.value = {}
|
tutorialProgress.value = {}
|
||||||
sessionSkipped.value = false
|
sessionSkipped.value = false
|
||||||
|
activeStep.value = null
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
activeStep.value = null
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
vi.restoreAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns true when tips are enabled, the step is unseen, and the session is not skipped', () => {
|
it('returns true when tips are enabled, the step is unseen, and the session is not skipped', () => {
|
||||||
@@ -46,3 +66,103 @@ describe('tutorial controller - shouldShowStep', () => {
|
|||||||
expect(shouldShowStep('status-pending', true)).toBe(true)
|
expect(shouldShowStep('status-pending', true)).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('tutorial controller - maybeShow', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
tutorialEnabled.value = true
|
||||||
|
tutorialProgress.value = {}
|
||||||
|
sessionSkipped.value = false
|
||||||
|
activeStep.value = null
|
||||||
|
setKebabAnchorHtml()
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
activeStep.value = null
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('promotes a step when the anchor is present', () => {
|
||||||
|
maybeShow('chore-kebab-menu')
|
||||||
|
expect(activeStep.value?.def.id).toBe('chore-kebab-menu')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not promote a step that has already been seen', () => {
|
||||||
|
tutorialProgress.value = { 'chore-kebab-menu': true }
|
||||||
|
maybeShow('chore-kebab-menu')
|
||||||
|
expect(activeStep.value).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is idempotent for the currently active step', () => {
|
||||||
|
maybeShow('chore-kebab-menu')
|
||||||
|
expect(activeStep.value?.def.id).toBe('chore-kebab-menu')
|
||||||
|
|
||||||
|
maybeShow('chore-kebab-menu')
|
||||||
|
expect(activeStep.value?.def.id).toBe('chore-kebab-menu')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('drops a step whose anchor is missing', () => {
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
maybeShow('chore-kebab-menu')
|
||||||
|
expect(activeStep.value).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('tutorial controller - dismissActive', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
tutorialEnabled.value = true
|
||||||
|
tutorialProgress.value = {}
|
||||||
|
sessionSkipped.value = false
|
||||||
|
activeStep.value = null
|
||||||
|
setKebabAnchorHtml()
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
activeStep.value = null
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('marks the active step seen and chains the next step', () => {
|
||||||
|
maybeShow('chore-kebab-menu')
|
||||||
|
expect(activeStep.value?.def.id).toBe('chore-kebab-menu')
|
||||||
|
|
||||||
|
dismissActive(true)
|
||||||
|
expect(tutorialProgress.value['chore-kebab-menu']).toBe(true)
|
||||||
|
expect(activeStep.value?.def.id).toBe('chore-edit-points')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not duplicate a chained step that is already queued', () => {
|
||||||
|
// Set up: open the chore kebab menu, then queue the next step early.
|
||||||
|
maybeShow('chore-kebab-menu')
|
||||||
|
maybeShow('chore-edit-points')
|
||||||
|
expect(activeStep.value?.def.id).toBe('chore-kebab-menu')
|
||||||
|
|
||||||
|
// Dismissing the menu should chain to chore-edit-points only once.
|
||||||
|
dismissActive(true)
|
||||||
|
expect(activeStep.value?.def.id).toBe('chore-edit-points')
|
||||||
|
|
||||||
|
// If a duplicate had been added, we would still be on chore-edit-points.
|
||||||
|
dismissActive(true)
|
||||||
|
expect(activeStep.value?.def.id).toBe('chore-schedule')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not re-chain a step that is currently active', () => {
|
||||||
|
maybeShow('chore-kebab-menu')
|
||||||
|
dismissActive(true)
|
||||||
|
expect(activeStep.value?.def.id).toBe('chore-edit-points')
|
||||||
|
|
||||||
|
// Dismissing chore-edit-points should move to chore-schedule, not loop.
|
||||||
|
dismissActive(true)
|
||||||
|
expect(activeStep.value?.def.id).toBe('chore-schedule')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not mark the step seen when markSeen is false', () => {
|
||||||
|
maybeShow('chore-kebab-menu')
|
||||||
|
dismissActive(false)
|
||||||
|
expect(tutorialProgress.value['chore-kebab-menu']).toBeUndefined()
|
||||||
|
expect(activeStep.value).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export function dismissActive(markSeen = true) {
|
|||||||
}
|
}
|
||||||
if (current.def.next) {
|
if (current.def.next) {
|
||||||
const nextDef = stepRegistry[current.def.next]
|
const nextDef = stepRegistry[current.def.next]
|
||||||
if (nextDef && shouldShowStep(nextDef.id)) {
|
if (nextDef && shouldShowStep(nextDef.id) && !queue.some((q) => q.def.id === nextDef.id)) {
|
||||||
queue.unshift({ def: nextDef, anchor: null })
|
queue.unshift({ def: nextDef, anchor: null })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-12
@@ -5,20 +5,15 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"type": "local",
|
"type": "local",
|
||||||
"command": [
|
"command": [
|
||||||
"docker",
|
"ssh",
|
||||||
"run",
|
"-T",
|
||||||
"-i",
|
"devserver.lan",
|
||||||
"--rm",
|
"/home/ryan/gitea-mcp-wrapper.sh"
|
||||||
"-e",
|
|
||||||
"GITEA_ACCESS_TOKEN",
|
|
||||||
"-e",
|
|
||||||
"GITEA_HOST",
|
|
||||||
"docker.gitea.com/gitea-mcp-server"
|
|
||||||
],
|
],
|
||||||
"environment": {
|
"environment": {
|
||||||
"GITEA_HOST": "https://git.ryankegel.com",
|
"GITEA_HOST": "https://git.ryankegel.com"
|
||||||
"GITEA_ACCESS_TOKEN": "{env:GITEA_ACCESS_TOKEN}"
|
},
|
||||||
}
|
"timeout": 30000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user