0e9bb5969d
The ZWSP-based core agent sort prefixes silently failed to produce the canonical sisyphus -> hephaestus -> prometheus -> atlas order. Empirical testing of OpenCode's Agent.list() sort behavior shows that Unicode collation treats zero-width characters as ignorable at the primary level, so ZWSP-prefixed names sorted alphabetically with non-core agents interleaved (e.g. Sisyphus, athena, Atlas, explore, Hephaestus, ...). This commit replaces the ZWSP prefixes with leading ASCII spaces in descending lengths (sisyphus=4, hephaestus=3, prometheus=2, atlas=1). ASCII spaces sort reliably before alphabetic characters in localeCompare under all locales and render correctly in every terminal. Changes: - AGENT_LIST_SORT_PREFIXES: ZWSP -> leading spaces (4-3-2-1 descending) - stripAgentListSortPrefix: now strips both legacy ZWSP and new leading whitespace, preserving backward compatibility with existing sessions - normalizeStoredAgentName / normalizeRegisteredAgentName: extract a shared stripSortPrefix helper that handles both prefix formats - agent-config-handler: resolve user-provided default_agent display names through getAgentConfigKey before applying the runtime prefix, so configs like default_agent="Hephaestus - Deep Agent" are normalized - agent-runtime-name-sort.test.ts: new regression test simulating OpenCode's exact sortBy logic (default_agent desc + name asc localeCompare) to verify canonical core agent order under randomised input permutations - AGENTS.md: document the empirical finding that ZWSP was broken, why ASCII spaces work, and the descending prefix-length contract Existing strip functions retain ZWSP support so legacy session state and configs continue to resolve correctly without migration.
153 lines
5.4 KiB
TypeScript
153 lines
5.4 KiB
TypeScript
/// <reference types="bun-types" />
|
|
|
|
import { describe, expect, it, test } from "bun:test"
|
|
|
|
import {
|
|
AGENT_DISPLAY_NAMES,
|
|
getAgentRuntimeName,
|
|
normalizeAgentForPromptKey,
|
|
} from "./agent-display-names"
|
|
|
|
// OpenCode Agent.list() sorts via remeda sortBy: default_agent desc, then name asc localeCompare.
|
|
// Reference: ../opencode/packages/opencode/src/agent/agent.ts:284-293.
|
|
// Earlier ZWSP prefixes silently failed: Unicode collation treats zero-width chars as ignorable.
|
|
function simulateOpencodeSort(agentNames: string[], defaultName: string): string[] {
|
|
return [...agentNames].sort((a, b) => {
|
|
const aIsDefault = a === defaultName ? 1 : 0
|
|
const bIsDefault = b === defaultName ? 1 : 0
|
|
if (aIsDefault !== bIsDefault) return bIsDefault - aIsDefault
|
|
return a.localeCompare(b)
|
|
})
|
|
}
|
|
|
|
describe("OpenCode Agent.list() sort with runtime-name prefixes", () => {
|
|
describe("#given the four core agents and a mix of non-core agents", () => {
|
|
test("#when sorted using opencode-style sortBy #then core agents come first in canonical order", () => {
|
|
const sisyphus = getAgentRuntimeName("sisyphus")
|
|
const hephaestus = getAgentRuntimeName("hephaestus")
|
|
const prometheus = getAgentRuntimeName("prometheus")
|
|
const atlas = getAgentRuntimeName("atlas")
|
|
|
|
const allAgents = [
|
|
sisyphus,
|
|
hephaestus,
|
|
prometheus,
|
|
atlas,
|
|
"athena",
|
|
"explore",
|
|
"metis",
|
|
"oracle",
|
|
]
|
|
|
|
const sorted = simulateOpencodeSort(allAgents, sisyphus)
|
|
const orderedConfigKeys = sorted.map((name) => normalizeAgentForPromptKey(name))
|
|
|
|
expect(orderedConfigKeys).toEqual([
|
|
"sisyphus",
|
|
"hephaestus",
|
|
"prometheus",
|
|
"atlas",
|
|
"athena",
|
|
"explore",
|
|
"metis",
|
|
"oracle",
|
|
])
|
|
})
|
|
|
|
test("#when default_agent is unset #then canonical core order still holds via prefix alone", () => {
|
|
const sisyphus = getAgentRuntimeName("sisyphus")
|
|
const hephaestus = getAgentRuntimeName("hephaestus")
|
|
const prometheus = getAgentRuntimeName("prometheus")
|
|
const atlas = getAgentRuntimeName("atlas")
|
|
|
|
const allAgents = [hephaestus, prometheus, atlas, sisyphus, "athena", "oracle"]
|
|
|
|
const sorted = simulateOpencodeSort(allAgents, "no-such-default-agent")
|
|
const orderedConfigKeys = sorted.map((name) => normalizeAgentForPromptKey(name))
|
|
|
|
expect(orderedConfigKeys.slice(0, 4)).toEqual([
|
|
"sisyphus",
|
|
"hephaestus",
|
|
"prometheus",
|
|
"atlas",
|
|
])
|
|
})
|
|
})
|
|
|
|
describe("#given input array in random order", () => {
|
|
test("#when sorted with opencode comparator #then result is always canonical", () => {
|
|
const sisyphus = getAgentRuntimeName("sisyphus")
|
|
const hephaestus = getAgentRuntimeName("hephaestus")
|
|
const prometheus = getAgentRuntimeName("prometheus")
|
|
const atlas = getAgentRuntimeName("atlas")
|
|
const nonCore = ["athena", "explore", "librarian", "metis", "oracle"]
|
|
const allAgents = [...nonCore, atlas, prometheus, hephaestus, sisyphus]
|
|
|
|
for (let attempt = 0; attempt < 25; attempt += 1) {
|
|
const shuffled = [...allAgents]
|
|
for (let i = shuffled.length - 1; i > 0; i -= 1) {
|
|
const j = Math.floor(Math.random() * (i + 1))
|
|
;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
|
|
}
|
|
const sorted = simulateOpencodeSort(shuffled, sisyphus)
|
|
const orderedConfigKeys = sorted.map((name) => normalizeAgentForPromptKey(name))
|
|
|
|
expect(orderedConfigKeys).toEqual([
|
|
"sisyphus",
|
|
"hephaestus",
|
|
"prometheus",
|
|
"atlas",
|
|
"athena",
|
|
"explore",
|
|
"librarian",
|
|
"metis",
|
|
"oracle",
|
|
])
|
|
}
|
|
})
|
|
})
|
|
|
|
describe("#given runtime names containing only core agents", () => {
|
|
test("#when sorted #then sisyphus, hephaestus, prometheus, atlas in that order", () => {
|
|
const sisyphus = getAgentRuntimeName("sisyphus")
|
|
const hephaestus = getAgentRuntimeName("hephaestus")
|
|
const prometheus = getAgentRuntimeName("prometheus")
|
|
const atlas = getAgentRuntimeName("atlas")
|
|
|
|
const sorted = simulateOpencodeSort([atlas, prometheus, hephaestus, sisyphus], sisyphus)
|
|
const orderedConfigKeys = sorted.map((name) => normalizeAgentForPromptKey(name))
|
|
|
|
expect(orderedConfigKeys).toEqual([
|
|
"sisyphus",
|
|
"hephaestus",
|
|
"prometheus",
|
|
"atlas",
|
|
])
|
|
})
|
|
})
|
|
|
|
describe("#given the prefix is meant to render in OpenCode TUI", () => {
|
|
it("uses ASCII whitespace so terminals render the prefix without character corruption", () => {
|
|
const runtimeNames = Object.keys(AGENT_DISPLAY_NAMES).map(getAgentRuntimeName)
|
|
const invisibleCharsRegex = /[\u200B\u200C\u200D\uFEFF]/
|
|
|
|
for (const name of runtimeNames) {
|
|
expect(invisibleCharsRegex.test(name)).toBe(false)
|
|
}
|
|
})
|
|
|
|
it("only adds leading whitespace, never trailing or interior whitespace beyond the display name", () => {
|
|
const sisyphus = getAgentRuntimeName("sisyphus")
|
|
const hephaestus = getAgentRuntimeName("hephaestus")
|
|
const prometheus = getAgentRuntimeName("prometheus")
|
|
const atlas = getAgentRuntimeName("atlas")
|
|
|
|
for (const name of [sisyphus, hephaestus, prometheus, atlas]) {
|
|
const trimmed = name.trimStart()
|
|
expect(name.length).toBeGreaterThanOrEqual(trimmed.length)
|
|
expect(trimmed.endsWith(" ")).toBe(false)
|
|
}
|
|
})
|
|
})
|
|
})
|