oc-tweaks 0.1.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.
@@ -0,0 +1,180 @@
1
+ // @ts-nocheck
2
+
3
+ import { afterEach, describe, expect, test } from "bun:test"
4
+
5
+ import {
6
+ backgroundSubagentPlugin,
7
+ compactionPlugin,
8
+ leaderboardPlugin,
9
+ notifyPlugin,
10
+ } from "../index"
11
+
12
+ const originalBunFile = Bun.file
13
+ const originalBunWrite = Bun.write
14
+ const originalFetch = globalThis.fetch
15
+ const originalHome = Bun.env?.HOME
16
+
17
+ function mockBunFile(mockData: Record<string, any>) {
18
+ ;(globalThis as any).Bun.file = (path: string) => ({
19
+ exists: async () => path in mockData,
20
+ json: async () => {
21
+ if (!(path in mockData)) throw new Error("ENOENT")
22
+ const data = mockData[path]
23
+ if (data instanceof Error) throw data
24
+ return data
25
+ },
26
+ text: async () => JSON.stringify(mockData[path] ?? ""),
27
+ })
28
+ }
29
+
30
+ function createShellMock(options?: { availableCommands?: string[] }) {
31
+ const available = new Set(options?.availableCommands ?? [])
32
+ const calls: Array<{ command: string }> = []
33
+
34
+ const $ = async (strings: TemplateStringsArray, ...values: any[]) => {
35
+ const segments = Array.from(strings)
36
+ const command = segments.reduce(
37
+ (acc, segment, index) =>
38
+ acc + segment + (index < values.length ? String(values[index]) : ""),
39
+ "",
40
+ )
41
+ calls.push({ command })
42
+
43
+ if (command.startsWith("which ")) {
44
+ const bin = String(values[0] ?? command.slice("which ".length).trim())
45
+ if (available.has(bin)) return { stdout: `${bin}\n` }
46
+ throw new Error(`missing ${bin}`)
47
+ }
48
+
49
+ return { stdout: "" }
50
+ }
51
+
52
+ return { $, calls }
53
+ }
54
+
55
+ afterEach(() => {
56
+ ;(globalThis as any).Bun.file = originalBunFile
57
+ ;(globalThis as any).Bun.write = originalBunWrite
58
+ globalThis.fetch = originalFetch
59
+ if (originalHome === undefined) {
60
+ delete (Bun.env as any).HOME
61
+ } else {
62
+ ;(Bun.env as any).HOME = originalHome
63
+ }
64
+ })
65
+
66
+ describe("index exports", () => {
67
+ test("all four named exports are functions", () => {
68
+ expect(typeof backgroundSubagentPlugin).toBe("function")
69
+ expect(typeof compactionPlugin).toBe("function")
70
+ expect(typeof leaderboardPlugin).toBe("function")
71
+ expect(typeof notifyPlugin).toBe("function")
72
+ })
73
+
74
+ test("leaderboardPlugin with default config returns object with event hook", async () => {
75
+ const home = "/tmp/oc-index-lb-default"
76
+ ;(Bun.env as any).HOME = home
77
+ ;(globalThis as any).Bun.write = async () => {}
78
+ const ocTweaksPath = `${home}/.config/opencode/oc-tweaks.json`
79
+ const leaderboardPath = `${home}/.claude/leaderboard.json`
80
+
81
+ mockBunFile({
82
+ [ocTweaksPath]: { leaderboard: { enabled: true } },
83
+ [leaderboardPath]: { twitter_handle: "test", twitter_user_id: "u1" },
84
+ })
85
+
86
+ const hooks = await leaderboardPlugin()
87
+ expect(typeof hooks).toBe("object")
88
+ expect(typeof hooks.event === "function" || Object.keys(hooks).length === 0).toBe(true)
89
+ })
90
+
91
+ test("leaderboardPlugin with enabled:false returns {}", async () => {
92
+ const home = "/tmp/oc-index-lb-disabled"
93
+ ;(Bun.env as any).HOME = home
94
+ const ocTweaksPath = `${home}/.config/opencode/oc-tweaks.json`
95
+ const leaderboardPath = `${home}/.claude/leaderboard.json`
96
+
97
+ mockBunFile({
98
+ [ocTweaksPath]: { leaderboard: { enabled: false } },
99
+ [leaderboardPath]: { twitter_handle: "test", twitter_user_id: "u1" },
100
+ })
101
+
102
+ const hooks = await leaderboardPlugin()
103
+ expect(hooks).toEqual({})
104
+ })
105
+
106
+ test("notifyPlugin with default config returns object with event hook", async () => {
107
+ const home = "/tmp/oc-index-notify-default"
108
+ ;(Bun.env as any).HOME = home
109
+ const path = `${home}/.config/opencode/oc-tweaks.json`
110
+ mockBunFile({ [path]: { notify: { enabled: true } } })
111
+
112
+ const { $ } = createShellMock({ availableCommands: ["notify-send"] })
113
+ const hooks = await notifyPlugin({ $, directory: "/tmp/demo", client: {} })
114
+ expect(typeof hooks).toBe("object")
115
+ expect(typeof hooks.event).toBe("function")
116
+ })
117
+
118
+ test("notifyPlugin with enabled:false returns {}", async () => {
119
+ const home = "/tmp/oc-index-notify-disabled"
120
+ ;(Bun.env as any).HOME = home
121
+ const path = `${home}/.config/opencode/oc-tweaks.json`
122
+ mockBunFile({ [path]: { notify: { enabled: false } } })
123
+
124
+ const { $ } = createShellMock({ availableCommands: ["notify-send"] })
125
+ const hooks = await notifyPlugin({ $, directory: "/tmp/demo", client: {} })
126
+ expect(hooks).toEqual({})
127
+ })
128
+
129
+ test("leaderboard and notify event handlers coexist without interference", async () => {
130
+ const home = "/tmp/oc-index-coexist"
131
+ ;(Bun.env as any).HOME = home
132
+ ;(globalThis as any).Bun.write = async () => {}
133
+ const ocTweaksPath = `${home}/.config/opencode/oc-tweaks.json`
134
+ const leaderboardPath = `${home}/.claude/leaderboard.json`
135
+
136
+ mockBunFile({
137
+ [ocTweaksPath]: { leaderboard: { enabled: true }, notify: { enabled: true } },
138
+ [leaderboardPath]: { twitter_handle: "coexist", twitter_user_id: "ux" },
139
+ })
140
+
141
+ globalThis.fetch = async () =>
142
+ ({ ok: true, status: 200, text: async () => "" }) as any
143
+
144
+ const lbHooks = await leaderboardPlugin()
145
+
146
+ const { $ } = createShellMock({ availableCommands: ["notify-send"] })
147
+ const notifyHooks = await notifyPlugin({ $, directory: "/tmp/coexist", client: {} })
148
+
149
+ expect(typeof lbHooks.event === "function" || Object.keys(lbHooks).length === 0).toBe(true)
150
+ expect(typeof notifyHooks.event).toBe("function")
151
+
152
+ // leaderboard event
153
+ if (typeof lbHooks.event === "function") {
154
+ await expect(
155
+ lbHooks.event({
156
+ event: {
157
+ type: "message.updated",
158
+ properties: {
159
+ info: {
160
+ id: "msg-coexist",
161
+ sessionID: "session-coexist",
162
+ role: "assistant",
163
+ time: { created: 1730000000000, completed: 1730000001000 },
164
+ modelID: "gpt-5.1-codex",
165
+ providerID: "provider",
166
+ cost: 0,
167
+ tokens: { input: 10, output: 5, reasoning: 0, cache: { read: 0, write: 0 } },
168
+ },
169
+ },
170
+ },
171
+ }),
172
+ ).resolves.toBeUndefined()
173
+ }
174
+
175
+ // notify event (session.error does not require client.session.messages)
176
+ await expect(
177
+ notifyHooks.event({ event: { type: "session.error", properties: {} } }),
178
+ ).resolves.toBeUndefined()
179
+ })
180
+ })
@@ -0,0 +1,244 @@
1
+ // @ts-nocheck
2
+
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
4
+
5
+ const TEST_HOME = "/tmp/oc-leaderboard"
6
+
7
+ const originalBunFile = Bun.file
8
+ const originalBunWrite = Bun.write
9
+ const originalFetch = globalThis.fetch
10
+ const originalHome = Bun.env?.HOME
11
+
12
+ function mockBunFile(mockData: Record<string, any>, existsCalls?: string[]) {
13
+ ;(globalThis as any).Bun.file = (path: string) => ({
14
+ exists: async () => {
15
+ existsCalls?.push(path)
16
+ return path in mockData
17
+ },
18
+ json: async () => {
19
+ if (!(path in mockData)) throw new Error("ENOENT")
20
+ const data = mockData[path]
21
+ if (data instanceof Error) throw data
22
+ return data
23
+ },
24
+ text: async () => {
25
+ if (!(path in mockData)) return ""
26
+ const value = mockData[path]
27
+ return typeof value === "string" ? value : JSON.stringify(value)
28
+ },
29
+ })
30
+ }
31
+
32
+ function buildAssistantInfo(overrides: Record<string, any> = {}) {
33
+ return {
34
+ id: "msg-1",
35
+ sessionID: "session-1",
36
+ role: "assistant",
37
+ time: { created: 1730000000000, completed: 1730000001000 },
38
+ modelID: "gpt-5.1-codex",
39
+ providerID: "provider",
40
+ cost: 0,
41
+ tokens: {
42
+ input: 120,
43
+ output: 80,
44
+ reasoning: 0,
45
+ cache: { read: 4, write: 2 },
46
+ },
47
+ ...overrides,
48
+ }
49
+ }
50
+
51
+ function buildMessageUpdatedEvent(infoOverrides: Record<string, any> = {}) {
52
+ return {
53
+ event: {
54
+ type: "message.updated",
55
+ properties: {
56
+ info: buildAssistantInfo(infoOverrides),
57
+ },
58
+ },
59
+ }
60
+ }
61
+
62
+ async function loadPlugin() {
63
+ const mod = await import("../plugins/leaderboard")
64
+ return mod.leaderboardPlugin
65
+ }
66
+
67
+ beforeEach(() => {
68
+ ;(Bun.env as any).HOME = TEST_HOME
69
+ ;(globalThis as any).Bun.write = async () => {}
70
+ })
71
+
72
+ afterEach(() => {
73
+ ;(globalThis as any).Bun.file = originalBunFile
74
+ ;(globalThis as any).Bun.write = originalBunWrite
75
+ globalThis.fetch = originalFetch
76
+
77
+ if (originalHome === undefined) {
78
+ delete (Bun.env as any).HOME
79
+ } else {
80
+ ;(Bun.env as any).HOME = originalHome
81
+ }
82
+ })
83
+
84
+ describe("leaderboardPlugin", () => {
85
+ test("mapModel covers direct, regex, and fallback branches", async () => {
86
+ const ocTweaksPath = `${TEST_HOME}/.config/opencode/oc-tweaks.json`
87
+ const leaderboardPath = `${TEST_HOME}/.claude/leaderboard.json`
88
+ const postedPayloads: any[] = []
89
+
90
+ mockBunFile({
91
+ [ocTweaksPath]: { leaderboard: { enabled: true } },
92
+ [leaderboardPath]: {
93
+ twitter_handle: "alice",
94
+ twitter_user_id: "u1",
95
+ },
96
+ })
97
+
98
+ globalThis.fetch = (async (_url: string, init: any) => {
99
+ postedPayloads.push(JSON.parse(init.body))
100
+ return {
101
+ ok: true,
102
+ status: 200,
103
+ text: async () => "",
104
+ } as any
105
+ }) as any
106
+
107
+ const leaderboardPlugin = await loadPlugin()
108
+ const hooks = await leaderboardPlugin()
109
+
110
+ await hooks.event(
111
+ buildMessageUpdatedEvent({
112
+ id: "msg-direct",
113
+ modelID: "gpt-5.1-codex",
114
+ }),
115
+ )
116
+ await hooks.event(
117
+ buildMessageUpdatedEvent({
118
+ id: "msg-regex",
119
+ modelID: "claude-opus-4-20260101",
120
+ }),
121
+ )
122
+ await hooks.event(
123
+ buildMessageUpdatedEvent({
124
+ id: "msg-fallback",
125
+ modelID: "totally-unknown-model",
126
+ }),
127
+ )
128
+
129
+ expect(postedPayloads.length).toBe(3)
130
+ expect(postedPayloads[0].model).toBe("claude-sonnet-4-20250514")
131
+ expect(postedPayloads[1].model).toBe("claude-opus-4-20260101")
132
+ expect(postedPayloads[2].model).toBe("claude-sonnet-4-20250514")
133
+ })
134
+
135
+ test("returns empty hooks when leaderboard is disabled", async () => {
136
+ const ocTweaksPath = `${TEST_HOME}/.config/opencode/oc-tweaks.json`
137
+ const leaderboardPath = `${TEST_HOME}/.claude/leaderboard.json`
138
+
139
+ mockBunFile({
140
+ [ocTweaksPath]: {
141
+ leaderboard: { enabled: false },
142
+ },
143
+ [leaderboardPath]: {
144
+ twitter_handle: "alice",
145
+ twitter_user_id: "u1",
146
+ },
147
+ })
148
+
149
+ const leaderboardPlugin = await loadPlugin()
150
+ const hooks = await leaderboardPlugin()
151
+
152
+ expect(hooks).toEqual({})
153
+ })
154
+
155
+ test("returns empty hooks when leaderboard config file is missing", async () => {
156
+ const ocTweaksPath = `${TEST_HOME}/.config/opencode/oc-tweaks.json`
157
+ mockBunFile({
158
+ [ocTweaksPath]: { leaderboard: { enabled: true } },
159
+ })
160
+
161
+ const leaderboardPlugin = await loadPlugin()
162
+ const hooks = await leaderboardPlugin()
163
+
164
+ expect(hooks).toEqual({})
165
+ })
166
+
167
+ test("loads valid leaderboard config from default search paths", async () => {
168
+ const ocTweaksPath = `${TEST_HOME}/.config/opencode/oc-tweaks.json`
169
+ const secondPath = `${TEST_HOME}/.config/claude/leaderboard.json`
170
+
171
+ mockBunFile({
172
+ [ocTweaksPath]: { leaderboard: { enabled: true } },
173
+ [secondPath]: {
174
+ twitter_handle: "bob",
175
+ twitter_user_id: "u2",
176
+ },
177
+ })
178
+
179
+ const leaderboardPlugin = await loadPlugin()
180
+ const hooks = await leaderboardPlugin()
181
+
182
+ expect(typeof hooks.event).toBe("function")
183
+ })
184
+
185
+ test("configPath override short-circuits default search", async () => {
186
+ const existsCalls: string[] = []
187
+ const ocTweaksPath = `${TEST_HOME}/.config/opencode/oc-tweaks.json`
188
+ const overridePath = "/tmp/custom/leaderboard.json"
189
+ const defaultPath1 = `${TEST_HOME}/.claude/leaderboard.json`
190
+ const defaultPath2 = `${TEST_HOME}/.config/claude/leaderboard.json`
191
+
192
+ mockBunFile(
193
+ {
194
+ [ocTweaksPath]: {
195
+ leaderboard: { enabled: true, configPath: overridePath },
196
+ },
197
+ [overridePath]: {
198
+ twitter_handle: "override",
199
+ twitter_user_id: "u3",
200
+ },
201
+ },
202
+ existsCalls,
203
+ )
204
+
205
+ const leaderboardPlugin = await loadPlugin()
206
+ const hooks = await leaderboardPlugin()
207
+
208
+ expect(typeof hooks.event).toBe("function")
209
+ expect(existsCalls).toContain(overridePath)
210
+ expect(existsCalls).not.toContain(defaultPath1)
211
+ expect(existsCalls).not.toContain(defaultPath2)
212
+ })
213
+
214
+ test("submit flow remains non-blocking and submitted set dedupes", async () => {
215
+ const ocTweaksPath = `${TEST_HOME}/.config/opencode/oc-tweaks.json`
216
+ const leaderboardPath = `${TEST_HOME}/.claude/leaderboard.json`
217
+ const fetchCalls: any[] = []
218
+
219
+ mockBunFile({
220
+ [ocTweaksPath]: { leaderboard: { enabled: true } },
221
+ [leaderboardPath]: {
222
+ twitter_handle: "alice",
223
+ twitter_user_id: "u1",
224
+ },
225
+ })
226
+
227
+ globalThis.fetch = (async () => {
228
+ fetchCalls.push(1)
229
+ return {
230
+ ok: false,
231
+ status: 500,
232
+ text: async () => "server error",
233
+ } as any
234
+ }) as any
235
+
236
+ const leaderboardPlugin = await loadPlugin()
237
+ const hooks = await leaderboardPlugin()
238
+ const eventInput = buildMessageUpdatedEvent({ id: "msg-dedupe" })
239
+
240
+ await expect(hooks.event(eventInput)).resolves.toBeUndefined()
241
+ await expect(hooks.event(eventInput)).resolves.toBeUndefined()
242
+ expect(fetchCalls.length).toBe(1)
243
+ })
244
+ })
@@ -0,0 +1,84 @@
1
+ // @ts-nocheck
2
+
3
+ import { describe, test, expect, afterEach } from "bun:test"
4
+ import { log } from "../utils/logger"
5
+
6
+ const originalBunFile = Bun.file
7
+ const originalBunWrite = Bun.write
8
+ const originalHome = Bun.env?.HOME
9
+
10
+ afterEach(() => {
11
+ ;(globalThis as any).Bun.file = originalBunFile
12
+ ;(globalThis as any).Bun.write = originalBunWrite
13
+ if (originalHome === undefined) {
14
+ delete (Bun.env as any).HOME
15
+ } else {
16
+ ;(Bun.env as any).HOME = originalHome
17
+ }
18
+ })
19
+
20
+ describe("logger", () => {
21
+ test("does not write when logging is disabled", async () => {
22
+ let writeCount = 0
23
+ ;(globalThis as any).Bun.write = async () => {
24
+ writeCount++
25
+ return 0
26
+ }
27
+ ;(globalThis as any).Bun.file = () => ({
28
+ exists: async () => false,
29
+ text: async () => ""
30
+ })
31
+
32
+ await log({ enabled: false }, "INFO", "test message")
33
+ await log(undefined, "INFO", "test message")
34
+
35
+ expect(writeCount).toBe(0)
36
+ })
37
+
38
+ test("writes log when enabled", async () => {
39
+ const written: Record<string, string> = {}
40
+ ;(globalThis as any).Bun.write = async (path: string, content: string) => {
41
+ written[path] = content
42
+ return 0
43
+ }
44
+ ;(globalThis as any).Bun.file = (path: string) => ({
45
+ exists: async () => path in written,
46
+ text: async () => written[path] ?? ""
47
+ })
48
+ ;(Bun.env as any).HOME = "/tmp/oc-logger-test"
49
+
50
+ await log({ enabled: true }, "INFO", "hello world")
51
+
52
+ const logPath = "/tmp/oc-logger-test/.config/opencode/plugins/oc-tweaks.log"
53
+ expect(written[logPath]).toBeDefined()
54
+ expect(written[logPath]).toContain("[INFO] hello world")
55
+ })
56
+
57
+ test("truncates to keep lines when max exceeded", async () => {
58
+ const maxLines = 5
59
+ const keepLines = Math.floor(maxLines / 2) // 2
60
+
61
+ // Pre-fill with maxLines lines
62
+ const existingLines = Array.from({ length: maxLines }, (_, i) => `line${i}`)
63
+ .join("\n")
64
+ .concat("\n")
65
+ const written: Record<string, string> = {}
66
+ ;(globalThis as any).Bun.write = async (path: string, content: string) => {
67
+ written[path] = content
68
+ return 0
69
+ }
70
+ ;(globalThis as any).Bun.file = (path: string) => ({
71
+ exists: async () => true,
72
+ text: async () => existingLines
73
+ })
74
+ ;(Bun.env as any).HOME = "/tmp/oc-logger-truncate"
75
+
76
+ await log({ enabled: true, maxLines }, "INFO", "new line")
77
+
78
+ const logPath = "/tmp/oc-logger-truncate/.config/opencode/plugins/oc-tweaks.log"
79
+ const lines = (written[logPath] ?? "")
80
+ .split("\n")
81
+ .filter((l: string) => l.length > 0)
82
+ expect(lines.length).toBeLessThanOrEqual(keepLines)
83
+ })
84
+ })