File size: 2,661 Bytes
7cc81cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from pathlib import Path

import pytest
from pydantic import ValidationError

from app.copilot.actions import ACTION_DEFINITIONS
from app.social.schemas.posts import SocialPostCreate, SocialPostValidation


def test_canonical_copy_and_project_provenance_are_strict() -> None:
    payload = SocialPostCreate.model_validate({
        "project_id": "123e4567-e89b-12d3-a456-426614174000",
        "caption": "Release update",
        "hashtags": ["#release", "release", "media"],
        "targets": [{
            "social_account_id": "x-account",
            "caption": {"text": "X override"},
            "x": {"text": "X override"},
        }],
    })
    assert payload.caption == "Release update"
    assert payload.hashtags == ["release", "media"]
    with pytest.raises(ValidationError):
        SocialPostCreate.model_validate({
            "targets": [{
                "social_account_id": "x-account",
                "caption": {},
                "x": {"text": "valid"},
                "unknown_provider_payload": {},
            }],
        })


def test_structured_validation_is_per_target() -> None:
    result = SocialPostValidation.model_validate({
        "post_id": "post-1",
        "valid": False,
        "targets": [{
            "target_id": "target-1",
            "provider": "youtube",
            "account_id": "account-1",
            "valid": False,
            "errors": [{"code": "SOCIAL_MEDIA_INVALID", "message": "Invalid media."}],
            "warnings": [],
        }],
    })
    assert not result.valid
    assert result.targets[0].errors[0].code == "SOCIAL_MEDIA_INVALID"


def test_copilot_external_publishing_actions_require_confirmation() -> None:
    definitions = {
        item.type: item for item in ACTION_DEFINITIONS
        if item.type.startswith("publishing.")
    }
    assert set(definitions) == {
        "publishing.validate",
        "publishing.create_post",
        "publishing.schedule",
        "publishing.publish",
        "publishing.cancel",
    }
    for name in ("publishing.schedule", "publishing.publish", "publishing.cancel"):
        assert definitions[name].external_side_effect
        assert definitions[name].requires_confirmation


def test_additive_migration_extends_existing_social_tables() -> None:
    sql = Path("app/social/migrations/0008_unified_publishing.sql").read_text()
    lowered = sql.lower()
    assert "alter table social_posts" in lowered
    assert "foreign key (project_id) references projects(id)" in lowered
    assert "force row level security" in lowered
    assert "create table social_posts" not in lowered
    assert "create table social_jobs" not in lowered