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.
- package/README.md +333 -0
- package/package.json +27 -0
- package/src/__tests__/.gitkeep +0 -0
- package/src/__tests__/background-subagent.test.ts +106 -0
- package/src/__tests__/cli-init.test.ts +70 -0
- package/src/__tests__/compaction.test.ts +113 -0
- package/src/__tests__/index.test.ts +180 -0
- package/src/__tests__/leaderboard.test.ts +244 -0
- package/src/__tests__/logger.test.ts +84 -0
- package/src/__tests__/notify.test.ts +318 -0
- package/src/__tests__/utils.test.ts +164 -0
- package/src/bun-test.d.ts +12 -0
- package/src/cli/init.ts +44 -0
- package/src/index.ts +4 -0
- package/src/plugins/.gitkeep +0 -0
- package/src/plugins/background-subagent.ts +59 -0
- package/src/plugins/compaction.ts +28 -0
- package/src/plugins/leaderboard.ts +184 -0
- package/src/plugins/notify.ts +383 -0
- package/src/types.ts +2 -0
- package/src/utils/.gitkeep +0 -0
- package/src/utils/config.ts +71 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/logger.ts +52 -0
- package/src/utils/safe-hook.ts +16 -0
|
@@ -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
|
+
})
|