Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions packages/opencode/test/config/paths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { describe, test, expect } from "bun:test"
import { ConfigPaths } from "../../src/config/paths"
import { tmpdir } from "../fixture/fixture"
import * as fs from "fs/promises"
import path from "path"

describe("ConfigPaths.parseText: valid JSONC", () => {
test("parses valid JSONC with trailing comma", async () => {
const text = '{\n "name": "test",\n "value": 42,\n}'
const result = await ConfigPaths.parseText(text, "/fake/config.json")
expect(result).toEqual({ name: "test", value: 42 })
})

test("parses JSONC with line comments", async () => {
const text = '{\n // a comment\n "key": "val"\n}'
const result = await ConfigPaths.parseText(text, "/fake/config.json")
expect(result).toEqual({ key: "val" })
})
})

describe("ConfigPaths.parseText: JSONC error reporting", () => {
test("throws JsonError with line and column for missing comma", async () => {
// Missing comma between "name" and "value" properties
const text = '{\n "name": "test"\n "value": 42\n}'
await expect(ConfigPaths.parseText(text, "/fake/config.json")).rejects.toMatchObject({
name: "ConfigJsonError",
data: {
path: "/fake/config.json",
},
})
// Verify the error message includes position details
try {
await ConfigPaths.parseText(text, "/fake/config.json")
} catch (e: any) {
expect(e.data.message).toMatch(/line \d+/)
expect(e.data.message).toMatch(/column \d+/)
}
})

test("error message includes the offending line content", async () => {
const text = '{\n "a": [1, 2\n "b": 3\n}'
try {
await ConfigPaths.parseText(text, "/fake/config.json")
throw new Error("should have thrown")
} catch (e: any) {
if (e.message === "should have thrown") throw e
// The error message should include the JSONC input and a caret marker
expect(e.data.message).toContain("JSONC Input")
expect(e.data.message).toContain("^")
}
})
})

describe("ConfigPaths.parseText: {env:} substitution", () => {
test("replaces {env:VAR} with environment variable value", async () => {
const prev = process.env["TEST_PARSE_TEXT_VAR"]
process.env["TEST_PARSE_TEXT_VAR"] = "hello_world"
try {
const result = await ConfigPaths.parseText(
'{ "key": "{env:TEST_PARSE_TEXT_VAR}" }',
"/fake/config.json",
)
expect(result.key).toBe("hello_world")
} finally {
if (prev === undefined) delete process.env["TEST_PARSE_TEXT_VAR"]
else process.env["TEST_PARSE_TEXT_VAR"] = prev
}
})

test("replaces missing env var with empty string", async () => {
delete process.env["DEFINITELY_MISSING_PARSE_TEXT_VAR_98765"]
const result = await ConfigPaths.parseText(
'{ "key": "{env:DEFINITELY_MISSING_PARSE_TEXT_VAR_98765}" }',
"/fake/config.json",
)
expect(result.key).toBe("")
})
Comment on lines +70 to +77
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Restore env state after this test.

Line 71 mutates global process.env without restoring a prior value. If that variable is set in CI, this test can leak state into later tests.

🔧 Suggested fix
 test("replaces missing env var with empty string", async () => {
-  delete process.env["DEFINITELY_MISSING_PARSE_TEXT_VAR_98765"]
-  const result = await ConfigPaths.parseText(
-    '{ "key": "{env:DEFINITELY_MISSING_PARSE_TEXT_VAR_98765}" }',
-    "/fake/config.json",
-  )
-  expect(result.key).toBe("")
+  const key = "DEFINITELY_MISSING_PARSE_TEXT_VAR_98765"
+  const prev = process.env[key]
+  delete process.env[key]
+  try {
+    const result = await ConfigPaths.parseText('{ "key": "{env:DEFINITELY_MISSING_PARSE_TEXT_VAR_98765}" }', "/fake/config.json")
+    expect(result.key).toBe("")
+  } finally {
+    if (prev === undefined) delete process.env[key]
+    else process.env[key] = prev
+  }
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("replaces missing env var with empty string", async () => {
delete process.env["DEFINITELY_MISSING_PARSE_TEXT_VAR_98765"]
const result = await ConfigPaths.parseText(
'{ "key": "{env:DEFINITELY_MISSING_PARSE_TEXT_VAR_98765}" }',
"/fake/config.json",
)
expect(result.key).toBe("")
})
test("replaces missing env var with empty string", async () => {
const key = "DEFINITELY_MISSING_PARSE_TEXT_VAR_98765"
const prev = process.env[key]
delete process.env[key]
try {
const result = await ConfigPaths.parseText('{ "key": "{env:DEFINITELY_MISSING_PARSE_TEXT_VAR_98765}" }', "/fake/config.json")
expect(result.key).toBe("")
} finally {
if (prev === undefined) delete process.env[key]
else process.env[key] = prev
}
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/test/config/paths.test.ts` around lines 70 - 77, The test
"replaces missing env var with empty string" mutates
process.env["DEFINITELY_MISSING_PARSE_TEXT_VAR_98765"] without restoring it;
update the test around ConfigPaths.parseText to save the original value (e.g.,
const _orig = process.env["DEFINITELY_MISSING_PARSE_TEXT_VAR_98765"]), then
remove or unset it for the assertion, and finally restore the original value in
a finally block or after the assertion (or use afterEach) so the global env is
returned to its prior state regardless of test outcome.

})

describe("ConfigPaths.parseText: {file:} comment skipping", () => {
test("skips {file:} tokens inside line comments", async () => {
// If the comment-skip logic is broken, this would throw ENOENT
const text = '{\n // {file:nonexistent-file-that-does-not-exist.txt}\n "key": "value"\n}'
const result = await ConfigPaths.parseText(text, "/fake/config.json")
expect(result.key).toBe("value")
})
})

describe("ConfigPaths.parseText: {file:} missing file error", () => {
test("throws InvalidError when referenced file does not exist", async () => {
const text = '{ "key": "{file:/tmp/definitely-nonexistent-parse-text-file-abc123.txt}" }'
await expect(ConfigPaths.parseText(text, "/fake/config.json")).rejects.toMatchObject({
name: "ConfigInvalidError",
data: {
path: "/fake/config.json",
},
})
// Verify specific error details
try {
await ConfigPaths.parseText(text, "/fake/config.json")
} catch (e: any) {
expect(e.data.message).toContain("does not exist")
}
})

test("returns empty string for missing file when missing='empty'", async () => {
const text = '{ "key": "{file:/tmp/definitely-nonexistent-parse-text-file-abc123.txt}" }'
const result = await ConfigPaths.parseText(text, "/fake/config.json", "empty")
expect(result.key).toBe("")
})
})

describe("ConfigPaths.parseText: {file:} real file substitution", () => {
test("substitutes {file:path} with actual file content", async () => {
await using tmp = await tmpdir()
const secretFile = path.join(tmp.path, "secret.txt")
await fs.writeFile(secretFile, "my-secret-value")

const text = `{ "apiKey": "{file:${secretFile}}" }`
const result = await ConfigPaths.parseText(text, "/fake/config.json")
expect(result.apiKey).toBe("my-secret-value")
})

test("trims whitespace from file content", async () => {
await using tmp = await tmpdir()
const secretFile = path.join(tmp.path, "secret.txt")
await fs.writeFile(secretFile, " my-value \n")

const text = `{ "apiKey": "{file:${secretFile}}" }`
const result = await ConfigPaths.parseText(text, "/fake/config.json")
expect(result.apiKey).toBe("my-value")
})

test("resolves relative {file:} paths against config directory", async () => {
await using tmp = await tmpdir()
const secretFile = path.join(tmp.path, "api-key.txt")
await fs.writeFile(secretFile, "relative-secret")

const configPath = path.join(tmp.path, "opencode.json")
const text = '{ "apiKey": "{file:api-key.txt}" }'
const result = await ConfigPaths.parseText(text, configPath)
expect(result.apiKey).toBe("relative-secret")
})
})
Loading