@te-river/opencode-team-mode 1.6.1 → 1.6.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@te-river/opencode-team-mode",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "Team collaboration mode plugin for OpenCode Desktop — injects specialized agents (architect, reviewer, implementer, tester, researcher) and team workflow commands into your OpenCode workspace.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,146 +1,169 @@
1
- // A fake OpenCode v2 plugin context for the adapter tests.
2
- //
3
- // There was no such thing before the v2 port: every existing suite builds a v1
4
- // tool ctx as an inline literal ({directory, sessionID, agent, ask}), because
5
- // v1 hands tools a plain object. v2 hands the PLUGIN a context of domains with
6
- // transform()/hook() registrars, so testing the v2 personality needs a host
7
- // double that records what was registered and can replay a hook.
8
- //
9
- // Deliberately faithful on the three points that matter:
10
- // - transform(cb) runs the callback SYNCHRONOUSLY against a live editor and
11
- // returns a disposable (a fake that defers would let a plugin pass tests
12
- // against an API shape the host does not have);
13
- // - hook callbacks are callable by the test so a hook body can be exercised
14
- // with a real payload;
15
- // - nothing here touches the filesystem or the network.
16
-
17
- export function makeFakeCtx({
18
- directory = process.cwd(),
19
- options = {},
20
- agents: seedAgents = [],
21
- tools: seedTools = [],
22
- sessionData = null,
23
- } = {}) {
24
- const registrations = []
25
- const hooks = new Map()
26
-
27
- const disposable = (label) => {
28
- const rec = { label, disposed: false, dispose: async () => { rec.disposed = true } }
29
- registrations.push(rec)
30
- return rec
31
- }
32
-
33
- const toolMap = new Map()
34
- for (const t of seedTools) toolMap.set(t.name ?? t.id, { ...t, id: t.id ?? t.name })
35
- const toolEditor = {
36
- list: () => [...toolMap.values()],
37
- get: (id) => toolMap.get(id),
38
- add: (tool) => { toolMap.set(tool.name, { ...tool, id: tool.name }) },
39
- update: (id, fn) => { const cur = toolMap.get(id); if (cur) fn(cur) },
40
- remove: (id) => { toolMap.delete(id) },
41
- namespace: () => {},
42
- }
43
-
44
- const agentMap = new Map()
45
- for (const a of seedAgents) agentMap.set(a.id, { ...a })
46
- const agentEditor = {
47
- list: () => [...agentMap.values()],
48
- get: (id) => agentMap.get(id),
49
- default: (id) => { agentEditor.__default = id },
50
- update: (id, fn) => { const cur = agentMap.get(id); if (cur) fn(cur) },
51
- remove: (id) => { agentMap.delete(id) },
52
- }
53
-
54
- const hookHandle = (domain, name) => {
55
- const key = `${domain}.${name}`
56
- const entry = hooks.get(key) ?? { key, calls: [], fire: async (input) => {
57
- for (const cb of entry.handlers) await cb(input)
58
- return input
59
- }, handlers: [] }
60
- hooks.set(key, entry)
61
- return entry
62
- }
63
-
64
- const mkHook = (domain) => async (name, callback) => {
65
- hookHandle(domain, name).handlers.push(callback)
66
- return disposable(`${domain}.hook(${name})`)
67
- }
68
-
69
- const warnLines = []
70
- const errorLines = []
71
-
72
- return {
73
- ctx: {
74
- location: { directory, project: { directory } },
75
- options,
76
- tool: {
77
- transform: async (cb) => { cb(toolEditor); return disposable("tool.transform") },
78
- list: async () => [...toolMap.values()],
79
- hook: mkHook("tool"),
80
- reload: async () => {},
81
- },
82
- agent: {
83
- transform: async (cb) => { cb(agentEditor); return disposable("agent.transform") },
84
- list: async () => [...agentMap.values()],
85
- },
86
- permission: { hook: mkHook("permission"), list: async () => [], get: async () => ({}), reply: async () => ({}) },
87
- // `session.get` / `session.context` exist ONLY when a test seeds them, because a fake
88
- // that always answers would let a plugin pass while the real host refuses — and the
89
- // shapes below are the ones measured on 2.0.18 (`get` → {id,parentID,…}, `context` →
90
- // an array of {id,time,text,type}), not a guess. See src/host/v2-transcript.ts.
91
- session: sessionData
92
- ? {
93
- hook: mkHook("session"),
94
- async get({ sessionID } = {}) {
95
- const s = sessionData.sessions?.[sessionID]
96
- if (!s) throw new Error(`Session not found: ${sessionID}`)
97
- return { id: sessionID, ...s }
98
- },
99
- async context({ sessionID } = {}) {
100
- return sessionData.messages?.[sessionID] ?? []
101
- },
102
- }
103
- : { hook: mkHook("session") },
104
- shell: { hook: mkHook("shell") },
105
- storage: {
106
- _map: new Map(),
107
- async get(k) { return this._map.get(k) },
108
- async set(k, v) { this._map.set(k, v) },
109
- async remove(k) { this._map.delete(k) },
110
- async scan() { return { entries: [] } },
111
- },
112
- event: { subscribe: () => ({ async [Symbol.asyncIterator]() { return { done: true, value: undefined } } }) },
113
- app: { name: "opencode-fake", version: "2.0.16", channel: "test" },
114
- },
115
- /** Introspection for assertions. */
116
- registrations,
117
- tools: toolEditor,
118
- agents: agentEditor,
119
- /** Introspection for assertions. Only the FIRST dot separates the domain
120
- * from the hook name — `tool.execute.before` is domain "tool" + hook
121
- * "execute.before", and splitting on every dot would look up a key that
122
- * nothing ever registered, so a working hook would read as an absent one. */
123
- hook: (name) => {
124
- const [domain, ...rest] = name.split(".")
125
- return hookHandle(domain, rest.join("."))
126
- },
127
- hookNames: () => [...hooks.keys()],
128
- consoleWarn: warnLines,
129
- consoleError: errorLines,
130
- }
131
- }
132
-
133
- /** Capture what the personality logs during setup — a v2 plugin has no toast,
134
- * so the server log IS the user-visible channel for these gaps. */
135
- export function withCapturedConsole(fn) {
136
- const warns = []
137
- const errors = []
138
- const w = console.warn
139
- const e = console.error
140
- console.warn = (...a) => { warns.push(a.join(" ")) }
141
- console.error = (...a) => { errors.push(a.join(" ")) }
142
- return Promise.resolve()
143
- .then(fn)
144
- .finally(() => { console.warn = w; console.error = e })
145
- .then((value) => ({ value, warns, errors }))
146
- }
1
+ // A fake OpenCode v2 plugin context for the adapter tests.
2
+ //
3
+ // There was no such thing before the v2 port: every existing suite builds a v1
4
+ // tool ctx as an inline literal ({directory, sessionID, agent, ask}), because
5
+ // v1 hands tools a plain object. v2 hands the PLUGIN a context of domains with
6
+ // transform()/hook() registrars, so testing the v2 personality needs a host
7
+ // double that records what was registered and can replay a hook.
8
+ //
9
+ // Deliberately faithful on the three points that matter:
10
+ // - transform(cb) runs the callback SYNCHRONOUSLY against a live editor and
11
+ // returns a disposable (a fake that defers would let a plugin pass tests
12
+ // against an API shape the host does not have);
13
+ // - hook callbacks are callable by the test so a hook body can be exercised
14
+ // with a real payload;
15
+ // - nothing here touches the filesystem or the network.
16
+
17
+ export function makeFakeCtx({
18
+ directory = process.cwd(),
19
+ options = {},
20
+ agents: seedAgents = [],
21
+ tools: seedTools = [],
22
+ sessionData = null,
23
+ } = {}) {
24
+ const registrations = []
25
+ const hooks = new Map()
26
+
27
+ const disposable = (label) => {
28
+ const rec = { label, disposed: false, dispose: async () => { rec.disposed = true } }
29
+ registrations.push(rec)
30
+ return rec
31
+ }
32
+
33
+ const toolMap = new Map()
34
+ for (const t of seedTools) toolMap.set(t.name ?? t.id, { ...t, id: t.id ?? t.name })
35
+ const toolEditor = {
36
+ list: () => [...toolMap.values()],
37
+ get: (id) => toolMap.get(id),
38
+ add: (tool) => { toolMap.set(tool.name, { ...tool, id: tool.name }) },
39
+ update: (id, fn) => { const cur = toolMap.get(id); if (cur) fn(cur) },
40
+ remove: (id) => { toolMap.delete(id) },
41
+ namespace: () => {},
42
+ }
43
+
44
+ const agentMap = new Map()
45
+ for (const a of seedAgents) agentMap.set(a.id, { ...a })
46
+ const agentEditor = {
47
+ list: () => [...agentMap.values()],
48
+ get: (id) => agentMap.get(id),
49
+ default: (id) => { agentEditor.__default = id },
50
+ update: (id, fn) => { const cur = agentMap.get(id); if (cur) fn(cur) },
51
+ remove: (id) => { agentMap.delete(id) },
52
+ }
53
+
54
+ const hookHandle = (domain, name) => {
55
+ const key = `${domain}.${name}`
56
+ const entry = hooks.get(key) ?? { key, calls: [], fire: async (input) => {
57
+ for (const cb of entry.handlers) await cb(input)
58
+ return input
59
+ }, handlers: [] }
60
+ hooks.set(key, entry)
61
+ return entry
62
+ }
63
+
64
+ const mkHook = (domain) => async (name, callback) => {
65
+ hookHandle(domain, name).handlers.push(callback)
66
+ return disposable(`${domain}.hook(${name})`)
67
+ }
68
+
69
+ const warnLines = []
70
+ const errorLines = []
71
+ /** #33: which child sessions a stop attempt actually reached, in order. */
72
+ const stopCalls = []
73
+
74
+ return {
75
+ ctx: {
76
+ location: { directory, project: { directory } },
77
+ options,
78
+ tool: {
79
+ transform: async (cb) => { cb(toolEditor); return disposable("tool.transform") },
80
+ list: async () => [...toolMap.values()],
81
+ hook: mkHook("tool"),
82
+ reload: async () => {},
83
+ },
84
+ agent: {
85
+ transform: async (cb) => { cb(agentEditor); return disposable("agent.transform") },
86
+ list: async () => [...agentMap.values()],
87
+ },
88
+ permission: { hook: mkHook("permission"), list: async () => [], get: async () => ({}), reply: async () => ({}) },
89
+ // `session.get` / `session.context` exist ONLY when a test seeds them, because a fake
90
+ // that always answers would let a plugin pass while the real host refuses — and the
91
+ // shapes below are the ones measured on 2.0.18 (`get` → {id,parentID,…}, `context` →
92
+ // an array of {id,time,text,type}), not a guess. See src/host/v2-transcript.ts.
93
+ //
94
+ // `session.interrupt` (#33) is seeded through `stop`, so a test can hand back each of
95
+ // the host's documented answers separately — `{interrupted:true}` (an active execution
96
+ // was stopped), `{interrupted:false}` (the idle no-op), no boolean at all, or a throw.
97
+ // A fake that always said `true` would let the tool claim 已停止 against a host that
98
+ // never stops anything, which is the exact overstatement the real code refuses.
99
+ session: sessionData
100
+ ? {
101
+ hook: mkHook("session"),
102
+ async get({ sessionID } = {}) {
103
+ const s = sessionData.sessions?.[sessionID]
104
+ if (!s) throw new Error(`Session not found: ${sessionID}`)
105
+ return { id: sessionID, ...s }
106
+ },
107
+ async context({ sessionID } = {}) {
108
+ return sessionData.messages?.[sessionID] ?? []
109
+ },
110
+ ...(sessionData.stop
111
+ ? {
112
+ async interrupt({ sessionID } = {}) {
113
+ stopCalls.push({ sessionID })
114
+ const verdict = typeof sessionData.stop === "function"
115
+ ? sessionData.stop(sessionID)
116
+ : sessionData.stop
117
+ if (verdict instanceof Error) throw verdict
118
+ return verdict
119
+ },
120
+ }
121
+ : {}),
122
+ }
123
+ : { hook: mkHook("session") },
124
+ shell: { hook: mkHook("shell") },
125
+ storage: {
126
+ _map: new Map(),
127
+ async get(k) { return this._map.get(k) },
128
+ async set(k, v) { this._map.set(k, v) },
129
+ async remove(k) { this._map.delete(k) },
130
+ async scan() { return { entries: [] } },
131
+ },
132
+ event: { subscribe: () => ({ async [Symbol.asyncIterator]() { return { done: true, value: undefined } } }) },
133
+ app: { name: "opencode-fake", version: "2.0.16", channel: "test" },
134
+ },
135
+ /** Introspection for assertions. */
136
+ registrations,
137
+ tools: toolEditor,
138
+ agents: agentEditor,
139
+ /** Introspection for assertions. Only the FIRST dot separates the domain
140
+ * from the hook name — `tool.execute.before` is domain "tool" + hook
141
+ * "execute.before", and splitting on every dot would look up a key that
142
+ * nothing ever registered, so a working hook would read as an absent one. */
143
+ hook: (name) => {
144
+ const [domain, ...rest] = name.split(".")
145
+ return hookHandle(domain, rest.join("."))
146
+ },
147
+ hookNames: () => [...hooks.keys()],
148
+ consoleWarn: warnLines,
149
+ consoleError: errorLines,
150
+ /** #33 — the stop attempts this fake actually received, so a test can pin that a
151
+ * cancel reached the host with the child's id rather than trusting the tool's prose. */
152
+ stopCalls,
153
+ }
154
+ }
155
+
156
+ /** Capture what the personality logs during setup — a v2 plugin has no toast,
157
+ * so the server log IS the user-visible channel for these gaps. */
158
+ export function withCapturedConsole(fn) {
159
+ const warns = []
160
+ const errors = []
161
+ const w = console.warn
162
+ const e = console.error
163
+ console.warn = (...a) => { warns.push(a.join(" ")) }
164
+ console.error = (...a) => { errors.push(a.join(" ")) }
165
+ return Promise.resolve()
166
+ .then(fn)
167
+ .finally(() => { console.warn = w; console.error = e })
168
+ .then((value) => ({ value, warns, errors }))
169
+ }