fix: reject partial numeric parsing · openclaw/openclaw@f24844d
steipete
·
2026-05-28
·
via Recent Commits to openclaw:main
File tree
mattermost/src/mattermost
| Original file line number | Diff line number | Diff line change |
|---|
@@ -148,9 +148,9 @@ function parseRawPage(value: unknown): number {
|
148 | 148 | if (typeof value === "number") { |
149 | 149 | return normalizeModelPickerPage(value); |
150 | 150 | } |
151 | | -if (typeof value === "string" && value.trim()) { |
152 | | -const parsed = Number.parseInt(value, 10); |
153 | | -if (Number.isFinite(parsed)) { |
| 151 | +if (typeof value === "string" && /^[+-]?\d+$/.test(value.trim())) { |
| 152 | +const parsed = Number(value.trim()); |
| 153 | +if (Number.isSafeInteger(parsed)) { |
154 | 154 | return normalizeModelPickerPage(parsed); |
155 | 155 | } |
156 | 156 | } |
@@ -161,11 +161,15 @@ function parseRawPositiveInt(value: unknown): number | undefined {
|
161 | 161 | if (typeof value !== "string" && typeof value !== "number") { |
162 | 162 | return undefined; |
163 | 163 | } |
164 | | -const parsed = Number.parseInt(String(value), 10); |
165 | | -if (!Number.isFinite(parsed) || parsed < 1) { |
| 164 | +const raw = String(value).trim(); |
| 165 | +if (!/^[+]?\d+$/.test(raw)) { |
166 | 166 | return undefined; |
167 | 167 | } |
168 | | -return Math.floor(parsed); |
| 168 | +const parsed = Number(raw); |
| 169 | +if (!Number.isSafeInteger(parsed) || parsed < 1) { |
| 170 | +return undefined; |
| 171 | +} |
| 172 | +return parsed; |
169 | 173 | } |
170 | 174 | |
171 | 175 | function coerceString(value: unknown): string { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -184,6 +184,27 @@ describe("Discord model picker custom_id", () => {
|
184 | 184 | }); |
185 | 185 | }); |
186 | 186 | |
| 187 | +it("does not coerce partial numeric custom_id fields", () => { |
| 188 | +expect( |
| 189 | +parseDiscordModelPickerData({ |
| 190 | +cmd: "models", |
| 191 | +act: "submit", |
| 192 | +view: "models", |
| 193 | +u: "42", |
| 194 | +p: "openai", |
| 195 | +pg: "3next", |
| 196 | +mi: "7model", |
| 197 | +}), |
| 198 | +).toEqual({ |
| 199 | +command: "models", |
| 200 | +action: "submit", |
| 201 | +view: "models", |
| 202 | +userId: "42", |
| 203 | +provider: "openai", |
| 204 | +page: 1, |
| 205 | +}); |
| 206 | +}); |
| 207 | + |
187 | 208 | it("rejects invalid command/action/view values", () => { |
188 | 209 | expect( |
189 | 210 | parseDiscordModelPickerData({ |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -136,6 +136,23 @@ describe("Mattermost model picker", () => {
|
136 | 136 | expect(parseMattermostModelPickerContext({ action: "select" })).toBeNull(); |
137 | 137 | }); |
138 | 138 | |
| 139 | +it("does not coerce partial page strings in signed picker contexts", () => { |
| 140 | +expect( |
| 141 | +parseMattermostModelPickerContext({ |
| 142 | +oc_model_picker: true, |
| 143 | +action: "list", |
| 144 | +ownerUserId: "user-1", |
| 145 | +provider: "openai", |
| 146 | +page: "2next", |
| 147 | +}), |
| 148 | +).toEqual({ |
| 149 | +action: "list", |
| 150 | +ownerUserId: "user-1", |
| 151 | +provider: "openai", |
| 152 | +page: 1, |
| 153 | +}); |
| 154 | +}); |
| 155 | + |
139 | 156 | it("falls back to the routed agent default model when no override is stored", () => { |
140 | 157 | const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "mm-model-picker-")); |
141 | 158 | try { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -62,9 +62,9 @@ function readContextNumber(context: Record<string, unknown>, key: string): numbe
|
62 | 62 | if (typeof value === "number" && Number.isFinite(value)) { |
63 | 63 | return value; |
64 | 64 | } |
65 | | -if (typeof value === "string") { |
66 | | -const parsed = Number.parseInt(value.trim(), 10); |
67 | | -if (Number.isFinite(parsed)) { |
| 65 | +if (typeof value === "string" && /^[+-]?\d+$/.test(value.trim())) { |
| 66 | +const parsed = Number(value.trim()); |
| 67 | +if (Number.isSafeInteger(parsed)) { |
68 | 68 | return parsed; |
69 | 69 | } |
70 | 70 | } |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -935,7 +935,8 @@ export default definePluginEntry({
|
935 | 935 | .option("--limit <n>", "Max results", "5") |
936 | 936 | .action(async (query, opts) => { |
937 | 937 | const vector = await embeddings.embed(normalizeRecallQuery(query, cfg.recallMaxChars)); |
938 | | -const results = await db.search(vector, Number.parseInt(opts.limit, 10), 0.3); |
| 938 | +const limit = parsePositiveIntegerOption(opts.limit, "--limit"); |
| 939 | +const results = await db.search(vector, limit, 0.3); |
939 | 940 | // Strip vectors for output |
940 | 941 | const output = results.map((r) => ({ |
941 | 942 | id: r.entry.id, |
@@ -983,10 +984,7 @@ export default definePluginEntry({
|
983 | 984 | } |
984 | 985 | query = query.where(filterCondition); |
985 | 986 | } |
986 | | -const limit = Number.parseInt(opts.limit, 10); |
987 | | -if (Number.isNaN(limit) || limit <= 0) { |
988 | | -throw new Error("Invalid limit: must be a positive integer"); |
989 | | -} |
| 987 | +const limit = parsePositiveIntegerOption(opts.limit, "--limit") ?? 10; |
990 | 988 | |
991 | 989 | // Fetch all filtered rows first if we need to order them in memory |
992 | 990 | if (!opts.orderBy) { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -36,7 +36,7 @@ function account(
|
36 | 36 | }; |
37 | 37 | } |
38 | 38 | |
39 | | -function mockBotAdmin(features: number): void { |
| 39 | +function mockBotAdmin(features: number | string): void { |
40 | 40 | hoisted.fetchWithSsrFGuard.mockResolvedValueOnce({ |
41 | 41 | response: new Response( |
42 | 42 | JSON.stringify({ |
@@ -94,6 +94,19 @@ describe("probeNextcloudTalkBotResponseFeature", () => {
|
94 | 94 | }); |
95 | 95 | }); |
96 | 96 | |
| 97 | +it("does not coerce partial bot feature strings", async () => { |
| 98 | +mockBotAdmin("2response"); |
| 99 | + |
| 100 | +await expect(probeNextcloudTalkBotResponseFeature({ account: account() })).resolves.toEqual({ |
| 101 | +ok: false, |
| 102 | +code: "missing_response_feature", |
| 103 | +botId: "7", |
| 104 | +botName: "OpenClaw", |
| 105 | +message: |
| 106 | +'Nextcloud Talk bot "OpenClaw" (7) is missing the response feature; outbound replies will fail. Run ./occ talk:bot:state --feature webhook --feature response --feature reaction 7 1 or reinstall the bot with --feature response.', |
| 107 | +}); |
| 108 | +}); |
| 109 | + |
97 | 110 | it("reports malformed bot admin JSON with a stable channel error", async () => { |
98 | 111 | hoisted.fetchWithSsrFGuard.mockResolvedValueOnce({ |
99 | 112 | response: new Response("{ nope", { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -50,9 +50,9 @@ function coerceFeatureMask(value: unknown): number | undefined {
|
50 | 50 | if (typeof value === "number" && Number.isFinite(value)) { |
51 | 51 | return value; |
52 | 52 | } |
53 | | -if (typeof value === "string" && value.trim()) { |
54 | | -const parsed = Number.parseInt(value, 10); |
55 | | -return Number.isFinite(parsed) ? parsed : undefined; |
| 53 | +if (typeof value === "string" && /^[+-]?\d+$/.test(value.trim())) { |
| 54 | +const parsed = Number(value.trim()); |
| 55 | +return Number.isSafeInteger(parsed) ? parsed : undefined; |
56 | 56 | } |
57 | 57 | return undefined; |
58 | 58 | } |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -73,6 +73,36 @@ describe("nextcloud talk room info", () => {
|
73 | 73 | expect(release).toHaveBeenCalledTimes(1); |
74 | 74 | }); |
75 | 75 | |
| 76 | +it("does not coerce partial room type strings", async () => { |
| 77 | +fetchWithSsrFGuard.mockResolvedValue({ |
| 78 | +response: { |
| 79 | +ok: true, |
| 80 | +json: async () => ({ |
| 81 | +ocs: { |
| 82 | +data: { |
| 83 | +type: "1direct", |
| 84 | +}, |
| 85 | +}, |
| 86 | +}), |
| 87 | +}, |
| 88 | +release: vi.fn(async () => {}), |
| 89 | +}); |
| 90 | + |
| 91 | +await expect( |
| 92 | +resolveNextcloudTalkRoomKind({ |
| 93 | +account: { |
| 94 | +accountId: "acct-partial", |
| 95 | +baseUrl: "https://nc.example.com", |
| 96 | +config: { |
| 97 | +apiUser: "bot", |
| 98 | +apiPassword: "secret", |
| 99 | +}, |
| 100 | +} as never, |
| 101 | +roomToken: "room-partial", |
| 102 | +}), |
| 103 | +).resolves.toBeUndefined(); |
| 104 | +}); |
| 105 | + |
76 | 106 | it("reads the api password from a file and logs non-ok room info responses", async () => { |
77 | 107 | const release = vi.fn(async () => {}); |
78 | 108 | const log = vi.fn(); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -27,9 +27,9 @@ function coerceRoomType(value: unknown): number | undefined {
|
27 | 27 | if (typeof value === "number" && Number.isFinite(value)) { |
28 | 28 | return value; |
29 | 29 | } |
30 | | -if (typeof value === "string" && value.trim()) { |
31 | | -const parsed = Number.parseInt(value, 10); |
32 | | -return Number.isFinite(parsed) ? parsed : undefined; |
| 30 | +if (typeof value === "string" && /^[+-]?\d+$/.test(value.trim())) { |
| 31 | +const parsed = Number(value.trim()); |
| 32 | +return Number.isSafeInteger(parsed) ? parsed : undefined; |
33 | 33 | } |
34 | 34 | return undefined; |
35 | 35 | } |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -44,6 +44,19 @@ describe("TwilioStreamFrameAdapter", () => {
|
44 | 44 | ).toEqual({ kind: "ignored" }); |
45 | 45 | }); |
46 | 46 | |
| 47 | +it("ignores partial numeric media timestamps", () => { |
| 48 | +const adapter = new TwilioStreamFrameAdapter(); |
| 49 | + |
| 50 | +expect( |
| 51 | +adapter.parseInbound( |
| 52 | +JSON.stringify({ |
| 53 | +event: "media", |
| 54 | +media: { payload: "AAA=", timestamp: "20ms" }, |
| 55 | +}), |
| 56 | +), |
| 57 | +).toEqual({ kind: "media", payloadBase64: "AAA=" }); |
| 58 | +}); |
| 59 | + |
47 | 60 | it("serializes outbound frames with the streamSid captured at start", () => { |
48 | 61 | const adapter = new TwilioStreamFrameAdapter(); |
49 | 62 | adapter.parseInbound( |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。