Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitb20b801

Browse files
committed
feat: add notification for task status
1 parenta78790c commitb20b801

10 files changed

+521
-1
lines changed

‎coderd/aitasks_test.go‎

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package coderd_test
22

33
import (
4+
"database/sql"
45
"fmt"
56
"io"
67
"net/http"
@@ -17,7 +18,10 @@ import (
1718
"github.com/coder/coder/v2/coderd/coderdtest"
1819
"github.com/coder/coder/v2/coderd/database"
1920
"github.com/coder/coder/v2/coderd/database/dbauthz"
21+
"github.com/coder/coder/v2/coderd/database/dbfake"
2022
"github.com/coder/coder/v2/coderd/database/dbtestutil"
23+
"github.com/coder/coder/v2/coderd/notifications"
24+
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
2125
"github.com/coder/coder/v2/coderd/util/slice"
2226
"github.com/coder/coder/v2/codersdk"
2327
"github.com/coder/coder/v2/codersdk/agentsdk"
@@ -922,3 +926,124 @@ func TestTasksCreate(t *testing.T) {
922926
assert.Equal(t,http.StatusNotFound,sdkErr.StatusCode())
923927
})
924928
}
929+
930+
funcTestTasksNotification(t*testing.T) {
931+
t.Parallel()
932+
933+
for_,tc:=range []struct {
934+
namestring
935+
appStatus codersdk.WorkspaceAppStatusState
936+
isAITaskbool
937+
isNotificationSentbool
938+
notificationTemplate uuid.UUID
939+
}{
940+
// Should not send a notification when the agent app is not an AI task.
941+
{
942+
name:"NoAITask",
943+
appStatus:codersdk.WorkspaceAppStatusStateWorking,
944+
isAITask:false,
945+
isNotificationSent:false,
946+
},
947+
// Should not send a notification when the app status is neither 'Working' nor 'Idle'.
948+
{
949+
name:"NoNotifiedStatus",
950+
appStatus:codersdk.WorkspaceAppStatusStateComplete,
951+
isAITask:true,
952+
isNotificationSent:false,
953+
},
954+
// Should send TemplateTaskWorking when the AI task transitions to 'Working'.
955+
{
956+
name:"TemplateTaskWorking",
957+
appStatus:codersdk.WorkspaceAppStatusStateWorking,
958+
isAITask:true,
959+
isNotificationSent:true,
960+
notificationTemplate:notifications.TemplateTaskWorking,
961+
},
962+
// Should send TemplateTaskIdle when the AI task transitions to 'Idle'.
963+
{
964+
name:"TemplateTaskIdle",
965+
appStatus:codersdk.WorkspaceAppStatusStateIdle,
966+
isAITask:true,
967+
isNotificationSent:true,
968+
notificationTemplate:notifications.TemplateTaskIdle,
969+
},
970+
} {
971+
t.Run(tc.name,func(t*testing.T) {
972+
t.Parallel()
973+
974+
ctx:=testutil.Context(t,testutil.WaitShort)
975+
notifyEnq:=&notificationstest.FakeEnqueuer{}
976+
client,db:=coderdtest.NewWithDatabase(t,&coderdtest.Options{
977+
DeploymentValues:coderdtest.DeploymentValues(t),
978+
NotificationsEnqueuer:notifyEnq,
979+
})
980+
981+
// Given: A member user
982+
ownerUser:=coderdtest.CreateFirstUser(t,client)
983+
client,memberUser:=coderdtest.CreateAnotherUser(t,client,ownerUser.OrganizationID)
984+
985+
// Given: a workspace build with an agent containing an App
986+
workspaceAgentID:=uuid.NewString()
987+
workspaceBuildID:=uuid.New()
988+
workspaceBuildSeed:= database.WorkspaceBuild{
989+
ID:workspaceBuildID,
990+
}
991+
iftc.isAITask {
992+
workspaceBuildSeed= database.WorkspaceBuild{
993+
ID:workspaceBuildID,
994+
// AI Task configuration
995+
HasAITask: sql.NullBool{Bool:true,Valid:true},
996+
AITaskSidebarAppID: uuid.NullUUID{UUID:uuid.MustParse(workspaceAgentID),Valid:true},
997+
}
998+
}
999+
workspaceBuild:=dbfake.WorkspaceBuild(t,db, database.WorkspaceTable{
1000+
OrganizationID:ownerUser.OrganizationID,
1001+
OwnerID:memberUser.ID,
1002+
}).Seed(workspaceBuildSeed).Params(database.WorkspaceBuildParameter{
1003+
WorkspaceBuildID:workspaceBuildID,
1004+
Name:codersdk.AITaskPromptParameterName,
1005+
Value:"task prompt",
1006+
}).WithAgent(func(agent []*proto.Agent) []*proto.Agent {
1007+
agent[0].Apps= []*proto.App{{
1008+
Id:workspaceAgentID,
1009+
Slug:"ccw",
1010+
}}
1011+
returnagent
1012+
}).Do()
1013+
1014+
// When: The agent updates the app status
1015+
agentClient:=agentsdk.New(client.URL,agentsdk.WithFixedToken(workspaceBuild.AgentToken))
1016+
err:=agentClient.PatchAppStatus(ctx, agentsdk.PatchAppStatus{
1017+
AppSlug:"ccw",
1018+
Message:"testing",
1019+
URI:"https://example.com",
1020+
State:tc.appStatus,
1021+
})
1022+
require.NoError(t,err)
1023+
1024+
// Then: The workspace app status transitions successfully
1025+
workspace,err:=client.Workspace(ctx,workspaceBuild.Workspace.ID)
1026+
require.NoError(t,err)
1027+
workspaceAgent,err:=client.WorkspaceAgent(ctx,workspace.LatestBuild.Resources[0].Agents[0].ID)
1028+
require.NoError(t,err)
1029+
require.Len(t,workspaceAgent.Apps[0].Statuses,1)
1030+
require.Equal(t,workspaceAgent.Apps[0].Statuses[0].State,tc.appStatus)
1031+
1032+
iftc.isNotificationSent {
1033+
// Then: A notification is sent to the workspace owner (memberUser)
1034+
sent:=notifyEnq.Sent(notificationstest.WithTemplateID(tc.notificationTemplate))
1035+
require.Len(t,sent,1)
1036+
require.Equal(t,memberUser.ID,sent[0].UserID)
1037+
require.Len(t,sent[0].Labels,2)
1038+
require.Equal(t,"task prompt",sent[0].Labels["task"])
1039+
require.Equal(t,workspace.Name,sent[0].Labels["workspace"])
1040+
}else {
1041+
// Then: No notification is sent
1042+
sentWorking:=notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTaskWorking))
1043+
sentIdle:=notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTaskIdle))
1044+
require.Len(t,sentWorking,0)
1045+
require.Len(t,sentIdle,0)
1046+
}
1047+
})
1048+
}
1049+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- Remove Task 'working' transition template notification
2+
DELETEFROM notification_templatesWHERE id='bd4b7168-d05e-4e19-ad0f-3593b77aa90f';
3+
-- Remove Task 'idle' transition template notification
4+
DELETEFROM notification_templatesWHERE id='d4a6271c-cced-4ed0-84ad-afd02a9c7799';
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
-- Task transition to 'working' status
2+
INSERT INTO notification_templates (
3+
id,
4+
name,
5+
title_template,
6+
body_template,
7+
actions,
8+
"group",
9+
method,
10+
kind,
11+
enabled_by_default
12+
)VALUES (
13+
'bd4b7168-d05e-4e19-ad0f-3593b77aa90f',
14+
'Task Working',
15+
E'Task''{{.Labels.workspace}}'' is working',
16+
E'The task''{{.Labels.task}}'' transitioned to a working state.',
17+
'[
18+
{
19+
"label": "View task",
20+
"url": "{{base_url}}/tasks/{{.UserUsername}}/{{.Labels.workspace}}"
21+
},
22+
{
23+
"label": "View workspace",
24+
"url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}"
25+
}
26+
]'::jsonb,
27+
'Task Events',
28+
NULL,
29+
'system'::notification_template_kind,
30+
true
31+
);
32+
33+
-- Task transition to 'idle' status
34+
INSERT INTO notification_templates (
35+
id,
36+
name,
37+
title_template,
38+
body_template,
39+
actions,
40+
"group",
41+
method,
42+
kind,
43+
enabled_by_default
44+
)VALUES (
45+
'd4a6271c-cced-4ed0-84ad-afd02a9c7799',
46+
'Task Idle',
47+
E'Task''{{.Labels.workspace}}'' is idle',
48+
E'The task''{{.Labels.task}}'' is idle and ready for input.',
49+
'[
50+
{
51+
"label": "View task",
52+
"url": "{{base_url}}/tasks/{{.UserUsername}}/{{.Labels.workspace}}"
53+
},
54+
{
55+
"label": "View workspace",
56+
"url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}"
57+
}
58+
]'::jsonb,
59+
'Task Events',
60+
NULL,
61+
'system'::notification_template_kind,
62+
true
63+
);

‎coderd/notifications/events.go‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ var (
4242
TemplateWorkspaceResourceReplaced=uuid.MustParse("89d9745a-816e-4695-a17f-3d0a229e2b8d")
4343
)
4444

45-
// Prebuilds-related events
45+
// Prebuilds-related events.
4646
var (
4747
PrebuildFailureLimitReached=uuid.MustParse("414d9331-c1fc-4761-b40c-d1f4702279eb")
4848
)
@@ -52,3 +52,9 @@ var (
5252
TemplateTestNotification=uuid.MustParse("c425f63e-716a-4bf4-ae24-78348f706c3f")
5353
TemplateCustomNotification=uuid.MustParse("39b1e189-c857-4b0c-877a-511144c18516")
5454
)
55+
56+
// Task-related events.
57+
var (
58+
TemplateTaskWorking=uuid.MustParse("bd4b7168-d05e-4e19-ad0f-3593b77aa90f")
59+
TemplateTaskIdle=uuid.MustParse("d4a6271c-cced-4ed0-84ad-afd02a9c7799")
60+
)

‎coderd/notifications/notifications_test.go‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,34 @@ func TestNotificationTemplates_Golden(t *testing.T) {
12731273
Data:map[string]any{},
12741274
},
12751275
},
1276+
{
1277+
name:"TemplateTaskWorking",
1278+
id:notifications.TemplateTaskWorking,
1279+
payload: types.MessagePayload{
1280+
UserName:"Bobby",
1281+
UserEmail:"bobby@coder.com",
1282+
UserUsername:"bobby",
1283+
Labels:map[string]string{
1284+
"task":"my-task",
1285+
"workspace":"my-workspace",
1286+
},
1287+
Data:map[string]any{},
1288+
},
1289+
},
1290+
{
1291+
name:"TemplateTaskIdle",
1292+
id:notifications.TemplateTaskIdle,
1293+
payload: types.MessagePayload{
1294+
UserName:"Bobby",
1295+
UserEmail:"bobby@coder.com",
1296+
UserUsername:"bobby",
1297+
Labels:map[string]string{
1298+
"task":"my-task",
1299+
"workspace":"my-workspace",
1300+
},
1301+
Data:map[string]any{},
1302+
},
1303+
},
12761304
}
12771305

12781306
// We must have a test case for every notification_template. This is enforced below:
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
From: system@coder.com
2+
To: bobby@coder.com
3+
Subject: Task 'my-workspace' is idle
4+
Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48
5+
Date: Fri, 11 Oct 2024 09:03:06 +0000
6+
Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
7+
MIME-Version: 1.0
8+
9+
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
10+
Content-Transfer-Encoding: quoted-printable
11+
Content-Type: text/plain; charset=UTF-8
12+
13+
Hi Bobby,
14+
15+
The task 'my-task' is idle and ready for input.
16+
17+
18+
View task: http://test.com/tasks/bobby/my-workspace
19+
20+
View workspace: http://test.com/@bobby/my-workspace
21+
22+
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
23+
Content-Transfer-Encoding: quoted-printable
24+
Content-Type: text/html; charset=UTF-8
25+
26+
<!doctype html>
27+
<html lang=3D"en">
28+
<head>
29+
<meta charset=3D"UTF-8" />
30+
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
31+
=3D1.0" />
32+
<title>Task 'my-workspace' is idle</title>
33+
</head>
34+
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
35+
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
36+
l', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617=
37+
; background: #f8fafc;">
38+
<div style=3D"max-width: 600px; margin: 20px auto; padding: 60px; borde=
39+
r: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-alig=
40+
n: left; font-size: 14px; line-height: 1.5;">
41+
<div style=3D"text-align: center;">
42+
<img src=3D"https://coder.com/coder-logo-horizontal.png" alt=3D"Cod=
43+
er Logo" style=3D"height: 40px;" />
44+
</div>
45+
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
46+
argin: 8px 0 32px; line-height: 1.5;">
47+
Task 'my-workspace' is idle
48+
</h1>
49+
<div style=3D"line-height: 1.5;">
50+
<p>Hi Bobby,</p>
51+
<p>The task &lsquo;my-task&rsquo; is idle and ready for input.</p>
52+
</div>
53+
<div style=3D"text-align: center; margin-top: 32px;">
54+
=20
55+
<a href=3D"http://test.com/tasks/bobby/my-workspace" style=3D"displ=
56+
ay: inline-block; padding: 13px 24px; background-color: #020617; color: #f8=
57+
fafc; text-decoration: none; border-radius: 8px; margin: 0 4px;">
58+
View task
59+
</a>
60+
=20
61+
<a href=3D"http://test.com/@bobby/my-workspace" style=3D"display: i=
62+
nline-block; padding: 13px 24px; background-color: #020617; color: #f8fafc;=
63+
text-decoration: none; border-radius: 8px; margin: 0 4px;">
64+
View workspace
65+
</a>
66+
=20
67+
</div>
68+
<div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz=
69+
e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">
70+
<p>&copy;&nbsp;2024&nbsp;Coder. All rights reserved&nbsp;-&nbsp;<a =
71+
href=3D"http://test.com" style=3D"color: #2563eb; text-decoration: none;">h=
72+
ttp://test.com</a></p>
73+
<p><a href=3D"http://test.com/settings/notifications" style=3D"colo=
74+
r: #2563eb; text-decoration: none;">Click here to manage your notification =
75+
settings</a></p>
76+
<p><a href=3D"http://test.com/settings/notifications?disabled=3Dd4a=
77+
6271c-cced-4ed0-84ad-afd02a9c7799" style=3D"color: #2563eb; text-decoration=
78+
: none;">Stop receiving emails like this</a></p>
79+
</div>
80+
</div>
81+
</body>
82+
</html>
83+
84+
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp