From 47d6a26a82635b72e9f3a296e732468ea6243487 Mon Sep 17 00:00:00 2001 From: Drewry Pope Date: Wed, 28 Oct 2026 20:14:34 -0500 Subject: [PATCH] fix(core): own-property lookups in MessagingTemplate.constant (#793) --- packages/core/src/messaging/template.ts | 5 ++- packages/core/test/messaging-template.test.ts | 41 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/core/src/messaging/template.ts b/packages/core/src/messaging/template.ts index d33ea1ecda97..6766cdf958a3 100644 --- a/packages/core/src/messaging/template.ts +++ b/packages/core/src/messaging/template.ts @@ -371,7 +371,10 @@ export function presetList(config?: ConfigMessaging.Info): ReadonlyArray /** Constant value with config overrides applied; custom variables are resolved by the caller. */ export function constant(name: string, config?: ConfigMessaging.Info) { - return config?.vars?.[name] ?? constants[name]?.value + // Own-property lookups only (#793): plain indexing reads through the prototype chain, so + // "constructor" / "__proto__" resolve to inherited non-string values when any vars exist. + const configured = config?.vars && Object.hasOwn(config.vars, name) ? config.vars[name] : undefined + return configured ?? (Object.hasOwn(constants, name) ? constants[name]?.value : undefined) } export interface Classification { diff --git a/packages/core/test/messaging-template.test.ts b/packages/core/test/messaging-template.test.ts index 85150fe3c9ca..6e4649adcc1c 100644 --- a/packages/core/test/messaging-template.test.ts +++ b/packages/core/test/messaging-template.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test" import { Cause, Effect } from "effect" import { MessagingTemplate } from "@opencode/core/messaging/template" +import { ConfigMessaging } from "@opencode/schema/config/messaging" const failure = async (template: string, vars: Record) => { const exit = await Effect.runPromiseExit( @@ -99,3 +100,43 @@ describe("MessagingTemplate.parseTime bare epochs", () => { expect(MessagingTemplate.parseTime("2h", now)).toBe(now - 2 * 3_600_000) }) }) + +describe("MessagingTemplate.constant own-property lookups (#793)", () => { + const config = new ConfigMessaging.Info({ vars: { custom: "configured" } }) + + test("prototype names resolve to undefined, not inherited values", () => { + expect(MessagingTemplate.constant("constructor", config)).toBeUndefined() + expect(MessagingTemplate.constant("__proto__", config)).toBeUndefined() + expect(MessagingTemplate.constant("toString", config)).toBeUndefined() + expect(MessagingTemplate.constant("hasOwnProperty", config)).toBeUndefined() + expect(MessagingTemplate.constant("constructor")).toBeUndefined() + expect(MessagingTemplate.constant("__proto__")).toBeUndefined() + }) + + test("configured vars and built-in constants still resolve, config first", () => { + expect(MessagingTemplate.constant("custom", config)).toBe("configured") + expect(MessagingTemplate.constant("separator", config)).toBe("/") + expect(MessagingTemplate.constant("separator", new ConfigMessaging.Info({ vars: { separator: ":" } }))).toBe(":") + expect(MessagingTemplate.constant("unknown", config)).toBeUndefined() + }) + + test("rendering a prototype name with vars configured reports an unknown variable, not a TypeError", async () => { + // The resolver mirrors MessagingVars.resolve: constant() is the configured/built-in lookup. + const exit = await Effect.runPromiseExit( + MessagingTemplate.render( + "{constructor}", + (name) => { + const value = MessagingTemplate.constant(name, config) + return Effect.succeed(value === undefined ? undefined : { value, template: true }) + }, + { strict: true }, + ), + ) + expect(exit._tag).toBe("Failure") + if (exit._tag !== "Failure") return + const error = Cause.squash(exit.cause) + expect(error).toBeInstanceOf(MessagingTemplate.TemplateError) + expect(error instanceof Error ? error.message : String(error)).toContain("Unknown variable") + expect(error instanceof Error ? error.message : String(error)).toContain("{constructor}") + }) +})