-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Show MCP agent info in Activity Log #25397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d506e3d
5d175c7
3e9c180
5ea404c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| @testable import WordPress | ||
| @testable import WordPressKit | ||
| import Testing | ||
|
|
||
| struct ActivityStringFormattingTests { | ||
|
|
||
| // MARK: - actorName | ||
|
|
||
| @Test func actorNameReturnsDisplayName() { | ||
| let actor = ActivityActor(dictionary: ["name": "Alice", "type": "Person", "role": "administrator"]) | ||
| #expect(ActivityStringFormatting.actorName(for: actor) == "Alice") | ||
| } | ||
|
|
||
| @Test func actorNameReturnsUnknownUserWhenEmpty() { | ||
| let actor = ActivityActor(dictionary: ["name": "", "type": "Person", "role": "editor"]) | ||
| #expect(ActivityStringFormatting.actorName(for: actor) == Activity.Strings.unknownUser) | ||
| } | ||
|
|
||
| @Test func actorNameReturnsUnknownUserWhenMissing() { | ||
| let actor = ActivityActor(dictionary: ["type": "Person", "role": "editor"]) | ||
| #expect(ActivityStringFormatting.actorName(for: actor) == Activity.Strings.unknownUser) | ||
| } | ||
|
|
||
| // MARK: - actorRole | ||
|
|
||
| @Test func actorRoleReturnsCapitalizedRole() { | ||
| let actor = ActivityActor(dictionary: ["name": "Alice", "type": "Person", "role": "administrator"]) | ||
| #expect(ActivityStringFormatting.actorRole(for: actor) == "Administrator") | ||
| } | ||
|
|
||
| @Test func actorRoleFallsBackToTypeWhenRoleEmpty() { | ||
| let actor = ActivityActor(dictionary: ["name": "Jetpack", "type": "Application", "role": ""]) | ||
| #expect(ActivityStringFormatting.actorRole(for: actor) == "Application") | ||
| } | ||
|
|
||
| @Test func actorRoleFallsBackToTypeWhenRoleMissing() { | ||
| let actor = ActivityActor(dictionary: ["name": "Jetpack", "type": "Application"]) | ||
| #expect(ActivityStringFormatting.actorRole(for: actor) == "Application") | ||
| } | ||
|
|
||
| // MARK: - botName | ||
|
|
||
| @Test func botNameForMCPAgent() { | ||
| let actor = ActivityActor(dictionary: [ | ||
| "name": "bot-user", | ||
| "type": "Person", | ||
| "role": "administrator", | ||
| "is_mcp_agent": true, | ||
| "mcp_client": "Claude" | ||
| ]) | ||
| #expect(ActivityStringFormatting.botName(for: actor) == "via Claude") | ||
| } | ||
|
|
||
| @Test func botNameForNonMCPActor() { | ||
| let actor = ActivityActor(dictionary: ["name": "Alice", "type": "Person", "role": "editor"]) | ||
| #expect(ActivityStringFormatting.botName(for: actor) == nil) | ||
| } | ||
|
|
||
| @Test func botNameForMCPAgentWithEmptyClient() { | ||
| let actor = ActivityActor(dictionary: [ | ||
| "name": "bot-user", | ||
| "type": "Person", | ||
| "role": "administrator", | ||
| "is_mcp_agent": true, | ||
| "mcp_client": "" | ||
| ]) | ||
| #expect(ActivityStringFormatting.botName(for: actor) == nil) | ||
| } | ||
|
|
||
| @Test func botNameForMCPAgentWithNoClient() { | ||
| let actor = ActivityActor(dictionary: [ | ||
| "name": "bot-user", | ||
| "type": "Person", | ||
| "role": "administrator", | ||
| "is_mcp_agent": true | ||
| ]) | ||
| #expect(ActivityStringFormatting.botName(for: actor) == nil) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,173 @@ | ||
| @testable import WordPressKit | ||
| import XCTest | ||
| import Testing | ||
|
|
||
| class ActivityTests: XCTestCase { | ||
| struct ActivityTests { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice to see more XCTest rework. |
||
|
|
||
| func testActivityDecoding() throws { | ||
| let data = try XCTUnwrap(activityLogComment.data(using: .utf8)) | ||
| // This is part of the test in itself. | ||
| // If Activity is not configured as expected to decode the JSON input, JSONDecode will throw. | ||
| @Test func activityDecoding() throws { | ||
| let data = try #require(activityLogComment.data(using: .utf8)) | ||
| let activity = try JSONDecoder().decode(Activity.self, from: data) | ||
| // Verify custom keys | ||
| XCTAssertEqual(activity.activityID, "AWRNRTAUjEqjFGbx8DZj") | ||
| XCTAssertFalse(activity.isRewindable) | ||
| XCTAssertEqual(activity.rewindID, "1530304735.2771") | ||
| #expect(activity.activityID == "AWRNRTAUjEqjFGbx8DZj") | ||
| #expect(activity.isRewindable == false) | ||
| #expect(activity.rewindID == "1530304735.2771") | ||
| } | ||
|
|
||
| @Test func actorWithMCPAgentFields() throws { | ||
| let data = try #require(activityLogMCPAgent.data(using: .utf8)) | ||
| let activity = try JSONDecoder().decode(Activity.self, from: data) | ||
| let actor = try #require(activity.actor) | ||
| #expect(actor.isMCPAgent == true) | ||
| #expect(actor.mcpClient == "Claude") | ||
| #expect(actor.displayName == "bot-user") | ||
| #expect(actor.role == "administrator") | ||
| } | ||
|
|
||
| @Test func actorWithoutMCPAgentFields() throws { | ||
| let data = try #require(activityLogComment.data(using: .utf8)) | ||
| let activity = try JSONDecoder().decode(Activity.self, from: data) | ||
| let actor = try #require(activity.actor) | ||
| #expect(actor.isMCPAgent == false) | ||
| #expect(actor.mcpClient == nil) | ||
| } | ||
|
|
||
| @Test func actorWithExplicitlyFalseMCPAgent() throws { | ||
| let data = try #require(activityLogExplicitlyNotMCPAgent.data(using: .utf8)) | ||
| let activity = try JSONDecoder().decode(Activity.self, from: data) | ||
| let actor = try #require(activity.actor) | ||
| #expect(actor.isMCPAgent == false) | ||
| #expect(actor.mcpClient == nil) | ||
| } | ||
|
|
||
| @Test func actorWithMCPAgentButNoClient() throws { | ||
| let data = try #require(activityLogMCPAgentNoClient.data(using: .utf8)) | ||
| let activity = try JSONDecoder().decode(Activity.self, from: data) | ||
| let actor = try #require(activity.actor) | ||
| #expect(actor.isMCPAgent == true) | ||
| #expect(actor.mcpClient == nil) | ||
| } | ||
| } | ||
|
|
||
| private let activityLogMCPAgent: String = """ | ||
| { | ||
| "summary": "Post published", | ||
| "content": { | ||
| "text": "Post published by MCP agent" | ||
| }, | ||
| "name": "post__published", | ||
| "actor": { | ||
| "type": "Person", | ||
| "name": "bot-user", | ||
| "external_user_id": 0, | ||
| "wpcom_user_id": 100000001, | ||
| "icon": { | ||
| "type": "Image", | ||
| "url": "https://secure.gravatar.com/avatar/example?s=96&d=identicon&r=g", | ||
| "width": 96, | ||
| "height": 96 | ||
| }, | ||
| "role": "administrator", | ||
| "is_mcp_agent": true, | ||
| "mcp_client": "Claude" | ||
| }, | ||
| "type": "Create", | ||
| "published": "2026-03-17T10:00:00.000+00:00", | ||
| "generator": { | ||
| "jetpack_version": 0, | ||
| "blog_id": 137726971 | ||
| }, | ||
| "is_rewindable": false, | ||
| "rewind_id": "1710000000.0001", | ||
| "gridicon": "posts", | ||
| "status": "success", | ||
| "activity_id": "test-mcp-agent-activity", | ||
| "object": { | ||
| "type": "Post", | ||
| "object_id": 100 | ||
| }, | ||
| "is_discarded": false | ||
| } | ||
| """ | ||
|
|
||
| private let activityLogExplicitlyNotMCPAgent: String = """ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (nit) It might be worth adding them in Resources to reduce the amount of work for the compiler and let you see these files with a JSON viewer.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I didn't love this either but I figured I'd leave it to match what's already there 🤷 |
||
| { | ||
| "summary": "Post updated", | ||
| "content": { | ||
| "text": "Post updated by a human" | ||
| }, | ||
| "name": "post__updated", | ||
| "actor": { | ||
| "type": "Person", | ||
| "name": "human-user", | ||
| "external_user_id": 0, | ||
| "wpcom_user_id": 100000002, | ||
| "icon": { | ||
| "type": "Image", | ||
| "url": "https://secure.gravatar.com/avatar/example2?s=96&d=identicon&r=g", | ||
| "width": 96, | ||
| "height": 96 | ||
| }, | ||
| "role": "editor", | ||
| "is_mcp_agent": false, | ||
| "mcp_client": null | ||
| }, | ||
| "type": "Update", | ||
| "published": "2026-03-17T11:00:00.000+00:00", | ||
| "generator": { | ||
| "jetpack_version": 0, | ||
| "blog_id": 137726971 | ||
| }, | ||
| "is_rewindable": false, | ||
| "rewind_id": "1710000001.0001", | ||
| "gridicon": "posts", | ||
| "status": "success", | ||
| "activity_id": "test-not-mcp-agent", | ||
| "object": { | ||
| "type": "Post", | ||
| "object_id": 101 | ||
| }, | ||
| "is_discarded": false | ||
| } | ||
| """ | ||
|
|
||
| private let activityLogMCPAgentNoClient: String = """ | ||
| { | ||
| "summary": "Post drafted", | ||
| "content": { | ||
| "text": "Post drafted by MCP agent without client" | ||
| }, | ||
| "name": "post__drafted", | ||
| "actor": { | ||
| "type": "Person", | ||
| "name": "agent-user", | ||
| "external_user_id": 0, | ||
| "wpcom_user_id": 100000003, | ||
| "icon": { | ||
| "type": "Image", | ||
| "url": "https://secure.gravatar.com/avatar/example3?s=96&d=identicon&r=g", | ||
| "width": 96, | ||
| "height": 96 | ||
| }, | ||
| "role": "administrator", | ||
| "is_mcp_agent": true | ||
| }, | ||
| "type": "Create", | ||
| "published": "2026-03-17T12:00:00.000+00:00", | ||
| "generator": { | ||
| "jetpack_version": 0, | ||
| "blog_id": 137726971 | ||
| }, | ||
| "is_rewindable": false, | ||
| "rewind_id": "1710000002.0001", | ||
| "gridicon": "posts", | ||
| "status": "success", | ||
| "activity_id": "test-mcp-agent-no-client", | ||
| "object": { | ||
| "type": "Post", | ||
| "object_id": 102 | ||
| }, | ||
| "is_discarded": false | ||
| } | ||
| """ | ||
|
|
||
| // See https://github.com/wordpress-mobile/WordPress-iOS/blob/16adc688f718136ea57c45d5d26c5c13de9d2b9f/WordPress/WordPressTest/Test%20Data/activity-log-comment.json | ||
| private let activityLogComment: String = """ | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import Foundation | ||
| import WordPressData | ||
|
|
||
| struct ActivityStringFormatting { | ||
| private static let agentString = NSLocalizedString( | ||
| "activityDetail.section.agent", | ||
| value: "via %1$@", | ||
| comment: "Shows the MCP client used for an activity. %1$@ is the MCP client name (e.g. Claude)." | ||
| ) | ||
|
|
||
| static func actorName(for actor: ActivityActor) -> String { | ||
| actor.displayName.isEmpty ? Activity.Strings.unknownUser : actor.displayName | ||
| } | ||
|
|
||
| static func actorRole(for actor: ActivityActor) -> String { | ||
| actor.role.isEmpty ? actor.type.localizedCapitalized : actor.role.localizedCapitalized | ||
| } | ||
|
|
||
| static func botName(for actor: ActivityActor) -> String? { | ||
| guard actor.isMCPAgent, let mcpClient = actor.mcpClient, !mcpClient.isEmpty else { | ||
| return nil | ||
| } | ||
|
|
||
| return String(format: Self.agentString, mcpClient) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the change related to the PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only insomuch as I hit it while testing and didn't think it was worth its own PR.