openclaw-codex-app-server 0.0.0
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/AGENTS.md +22 -0
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/index.ts +69 -0
- package/openclaw.plugin.json +105 -0
- package/package.json +28 -0
- package/src/client.test.ts +332 -0
- package/src/client.ts +2914 -0
- package/src/config.ts +103 -0
- package/src/controller.test.ts +1177 -0
- package/src/controller.ts +3232 -0
- package/src/format.test.ts +502 -0
- package/src/format.ts +869 -0
- package/src/openclaw-plugin-sdk.d.ts +237 -0
- package/src/pending-input.test.ts +298 -0
- package/src/pending-input.ts +785 -0
- package/src/state.test.ts +228 -0
- package/src/state.ts +354 -0
- package/src/thread-picker.test.ts +47 -0
- package/src/thread-picker.ts +98 -0
- package/src/thread-selection.test.ts +89 -0
- package/src/thread-selection.ts +106 -0
- package/src/types.ts +372 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
declare module "openclaw/plugin-sdk" {
|
|
2
|
+
export type ReplyPayload = {
|
|
3
|
+
text?: string;
|
|
4
|
+
mediaUrl?: string;
|
|
5
|
+
mediaUrls?: string[];
|
|
6
|
+
replyToId?: string;
|
|
7
|
+
replyToTag?: boolean;
|
|
8
|
+
replyToCurrent?: boolean;
|
|
9
|
+
audioAsVoice?: boolean;
|
|
10
|
+
isError?: boolean;
|
|
11
|
+
isReasoning?: boolean;
|
|
12
|
+
channelData?: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ConversationRef = {
|
|
16
|
+
channel: string;
|
|
17
|
+
accountId: string;
|
|
18
|
+
conversationId: string;
|
|
19
|
+
parentConversationId?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type PluginLogger = {
|
|
23
|
+
info: (message: string) => void;
|
|
24
|
+
warn: (message: string) => void;
|
|
25
|
+
error: (message: string) => void;
|
|
26
|
+
debug: (message: string) => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type PluginCommandContext = {
|
|
30
|
+
senderId?: string;
|
|
31
|
+
channel: string;
|
|
32
|
+
channelId?: string;
|
|
33
|
+
isAuthorizedSender: boolean;
|
|
34
|
+
args?: string;
|
|
35
|
+
commandBody: string;
|
|
36
|
+
config: unknown;
|
|
37
|
+
from?: string;
|
|
38
|
+
to?: string;
|
|
39
|
+
accountId?: string;
|
|
40
|
+
messageThreadId?: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type PluginInteractiveButtons = Array<
|
|
44
|
+
Array<{ text: string; callback_data: string; style?: "danger" | "success" | "primary" }>
|
|
45
|
+
>;
|
|
46
|
+
|
|
47
|
+
export type PluginInteractiveTelegramHandlerContext = {
|
|
48
|
+
channel: "telegram";
|
|
49
|
+
accountId: string;
|
|
50
|
+
callbackId: string;
|
|
51
|
+
conversationId: string;
|
|
52
|
+
parentConversationId?: string;
|
|
53
|
+
senderId?: string;
|
|
54
|
+
senderUsername?: string;
|
|
55
|
+
threadId?: number;
|
|
56
|
+
isGroup: boolean;
|
|
57
|
+
isForum: boolean;
|
|
58
|
+
auth: { isAuthorizedSender: boolean };
|
|
59
|
+
callback: {
|
|
60
|
+
data: string;
|
|
61
|
+
namespace: string;
|
|
62
|
+
payload: string;
|
|
63
|
+
messageId: number;
|
|
64
|
+
chatId: string;
|
|
65
|
+
messageText?: string;
|
|
66
|
+
};
|
|
67
|
+
respond: {
|
|
68
|
+
reply: (params: { text: string; buttons?: PluginInteractiveButtons }) => Promise<void>;
|
|
69
|
+
editMessage: (params: { text: string; buttons?: PluginInteractiveButtons }) => Promise<void>;
|
|
70
|
+
editButtons: (params: { buttons: PluginInteractiveButtons }) => Promise<void>;
|
|
71
|
+
clearButtons: () => Promise<void>;
|
|
72
|
+
deleteMessage: () => Promise<void>;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type PluginInteractiveDiscordHandlerContext = {
|
|
77
|
+
channel: "discord";
|
|
78
|
+
accountId: string;
|
|
79
|
+
interactionId: string;
|
|
80
|
+
conversationId: string;
|
|
81
|
+
parentConversationId?: string;
|
|
82
|
+
guildId?: string;
|
|
83
|
+
senderId?: string;
|
|
84
|
+
senderUsername?: string;
|
|
85
|
+
auth: { isAuthorizedSender: boolean };
|
|
86
|
+
interaction: {
|
|
87
|
+
kind: "button" | "select" | "modal";
|
|
88
|
+
data: string;
|
|
89
|
+
namespace: string;
|
|
90
|
+
payload: string;
|
|
91
|
+
messageId?: string;
|
|
92
|
+
values?: string[];
|
|
93
|
+
fields?: Array<{ id: string; name: string; values: string[] }>;
|
|
94
|
+
};
|
|
95
|
+
respond: {
|
|
96
|
+
acknowledge: () => Promise<void>;
|
|
97
|
+
reply: (params: { text: string; ephemeral?: boolean }) => Promise<void>;
|
|
98
|
+
followUp: (params: { text: string; ephemeral?: boolean }) => Promise<void>;
|
|
99
|
+
editMessage: (params: { text?: string; components?: unknown[] }) => Promise<void>;
|
|
100
|
+
clearComponents: (params?: { text?: string }) => Promise<void>;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export type OpenClawPluginService = {
|
|
105
|
+
id: string;
|
|
106
|
+
start: (ctx: { workspaceDir?: string }) => void | Promise<void>;
|
|
107
|
+
stop?: (ctx: { workspaceDir?: string }) => void | Promise<void>;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type SessionBindingRecord = {
|
|
111
|
+
targetSessionKey: string;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export type OpenClawPluginApi = {
|
|
115
|
+
id: string;
|
|
116
|
+
pluginConfig?: Record<string, unknown>;
|
|
117
|
+
logger: PluginLogger;
|
|
118
|
+
runtime: {
|
|
119
|
+
state: {
|
|
120
|
+
resolveStateDir: () => string;
|
|
121
|
+
};
|
|
122
|
+
channel: {
|
|
123
|
+
bindings: {
|
|
124
|
+
bind: (input: {
|
|
125
|
+
targetSessionKey: string;
|
|
126
|
+
targetKind: "session" | "subagent";
|
|
127
|
+
conversation: ConversationRef;
|
|
128
|
+
placement?: "current" | "child";
|
|
129
|
+
metadata?: Record<string, unknown>;
|
|
130
|
+
}) => Promise<unknown>;
|
|
131
|
+
unbind: (input: {
|
|
132
|
+
targetSessionKey?: string;
|
|
133
|
+
bindingId?: string;
|
|
134
|
+
reason: string;
|
|
135
|
+
}) => Promise<unknown>;
|
|
136
|
+
resolveByConversation: (ref: ConversationRef) => SessionBindingRecord | null;
|
|
137
|
+
};
|
|
138
|
+
text: {
|
|
139
|
+
chunkText: (text: string, limit: number) => string[];
|
|
140
|
+
resolveTextChunkLimit: (
|
|
141
|
+
cfg: unknown,
|
|
142
|
+
provider?: string,
|
|
143
|
+
accountId?: string | null,
|
|
144
|
+
opts?: { fallbackLimit?: number },
|
|
145
|
+
) => number;
|
|
146
|
+
};
|
|
147
|
+
telegram: {
|
|
148
|
+
sendMessageTelegram: (
|
|
149
|
+
to: string,
|
|
150
|
+
text: string,
|
|
151
|
+
opts?: {
|
|
152
|
+
accountId?: string;
|
|
153
|
+
messageThreadId?: number;
|
|
154
|
+
mediaUrl?: string;
|
|
155
|
+
mediaLocalRoots?: readonly string[];
|
|
156
|
+
plainText?: string;
|
|
157
|
+
textMode?: "markdown" | "html";
|
|
158
|
+
buttons?: PluginInteractiveButtons;
|
|
159
|
+
},
|
|
160
|
+
) => Promise<unknown>;
|
|
161
|
+
typing: {
|
|
162
|
+
start: (params: {
|
|
163
|
+
to: string;
|
|
164
|
+
accountId?: string;
|
|
165
|
+
messageThreadId?: number;
|
|
166
|
+
}) => Promise<{ refresh: () => Promise<void>; stop: () => void }>;
|
|
167
|
+
};
|
|
168
|
+
conversationActions: {
|
|
169
|
+
renameTopic: (
|
|
170
|
+
chatId: string,
|
|
171
|
+
messageThreadId: number,
|
|
172
|
+
name: string,
|
|
173
|
+
opts?: { accountId?: string },
|
|
174
|
+
) => Promise<unknown>;
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
discord: {
|
|
178
|
+
sendMessageDiscord: (
|
|
179
|
+
to: string,
|
|
180
|
+
text: string,
|
|
181
|
+
opts?: {
|
|
182
|
+
accountId?: string;
|
|
183
|
+
mediaUrl?: string;
|
|
184
|
+
mediaLocalRoots?: readonly string[];
|
|
185
|
+
},
|
|
186
|
+
) => Promise<unknown>;
|
|
187
|
+
sendComponentMessage: (
|
|
188
|
+
to: string,
|
|
189
|
+
spec: unknown,
|
|
190
|
+
opts?: { accountId?: string },
|
|
191
|
+
) => Promise<unknown>;
|
|
192
|
+
typing: {
|
|
193
|
+
start: (params: {
|
|
194
|
+
channelId: string;
|
|
195
|
+
accountId?: string;
|
|
196
|
+
}) => Promise<{ refresh: () => Promise<void>; stop: () => void }>;
|
|
197
|
+
};
|
|
198
|
+
conversationActions: {
|
|
199
|
+
editChannel: (
|
|
200
|
+
channelId: string,
|
|
201
|
+
params: { name?: string },
|
|
202
|
+
opts?: { accountId?: string },
|
|
203
|
+
) => Promise<unknown>;
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
registerService: (service: OpenClawPluginService) => void;
|
|
209
|
+
registerInteractiveHandler: (registration: {
|
|
210
|
+
channel: "telegram" | "discord";
|
|
211
|
+
namespace: string;
|
|
212
|
+
handler: (ctx: any) => Promise<{ handled?: boolean } | void> | { handled?: boolean } | void;
|
|
213
|
+
}) => void;
|
|
214
|
+
registerCommand: (command: {
|
|
215
|
+
name: string;
|
|
216
|
+
description: string;
|
|
217
|
+
acceptsArgs?: boolean;
|
|
218
|
+
handler: (ctx: PluginCommandContext) => Promise<ReplyPayload> | ReplyPayload;
|
|
219
|
+
}) => void;
|
|
220
|
+
on: (
|
|
221
|
+
hookName: "inbound_claim",
|
|
222
|
+
handler: (event: {
|
|
223
|
+
content: string;
|
|
224
|
+
channel: string;
|
|
225
|
+
accountId?: string;
|
|
226
|
+
conversationId?: string;
|
|
227
|
+
parentConversationId?: string;
|
|
228
|
+
threadId?: string | number;
|
|
229
|
+
}) => Promise<{ handled: boolean }> | { handled: boolean },
|
|
230
|
+
) => void;
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
declare module "ws" {
|
|
235
|
+
const WebSocket: any;
|
|
236
|
+
export default WebSocket;
|
|
237
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildPendingQuestionnaireResponse,
|
|
4
|
+
buildPendingPromptText,
|
|
5
|
+
buildPendingUserInputActions,
|
|
6
|
+
createPendingInputState,
|
|
7
|
+
formatPendingQuestionnairePrompt,
|
|
8
|
+
parseCodexUserInput,
|
|
9
|
+
parsePendingQuestionnaire,
|
|
10
|
+
questionnaireCurrentQuestionHasAnswer,
|
|
11
|
+
questionnaireIsComplete,
|
|
12
|
+
requestToken,
|
|
13
|
+
} from "./pending-input.js";
|
|
14
|
+
|
|
15
|
+
describe("pending-input helpers", () => {
|
|
16
|
+
it("parses numeric option replies", () => {
|
|
17
|
+
expect(parseCodexUserInput("2", 3)).toEqual({ kind: "option", index: 1 });
|
|
18
|
+
expect(parseCodexUserInput("option 1", 3)).toEqual({ kind: "option", index: 0 });
|
|
19
|
+
expect(parseCodexUserInput("hello", 3)).toEqual({ kind: "text", text: "hello" });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("builds approval actions from request decisions", () => {
|
|
23
|
+
const actions = buildPendingUserInputActions({
|
|
24
|
+
method: "turn/requestApproval",
|
|
25
|
+
requestParams: {
|
|
26
|
+
availableDecisions: ["accept", "acceptForSession", "decline", "cancel"],
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
expect(actions.map((action) => action.label)).toEqual([
|
|
30
|
+
"Approve Once",
|
|
31
|
+
"Approve for Session",
|
|
32
|
+
"Decline",
|
|
33
|
+
"Cancel",
|
|
34
|
+
"Tell Codex What To Do",
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("defaults file change approvals to approve and decline actions", () => {
|
|
39
|
+
const actions = buildPendingUserInputActions({
|
|
40
|
+
method: "item/fileChange/requestApproval",
|
|
41
|
+
requestParams: {
|
|
42
|
+
threadId: "019cd368-7eda-7863-86ba-6586598bc5a3",
|
|
43
|
+
turnId: "turn-1",
|
|
44
|
+
itemId: "item-1",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
expect(actions.map((action) => action.label)).toEqual([
|
|
48
|
+
"Approve File Changes",
|
|
49
|
+
"Decline",
|
|
50
|
+
"Tell Codex What To Do",
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("does not treat ids as shell commands for file change approvals", () => {
|
|
55
|
+
const text = buildPendingPromptText({
|
|
56
|
+
method: "item/fileChange/requestApproval",
|
|
57
|
+
requestId: "req-file-1",
|
|
58
|
+
requestParams: {
|
|
59
|
+
threadId: "019cd368-7eda-7863-86ba-6586598bc5a3",
|
|
60
|
+
turnId: "turn-1",
|
|
61
|
+
itemId: "item-1",
|
|
62
|
+
reason: "Codex wants to apply the proposed patch.",
|
|
63
|
+
},
|
|
64
|
+
options: [],
|
|
65
|
+
actions: buildPendingUserInputActions({
|
|
66
|
+
method: "item/fileChange/requestApproval",
|
|
67
|
+
requestParams: {
|
|
68
|
+
threadId: "019cd368-7eda-7863-86ba-6586598bc5a3",
|
|
69
|
+
turnId: "turn-1",
|
|
70
|
+
itemId: "item-1",
|
|
71
|
+
reason: "Codex wants to apply the proposed patch.",
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
expiresAt: Date.now() + 60_000,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(text).toContain("Codex file change approval requested");
|
|
78
|
+
expect(text).toContain("Codex wants to apply the proposed patch.");
|
|
79
|
+
expect(text).not.toContain("Command:");
|
|
80
|
+
expect(text).not.toContain("019cd368-7eda-7863-86ba-6586598bc5a3");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("lists changed files for file change approvals", () => {
|
|
84
|
+
const text = buildPendingPromptText({
|
|
85
|
+
method: "item/fileChange/requestApproval",
|
|
86
|
+
requestId: "req-file-2",
|
|
87
|
+
requestParams: {
|
|
88
|
+
threadId: "thread-1",
|
|
89
|
+
turnId: "turn-1",
|
|
90
|
+
itemId: "item-1",
|
|
91
|
+
filePaths: ["src/app.ts", "README.md", "/tmp/outside.txt"],
|
|
92
|
+
},
|
|
93
|
+
options: [],
|
|
94
|
+
actions: buildPendingUserInputActions({
|
|
95
|
+
method: "item/fileChange/requestApproval",
|
|
96
|
+
requestParams: {
|
|
97
|
+
threadId: "thread-1",
|
|
98
|
+
turnId: "turn-1",
|
|
99
|
+
itemId: "item-1",
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
expiresAt: Date.now() + 60_000,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(text).toContain("Files:");
|
|
106
|
+
expect(text).toContain("`src/app.ts`");
|
|
107
|
+
expect(text).toContain("`README.md`");
|
|
108
|
+
expect(text).toContain("`/tmp/outside.txt`");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("creates a stable request token", () => {
|
|
112
|
+
expect(requestToken("abc")).toBe(requestToken("abc"));
|
|
113
|
+
expect(requestToken("abc")).not.toBe(requestToken("def"));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("creates a prompt text for pending input", () => {
|
|
117
|
+
const state = createPendingInputState({
|
|
118
|
+
method: "item/tool/requestUserInput",
|
|
119
|
+
requestId: "req-1",
|
|
120
|
+
requestParams: {
|
|
121
|
+
question: "Pick one",
|
|
122
|
+
},
|
|
123
|
+
options: ["A", "B"],
|
|
124
|
+
expiresAt: Date.now() + 60_000,
|
|
125
|
+
});
|
|
126
|
+
expect(state.promptText).toContain("Codex input requested");
|
|
127
|
+
expect(state.promptText).toContain("Choices:");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("truncates oversized pending request prompts for chat delivery", () => {
|
|
131
|
+
const text = buildPendingPromptText({
|
|
132
|
+
method: "item/tool/requestUserInput",
|
|
133
|
+
requestId: "req-2",
|
|
134
|
+
requestParams: {
|
|
135
|
+
prompt: "A".repeat(5000),
|
|
136
|
+
},
|
|
137
|
+
options: ["A", "B"],
|
|
138
|
+
actions: [],
|
|
139
|
+
expiresAt: Date.now() + 60_000,
|
|
140
|
+
});
|
|
141
|
+
expect(text.length).toBeLessThan(2400);
|
|
142
|
+
expect(text).toContain("[Request details truncated.");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("parses multi-question plan prompts into a questionnaire state", () => {
|
|
146
|
+
const questionnaire = parsePendingQuestionnaire(`
|
|
147
|
+
1. What do you want the final artifact to be?
|
|
148
|
+
|
|
149
|
+
• A Single static binary
|
|
150
|
+
• B Normal runtime-managed CLI
|
|
151
|
+
|
|
152
|
+
Guidance:
|
|
153
|
+
• A points toward Go or Rust.
|
|
154
|
+
|
|
155
|
+
2. What do you care about more: delivery speed or long-term rigor?
|
|
156
|
+
|
|
157
|
+
• A Fastest rewrite
|
|
158
|
+
• B Balanced
|
|
159
|
+
`);
|
|
160
|
+
|
|
161
|
+
expect(questionnaire?.questions).toHaveLength(2);
|
|
162
|
+
expect(questionnaire?.questions[0]).toMatchObject({
|
|
163
|
+
id: "q1",
|
|
164
|
+
prompt: "What do you want the final artifact to be?",
|
|
165
|
+
options: [
|
|
166
|
+
{ key: "A", label: "Single static binary" },
|
|
167
|
+
{ key: "B", label: "Normal runtime-managed CLI" },
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
expect(formatPendingQuestionnairePrompt(questionnaire!)).toContain("Codex plan question 1 of 2");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("renders a compact questionnaire reply once all answers are filled in", () => {
|
|
174
|
+
const questionnaire = parsePendingQuestionnaire(`
|
|
175
|
+
1. What do you want the final artifact to be?
|
|
176
|
+
• A Single static binary
|
|
177
|
+
• B Normal runtime-managed CLI
|
|
178
|
+
|
|
179
|
+
2. What do you care about more?
|
|
180
|
+
• A Fastest rewrite
|
|
181
|
+
• B Balanced
|
|
182
|
+
`)!;
|
|
183
|
+
questionnaire.answers[0] = {
|
|
184
|
+
kind: "option",
|
|
185
|
+
optionKey: "A",
|
|
186
|
+
optionLabel: "Single static binary",
|
|
187
|
+
};
|
|
188
|
+
questionnaire.answers[1] = {
|
|
189
|
+
kind: "text",
|
|
190
|
+
text: "Balanced, but only if we keep the migration simple.",
|
|
191
|
+
};
|
|
192
|
+
expect(questionnaireIsComplete(questionnaire)).toBe(true);
|
|
193
|
+
expect(buildPendingQuestionnaireResponse(questionnaire)).toBe(
|
|
194
|
+
"1A 2: Balanced, but only if we keep the migration simple.",
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("requires an answer before advancing to the next questionnaire question", () => {
|
|
199
|
+
const questionnaire = parsePendingQuestionnaire(`
|
|
200
|
+
1. What do you want the final artifact to be?
|
|
201
|
+
• A Single static binary
|
|
202
|
+
• B Normal runtime-managed CLI
|
|
203
|
+
|
|
204
|
+
2. What do you care about more?
|
|
205
|
+
• A Fastest rewrite
|
|
206
|
+
• B Balanced
|
|
207
|
+
`)!;
|
|
208
|
+
|
|
209
|
+
expect(questionnaireCurrentQuestionHasAnswer(questionnaire)).toBe(false);
|
|
210
|
+
|
|
211
|
+
questionnaire.answers[0] = {
|
|
212
|
+
kind: "option",
|
|
213
|
+
optionKey: "A",
|
|
214
|
+
optionLabel: "Single static binary",
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
expect(questionnaireCurrentQuestionHasAnswer(questionnaire)).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("parses structured request_user_input questions into questionnaire state", () => {
|
|
221
|
+
const state = createPendingInputState({
|
|
222
|
+
method: "item/tool/requestUserInput",
|
|
223
|
+
requestId: "req-3",
|
|
224
|
+
requestParams: {
|
|
225
|
+
questions: [
|
|
226
|
+
{
|
|
227
|
+
id: "runtime",
|
|
228
|
+
header: "Runtime",
|
|
229
|
+
question: "Which runtime shape should we optimize for?",
|
|
230
|
+
isOther: true,
|
|
231
|
+
options: [
|
|
232
|
+
{
|
|
233
|
+
label: "Long-lived service (Recommended)",
|
|
234
|
+
description: "Best fit for stateful flows.",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
label: "Mostly serverless",
|
|
238
|
+
description: "Best fit for stateless handlers.",
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: "db",
|
|
244
|
+
header: "DB",
|
|
245
|
+
question: "What kind of database migration do you want from SQLite?",
|
|
246
|
+
options: [{ label: "Postgres (Recommended)" }, { label: "Firestore" }],
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
options: [],
|
|
251
|
+
expiresAt: Date.now() + 60_000,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(state.questionnaire?.questions).toHaveLength(2);
|
|
255
|
+
expect(state.questionnaire?.questions[0]).toMatchObject({
|
|
256
|
+
id: "runtime",
|
|
257
|
+
header: "Runtime",
|
|
258
|
+
prompt: "Which runtime shape should we optimize for?",
|
|
259
|
+
allowFreeform: true,
|
|
260
|
+
options: [
|
|
261
|
+
{
|
|
262
|
+
key: "A",
|
|
263
|
+
label: "Long-lived service (Recommended)",
|
|
264
|
+
description: "Best fit for stateful flows.",
|
|
265
|
+
recommended: true,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
key: "B",
|
|
269
|
+
label: "Mostly serverless",
|
|
270
|
+
description: "Best fit for stateless handlers.",
|
|
271
|
+
recommended: false,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
});
|
|
275
|
+
expect(formatPendingQuestionnairePrompt(state.questionnaire!)).toContain(
|
|
276
|
+
"Runtime: Which runtime shape should we optimize for?",
|
|
277
|
+
);
|
|
278
|
+
expect(formatPendingQuestionnairePrompt(state.questionnaire!)).toContain(
|
|
279
|
+
"Other: You can reply with free text.",
|
|
280
|
+
);
|
|
281
|
+
state.questionnaire!.answers[0] = {
|
|
282
|
+
kind: "option",
|
|
283
|
+
optionKey: "A",
|
|
284
|
+
optionLabel: "Long-lived service (Recommended)",
|
|
285
|
+
};
|
|
286
|
+
state.questionnaire!.answers[1] = {
|
|
287
|
+
kind: "option",
|
|
288
|
+
optionKey: "A",
|
|
289
|
+
optionLabel: "Postgres (Recommended)",
|
|
290
|
+
};
|
|
291
|
+
expect(buildPendingQuestionnaireResponse(state.questionnaire!)).toEqual({
|
|
292
|
+
answers: {
|
|
293
|
+
runtime: { answers: ["Long-lived service (Recommended)"] },
|
|
294
|
+
db: { answers: ["Postgres (Recommended)"] },
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|