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

Commitffcb7a1

Browse files
authored
fix(coderd): truncate task prompt to 160 characters in notifications (#20147)
Truncates the task prompt used in notifications to a maximum of 160characters. The length of 160 characters was chosen arbitrarily.
1 parent4d1003e commitffcb7a1

File tree

4 files changed

+123
-13
lines changed

4 files changed

+123
-13
lines changed

‎coderd/aitasks_test.go‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import (
66
"io"
77
"net/http"
88
"net/http/httptest"
9+
"strings"
910
"testing"
1011
"time"
12+
"unicode/utf8"
1113

1214
"github.com/google/uuid"
1315
"github.com/stretchr/testify/assert"
@@ -977,6 +979,7 @@ func TestTasksNotification(t *testing.T) {
977979
isAITaskbool
978980
isNotificationSentbool
979981
notificationTemplate uuid.UUID
982+
taskPromptstring
980983
}{
981984
// Should not send a notification when the agent app is not an AI task.
982985
{
@@ -985,6 +988,7 @@ func TestTasksNotification(t *testing.T) {
985988
newAppStatus:codersdk.WorkspaceAppStatusStateWorking,
986989
isAITask:false,
987990
isNotificationSent:false,
991+
taskPrompt:"NoAITask",
988992
},
989993
// Should not send a notification when the new app status is neither 'Working' nor 'Idle'.
990994
{
@@ -993,6 +997,7 @@ func TestTasksNotification(t *testing.T) {
993997
newAppStatus:codersdk.WorkspaceAppStatusStateComplete,
994998
isAITask:true,
995999
isNotificationSent:false,
1000+
taskPrompt:"NonNotifiedState",
9961001
},
9971002
// Should not send a notification when the new app status equals the latest status (Working).
9981003
{
@@ -1001,6 +1006,7 @@ func TestTasksNotification(t *testing.T) {
10011006
newAppStatus:codersdk.WorkspaceAppStatusStateWorking,
10021007
isAITask:true,
10031008
isNotificationSent:false,
1009+
taskPrompt:"NonNotifiedTransition",
10041010
},
10051011
// Should send TemplateTaskWorking when the AI task transitions to 'Working'.
10061012
{
@@ -1010,6 +1016,7 @@ func TestTasksNotification(t *testing.T) {
10101016
isAITask:true,
10111017
isNotificationSent:true,
10121018
notificationTemplate:notifications.TemplateTaskWorking,
1019+
taskPrompt:"TemplateTaskWorking",
10131020
},
10141021
// Should send TemplateTaskWorking when the AI task transitions to 'Working' from 'Idle'.
10151022
{
@@ -1022,6 +1029,7 @@ func TestTasksNotification(t *testing.T) {
10221029
isAITask:true,
10231030
isNotificationSent:true,
10241031
notificationTemplate:notifications.TemplateTaskWorking,
1032+
taskPrompt:"TemplateTaskWorkingFromIdle",
10251033
},
10261034
// Should send TemplateTaskIdle when the AI task transitions to 'Idle'.
10271035
{
@@ -1031,6 +1039,17 @@ func TestTasksNotification(t *testing.T) {
10311039
isAITask:true,
10321040
isNotificationSent:true,
10331041
notificationTemplate:notifications.TemplateTaskIdle,
1042+
taskPrompt:"TemplateTaskIdle",
1043+
},
1044+
// Long task prompts should be truncated to 160 characters.
1045+
{
1046+
name:"LongTaskPrompt",
1047+
latestAppStatuses: []codersdk.WorkspaceAppStatusState{codersdk.WorkspaceAppStatusStateWorking},
1048+
newAppStatus:codersdk.WorkspaceAppStatusStateIdle,
1049+
isAITask:true,
1050+
isNotificationSent:true,
1051+
notificationTemplate:notifications.TemplateTaskIdle,
1052+
taskPrompt:"This is a very long task prompt that should be truncated to 160 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
10341053
},
10351054
} {
10361055
t.Run(tc.name,func(t*testing.T) {
@@ -1067,7 +1086,7 @@ func TestTasksNotification(t *testing.T) {
10671086
}).Seed(workspaceBuildSeed).Params(database.WorkspaceBuildParameter{
10681087
WorkspaceBuildID:workspaceBuildID,
10691088
Name:codersdk.AITaskPromptParameterName,
1070-
Value:"task prompt",
1089+
Value:tc.taskPrompt,
10711090
}).WithAgent(func(agent []*proto.Agent) []*proto.Agent {
10721091
agent[0].Apps= []*proto.App{{
10731092
Id:workspaceAgentAppID.String(),
@@ -1115,7 +1134,13 @@ func TestTasksNotification(t *testing.T) {
11151134
require.Len(t,sent,1)
11161135
require.Equal(t,memberUser.ID,sent[0].UserID)
11171136
require.Len(t,sent[0].Labels,2)
1118-
require.Equal(t,"task prompt",sent[0].Labels["task"])
1137+
// NOTE: len(string) is the number of bytes in the string, not the number of runes.
1138+
require.LessOrEqual(t,utf8.RuneCountInString(sent[0].Labels["task"]),160)
1139+
iflen(tc.taskPrompt)>160 {
1140+
require.Contains(t,tc.taskPrompt,strings.TrimSuffix(sent[0].Labels["task"],"…"))
1141+
}else {
1142+
require.Equal(t,tc.taskPrompt,sent[0].Labels["task"])
1143+
}
11191144
require.Equal(t,workspace.Name,sent[0].Labels["workspace"])
11201145
}else {
11211146
// Then: No notification is sent

‎coderd/util/strings/strings.go‎

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,64 @@ func JoinWithConjunction(s []string) string {
2323
)
2424
}
2525

26-
// Truncate returns the first n characters of s.
27-
funcTruncate(sstring,nint)string {
26+
typeTruncateOptionint
27+
28+
func (oTruncateOption)String()string {
29+
switcho {
30+
caseTruncateWithEllipsis:
31+
return"TruncateWithEllipsis"
32+
caseTruncateWithFullWords:
33+
return"TruncateWithFullWords"
34+
default:
35+
returnfmt.Sprintf("TruncateOption(%d)",o)
36+
}
37+
}
38+
39+
const (
40+
// TruncateWithEllipsis adds a Unicode ellipsis character to the end of the string.
41+
TruncateWithEllipsisTruncateOption=1<<0
42+
// TruncateWithFullWords ensures that words are not split in the middle.
43+
// As a special case, if there is no word boundary, the string is truncated.
44+
TruncateWithFullWordsTruncateOption=1<<1
45+
)
46+
47+
// Truncate truncates s to n characters.
48+
// Additional behaviors can be specified using TruncateOptions.
49+
funcTruncate(sstring,nint,opts...TruncateOption)string {
50+
varoptionsTruncateOption
51+
for_,opt:=rangeopts {
52+
options|=opt
53+
}
2854
ifn<1 {
2955
return""
3056
}
3157
iflen(s)<=n {
3258
returns
3359
}
34-
returns[:n]
60+
61+
maxLen:=n
62+
ifoptions&TruncateWithEllipsis!=0 {
63+
maxLen--
64+
}
65+
varsb strings.Builder
66+
// If we need to truncate to full words, find the last word boundary before n.
67+
ifoptions&TruncateWithFullWords!=0 {
68+
lastWordBoundary:=strings.LastIndexFunc(s[:maxLen],unicode.IsSpace)
69+
iflastWordBoundary<0 {
70+
// We cannot find a word boundary. At this point, we'll truncate the string.
71+
// It's better than nothing.
72+
_,_=sb.WriteString(s[:maxLen])
73+
}else {// lastWordBoundary <= maxLen
74+
_,_=sb.WriteString(s[:lastWordBoundary])
75+
}
76+
}else {
77+
_,_=sb.WriteString(s[:maxLen])
78+
}
79+
80+
ifoptions&TruncateWithEllipsis!=0 {
81+
_,_=sb.WriteString("…")
82+
}
83+
returnsb.String()
3584
}
3685

3786
varbmPolicy=bluemonday.StrictPolicy()

‎coderd/util/strings/strings_test.go‎

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package strings_test
22

33
import (
4+
"fmt"
45
"testing"
56

67
"github.com/stretchr/testify/assert"
@@ -23,17 +24,47 @@ func TestTruncate(t *testing.T) {
2324
sstring
2425
nint
2526
expectedstring
27+
options []strings.TruncateOption
2628
}{
27-
{"foo",4,"foo"},
28-
{"foo",3,"foo"},
29-
{"foo",2,"fo"},
30-
{"foo",1,"f"},
31-
{"foo",0,""},
32-
{"foo",-1,""},
29+
{"foo",4,"foo",nil},
30+
{"foo",3,"foo",nil},
31+
{"foo",2,"fo",nil},
32+
{"foo",1,"f",nil},
33+
{"foo",0,"",nil},
34+
{"foo",-1,"",nil},
35+
{"foo bar",7,"foo bar", []strings.TruncateOption{strings.TruncateWithEllipsis}},
36+
{"foo bar",6,"foo b…", []strings.TruncateOption{strings.TruncateWithEllipsis}},
37+
{"foo bar",5,"foo …", []strings.TruncateOption{strings.TruncateWithEllipsis}},
38+
{"foo bar",4,"foo…", []strings.TruncateOption{strings.TruncateWithEllipsis}},
39+
{"foo bar",3,"fo…", []strings.TruncateOption{strings.TruncateWithEllipsis}},
40+
{"foo bar",2,"f…", []strings.TruncateOption{strings.TruncateWithEllipsis}},
41+
{"foo bar",1,"…", []strings.TruncateOption{strings.TruncateWithEllipsis}},
42+
{"foo bar",0,"", []strings.TruncateOption{strings.TruncateWithEllipsis}},
43+
{"foo bar",7,"foo bar", []strings.TruncateOption{strings.TruncateWithFullWords}},
44+
{"foo bar",6,"foo", []strings.TruncateOption{strings.TruncateWithFullWords}},
45+
{"foo bar",5,"foo", []strings.TruncateOption{strings.TruncateWithFullWords}},
46+
{"foo bar",4,"foo", []strings.TruncateOption{strings.TruncateWithFullWords}},
47+
{"foo bar",3,"foo", []strings.TruncateOption{strings.TruncateWithFullWords}},
48+
{"foo bar",2,"fo", []strings.TruncateOption{strings.TruncateWithFullWords}},
49+
{"foo bar",1,"f", []strings.TruncateOption{strings.TruncateWithFullWords}},
50+
{"foo bar",0,"", []strings.TruncateOption{strings.TruncateWithFullWords}},
51+
{"foo bar",7,"foo bar", []strings.TruncateOption{strings.TruncateWithFullWords,strings.TruncateWithEllipsis}},
52+
{"foo bar",6,"foo…", []strings.TruncateOption{strings.TruncateWithFullWords,strings.TruncateWithEllipsis}},
53+
{"foo bar",5,"foo…", []strings.TruncateOption{strings.TruncateWithFullWords,strings.TruncateWithEllipsis}},
54+
{"foo bar",4,"foo…", []strings.TruncateOption{strings.TruncateWithFullWords,strings.TruncateWithEllipsis}},
55+
{"foo bar",3,"fo…", []strings.TruncateOption{strings.TruncateWithFullWords,strings.TruncateWithEllipsis}},
56+
{"foo bar",2,"f…", []strings.TruncateOption{strings.TruncateWithFullWords,strings.TruncateWithEllipsis}},
57+
{"foo bar",1,"…", []strings.TruncateOption{strings.TruncateWithFullWords,strings.TruncateWithEllipsis}},
58+
{"foo bar",0,"", []strings.TruncateOption{strings.TruncateWithFullWords,strings.TruncateWithEllipsis}},
59+
{"This is a very long task prompt that should be truncated to 160 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",160,"This is a very long task prompt that should be truncated to 160 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor…", []strings.TruncateOption{strings.TruncateWithFullWords,strings.TruncateWithEllipsis}},
3360
} {
34-
t.Run(tt.expected,func(t*testing.T) {
61+
tName:=fmt.Sprintf("%s_%d",tt.s,tt.n)
62+
for_,opt:=rangett.options {
63+
tName+=fmt.Sprintf("_%v",opt)
64+
}
65+
t.Run(tName,func(t*testing.T) {
3566
t.Parallel()
36-
actual:=strings.Truncate(tt.s,tt.n)
67+
actual:=strings.Truncate(tt.s,tt.n,tt.options...)
3768
require.Equal(t,tt.expected,actual)
3869
})
3970
}

‎coderd/workspaceagents.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,11 @@ func (api *API) enqueueAITaskStateNotification(
484484
}
485485
}
486486

487+
// As task prompt may be particularly long, truncate it to 160 characters for notifications.
488+
iflen(taskName)>160 {
489+
taskName=strutil.Truncate(taskName,160,strutil.TruncateWithEllipsis,strutil.TruncateWithFullWords)
490+
}
491+
487492
if_,err:=api.NotificationsEnqueuer.EnqueueWithData(
488493
// nolint:gocritic // Need notifier actor to enqueue notifications
489494
dbauthz.AsNotifier(ctx),

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp