Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ca5f8085f |
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 4.2 on 2026-03-15 21:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("generation", "0003_generationrecord_ark_task_id_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="generationrecord",
|
||||||
|
name="model",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("seedance_2.0", "AirDrama"),
|
||||||
|
("seedance_2.0_fast", "AirDrama Fast"),
|
||||||
|
],
|
||||||
|
max_length=30,
|
||||||
|
verbose_name="模型",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
26
backend/apps/generation/migrations/0005_convert_utf8mb4.py
Normal file
26
backend/apps/generation/migrations/0005_convert_utf8mb4.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"""Convert GenerationRecord table to utf8mb4 to support emoji/4-byte Unicode in prompt field.
|
||||||
|
|
||||||
|
Bug #65: OperationalError (1366) "Incorrect string value" when prompt contains emoji characters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('generation', '0004_alter_generationrecord_model'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunSQL(
|
||||||
|
sql=[
|
||||||
|
"ALTER TABLE generation_generationrecord CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;",
|
||||||
|
"ALTER TABLE generation_quotaconfig CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;",
|
||||||
|
],
|
||||||
|
reverse_sql=[
|
||||||
|
"ALTER TABLE generation_generationrecord CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;",
|
||||||
|
"ALTER TABLE generation_quotaconfig CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -45,7 +45,7 @@ class GenerationRecord(models.Model):
|
|||||||
verbose_name_plural = '生成记录'
|
verbose_name_plural = '生成记录'
|
||||||
ordering = ['-created_at']
|
ordering = ['-created_at']
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['user', 'created_at']),
|
models.Index(fields=['user', 'created_at'], name='generation__user_id_371350_idx'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@ -87,7 +87,7 @@ elif os.environ.get('USE_MYSQL', 'false').lower() in ('true', '1', 'yes'):
|
|||||||
'PORT': os.environ.get('DB_PORT', '3306'),
|
'PORT': os.environ.get('DB_PORT', '3306'),
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'charset': 'utf8mb4',
|
'charset': 'utf8mb4',
|
||||||
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
|
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'; SET NAMES utf8mb4;",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
84
repair_test_bug_65.py
Normal file
84
repair_test_bug_65.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
"""
|
||||||
|
Test for Bug #65 fix: OperationalError (1366) Incorrect string value for emoji in prompt.
|
||||||
|
|
||||||
|
Verifies that GenerationRecord.prompt can store 4-byte UTF-8 characters (emoji).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import django
|
||||||
|
|
||||||
|
# Setup Django
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
os.environ.setdefault('TESTING', 'true')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from apps.generation.models import GenerationRecord
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class Bug65EmojiPromptTest(TestCase):
|
||||||
|
"""Test that prompts with emoji characters can be saved to the database."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
username='testuser_bug65',
|
||||||
|
email='bug65@test.com',
|
||||||
|
password='testpass123',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_prompt_with_emoji_saves_successfully(self):
|
||||||
|
"""Bug #65: prompt containing 🔊 (4-byte UTF-8) should not raise OperationalError."""
|
||||||
|
emoji_prompt = '🔊 这是一个包含emoji的提示词 🎬🎥✨'
|
||||||
|
record = GenerationRecord.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
prompt=emoji_prompt,
|
||||||
|
mode='universal',
|
||||||
|
model='seedance_2.0',
|
||||||
|
aspect_ratio='16:9',
|
||||||
|
duration=5,
|
||||||
|
)
|
||||||
|
record.refresh_from_db()
|
||||||
|
self.assertEqual(record.prompt, emoji_prompt)
|
||||||
|
|
||||||
|
def test_prompt_with_mixed_unicode(self):
|
||||||
|
"""Prompt with mixed CJK + emoji + ASCII should save correctly."""
|
||||||
|
mixed_prompt = '🔊 大象在草原上奔跑 🐘 — cinematic 4K, slow-motion 🎬'
|
||||||
|
record = GenerationRecord.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
prompt=mixed_prompt,
|
||||||
|
mode='universal',
|
||||||
|
model='seedance_2.0',
|
||||||
|
aspect_ratio='16:9',
|
||||||
|
duration=5,
|
||||||
|
)
|
||||||
|
record.refresh_from_db()
|
||||||
|
self.assertEqual(record.prompt, mixed_prompt)
|
||||||
|
|
||||||
|
def test_prompt_with_only_basic_text(self):
|
||||||
|
"""Ensure basic text still works after the charset change."""
|
||||||
|
basic_prompt = '一只猫在跑步'
|
||||||
|
record = GenerationRecord.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
prompt=basic_prompt,
|
||||||
|
mode='universal',
|
||||||
|
model='seedance_2.0',
|
||||||
|
aspect_ratio='16:9',
|
||||||
|
duration=5,
|
||||||
|
)
|
||||||
|
record.refresh_from_db()
|
||||||
|
self.assertEqual(record.prompt, basic_prompt)
|
||||||
|
|
||||||
|
def test_settings_mysql_charset(self):
|
||||||
|
"""Verify MySQL OPTIONS includes utf8mb4 charset and SET NAMES utf8mb4."""
|
||||||
|
from django.conf import settings
|
||||||
|
# Only check if MySQL config is present (prod uses MySQL, test uses SQLite)
|
||||||
|
db_config = settings.DATABASES.get('default', {})
|
||||||
|
if db_config.get('ENGINE', '').endswith('mysql'):
|
||||||
|
options = db_config.get('OPTIONS', {})
|
||||||
|
self.assertEqual(options.get('charset'), 'utf8mb4')
|
||||||
|
self.assertIn('SET NAMES utf8mb4', options.get('init_command', ''))
|
||||||
129
repair_test_bug_66.py
Normal file
129
repair_test_bug_66.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
"""
|
||||||
|
Verification script for Bug #66: CrashLoopBackOff fix.
|
||||||
|
|
||||||
|
Root cause: Two uncommitted changes caused Django migration drift in the
|
||||||
|
Docker container:
|
||||||
|
1. MODEL_CHOICES labels changed from 'Seedance 2.0' to 'AirDrama' in models.py
|
||||||
|
but migration 0004 (which records this change) was untracked by git.
|
||||||
|
2. Index name='generation__user_id_371350_idx' was added to models.py but not
|
||||||
|
committed — the migration already had this name.
|
||||||
|
|
||||||
|
Without migration 0004 in the Docker image, Django detected model-migration
|
||||||
|
mismatch at startup, causing the pod to enter CrashLoopBackOff.
|
||||||
|
|
||||||
|
Fix: Commit both models.py (with explicit index name) and migration 0004.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
# Ensure we're in the backend directory
|
||||||
|
BACKEND_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
if os.path.basename(BACKEND_DIR) != 'backend':
|
||||||
|
BACKEND_DIR = os.path.join(BACKEND_DIR, 'backend')
|
||||||
|
|
||||||
|
os.chdir(BACKEND_DIR)
|
||||||
|
sys.path.insert(0, BACKEND_DIR)
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
|
|
||||||
|
def test_django_check():
|
||||||
|
"""Verify Django system check passes with no issues."""
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, 'manage.py', 'check'],
|
||||||
|
capture_output=True, text=True, cwd=BACKEND_DIR,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0, f"Django check failed:\n{result.stderr}"
|
||||||
|
print("PASS: Django system check — no issues found")
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_pending_migrations():
|
||||||
|
"""Verify there are no pending migrations (the root cause of Bug #66)."""
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, 'manage.py', 'makemigrations', '--check', '--dry-run'],
|
||||||
|
capture_output=True, text=True, cwd=BACKEND_DIR,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0, (
|
||||||
|
f"Pending migrations detected (this was the Bug #66 root cause):\n"
|
||||||
|
f"{result.stdout}\n{result.stderr}"
|
||||||
|
)
|
||||||
|
print("PASS: No pending migrations detected")
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_name_matches_migration():
|
||||||
|
"""Verify the model index name matches what's in the migration file."""
|
||||||
|
import django
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from apps.generation.models import GenerationRecord
|
||||||
|
|
||||||
|
meta_indexes = GenerationRecord._meta.indexes
|
||||||
|
assert len(meta_indexes) >= 1, "GenerationRecord should have at least 1 index"
|
||||||
|
|
||||||
|
user_created_index = meta_indexes[0]
|
||||||
|
assert user_created_index.name == 'generation__user_id_371350_idx', (
|
||||||
|
f"Index name mismatch: got '{user_created_index.name}', "
|
||||||
|
f"expected 'generation__user_id_371350_idx'"
|
||||||
|
)
|
||||||
|
print("PASS: Index name matches migration file")
|
||||||
|
|
||||||
|
|
||||||
|
def test_migration_0004_exists():
|
||||||
|
"""Verify migration 0004 for model choice label change exists."""
|
||||||
|
migration_path = os.path.join(
|
||||||
|
BACKEND_DIR, 'apps', 'generation', 'migrations',
|
||||||
|
'0004_alter_generationrecord_model.py'
|
||||||
|
)
|
||||||
|
assert os.path.exists(migration_path), (
|
||||||
|
f"Migration 0004 not found at {migration_path}"
|
||||||
|
)
|
||||||
|
print("PASS: Migration 0004 exists")
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_choices_match_migration():
|
||||||
|
"""Verify MODEL_CHOICES values match what migration 0004 expects."""
|
||||||
|
import django
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from apps.generation.models import GenerationRecord
|
||||||
|
|
||||||
|
choices_dict = dict(GenerationRecord.MODEL_CHOICES)
|
||||||
|
assert choices_dict.get('seedance_2.0') == 'AirDrama', (
|
||||||
|
f"Expected 'AirDrama', got {choices_dict.get('seedance_2.0')!r}"
|
||||||
|
)
|
||||||
|
assert choices_dict.get('seedance_2.0_fast') == 'AirDrama Fast', (
|
||||||
|
f"Expected 'AirDrama Fast', got {choices_dict.get('seedance_2.0_fast')!r}"
|
||||||
|
)
|
||||||
|
print("PASS: MODEL_CHOICES labels match migration 0004")
|
||||||
|
|
||||||
|
|
||||||
|
def test_wsgi_loads():
|
||||||
|
"""Verify WSGI application loads (gunicorn entrypoint)."""
|
||||||
|
import django
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from config.wsgi import application
|
||||||
|
assert application is not None, "WSGI application failed to load"
|
||||||
|
print("PASS: WSGI application loads successfully")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
tests = [
|
||||||
|
test_django_check,
|
||||||
|
test_no_pending_migrations,
|
||||||
|
test_index_name_matches_migration,
|
||||||
|
test_migration_0004_exists,
|
||||||
|
test_model_choices_match_migration,
|
||||||
|
test_wsgi_loads,
|
||||||
|
]
|
||||||
|
passed = failed = 0
|
||||||
|
for test in tests:
|
||||||
|
try:
|
||||||
|
test()
|
||||||
|
passed += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"FAIL: {test.__name__}: {e}")
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
print(f"\nResults: {passed} passed, {failed} failed")
|
||||||
|
sys.exit(1 if failed else 0)
|
||||||
127
repair_test_bug_67_66.py
Normal file
127
repair_test_bug_67_66.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"""
|
||||||
|
Verification script for Bug #67 (DockerBuildError) and Bug #66 (CrashLoopBackOff).
|
||||||
|
|
||||||
|
Bug #67: AdminLayout.tsx (and 3 other files) imported '../assets/logo_32.png'
|
||||||
|
which didn't exist, causing Vite build failure.
|
||||||
|
Fix: Created the missing logo_32.png asset file.
|
||||||
|
|
||||||
|
Bug #66: GenerationRecord model's index lacked an explicit name, causing
|
||||||
|
Django to detect a model-migration mismatch on every container start.
|
||||||
|
Fix: Added name='generation__user_id_371350_idx' to the index, plus
|
||||||
|
created migration 0004 for MODEL_CHOICES display name change.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
BACKEND_DIR = os.path.join(PROJECT_ROOT, 'backend')
|
||||||
|
WEB_DIR = os.path.join(PROJECT_ROOT, 'web')
|
||||||
|
|
||||||
|
|
||||||
|
def test_bug_67_logo_asset_exists():
|
||||||
|
"""Bug #67: Verify logo_32.png asset file exists."""
|
||||||
|
logo_path = os.path.join(WEB_DIR, 'src', 'assets', 'logo_32.png')
|
||||||
|
assert os.path.isfile(logo_path), f"Missing file: {logo_path}"
|
||||||
|
assert os.path.getsize(logo_path) > 0, f"File is empty: {logo_path}"
|
||||||
|
# Verify it's a valid PNG (magic bytes)
|
||||||
|
with open(logo_path, 'rb') as f:
|
||||||
|
header = f.read(8)
|
||||||
|
assert header[:4] == b'\x89PNG', f"Not a valid PNG file: {logo_path}"
|
||||||
|
print("PASS: logo_32.png exists and is a valid PNG")
|
||||||
|
|
||||||
|
|
||||||
|
def test_bug_67_no_missing_imports():
|
||||||
|
"""Bug #67: Verify all files importing logo_32.png can resolve the asset."""
|
||||||
|
files_using_logo = [
|
||||||
|
'src/pages/AdminLayout.tsx',
|
||||||
|
'src/pages/TeamAdminLayout.tsx',
|
||||||
|
'src/components/Sidebar.tsx',
|
||||||
|
'src/components/LoginModal.tsx',
|
||||||
|
]
|
||||||
|
logo_path = os.path.join(WEB_DIR, 'src', 'assets', 'logo_32.png')
|
||||||
|
for f in files_using_logo:
|
||||||
|
full_path = os.path.join(WEB_DIR, f)
|
||||||
|
if not os.path.isfile(full_path):
|
||||||
|
print(f"SKIP: {f} does not exist")
|
||||||
|
continue
|
||||||
|
with open(full_path, 'r') as fh:
|
||||||
|
content = fh.read()
|
||||||
|
if 'logo_32.png' in content:
|
||||||
|
assert os.path.isfile(logo_path), \
|
||||||
|
f"{f} imports logo_32.png but asset doesn't exist"
|
||||||
|
print(f"PASS: {f} imports logo_32.png and asset exists")
|
||||||
|
else:
|
||||||
|
print(f"INFO: {f} no longer imports logo_32.png")
|
||||||
|
|
||||||
|
|
||||||
|
def test_bug_66_no_pending_migrations():
|
||||||
|
"""Bug #66: Verify Django detects no pending migration changes."""
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, 'manage.py', 'makemigrations', '--check'],
|
||||||
|
capture_output=True, text=True, cwd=BACKEND_DIR
|
||||||
|
)
|
||||||
|
assert result.returncode == 0, \
|
||||||
|
f"Pending migrations detected:\nstdout: {result.stdout}\nstderr: {result.stderr}"
|
||||||
|
print("PASS: No pending migrations detected")
|
||||||
|
|
||||||
|
|
||||||
|
def test_bug_66_index_has_name():
|
||||||
|
"""Bug #66: Verify GenerationRecord index has explicit name."""
|
||||||
|
models_path = os.path.join(BACKEND_DIR, 'apps', 'generation', 'models.py')
|
||||||
|
with open(models_path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
assert "name='generation__user_id_371350_idx'" in content, \
|
||||||
|
"Index name not found in models.py"
|
||||||
|
print("PASS: GenerationRecord index has explicit name")
|
||||||
|
|
||||||
|
|
||||||
|
def test_bug_66_django_check():
|
||||||
|
"""Bug #66: Verify Django system check passes."""
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, 'manage.py', 'check'],
|
||||||
|
capture_output=True, text=True, cwd=BACKEND_DIR
|
||||||
|
)
|
||||||
|
assert result.returncode == 0, \
|
||||||
|
f"Django check failed:\nstdout: {result.stdout}\nstderr: {result.stderr}"
|
||||||
|
print("PASS: Django system check passed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_bug_66_migration_file_exists():
|
||||||
|
"""Bug #66: Verify migration 0004 exists for MODEL_CHOICES change."""
|
||||||
|
migration_path = os.path.join(
|
||||||
|
BACKEND_DIR, 'apps', 'generation', 'migrations',
|
||||||
|
'0004_alter_generationrecord_model.py'
|
||||||
|
)
|
||||||
|
assert os.path.isfile(migration_path), \
|
||||||
|
f"Migration 0004 not found: {migration_path}"
|
||||||
|
print("PASS: Migration 0004 exists")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
tests = [
|
||||||
|
test_bug_67_logo_asset_exists,
|
||||||
|
test_bug_67_no_missing_imports,
|
||||||
|
test_bug_66_no_pending_migrations,
|
||||||
|
test_bug_66_index_has_name,
|
||||||
|
test_bug_66_django_check,
|
||||||
|
test_bug_66_migration_file_exists,
|
||||||
|
]
|
||||||
|
failed = 0
|
||||||
|
for test in tests:
|
||||||
|
try:
|
||||||
|
test()
|
||||||
|
except AssertionError as e:
|
||||||
|
print(f"FAIL: {test.__name__}: {e}")
|
||||||
|
failed += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: {test.__name__}: {e}")
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
print(f"\n{'='*40}")
|
||||||
|
print(f"Results: {len(tests) - failed}/{len(tests)} passed")
|
||||||
|
if failed:
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print("All verification tests passed!")
|
||||||
Loading…
x
Reference in New Issue
Block a user