diff --git a/internal/command/root_sources_test.go b/internal/command/root_sources_test.go index 8ac9bb4..33268ca 100644 --- a/internal/command/root_sources_test.go +++ b/internal/command/root_sources_test.go @@ -1257,7 +1257,7 @@ func TestYouTubeSearchVideosCallsService(t *testing.T) { if exitCode != 0 { t.Fatalf("expected exit code 0, got %d, stderr=%s", exitCode, stderr.String()) } - assertDataOnlyOutputContains(t, stdout.String(), `"data":[]`) + assertCompactTableOutputContains(t, stdout.String(), `"columns":["video_id","title","published_time","duration","view_count","like_count","comment_count","channel_id","channel"]`, `"rows":[]`) } func TestInstagramPostCommentsCallsService(t *testing.T) { @@ -1297,7 +1297,7 @@ func TestInstagramPostCommentsCallsService(t *testing.T) { if exitCode != 0 { t.Fatalf("expected exit code 0, got %d, stderr=%s", exitCode, stderr.String()) } - assertDataOnlyOutputContains(t, stdout.String(), `"comment_id":"c1"`) + assertCompactTableOutputContains(t, stdout.String(), `"columns":["comment_id","text","create_time","like_count","reply_count","author"]`, `"c1"`) } func TestInstagramSearchUsersHelpPrintsSchemaFlags(t *testing.T) { diff --git a/internal/output/projection_rules.go b/internal/output/projection_rules.go index 510cc9e..fcad54f 100644 --- a/internal/output/projection_rules.go +++ b/internal/output/projection_rules.go @@ -495,6 +495,99 @@ var projectionRules = map[string]projectionRule{ }, }, }, + "instagram.get_comment_replies": { + Compact: projectionSpec{ + Tables: []tableRule{ + { + From: "$root", + To: "$root", + Limit: 20, + Columns: []fieldRule{ + {From: "comment_id", To: "comment_id"}, + {From: "text", To: "text"}, + {From: "create_time", To: "create_time"}, + {From: "like_count", To: "like_count"}, + {From: "author.username", To: "author"}, + }, + }, + }, + }, + }, + "instagram.get_post_comments": { + Compact: projectionSpec{ + Tables: []tableRule{ + { + From: "$root", + To: "$root", + Limit: 20, + Columns: []fieldRule{ + {From: "comment_id", To: "comment_id"}, + {From: "text", To: "text"}, + {From: "create_time", To: "create_time"}, + {From: "like_count", To: "like_count"}, + {From: "reply_count", To: "reply_count"}, + {From: "author.username", To: "author"}, + }, + }, + }, + }, + }, + "instagram.get_post_detail": { + Compact: projectionSpec{ + Scalars: []fieldRule{ + {From: "media_id", To: "media_id"}, + {From: "shortcode", To: "shortcode"}, + {From: "caption", To: "caption"}, + {From: "media_type", To: "media_type"}, + {From: "taken_at", To: "taken_at"}, + {From: "like_count", To: "like_count"}, + {From: "comment_count", To: "comment_count"}, + {From: "view_count", To: "view_count"}, + {From: "owner.username", To: "owner"}, + {From: "permalink", To: "permalink"}, + }, + }, + }, + "instagram.get_user_posts": instagramMediaListProjection(), + "instagram.get_user_profile": { + Compact: projectionSpec{ + Scalars: []fieldRule{ + {From: "user_id", To: "user_id"}, + {From: "username", To: "username"}, + {From: "full_name", To: "full_name"}, + {From: "bio", To: "bio"}, + {From: "category", To: "category"}, + {From: "follower_count", To: "follower_count"}, + {From: "following_count", To: "following_count"}, + {From: "media_count", To: "media_count"}, + {From: "is_business", To: "is_business"}, + {From: "is_verified", To: "is_verified"}, + }, + }, + }, + "instagram.get_user_reels": instagramMediaListProjection(), + "instagram.search_hashtags": instagramHashtagListProjection(), + "instagram.search_reels": instagramMediaListProjection(), + "instagram.search_users": { + Compact: projectionSpec{ + Tables: []tableRule{ + { + From: "$root", + To: "$root", + Limit: 20, + Columns: []fieldRule{ + {From: "user_id", To: "user_id"}, + {From: "username", To: "username"}, + {From: "full_name", To: "full_name"}, + {From: "category", To: "category"}, + {From: "follower_count", To: "follower_count"}, + {From: "media_count", To: "media_count"}, + {From: "is_verified", To: "is_verified"}, + }, + }, + }, + }, + }, "meta_ads.get_ad_detail": { Compact: projectionSpec{ Scalars: []fieldRule{ @@ -773,6 +866,81 @@ var projectionRules = map[string]projectionRule{ }, }, }, + "youtube.get_channel_detail": { + Compact: projectionSpec{ + Scalars: []fieldRule{ + {From: "channel_id", To: "channel_id"}, + {From: "title", To: "title"}, + {From: "handle", To: "handle"}, + {From: "description", To: "description"}, + {From: "subscriber_count", To: "subscriber_count"}, + {From: "video_count", To: "video_count"}, + {From: "view_count", To: "view_count"}, + {From: "url", To: "url"}, + }, + }, + }, + "youtube.get_channel_videos": youtubeVideoListProjection(), + "youtube.get_comment_replies": { + Compact: projectionSpec{ + Tables: []tableRule{ + { + From: "$root", + To: "$root", + Limit: 20, + Columns: []fieldRule{ + {From: "comment_id", To: "comment_id"}, + {From: "text", To: "text"}, + {From: "create_time", To: "create_time"}, + {From: "like_count", To: "like_count"}, + {From: "author.name", To: "author"}, + }, + }, + }, + }, + }, + "youtube.get_video_comments": { + Compact: projectionSpec{ + Tables: []tableRule{ + { + From: "$root", + To: "$root", + Limit: 20, + Columns: []fieldRule{ + {From: "comment_id", To: "comment_id"}, + {From: "text", To: "text"}, + {From: "create_time", To: "create_time"}, + {From: "like_count", To: "like_count"}, + {From: "reply_count", To: "reply_count"}, + {From: "author.name", To: "author"}, + }, + }, + }, + }, + }, + "youtube.get_video_detail": { + Compact: projectionSpec{ + Scalars: []fieldRule{ + {From: "video_id", To: "video_id"}, + {From: "title", To: "title"}, + {From: "description", To: "description"}, + {From: "published_time", To: "published_time"}, + {From: "duration", To: "duration"}, + {From: "view_count", To: "view_count"}, + {From: "like_count", To: "like_count"}, + {From: "comment_count", To: "comment_count"}, + {From: "channel.channel_id", To: "channel_id"}, + {From: "channel.title", To: "channel"}, + {From: "url", To: "url"}, + }, + }, + }, + "youtube.get_video_transcript": { + Compact: projectionSpec{ + PassThrough: true, + }, + }, + "youtube.search_videos": youtubeVideoListProjection(), "xiaohongshu.get_note_comments": { Compact: projectionSpec{ Tables: []tableRule{ @@ -823,6 +991,50 @@ var projectionRules = map[string]projectionRule{ }, } +func instagramHashtagListProjection() projectionRule { + return projectionRule{ + Compact: projectionSpec{ + Tables: []tableRule{ + { + From: "$root", + To: "$root", + Limit: 20, + Columns: []fieldRule{ + {From: "hashtag_id", To: "hashtag_id"}, + {From: "name", To: "name"}, + {From: "media_count", To: "media_count"}, + }, + }, + }, + }, + } +} + +func instagramMediaListProjection() projectionRule { + return projectionRule{ + Compact: projectionSpec{ + Tables: []tableRule{ + { + From: "$root", + To: "$root", + Limit: 10, + Columns: []fieldRule{ + {From: "media_id", To: "media_id"}, + {From: "shortcode", To: "shortcode"}, + {From: "caption", To: "caption"}, + {From: "media_type", To: "media_type"}, + {From: "taken_at", To: "taken_at"}, + {From: "like_count", To: "like_count"}, + {From: "comment_count", To: "comment_count"}, + {From: "view_count", To: "view_count"}, + {From: "owner.username", To: "owner"}, + }, + }, + }, + }, + } +} + func videoDetailScalarFields(identityField string) []fieldRule { return []fieldRule{ {From: "platform", To: "platform"}, @@ -842,3 +1054,28 @@ func videoDetailScalarFields(identityField string) []fieldRule { {From: "music", To: "music"}, } } + +func youtubeVideoListProjection() projectionRule { + return projectionRule{ + Compact: projectionSpec{ + Tables: []tableRule{ + { + From: "$root", + To: "$root", + Limit: 10, + Columns: []fieldRule{ + {From: "video_id", To: "video_id"}, + {From: "title", To: "title"}, + {From: "published_time", To: "published_time"}, + {From: "duration", To: "duration"}, + {From: "view_count", To: "view_count"}, + {From: "like_count", To: "like_count"}, + {From: "comment_count", To: "comment_count"}, + {From: "channel.channel_id", To: "channel_id"}, + {From: "channel.title", To: "channel"}, + }, + }, + }, + }, + } +} diff --git a/internal/output/projection_test.go b/internal/output/projection_test.go index 06c92f8..299e092 100644 --- a/internal/output/projection_test.go +++ b/internal/output/projection_test.go @@ -782,6 +782,69 @@ func TestProjectCapabilityAutoFallsBackToDataWhenUnsupported(t *testing.T) { } } +func TestInstagramSearchReelsCompactProjectionDropsMediaURLs(t *testing.T) { + payload := json.RawMessage(`[ + { + "media_id":"m1", + "shortcode":"ABC123", + "caption":"desk setup", + "media_type":"video", + "media_url":"https://cdn.example/video.mp4", + "thumbnail":"https://cdn.example/thumb.jpg", + "permalink":"https://www.instagram.com/reel/ABC123/", + "taken_at":"2026-06-28T00:00:00Z", + "like_count":10, + "comment_count":2, + "view_count":100, + "owner":{"username":"creator","profile_pic_url":"https://cdn.example/avatar.jpg"} + } + ]`) + body, err := projectCapability("instagram.search_reels", payload, FormatCompact) + if err != nil { + t.Fatalf("projectCapability() error = %v", err) + } + text := string(body) + if !strings.Contains(text, `"columns":["media_id","shortcode","caption","media_type","taken_at","like_count","comment_count","view_count","owner"]`) { + t.Fatalf("expected compact instagram reel columns, got %s", text) + } + for _, unwanted := range []string{"media_url", "thumbnail", "permalink", "cdn.example", "profile_pic_url"} { + if strings.Contains(text, unwanted) { + t.Fatalf("expected compact instagram reel output to drop %s, got %s", unwanted, text) + } + } +} + +func TestYouTubeSearchVideosCompactProjectionDropsURLs(t *testing.T) { + payload := json.RawMessage(`[ + { + "video_id":"v1", + "title":"demo", + "url":"https://www.youtube.com/watch?v=v1", + "description":"long snippet", + "thumbnail":"https://i.ytimg.com/vi/v1/default.jpg", + "published_time":"2026-06-28", + "duration":"1:23", + "view_count":1000, + "like_count":10, + "comment_count":2, + "channel":{"channel_id":"c1","title":"Channel","url":"https://www.youtube.com/@channel"} + } + ]`) + body, err := projectCapability("youtube.search_videos", payload, FormatCompact) + if err != nil { + t.Fatalf("projectCapability() error = %v", err) + } + text := string(body) + if !strings.Contains(text, `"columns":["video_id","title","published_time","duration","view_count","like_count","comment_count","channel_id","channel"]`) { + t.Fatalf("expected compact youtube video columns, got %s", text) + } + for _, unwanted := range []string{"url", "thumbnail", "description", "youtube.com", "ytimg.com"} { + if strings.Contains(text, unwanted) { + t.Fatalf("expected compact youtube search output to drop %s, got %s", unwanted, text) + } + } +} + func TestProjectionRulesCoverAllAgentTestCapabilities(t *testing.T) { required := []string{ "amazon.get_product", @@ -825,6 +888,22 @@ func TestProjectionRulesCoverAllAgentTestCapabilities(t *testing.T) { "tiktok.product_reviews", "xiaohongshu.get_note_comments", "douyin.get_comment_replies", + "instagram.search_users", + "instagram.search_hashtags", + "instagram.search_reels", + "instagram.get_user_profile", + "instagram.get_user_posts", + "instagram.get_user_reels", + "instagram.get_post_detail", + "instagram.get_post_comments", + "instagram.get_comment_replies", + "youtube.search_videos", + "youtube.get_video_detail", + "youtube.get_video_comments", + "youtube.get_comment_replies", + "youtube.get_channel_detail", + "youtube.get_channel_videos", + "youtube.get_video_transcript", } if len(projectionRules) != len(required) { t.Fatalf("projectionRules size = %d, want %d", len(projectionRules), len(required))