"""Studio integration tests use temporary repositories and synthetic engine events.""" import json import subprocess import uuid from pathlib import Path def repository(logged_in): client, app, projects, _, headers = logged_in root = projects / 'studio-demo' root.mkdir() for args in (["init", "-b", "main"], ["-c", "user.name=Test", "-c", "user.email=test@example.invalid", "commit", "--allow-empty", "-m", "Fixture"]): subprocess.run(['git', *args], cwd=root, check=True, capture_output=True) owner = app.state.db.one('SELECT id FROM users LIMIT 1')['id'] project_id = str(uuid.uuid4()) app.state.db.execute('INSERT INTO projects(id,name,path,owner_id,created_at) VALUES(?,?,?,?,?)', (project_id, 'Fixture', str(root), owner, '2026-01-01')) response = client.post('/api/dev-studio/requests', headers=headers, json={'project_id': project_id, 'name': 'Demo', 'description': 'Créer une application de démonstration avec tests essentiels.'}) assert response.status_code == 201, response.text return client, app, headers, root, response.json() def test_plan_does_not_launch_generation(logged_in): client, app, _, root, request = repository(logged_in) assert request['status'] == 'PLANNED' assert len(request['plan_hash']) == 64 assert request['base_sha'] == subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=root, text=True).strip() assert app.state.db.all('SELECT * FROM prompt_queue') == [] assert not list((app.state.codex.data_dir / 'worktrees').glob('*')) assert client.get(f"/api/dev-studio/requests/{request['id']}").status_code == 200 def test_studio_project_picker_lists_authorized_local_projects_in_all_modes(logged_in): client, app, projects, _, _ = logged_in owner = app.state.db.one('SELECT id FROM users LIMIT 1')['id'] safe_id, full_id, remote_id = (str(uuid.uuid4()) for _ in range(3)) for project_id, name, kind, full_access in ( (safe_id, 'Safe project', 'local', 0), (full_id, 'Full access project', 'local', 1), (remote_id, 'Remote project', 'remote', 0), ): root = projects / project_id root.mkdir() app.state.db.execute( 'INSERT INTO projects(id,name,path,owner_id,kind,full_access,created_at) VALUES(?,?,?,?,?,?,?)', (project_id, name, str(root), owner, kind, full_access, '2026-01-01'), ) response = client.get('/api/dev-studio/projects') assert response.status_code == 200 rows = response.json() assert {item['id'] for item in rows} == {safe_id, full_id} assert {item['name'] for item in rows} == {'Safe project', 'Full access project'} def test_approval_checks_hash_csrf_and_engine(logged_in): client, app, headers, root, request = repository(logged_in) url = f"/api/dev-studio/requests/{request['id']}/approve" assert client.post(url, json={'plan_hash': request['plan_hash']}).status_code == 403 assert client.post(url, headers=headers, json={'plan_hash': '0' * 64}).status_code == 409 result = client.post(url, headers=headers, json={'plan_hash': request['plan_hash']}) assert result.status_code == 409 assert 'Codex' in result.text assert not list((app.state.codex.data_dir / 'worktrees').glob('*')) def test_generation_uses_dedicated_worktree_and_project_mode(logged_in, monkeypatch): client, app, headers, root, request = repository(logged_in) monkeypatch.setattr(app.state.codex, 'detect', lambda *args: '/synthetic-engine') monkeypatch.setattr(app.state.codex, '_spawn', lambda *args: 123456789) url = f"/api/dev-studio/requests/{request['id']}/approve" response = client.post(url, headers=headers, json={'plan_hash': request['plan_hash']}) assert response.status_code == 200, response.text result = response.json() conversation = app.state.db.one('SELECT * FROM conversations WHERE id=?', (result['conversation_id'],)) worktree = Path(conversation['worktree_path']) assert worktree != root and (worktree / '.git').is_file() assert conversation['branch_name'].startswith('studio/') assert client.post(url, headers=headers, json={'plan_hash': request['plan_hash']}).status_code == 409 queue = app.state.db.one('SELECT * FROM prompt_queue WHERE id=?', (result['queue_id'],)) assert 'production' in queue['content'] (worktree / 'demo.txt').write_text('Synthetic fixture, not generated by AI') app.state.db.execute("UPDATE prompt_queue SET status='done' WHERE id=?", (queue['id'],)) detail = client.get(f"/api/dev-studio/requests/{request['id']}").json() assert detail['status'] == 'BLOCKED' # No evidence cannot mean success. metadata = {'queue_id': queue['id'], 'actions': [{'command': 'python3 -m pytest -q', 'status': 'done', 'exit_code': 0, 'output': '1 passed (synthetic fixture)'}]} app.state.db.execute('INSERT INTO messages(id,conversation_id,role,content,metadata,created_at) VALUES(?,?,?,?,?,?)', (str(uuid.uuid4()), conversation['id'], 'assistant', 'Synthetic V0 report', json.dumps(metadata), '2026-01-01')) detail = client.get(f"/api/dev-studio/requests/{request['id']}").json() assert detail['status'] == 'READY' assert detail['tests'][0]['output'].startswith('1 passed') assert detail['git']['files'][0]['path'] == 'demo.txt' assert not (root / 'demo.txt').exists() assert 'Synthetic V0 report' in detail['report'] def test_private_requests_and_project_access(logged_in): client, app, headers, root, request = repository(logged_in) app.state.auth.create_user('other', 'a-different-long-test-password', is_admin=False) client.post('/api/auth/logout', headers=headers) login = client.post('/api/auth/login', json={'username': 'other', 'password': 'a-different-long-test-password'}).json() other_headers = {'X-CSRF-Token': login['csrf_token']} assert client.get('/api/dev-studio/requests').json() == [] assert client.get(f"/api/dev-studio/requests/{request['id']}").status_code == 404 assert client.post(f"/api/dev-studio/requests/{request['id']}/approve", headers=other_headers, json={'plan_hash': request['plan_hash']}).status_code == 404 assert client.post('/api/dev-studio/requests', headers=other_headers, json={'project_id': request['project_id'], 'name': 'Forbidden', 'description': 'Tentative sur un autre projet utilisateur.'}).status_code == 404 def test_full_access_project_can_generate_in_its_dedicated_worktree(logged_in, monkeypatch): client, app, headers, root, request = repository(logged_in) app.state.db.execute('UPDATE projects SET full_access=1 WHERE id=?', (request['project_id'],)) monkeypatch.setattr(app.state.codex, 'detect', lambda *args: '/synthetic-engine') monkeypatch.setattr(app.state.codex, '_spawn', lambda *args: 123456789) result = client.post(f"/api/dev-studio/requests/{request['id']}/approve", headers=headers, json={'plan_hash': request['plan_hash']}) assert result.status_code == 200, result.text conversation = app.state.db.one('SELECT * FROM conversations WHERE id=?', (result.json()['conversation_id'],)) assert Path(conversation['worktree_path']) != root def test_unborn_repository_is_not_initialized_by_studio(logged_in): client, app, projects, _, headers = logged_in root = projects / 'unborn' root.mkdir() subprocess.run(['git', 'init', '-b', 'main'], cwd=root, check=True, capture_output=True) project_id = str(uuid.uuid4()) owner = app.state.db.one('SELECT id FROM users LIMIT 1')['id'] app.state.db.execute('INSERT INTO projects(id,name,path,owner_id,created_at) VALUES(?,?,?,?,?)', (project_id, 'Unborn', str(root), owner, '2026-01-01')) result = client.post('/api/dev-studio/requests', headers=headers, json={'project_id': project_id, 'name': 'Demo', 'description': 'Une application de démonstration testable.'}) assert result.status_code == 409 assert subprocess.run(['git', 'rev-parse', '--verify', 'HEAD'], cwd=root, capture_output=True).returncode != 0 def test_private_preview_is_limited_to_worktree_loopback(logged_in, monkeypatch): import socket import sys import time client, app, headers, root, request = repository(logged_in) monkeypatch.setattr(app.state.codex, 'detect', lambda *args: '/synthetic-engine') monkeypatch.setattr(app.state.codex, '_spawn', lambda *args: 123456789) response = client.post(f"/api/dev-studio/requests/{request['id']}/approve", headers=headers, json={'plan_hash': request['plan_hash']}).json() worktree = Path(response['worktree_path']) (worktree / 'index.html').write_text('