@frockbot/configuration-core 0.3.10 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/bot-identity.test.ts +36 -0
- package/src/index.ts +32 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/configuration-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@frockbot/connection-core": "0.3.
|
|
16
|
-
"@frockbot/kernel-composition": "0.3.
|
|
15
|
+
"@frockbot/connection-core": "0.3.11",
|
|
16
|
+
"@frockbot/kernel-composition": "0.3.11"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/bun": "1.3.6",
|
package/src/bot-identity.test.ts
CHANGED
|
@@ -182,4 +182,40 @@ describe("bot/set-profile", () => {
|
|
|
182
182
|
),
|
|
183
183
|
).toEqual({ name: "Housework" });
|
|
184
184
|
});
|
|
185
|
+
|
|
186
|
+
test("pins with an instant and unpins with the empty string", () => {
|
|
187
|
+
const at = "2026-09-03T10:15:00.000Z";
|
|
188
|
+
expect(
|
|
189
|
+
decodeBotSettingsViewV1(settings({ name: "Housework", pinnedAt: at })),
|
|
190
|
+
).toMatchObject({ profile: { pinnedAt: at } });
|
|
191
|
+
expect(() =>
|
|
192
|
+
decodeBotSettingsViewV1(
|
|
193
|
+
settings({ name: "Housework", pinnedAt: "whenever" }),
|
|
194
|
+
),
|
|
195
|
+
).toThrow("profile.pinnedAt is invalid");
|
|
196
|
+
|
|
197
|
+
const current: BotProfile = { name: "Housework", pinnedAt: at };
|
|
198
|
+
// A later save must not reshuffle the pinned row: the instant is durable,
|
|
199
|
+
// so only an explicit change moves it.
|
|
200
|
+
expect(
|
|
201
|
+
applyBotProfilePatchV1(current, { title: "Chief" }, "user").pinnedAt,
|
|
202
|
+
).toBe(at);
|
|
203
|
+
expect(applyBotProfilePatchV1(current, { pinnedAt: "" }, "user")).toEqual({
|
|
204
|
+
name: "Housework",
|
|
205
|
+
});
|
|
206
|
+
expect(
|
|
207
|
+
applyBotProfilePatchV1({ name: "Housework" }, { pinnedAt: at }, "user")
|
|
208
|
+
.pinnedAt,
|
|
209
|
+
).toBe(at);
|
|
210
|
+
expect(() =>
|
|
211
|
+
decodeConfigurationCommandV1({
|
|
212
|
+
schemaVersion: 1,
|
|
213
|
+
type: "bot/set-profile",
|
|
214
|
+
commandId: "command-1",
|
|
215
|
+
botId: "primary",
|
|
216
|
+
expectedRevision: 3,
|
|
217
|
+
profile: { pinnedAt: "soon" },
|
|
218
|
+
}),
|
|
219
|
+
).toThrow("profile.pinnedAt is invalid");
|
|
220
|
+
});
|
|
185
221
|
});
|
package/src/index.ts
CHANGED
|
@@ -99,6 +99,12 @@ export interface BotProfile {
|
|
|
99
99
|
namedBy?: BotNameProvenanceV1;
|
|
100
100
|
/** Keeps the Bot out of the default sidebar list without archiving it. */
|
|
101
101
|
hiddenFromSidebar?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* When the User pinned this Bot, as an ISO 8601 instant. Absent means not
|
|
104
|
+
* pinned. The instant rather than a flag, so the pinned row keeps a stable
|
|
105
|
+
* order — earliest pin first — without a second ordering field.
|
|
106
|
+
*/
|
|
107
|
+
pinnedAt?: string;
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
/**
|
|
@@ -112,6 +118,8 @@ export interface BotProfilePatchV1 {
|
|
|
112
118
|
description?: string;
|
|
113
119
|
title?: string;
|
|
114
120
|
hiddenFromSidebar?: boolean;
|
|
121
|
+
/** An ISO 8601 instant pins the Bot; the empty string unpins it. */
|
|
122
|
+
pinnedAt?: string;
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
export interface BotNotificationPolicy {
|
|
@@ -1065,6 +1073,7 @@ const BOT_PROFILE_OPTIONAL_FIELDS = [
|
|
|
1065
1073
|
"title",
|
|
1066
1074
|
"namedBy",
|
|
1067
1075
|
"hiddenFromSidebar",
|
|
1076
|
+
"pinnedAt",
|
|
1068
1077
|
] as const;
|
|
1069
1078
|
|
|
1070
1079
|
function botProfile(value: unknown): BotProfile {
|
|
@@ -1096,9 +1105,21 @@ function botProfile(value: unknown): BotProfile {
|
|
|
1096
1105
|
"profile.hiddenFromSidebar",
|
|
1097
1106
|
),
|
|
1098
1107
|
}),
|
|
1108
|
+
...(profile.pinnedAt === undefined
|
|
1109
|
+
? {}
|
|
1110
|
+
: { pinnedAt: profileTimestamp(profile.pinnedAt, "profile.pinnedAt") }),
|
|
1099
1111
|
};
|
|
1100
1112
|
}
|
|
1101
1113
|
|
|
1114
|
+
/** An ISO 8601 instant a profile field carries. Never a duration or a count. */
|
|
1115
|
+
function profileTimestamp(value: unknown, label: string): string {
|
|
1116
|
+
const candidate = text(value, label, 64);
|
|
1117
|
+
if (!Number.isFinite(Date.parse(candidate))) {
|
|
1118
|
+
throw new ConfigurationDecodeError(`${label} is invalid`);
|
|
1119
|
+
}
|
|
1120
|
+
return candidate;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1102
1123
|
/**
|
|
1103
1124
|
* A patch field that carries text: a non-empty string sets it, and the empty
|
|
1104
1125
|
* string clears it. A partial update has no other way to say "remove this".
|
|
@@ -1123,7 +1144,7 @@ function botProfilePatch(value: unknown): BotProfilePatchV1 {
|
|
|
1123
1144
|
value,
|
|
1124
1145
|
"profile",
|
|
1125
1146
|
[],
|
|
1126
|
-
["name", "label", "description", "title", "hiddenFromSidebar"],
|
|
1147
|
+
["name", "label", "description", "title", "hiddenFromSidebar", "pinnedAt"],
|
|
1127
1148
|
);
|
|
1128
1149
|
if (Reflect.ownKeys(patch).length === 0) {
|
|
1129
1150
|
throw new ConfigurationDecodeError("profile has invalid fields");
|
|
@@ -1148,6 +1169,15 @@ function botProfilePatch(value: unknown): BotProfilePatchV1 {
|
|
|
1148
1169
|
"profile.hiddenFromSidebar",
|
|
1149
1170
|
),
|
|
1150
1171
|
}),
|
|
1172
|
+
// An instant pins; the empty string unpins, the same way text clears.
|
|
1173
|
+
...(patch.pinnedAt === undefined
|
|
1174
|
+
? {}
|
|
1175
|
+
: {
|
|
1176
|
+
pinnedAt:
|
|
1177
|
+
patchText(patch.pinnedAt, "profile.pinnedAt", 64) === ""
|
|
1178
|
+
? ""
|
|
1179
|
+
: profileTimestamp(patch.pinnedAt, "profile.pinnedAt"),
|
|
1180
|
+
}),
|
|
1151
1181
|
};
|
|
1152
1182
|
}
|
|
1153
1183
|
|
|
@@ -1167,7 +1197,7 @@ export function applyBotProfilePatchV1(
|
|
|
1167
1197
|
next.name = patch.name;
|
|
1168
1198
|
next.namedBy = namedBy;
|
|
1169
1199
|
}
|
|
1170
|
-
for (const key of ["label", "description", "title"] as const) {
|
|
1200
|
+
for (const key of ["label", "description", "title", "pinnedAt"] as const) {
|
|
1171
1201
|
const value = patch[key];
|
|
1172
1202
|
if (value === undefined) continue;
|
|
1173
1203
|
if (value === "") delete next[key];
|