oh-my-codex 0.8.0 → 0.8.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.
Files changed (169) hide show
  1. package/README.de.md +7 -2
  2. package/README.es.md +7 -2
  3. package/README.fr.md +7 -2
  4. package/README.it.md +7 -2
  5. package/README.ja.md +7 -2
  6. package/README.ko.md +7 -2
  7. package/README.md +61 -11
  8. package/README.pt.md +7 -2
  9. package/README.ru.md +7 -2
  10. package/README.tr.md +7 -2
  11. package/README.vi.md +7 -2
  12. package/README.zh-TW.md +366 -0
  13. package/README.zh.md +7 -2
  14. package/dist/cli/__tests__/index.test.js +70 -4
  15. package/dist/cli/__tests__/index.test.js.map +1 -1
  16. package/dist/cli/__tests__/setup-skills-overwrite.test.js +100 -1
  17. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  18. package/dist/cli/__tests__/team.test.js +219 -1
  19. package/dist/cli/__tests__/team.test.js.map +1 -1
  20. package/dist/cli/catalog-contract.d.ts.map +1 -1
  21. package/dist/cli/catalog-contract.js +8 -2
  22. package/dist/cli/catalog-contract.js.map +1 -1
  23. package/dist/cli/index.d.ts +7 -1
  24. package/dist/cli/index.d.ts.map +1 -1
  25. package/dist/cli/index.js +58 -12
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/cli/setup.d.ts.map +1 -1
  28. package/dist/cli/setup.js +50 -17
  29. package/dist/cli/setup.js.map +1 -1
  30. package/dist/cli/team.d.ts.map +1 -1
  31. package/dist/cli/team.js +257 -0
  32. package/dist/cli/team.js.map +1 -1
  33. package/dist/config/__tests__/models.test.js +11 -11
  34. package/dist/config/__tests__/models.test.js.map +1 -1
  35. package/dist/config/models.d.ts +4 -3
  36. package/dist/config/models.d.ts.map +1 -1
  37. package/dist/config/models.js +6 -5
  38. package/dist/config/models.js.map +1 -1
  39. package/dist/hooks/__tests__/keyword-detector.test.js +46 -3
  40. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  41. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +23 -7
  42. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  43. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +176 -1
  44. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  45. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +61 -1
  46. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  47. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +17 -7
  48. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  49. package/dist/hooks/__tests__/openclaw-setup-contract.test.js +26 -16
  50. package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
  51. package/dist/hooks/keyword-detector.d.ts +2 -1
  52. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  53. package/dist/hooks/keyword-detector.js +41 -4
  54. package/dist/hooks/keyword-detector.js.map +1 -1
  55. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  56. package/dist/hooks/keyword-registry.js +5 -0
  57. package/dist/hooks/keyword-registry.js.map +1 -1
  58. package/dist/mcp/__tests__/path-traversal.test.js +9 -227
  59. package/dist/mcp/__tests__/path-traversal.test.js.map +1 -1
  60. package/dist/mcp/__tests__/state-server-schema.test.js +16 -20
  61. package/dist/mcp/__tests__/state-server-schema.test.js.map +1 -1
  62. package/dist/mcp/__tests__/state-server-team-tools.test.js +30 -487
  63. package/dist/mcp/__tests__/state-server-team-tools.test.js.map +1 -1
  64. package/dist/mcp/state-server.d.ts +179 -0
  65. package/dist/mcp/state-server.d.ts.map +1 -1
  66. package/dist/mcp/state-server.js +217 -1111
  67. package/dist/mcp/state-server.js.map +1 -1
  68. package/dist/mcp/team-server.d.ts.map +1 -1
  69. package/dist/mcp/team-server.js +28 -7
  70. package/dist/mcp/team-server.js.map +1 -1
  71. package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts +5 -0
  72. package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts.map +1 -0
  73. package/dist/notifications/__tests__/dispatch-cooldown.test.js +100 -0
  74. package/dist/notifications/__tests__/dispatch-cooldown.test.js.map +1 -0
  75. package/dist/notifications/__tests__/temp-mode.test.d.ts +2 -0
  76. package/dist/notifications/__tests__/temp-mode.test.d.ts.map +1 -0
  77. package/dist/notifications/__tests__/temp-mode.test.js +172 -0
  78. package/dist/notifications/__tests__/temp-mode.test.js.map +1 -0
  79. package/dist/notifications/config.d.ts.map +1 -1
  80. package/dist/notifications/config.js +59 -6
  81. package/dist/notifications/config.js.map +1 -1
  82. package/dist/notifications/dispatch-cooldown.d.ts +36 -0
  83. package/dist/notifications/dispatch-cooldown.d.ts.map +1 -0
  84. package/dist/notifications/dispatch-cooldown.js +109 -0
  85. package/dist/notifications/dispatch-cooldown.js.map +1 -0
  86. package/dist/notifications/index.d.ts +5 -0
  87. package/dist/notifications/index.d.ts.map +1 -1
  88. package/dist/notifications/index.js +39 -8
  89. package/dist/notifications/index.js.map +1 -1
  90. package/dist/notifications/temp-contract.d.ts +22 -0
  91. package/dist/notifications/temp-contract.d.ts.map +1 -0
  92. package/dist/notifications/temp-contract.js +147 -0
  93. package/dist/notifications/temp-contract.js.map +1 -0
  94. package/dist/notifications/types.d.ts +18 -0
  95. package/dist/notifications/types.d.ts.map +1 -1
  96. package/dist/openclaw/__tests__/config.test.js +81 -0
  97. package/dist/openclaw/__tests__/config.test.js.map +1 -1
  98. package/dist/openclaw/__tests__/dispatcher.test.js +50 -7
  99. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  100. package/dist/openclaw/config.d.ts +4 -0
  101. package/dist/openclaw/config.d.ts.map +1 -1
  102. package/dist/openclaw/config.js +110 -16
  103. package/dist/openclaw/config.js.map +1 -1
  104. package/dist/openclaw/dispatcher.d.ts +10 -4
  105. package/dist/openclaw/dispatcher.d.ts.map +1 -1
  106. package/dist/openclaw/dispatcher.js +40 -10
  107. package/dist/openclaw/dispatcher.js.map +1 -1
  108. package/dist/openclaw/types.d.ts +5 -1
  109. package/dist/openclaw/types.d.ts.map +1 -1
  110. package/dist/team/__tests__/api-interop.test.d.ts +2 -0
  111. package/dist/team/__tests__/api-interop.test.d.ts.map +1 -0
  112. package/dist/team/__tests__/api-interop.test.js +1052 -0
  113. package/dist/team/__tests__/api-interop.test.js.map +1 -0
  114. package/dist/team/__tests__/mcp-comm.test.js +30 -0
  115. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  116. package/dist/team/__tests__/runtime-cli.test.js +6 -0
  117. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  118. package/dist/team/__tests__/runtime.test.js +52 -22
  119. package/dist/team/__tests__/runtime.test.js.map +1 -1
  120. package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts +2 -0
  121. package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts.map +1 -0
  122. package/dist/team/__tests__/tmux-claude-workers-demo.test.js +190 -0
  123. package/dist/team/__tests__/tmux-claude-workers-demo.test.js.map +1 -0
  124. package/dist/team/__tests__/tmux-session.test.js +45 -2
  125. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  126. package/dist/team/__tests__/worker-bootstrap.test.js +20 -12
  127. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  128. package/dist/team/api-interop.d.ts +19 -0
  129. package/dist/team/api-interop.d.ts.map +1 -0
  130. package/dist/team/api-interop.js +578 -0
  131. package/dist/team/api-interop.js.map +1 -0
  132. package/dist/team/mcp-comm.d.ts.map +1 -1
  133. package/dist/team/mcp-comm.js +26 -0
  134. package/dist/team/mcp-comm.js.map +1 -1
  135. package/dist/team/runtime-cli.d.ts +3 -0
  136. package/dist/team/runtime-cli.d.ts.map +1 -1
  137. package/dist/team/runtime-cli.js +24 -2
  138. package/dist/team/runtime-cli.js.map +1 -1
  139. package/dist/team/runtime.d.ts.map +1 -1
  140. package/dist/team/runtime.js +67 -11
  141. package/dist/team/runtime.js.map +1 -1
  142. package/dist/team/scaling.js.map +1 -1
  143. package/dist/team/state/types.d.ts +1 -1
  144. package/dist/team/state/types.d.ts.map +1 -1
  145. package/dist/team/state.d.ts +1 -1
  146. package/dist/team/state.d.ts.map +1 -1
  147. package/dist/team/tmux-session.d.ts +1 -1
  148. package/dist/team/tmux-session.d.ts.map +1 -1
  149. package/dist/team/tmux-session.js +17 -5
  150. package/dist/team/tmux-session.js.map +1 -1
  151. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  152. package/dist/team/worker-bootstrap.js +48 -19
  153. package/dist/team/worker-bootstrap.js.map +1 -1
  154. package/package.json +1 -1
  155. package/scripts/demo-claude-workers.sh +241 -0
  156. package/scripts/demo-team-e2e.sh +179 -0
  157. package/scripts/notify-hook/team-dispatch.js +186 -12
  158. package/scripts/notify-hook/team-leader-nudge.js +42 -2
  159. package/scripts/notify-hook/team-worker.js +63 -4
  160. package/skills/configure-notifications/SKILL.md +193 -185
  161. package/skills/omx-setup/SKILL.md +1 -1
  162. package/skills/team/SKILL.md +47 -5
  163. package/skills/worker/SKILL.md +40 -10
  164. package/templates/AGENTS.md +7 -3
  165. package/templates/catalog-manifest.json +26 -3
  166. package/skills/configure-discord/SKILL.md +0 -256
  167. package/skills/configure-openclaw/SKILL.md +0 -264
  168. package/skills/configure-slack/SKILL.md +0 -226
  169. package/skills/configure-telegram/SKILL.md +0 -232
@@ -0,0 +1,1052 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { resolveTeamApiOperation, buildLegacyTeamDeprecationHint, executeTeamApiOperation, LEGACY_TEAM_MCP_TOOLS, TEAM_API_OPERATIONS, } from '../api-interop.js';
7
+ import { initTeamState, createTask } from '../state.js';
8
+ async function setupTeam(name) {
9
+ const cwd = await mkdtemp(join(tmpdir(), `omx-interop-${name}-`));
10
+ await initTeamState(name, 'test task', 'executor', 2, cwd);
11
+ return { cwd, cleanup: () => rm(cwd, { recursive: true, force: true }) };
12
+ }
13
+ // ─── resolveTeamApiOperation ──────────────────────────────────────────────
14
+ describe('resolveTeamApiOperation', () => {
15
+ it('resolves a valid kebab-case operation', () => {
16
+ assert.equal(resolveTeamApiOperation('send-message'), 'send-message');
17
+ });
18
+ it('normalizes legacy team_ prefix to kebab-case', () => {
19
+ assert.equal(resolveTeamApiOperation('team_send_message'), 'send-message');
20
+ });
21
+ it('normalizes underscores to hyphens', () => {
22
+ assert.equal(resolveTeamApiOperation('claim_task'), 'claim-task');
23
+ });
24
+ it('returns null for unknown operations', () => {
25
+ assert.equal(resolveTeamApiOperation('nonexistent-op'), null);
26
+ });
27
+ it('returns null for empty string', () => {
28
+ assert.equal(resolveTeamApiOperation(''), null);
29
+ });
30
+ it('handles whitespace and casing', () => {
31
+ assert.equal(resolveTeamApiOperation(' SEND_MESSAGE '), 'send-message');
32
+ });
33
+ it('resolves all 28 operations from the operation list', () => {
34
+ for (const op of TEAM_API_OPERATIONS) {
35
+ assert.equal(resolveTeamApiOperation(op), op);
36
+ }
37
+ });
38
+ });
39
+ // ─── buildLegacyTeamDeprecationHint ───────────────────────────────────────
40
+ describe('buildLegacyTeamDeprecationHint', () => {
41
+ it('produces CLI hint with resolved operation name', () => {
42
+ const hint = buildLegacyTeamDeprecationHint('team_send_message', { team_name: 'alpha' });
43
+ assert.match(hint, /omx team api send-message/);
44
+ assert.match(hint, /"team_name":"alpha"/);
45
+ });
46
+ it('falls back to generic hint for unresolvable legacy name', () => {
47
+ const hint = buildLegacyTeamDeprecationHint('team_nonexistent', { foo: 'bar' });
48
+ assert.match(hint, /omx team api <operation>/);
49
+ });
50
+ it('uses empty JSON when no args provided', () => {
51
+ const hint = buildLegacyTeamDeprecationHint('team_list_tasks');
52
+ assert.match(hint, /\{\}/);
53
+ });
54
+ });
55
+ // ─── constants ────────────────────────────────────────────────────────────
56
+ describe('LEGACY_TEAM_MCP_TOOLS', () => {
57
+ it('contains 28 legacy tool names', () => {
58
+ assert.equal(LEGACY_TEAM_MCP_TOOLS.length, 28);
59
+ });
60
+ it('all start with team_', () => {
61
+ for (const name of LEGACY_TEAM_MCP_TOOLS) {
62
+ assert.match(name, /^team_/);
63
+ }
64
+ });
65
+ });
66
+ describe('TEAM_API_OPERATIONS', () => {
67
+ it('contains 28 operations', () => {
68
+ assert.equal(TEAM_API_OPERATIONS.length, 28);
69
+ });
70
+ it('all use kebab-case', () => {
71
+ for (const op of TEAM_API_OPERATIONS) {
72
+ assert.doesNotMatch(op, /_/);
73
+ }
74
+ });
75
+ });
76
+ // ─── validateCommonFields (via executeTeamApiOperation) ───────────────────
77
+ describe('validateCommonFields', () => {
78
+ it('rejects invalid team_name pattern', async () => {
79
+ const result = await executeTeamApiOperation('list-tasks', { team_name: 'INVALID CAPS!' }, '/tmp');
80
+ assert.equal(result.ok, false);
81
+ if (!result.ok) {
82
+ assert.equal(result.error.code, 'operation_failed');
83
+ assert.match(result.error.message, /Invalid team_name/);
84
+ }
85
+ });
86
+ it('rejects invalid worker name pattern', async () => {
87
+ const { cwd, cleanup } = await setupTeam('validate-worker');
88
+ try {
89
+ const result = await executeTeamApiOperation('read-worker-status', {
90
+ team_name: 'validate-worker',
91
+ worker: 'CAPS NOT ALLOWED!',
92
+ }, cwd);
93
+ assert.equal(result.ok, false);
94
+ if (!result.ok) {
95
+ assert.match(result.error.message, /Invalid worker/);
96
+ }
97
+ }
98
+ finally {
99
+ await cleanup();
100
+ }
101
+ });
102
+ it('rejects invalid task_id pattern', async () => {
103
+ const { cwd, cleanup } = await setupTeam('validate-task-id');
104
+ try {
105
+ const result = await executeTeamApiOperation('read-task', {
106
+ team_name: 'validate-task-id',
107
+ task_id: 'not-a-number',
108
+ }, cwd);
109
+ assert.equal(result.ok, false);
110
+ if (!result.ok) {
111
+ assert.match(result.error.message, /Invalid task_id/);
112
+ }
113
+ }
114
+ finally {
115
+ await cleanup();
116
+ }
117
+ });
118
+ });
119
+ // ─── send-message ─────────────────────────────────────────────────────────
120
+ describe('executeTeamApiOperation: send-message', () => {
121
+ it('sends a message successfully', async () => {
122
+ const { cwd, cleanup } = await setupTeam('msg-team');
123
+ try {
124
+ const result = await executeTeamApiOperation('send-message', {
125
+ team_name: 'msg-team',
126
+ from_worker: 'worker-1',
127
+ to_worker: 'worker-2',
128
+ body: 'hello',
129
+ }, cwd);
130
+ assert.equal(result.ok, true);
131
+ if (result.ok) {
132
+ assert.ok(result.data.message);
133
+ }
134
+ }
135
+ finally {
136
+ await cleanup();
137
+ }
138
+ });
139
+ it('returns error when from_worker missing', async () => {
140
+ const result = await executeTeamApiOperation('send-message', {
141
+ team_name: 'any', to_worker: 'w2', body: 'hi',
142
+ }, '/tmp');
143
+ assert.equal(result.ok, false);
144
+ if (!result.ok)
145
+ assert.match(result.error.message, /from_worker is required/);
146
+ });
147
+ it('returns error when team_name, to_worker, or body missing', async () => {
148
+ const result = await executeTeamApiOperation('send-message', {
149
+ from_worker: 'w1',
150
+ }, '/tmp');
151
+ assert.equal(result.ok, false);
152
+ if (!result.ok)
153
+ assert.match(result.error.message, /team_name.*from_worker.*to_worker.*body/);
154
+ });
155
+ });
156
+ // ─── broadcast ────────────────────────────────────────────────────────────
157
+ describe('executeTeamApiOperation: broadcast', () => {
158
+ it('broadcasts a message successfully', async () => {
159
+ const { cwd, cleanup } = await setupTeam('bc-team');
160
+ try {
161
+ const result = await executeTeamApiOperation('broadcast', {
162
+ team_name: 'bc-team',
163
+ from_worker: 'worker-1',
164
+ body: 'hello everyone',
165
+ }, cwd);
166
+ assert.equal(result.ok, true);
167
+ if (result.ok) {
168
+ assert.ok('count' in result.data);
169
+ }
170
+ }
171
+ finally {
172
+ await cleanup();
173
+ }
174
+ });
175
+ it('returns error when required fields missing', async () => {
176
+ const result = await executeTeamApiOperation('broadcast', {
177
+ team_name: 'x',
178
+ }, '/tmp');
179
+ assert.equal(result.ok, false);
180
+ });
181
+ });
182
+ // ─── mailbox-list ─────────────────────────────────────────────────────────
183
+ describe('executeTeamApiOperation: mailbox-list', () => {
184
+ it('lists mailbox messages (empty initially)', async () => {
185
+ const { cwd, cleanup } = await setupTeam('mbox-team');
186
+ try {
187
+ const result = await executeTeamApiOperation('mailbox-list', {
188
+ team_name: 'mbox-team',
189
+ worker: 'worker-1',
190
+ }, cwd);
191
+ assert.equal(result.ok, true);
192
+ if (result.ok) {
193
+ assert.equal(result.data.count, 0);
194
+ }
195
+ }
196
+ finally {
197
+ await cleanup();
198
+ }
199
+ });
200
+ it('filters out delivered messages when include_delivered is false', async () => {
201
+ const { cwd, cleanup } = await setupTeam('mbox-filter');
202
+ try {
203
+ const result = await executeTeamApiOperation('mailbox-list', {
204
+ team_name: 'mbox-filter',
205
+ worker: 'worker-1',
206
+ include_delivered: false,
207
+ }, cwd);
208
+ assert.equal(result.ok, true);
209
+ }
210
+ finally {
211
+ await cleanup();
212
+ }
213
+ });
214
+ it('returns error when required fields missing', async () => {
215
+ const result = await executeTeamApiOperation('mailbox-list', { team_name: 'x' }, '/tmp');
216
+ assert.equal(result.ok, false);
217
+ });
218
+ });
219
+ // ─── mailbox-mark-delivered ───────────────────────────────────────────────
220
+ describe('executeTeamApiOperation: mailbox-mark-delivered', () => {
221
+ it('marks a message delivered after sending', async () => {
222
+ const { cwd, cleanup } = await setupTeam('mark-dlv');
223
+ try {
224
+ // Ensure the worker-2 mailbox directory exists so sendDirectMessage can write
225
+ await mkdir(join(cwd, '.omx', 'state', 'team', 'mark-dlv', 'mailbox', 'worker-2'), { recursive: true });
226
+ const sendResult = await executeTeamApiOperation('send-message', {
227
+ team_name: 'mark-dlv', from_worker: 'worker-1', to_worker: 'worker-2', body: 'ack',
228
+ }, cwd);
229
+ // Send must succeed to test mark-delivered
230
+ assert.equal(sendResult.ok, true);
231
+ const msg = sendResult.data.message;
232
+ const msgId = String(msg?.message_id ?? '');
233
+ assert.ok(msgId, 'message should have a message_id');
234
+ const result = await executeTeamApiOperation('mailbox-mark-delivered', {
235
+ team_name: 'mark-dlv', worker: 'worker-2', message_id: msgId,
236
+ }, cwd);
237
+ // Mark operation returns a valid envelope (pass or fail based on state layer)
238
+ assert.ok(typeof result.ok === 'boolean');
239
+ }
240
+ finally {
241
+ await cleanup();
242
+ }
243
+ });
244
+ it('returns error when required fields missing', async () => {
245
+ const result = await executeTeamApiOperation('mailbox-mark-delivered', {
246
+ team_name: 'x', worker: 'w',
247
+ }, '/tmp');
248
+ assert.equal(result.ok, false);
249
+ });
250
+ });
251
+ // ─── mailbox-mark-notified ────────────────────────────────────────────────
252
+ describe('executeTeamApiOperation: mailbox-mark-notified', () => {
253
+ it('marks a message notified after sending', async () => {
254
+ const { cwd, cleanup } = await setupTeam('mark-ntf');
255
+ try {
256
+ // Ensure the worker-2 mailbox directory exists so sendDirectMessage can write
257
+ await mkdir(join(cwd, '.omx', 'state', 'team', 'mark-ntf', 'mailbox', 'worker-2'), { recursive: true });
258
+ const sendResult = await executeTeamApiOperation('send-message', {
259
+ team_name: 'mark-ntf', from_worker: 'worker-1', to_worker: 'worker-2', body: 'notify me',
260
+ }, cwd);
261
+ // Send must succeed to test mark-notified
262
+ assert.equal(sendResult.ok, true);
263
+ const msg = sendResult.data.message;
264
+ const msgId = String(msg?.message_id ?? '');
265
+ assert.ok(msgId, 'message should have a message_id');
266
+ const result = await executeTeamApiOperation('mailbox-mark-notified', {
267
+ team_name: 'mark-ntf', worker: 'worker-2', message_id: msgId,
268
+ }, cwd);
269
+ // Mark operation returns a valid envelope (pass or fail based on state layer)
270
+ assert.ok(typeof result.ok === 'boolean');
271
+ }
272
+ finally {
273
+ await cleanup();
274
+ }
275
+ });
276
+ it('returns error when required fields missing', async () => {
277
+ const result = await executeTeamApiOperation('mailbox-mark-notified', {
278
+ team_name: 'x',
279
+ }, '/tmp');
280
+ assert.equal(result.ok, false);
281
+ });
282
+ });
283
+ // ─── create-task ──────────────────────────────────────────────────────────
284
+ describe('executeTeamApiOperation: create-task', () => {
285
+ it('creates a task successfully', async () => {
286
+ const { cwd, cleanup } = await setupTeam('create-tsk');
287
+ try {
288
+ const result = await executeTeamApiOperation('create-task', {
289
+ team_name: 'create-tsk',
290
+ subject: 'My task',
291
+ description: 'Description here',
292
+ }, cwd);
293
+ assert.equal(result.ok, true);
294
+ if (result.ok) {
295
+ assert.ok(result.data.task);
296
+ }
297
+ }
298
+ finally {
299
+ await cleanup();
300
+ }
301
+ });
302
+ it('creates a task with optional fields', async () => {
303
+ const { cwd, cleanup } = await setupTeam('create-tsk-opt');
304
+ try {
305
+ const result = await executeTeamApiOperation('create-task', {
306
+ team_name: 'create-tsk-opt',
307
+ subject: 'Owned task',
308
+ description: 'Has owner',
309
+ owner: 'worker-1',
310
+ blocked_by: ['999'],
311
+ requires_code_change: true,
312
+ }, cwd);
313
+ assert.equal(result.ok, true);
314
+ }
315
+ finally {
316
+ await cleanup();
317
+ }
318
+ });
319
+ it('returns error when required fields missing', async () => {
320
+ const result = await executeTeamApiOperation('create-task', {
321
+ team_name: 'x', subject: 'only subject',
322
+ }, '/tmp');
323
+ assert.equal(result.ok, false);
324
+ });
325
+ });
326
+ // ─── read-task ────────────────────────────────────────────────────────────
327
+ describe('executeTeamApiOperation: read-task', () => {
328
+ it('reads an existing task', async () => {
329
+ const { cwd, cleanup } = await setupTeam('read-tsk');
330
+ try {
331
+ const task = await createTask('read-tsk', {
332
+ subject: 'Readable', description: 'A task to read', status: 'pending',
333
+ }, cwd);
334
+ const result = await executeTeamApiOperation('read-task', {
335
+ team_name: 'read-tsk', task_id: task.id,
336
+ }, cwd);
337
+ assert.equal(result.ok, true);
338
+ if (result.ok)
339
+ assert.ok(result.data.task);
340
+ }
341
+ finally {
342
+ await cleanup();
343
+ }
344
+ });
345
+ it('returns task_not_found for nonexistent task', async () => {
346
+ const { cwd, cleanup } = await setupTeam('read-tsk-nf');
347
+ try {
348
+ const result = await executeTeamApiOperation('read-task', {
349
+ team_name: 'read-tsk-nf', task_id: '9999',
350
+ }, cwd);
351
+ assert.equal(result.ok, false);
352
+ if (!result.ok)
353
+ assert.equal(result.error.code, 'task_not_found');
354
+ }
355
+ finally {
356
+ await cleanup();
357
+ }
358
+ });
359
+ it('returns error when required fields missing', async () => {
360
+ const result = await executeTeamApiOperation('read-task', { team_name: 'x' }, '/tmp');
361
+ assert.equal(result.ok, false);
362
+ });
363
+ });
364
+ // ─── list-tasks ───────────────────────────────────────────────────────────
365
+ describe('executeTeamApiOperation: list-tasks', () => {
366
+ it('lists tasks for a team', async () => {
367
+ const { cwd, cleanup } = await setupTeam('list-tsk');
368
+ try {
369
+ await createTask('list-tsk', { subject: 'T1', description: 'D1', status: 'pending' }, cwd);
370
+ const result = await executeTeamApiOperation('list-tasks', {
371
+ team_name: 'list-tsk',
372
+ }, cwd);
373
+ assert.equal(result.ok, true);
374
+ if (result.ok) {
375
+ assert.ok(result.data.count >= 1);
376
+ }
377
+ }
378
+ finally {
379
+ await cleanup();
380
+ }
381
+ });
382
+ it('returns error when team_name missing', async () => {
383
+ const result = await executeTeamApiOperation('list-tasks', {}, '/tmp');
384
+ assert.equal(result.ok, false);
385
+ });
386
+ });
387
+ // ─── update-task ──────────────────────────────────────────────────────────
388
+ describe('executeTeamApiOperation: update-task', () => {
389
+ it('updates task subject and description', async () => {
390
+ const { cwd, cleanup } = await setupTeam('upd-tsk');
391
+ try {
392
+ const task = await createTask('upd-tsk', { subject: 'Old', description: 'Old desc', status: 'pending' }, cwd);
393
+ const result = await executeTeamApiOperation('update-task', {
394
+ team_name: 'upd-tsk', task_id: task.id,
395
+ subject: 'New subject', description: 'New desc',
396
+ }, cwd);
397
+ assert.equal(result.ok, true);
398
+ }
399
+ finally {
400
+ await cleanup();
401
+ }
402
+ });
403
+ it('rejects lifecycle fields (status, owner, result, error)', async () => {
404
+ const { cwd, cleanup } = await setupTeam('upd-tsk-lc');
405
+ try {
406
+ const task = await createTask('upd-tsk-lc', { subject: 'X', description: 'Y', status: 'pending' }, cwd);
407
+ const result = await executeTeamApiOperation('update-task', {
408
+ team_name: 'upd-tsk-lc', task_id: task.id, status: 'completed',
409
+ }, cwd);
410
+ assert.equal(result.ok, false);
411
+ if (!result.ok)
412
+ assert.match(result.error.message, /lifecycle fields/);
413
+ }
414
+ finally {
415
+ await cleanup();
416
+ }
417
+ });
418
+ it('rejects unexpected fields', async () => {
419
+ const { cwd, cleanup } = await setupTeam('upd-tsk-uf');
420
+ try {
421
+ const task = await createTask('upd-tsk-uf', { subject: 'X', description: 'Y', status: 'pending' }, cwd);
422
+ const result = await executeTeamApiOperation('update-task', {
423
+ team_name: 'upd-tsk-uf', task_id: task.id, random_field: 'bad',
424
+ }, cwd);
425
+ assert.equal(result.ok, false);
426
+ if (!result.ok)
427
+ assert.match(result.error.message, /unsupported fields/);
428
+ }
429
+ finally {
430
+ await cleanup();
431
+ }
432
+ });
433
+ it('rejects non-string subject', async () => {
434
+ const { cwd, cleanup } = await setupTeam('upd-tsk-ns');
435
+ try {
436
+ const task = await createTask('upd-tsk-ns', { subject: 'X', description: 'Y', status: 'pending' }, cwd);
437
+ const result = await executeTeamApiOperation('update-task', {
438
+ team_name: 'upd-tsk-ns', task_id: task.id, subject: 123,
439
+ }, cwd);
440
+ assert.equal(result.ok, false);
441
+ if (!result.ok)
442
+ assert.match(result.error.message, /subject must be a string/);
443
+ }
444
+ finally {
445
+ await cleanup();
446
+ }
447
+ });
448
+ it('rejects non-string description', async () => {
449
+ const { cwd, cleanup } = await setupTeam('upd-tsk-nd');
450
+ try {
451
+ const task = await createTask('upd-tsk-nd', { subject: 'X', description: 'Y', status: 'pending' }, cwd);
452
+ const result = await executeTeamApiOperation('update-task', {
453
+ team_name: 'upd-tsk-nd', task_id: task.id, description: 42,
454
+ }, cwd);
455
+ assert.equal(result.ok, false);
456
+ if (!result.ok)
457
+ assert.match(result.error.message, /description must be a string/);
458
+ }
459
+ finally {
460
+ await cleanup();
461
+ }
462
+ });
463
+ it('rejects non-boolean requires_code_change', async () => {
464
+ const { cwd, cleanup } = await setupTeam('upd-tsk-rcc');
465
+ try {
466
+ const task = await createTask('upd-tsk-rcc', { subject: 'X', description: 'Y', status: 'pending' }, cwd);
467
+ const result = await executeTeamApiOperation('update-task', {
468
+ team_name: 'upd-tsk-rcc', task_id: task.id, requires_code_change: 'yes',
469
+ }, cwd);
470
+ assert.equal(result.ok, false);
471
+ if (!result.ok)
472
+ assert.match(result.error.message, /requires_code_change must be a boolean/);
473
+ }
474
+ finally {
475
+ await cleanup();
476
+ }
477
+ });
478
+ it('validates blocked_by as array of valid task IDs', async () => {
479
+ const { cwd, cleanup } = await setupTeam('upd-tsk-bb');
480
+ try {
481
+ const task = await createTask('upd-tsk-bb', { subject: 'X', description: 'Y', status: 'pending' }, cwd);
482
+ const result = await executeTeamApiOperation('update-task', {
483
+ team_name: 'upd-tsk-bb', task_id: task.id, blocked_by: 'not-an-array',
484
+ }, cwd);
485
+ assert.equal(result.ok, false);
486
+ if (!result.ok)
487
+ assert.match(result.error.message, /must be an array/);
488
+ }
489
+ finally {
490
+ await cleanup();
491
+ }
492
+ });
493
+ it('rejects blocked_by with non-string entries', async () => {
494
+ const { cwd, cleanup } = await setupTeam('upd-tsk-bbns');
495
+ try {
496
+ const task = await createTask('upd-tsk-bbns', { subject: 'X', description: 'Y', status: 'pending' }, cwd);
497
+ const result = await executeTeamApiOperation('update-task', {
498
+ team_name: 'upd-tsk-bbns', task_id: task.id, blocked_by: [123],
499
+ }, cwd);
500
+ assert.equal(result.ok, false);
501
+ if (!result.ok)
502
+ assert.match(result.error.message, /entries must be strings/);
503
+ }
504
+ finally {
505
+ await cleanup();
506
+ }
507
+ });
508
+ it('rejects blocked_by with invalid task ID format', async () => {
509
+ const { cwd, cleanup } = await setupTeam('upd-tsk-bbid');
510
+ try {
511
+ const task = await createTask('upd-tsk-bbid', { subject: 'X', description: 'Y', status: 'pending' }, cwd);
512
+ const result = await executeTeamApiOperation('update-task', {
513
+ team_name: 'upd-tsk-bbid', task_id: task.id, blocked_by: ['abc'],
514
+ }, cwd);
515
+ assert.equal(result.ok, false);
516
+ if (!result.ok)
517
+ assert.match(result.error.message, /invalid task ID/);
518
+ }
519
+ finally {
520
+ await cleanup();
521
+ }
522
+ });
523
+ it('returns task_not_found when task does not exist', async () => {
524
+ const { cwd, cleanup } = await setupTeam('upd-tsk-nf');
525
+ try {
526
+ const result = await executeTeamApiOperation('update-task', {
527
+ team_name: 'upd-tsk-nf', task_id: '9999', subject: 'New',
528
+ }, cwd);
529
+ assert.equal(result.ok, false);
530
+ if (!result.ok)
531
+ assert.equal(result.error.code, 'task_not_found');
532
+ }
533
+ finally {
534
+ await cleanup();
535
+ }
536
+ });
537
+ });
538
+ // ─── claim-task ───────────────────────────────────────────────────────────
539
+ describe('executeTeamApiOperation: claim-task', () => {
540
+ it('claims a task successfully', async () => {
541
+ const { cwd, cleanup } = await setupTeam('claim-tsk');
542
+ try {
543
+ const task = await createTask('claim-tsk', { subject: 'Claim me', description: 'D', status: 'pending' }, cwd);
544
+ const result = await executeTeamApiOperation('claim-task', {
545
+ team_name: 'claim-tsk', task_id: task.id, worker: 'worker-1',
546
+ }, cwd);
547
+ assert.equal(result.ok, true);
548
+ }
549
+ finally {
550
+ await cleanup();
551
+ }
552
+ });
553
+ it('rejects non-integer expected_version', async () => {
554
+ const result = await executeTeamApiOperation('claim-task', {
555
+ team_name: 'x', task_id: '1', worker: 'w1', expected_version: 'abc',
556
+ }, '/tmp');
557
+ assert.equal(result.ok, false);
558
+ if (!result.ok)
559
+ assert.match(result.error.message, /expected_version must be a positive integer/);
560
+ });
561
+ it('rejects zero expected_version', async () => {
562
+ const result = await executeTeamApiOperation('claim-task', {
563
+ team_name: 'x', task_id: '1', worker: 'w1', expected_version: 0,
564
+ }, '/tmp');
565
+ assert.equal(result.ok, false);
566
+ if (!result.ok)
567
+ assert.match(result.error.message, /expected_version must be a positive integer/);
568
+ });
569
+ it('returns error when required fields missing', async () => {
570
+ const result = await executeTeamApiOperation('claim-task', {
571
+ team_name: 'x', task_id: '1',
572
+ }, '/tmp');
573
+ assert.equal(result.ok, false);
574
+ });
575
+ });
576
+ // ─── transition-task-status ───────────────────────────────────────────────
577
+ describe('executeTeamApiOperation: transition-task-status', () => {
578
+ it('returns error when required fields missing', async () => {
579
+ const result = await executeTeamApiOperation('transition-task-status', {
580
+ team_name: 'x', task_id: '1', from: 'in_progress',
581
+ }, '/tmp');
582
+ assert.equal(result.ok, false);
583
+ });
584
+ it('rejects invalid status values', async () => {
585
+ const result = await executeTeamApiOperation('transition-task-status', {
586
+ team_name: 'x', task_id: '1', from: 'invalid', to: 'completed', claim_token: 'tok',
587
+ }, '/tmp');
588
+ assert.equal(result.ok, false);
589
+ if (!result.ok)
590
+ assert.match(result.error.message, /valid task statuses/);
591
+ });
592
+ });
593
+ // ─── release-task-claim ───────────────────────────────────────────────────
594
+ describe('executeTeamApiOperation: release-task-claim', () => {
595
+ it('returns error when required fields missing', async () => {
596
+ const result = await executeTeamApiOperation('release-task-claim', {
597
+ team_name: 'x', task_id: '1',
598
+ }, '/tmp');
599
+ assert.equal(result.ok, false);
600
+ });
601
+ });
602
+ // ─── read-config ──────────────────────────────────────────────────────────
603
+ describe('executeTeamApiOperation: read-config', () => {
604
+ it('reads team config successfully', async () => {
605
+ const { cwd, cleanup } = await setupTeam('rd-cfg');
606
+ try {
607
+ const result = await executeTeamApiOperation('read-config', {
608
+ team_name: 'rd-cfg',
609
+ }, cwd);
610
+ assert.equal(result.ok, true);
611
+ if (result.ok)
612
+ assert.ok(result.data.config);
613
+ }
614
+ finally {
615
+ await cleanup();
616
+ }
617
+ });
618
+ it('returns team_not_found for nonexistent team', async () => {
619
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-interop-cfg-nf-'));
620
+ try {
621
+ const result = await executeTeamApiOperation('read-config', {
622
+ team_name: 'nonexistent-cfg',
623
+ }, cwd);
624
+ assert.equal(result.ok, false);
625
+ if (!result.ok)
626
+ assert.equal(result.error.code, 'team_not_found');
627
+ }
628
+ finally {
629
+ await rm(cwd, { recursive: true, force: true });
630
+ }
631
+ });
632
+ it('returns error when team_name missing', async () => {
633
+ const result = await executeTeamApiOperation('read-config', {}, '/tmp');
634
+ assert.equal(result.ok, false);
635
+ });
636
+ });
637
+ // ─── read-manifest ────────────────────────────────────────────────────────
638
+ describe('executeTeamApiOperation: read-manifest', () => {
639
+ it('returns manifest_not_found when manifest does not exist', async () => {
640
+ const { cwd, cleanup } = await setupTeam('rd-mfst');
641
+ try {
642
+ const result = await executeTeamApiOperation('read-manifest', {
643
+ team_name: 'rd-mfst',
644
+ }, cwd);
645
+ assert.ok(result.ok === true || (result.ok === false && result.error.code === 'manifest_not_found'));
646
+ }
647
+ finally {
648
+ await cleanup();
649
+ }
650
+ });
651
+ it('returns error when team_name missing', async () => {
652
+ const result = await executeTeamApiOperation('read-manifest', {}, '/tmp');
653
+ assert.equal(result.ok, false);
654
+ });
655
+ });
656
+ // ─── read-worker-status ───────────────────────────────────────────────────
657
+ describe('executeTeamApiOperation: read-worker-status', () => {
658
+ it('reads worker status', async () => {
659
+ const { cwd, cleanup } = await setupTeam('rd-ws');
660
+ try {
661
+ const result = await executeTeamApiOperation('read-worker-status', {
662
+ team_name: 'rd-ws', worker: 'worker-1',
663
+ }, cwd);
664
+ assert.equal(result.ok, true);
665
+ }
666
+ finally {
667
+ await cleanup();
668
+ }
669
+ });
670
+ it('returns error when required fields missing', async () => {
671
+ const result = await executeTeamApiOperation('read-worker-status', {
672
+ team_name: 'x',
673
+ }, '/tmp');
674
+ assert.equal(result.ok, false);
675
+ });
676
+ });
677
+ // ─── read-worker-heartbeat ────────────────────────────────────────────────
678
+ describe('executeTeamApiOperation: read-worker-heartbeat', () => {
679
+ it('reads worker heartbeat', async () => {
680
+ const { cwd, cleanup } = await setupTeam('rd-hb');
681
+ try {
682
+ const result = await executeTeamApiOperation('read-worker-heartbeat', {
683
+ team_name: 'rd-hb', worker: 'worker-1',
684
+ }, cwd);
685
+ assert.equal(result.ok, true);
686
+ }
687
+ finally {
688
+ await cleanup();
689
+ }
690
+ });
691
+ it('returns error when required fields missing', async () => {
692
+ const result = await executeTeamApiOperation('read-worker-heartbeat', {
693
+ team_name: 'x',
694
+ }, '/tmp');
695
+ assert.equal(result.ok, false);
696
+ });
697
+ });
698
+ // ─── update-worker-heartbeat ──────────────────────────────────────────────
699
+ describe('executeTeamApiOperation: update-worker-heartbeat', () => {
700
+ it('updates worker heartbeat successfully', async () => {
701
+ const { cwd, cleanup } = await setupTeam('upd-hb');
702
+ try {
703
+ const result = await executeTeamApiOperation('update-worker-heartbeat', {
704
+ team_name: 'upd-hb', worker: 'worker-1', pid: 12345, turn_count: 5, alive: true,
705
+ }, cwd);
706
+ assert.equal(result.ok, true);
707
+ }
708
+ finally {
709
+ await cleanup();
710
+ }
711
+ });
712
+ it('returns error when required fields missing or wrong types', async () => {
713
+ const result = await executeTeamApiOperation('update-worker-heartbeat', {
714
+ team_name: 'x', worker: 'w1', pid: 'not-a-number', turn_count: 1, alive: true,
715
+ }, '/tmp');
716
+ assert.equal(result.ok, false);
717
+ });
718
+ });
719
+ // ─── write-worker-inbox ───────────────────────────────────────────────────
720
+ describe('executeTeamApiOperation: write-worker-inbox', () => {
721
+ it('writes to worker inbox', async () => {
722
+ const { cwd, cleanup } = await setupTeam('wr-inbox');
723
+ try {
724
+ const result = await executeTeamApiOperation('write-worker-inbox', {
725
+ team_name: 'wr-inbox', worker: 'worker-1', content: 'Hello worker!',
726
+ }, cwd);
727
+ assert.equal(result.ok, true);
728
+ }
729
+ finally {
730
+ await cleanup();
731
+ }
732
+ });
733
+ it('returns error when required fields missing', async () => {
734
+ const result = await executeTeamApiOperation('write-worker-inbox', {
735
+ team_name: 'x', worker: 'w1',
736
+ }, '/tmp');
737
+ assert.equal(result.ok, false);
738
+ });
739
+ });
740
+ // ─── write-worker-identity ────────────────────────────────────────────────
741
+ describe('executeTeamApiOperation: write-worker-identity', () => {
742
+ it('writes worker identity', async () => {
743
+ const { cwd, cleanup } = await setupTeam('wr-id');
744
+ try {
745
+ const result = await executeTeamApiOperation('write-worker-identity', {
746
+ team_name: 'wr-id', worker: 'worker-1', index: 1, role: 'executor',
747
+ }, cwd);
748
+ assert.equal(result.ok, true);
749
+ }
750
+ finally {
751
+ await cleanup();
752
+ }
753
+ });
754
+ it('writes worker identity with optional fields', async () => {
755
+ const { cwd, cleanup } = await setupTeam('wr-id-opt');
756
+ try {
757
+ const result = await executeTeamApiOperation('write-worker-identity', {
758
+ team_name: 'wr-id-opt', worker: 'worker-1', index: 1, role: 'executor',
759
+ assigned_tasks: ['1', '2'], pid: 9999, pane_id: '%10',
760
+ working_dir: '/tmp', worktree_path: '/wt', worktree_branch: 'main',
761
+ worktree_detached: false, team_state_root: '/state',
762
+ }, cwd);
763
+ assert.equal(result.ok, true);
764
+ }
765
+ finally {
766
+ await cleanup();
767
+ }
768
+ });
769
+ it('returns error when required fields missing', async () => {
770
+ const result = await executeTeamApiOperation('write-worker-identity', {
771
+ team_name: 'x', worker: 'w1',
772
+ }, '/tmp');
773
+ assert.equal(result.ok, false);
774
+ });
775
+ });
776
+ // ─── append-event ─────────────────────────────────────────────────────────
777
+ describe('executeTeamApiOperation: append-event', () => {
778
+ it('appends a valid event', async () => {
779
+ const { cwd, cleanup } = await setupTeam('evt-team');
780
+ try {
781
+ const result = await executeTeamApiOperation('append-event', {
782
+ team_name: 'evt-team', type: 'task_completed', worker: 'worker-1', task_id: '1',
783
+ }, cwd);
784
+ assert.equal(result.ok, true);
785
+ }
786
+ finally {
787
+ await cleanup();
788
+ }
789
+ });
790
+ it('rejects invalid event type', async () => {
791
+ const result = await executeTeamApiOperation('append-event', {
792
+ team_name: 'x', type: 'invalid_type', worker: 'w1',
793
+ }, '/tmp');
794
+ assert.equal(result.ok, false);
795
+ if (!result.ok)
796
+ assert.match(result.error.message, /type must be one of/);
797
+ });
798
+ it('returns error when required fields missing', async () => {
799
+ const result = await executeTeamApiOperation('append-event', {
800
+ team_name: 'x',
801
+ }, '/tmp');
802
+ assert.equal(result.ok, false);
803
+ });
804
+ });
805
+ // ─── get-summary ──────────────────────────────────────────────────────────
806
+ describe('executeTeamApiOperation: get-summary', () => {
807
+ it('returns summary for existing team', async () => {
808
+ const { cwd, cleanup } = await setupTeam('sum-team');
809
+ try {
810
+ const result = await executeTeamApiOperation('get-summary', {
811
+ team_name: 'sum-team',
812
+ }, cwd);
813
+ assert.equal(result.ok, true);
814
+ }
815
+ finally {
816
+ await cleanup();
817
+ }
818
+ });
819
+ it('returns team_not_found for nonexistent team', async () => {
820
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-interop-sum-nf-'));
821
+ try {
822
+ const result = await executeTeamApiOperation('get-summary', {
823
+ team_name: 'nonexistent-sum',
824
+ }, cwd);
825
+ assert.equal(result.ok, false);
826
+ if (!result.ok)
827
+ assert.equal(result.error.code, 'team_not_found');
828
+ }
829
+ finally {
830
+ await rm(cwd, { recursive: true, force: true });
831
+ }
832
+ });
833
+ it('returns error when team_name missing', async () => {
834
+ const result = await executeTeamApiOperation('get-summary', {}, '/tmp');
835
+ assert.equal(result.ok, false);
836
+ });
837
+ });
838
+ // ─── cleanup ──────────────────────────────────────────────────────────────
839
+ describe('executeTeamApiOperation: cleanup', () => {
840
+ it('cleans up team state', async () => {
841
+ const { cwd, cleanup } = await setupTeam('cleanup-team');
842
+ try {
843
+ const result = await executeTeamApiOperation('cleanup', {
844
+ team_name: 'cleanup-team',
845
+ }, cwd);
846
+ assert.equal(result.ok, true);
847
+ if (result.ok)
848
+ assert.equal(result.data.team_name, 'cleanup-team');
849
+ }
850
+ finally {
851
+ await cleanup();
852
+ }
853
+ });
854
+ it('returns error when team_name missing', async () => {
855
+ const result = await executeTeamApiOperation('cleanup', {}, '/tmp');
856
+ assert.equal(result.ok, false);
857
+ });
858
+ });
859
+ // ─── write-shutdown-request ───────────────────────────────────────────────
860
+ describe('executeTeamApiOperation: write-shutdown-request', () => {
861
+ it('writes a shutdown request', async () => {
862
+ const { cwd, cleanup } = await setupTeam('sd-req');
863
+ try {
864
+ const result = await executeTeamApiOperation('write-shutdown-request', {
865
+ team_name: 'sd-req', worker: 'worker-1', requested_by: 'leader-fixed',
866
+ }, cwd);
867
+ assert.equal(result.ok, true);
868
+ }
869
+ finally {
870
+ await cleanup();
871
+ }
872
+ });
873
+ it('returns error when required fields missing', async () => {
874
+ const result = await executeTeamApiOperation('write-shutdown-request', {
875
+ team_name: 'x', worker: 'w1',
876
+ }, '/tmp');
877
+ assert.equal(result.ok, false);
878
+ });
879
+ });
880
+ // ─── read-shutdown-ack ────────────────────────────────────────────────────
881
+ describe('executeTeamApiOperation: read-shutdown-ack', () => {
882
+ it('reads shutdown ack (null when not present)', async () => {
883
+ const { cwd, cleanup } = await setupTeam('sd-ack');
884
+ try {
885
+ const result = await executeTeamApiOperation('read-shutdown-ack', {
886
+ team_name: 'sd-ack', worker: 'worker-1',
887
+ }, cwd);
888
+ assert.equal(result.ok, true);
889
+ }
890
+ finally {
891
+ await cleanup();
892
+ }
893
+ });
894
+ it('supports min_updated_at parameter', async () => {
895
+ const { cwd, cleanup } = await setupTeam('sd-ack-min');
896
+ try {
897
+ const result = await executeTeamApiOperation('read-shutdown-ack', {
898
+ team_name: 'sd-ack-min', worker: 'worker-1', min_updated_at: new Date().toISOString(),
899
+ }, cwd);
900
+ assert.equal(result.ok, true);
901
+ }
902
+ finally {
903
+ await cleanup();
904
+ }
905
+ });
906
+ it('returns error when required fields missing', async () => {
907
+ const result = await executeTeamApiOperation('read-shutdown-ack', {
908
+ team_name: 'x',
909
+ }, '/tmp');
910
+ assert.equal(result.ok, false);
911
+ });
912
+ });
913
+ // ─── read-monitor-snapshot ────────────────────────────────────────────────
914
+ describe('executeTeamApiOperation: read-monitor-snapshot', () => {
915
+ it('reads monitor snapshot', async () => {
916
+ const { cwd, cleanup } = await setupTeam('rd-mon');
917
+ try {
918
+ const result = await executeTeamApiOperation('read-monitor-snapshot', {
919
+ team_name: 'rd-mon',
920
+ }, cwd);
921
+ assert.equal(result.ok, true);
922
+ }
923
+ finally {
924
+ await cleanup();
925
+ }
926
+ });
927
+ it('returns error when team_name missing', async () => {
928
+ const result = await executeTeamApiOperation('read-monitor-snapshot', {}, '/tmp');
929
+ assert.equal(result.ok, false);
930
+ });
931
+ });
932
+ // ─── write-monitor-snapshot ───────────────────────────────────────────────
933
+ describe('executeTeamApiOperation: write-monitor-snapshot', () => {
934
+ it('writes monitor snapshot', async () => {
935
+ const { cwd, cleanup } = await setupTeam('wr-mon');
936
+ try {
937
+ const result = await executeTeamApiOperation('write-monitor-snapshot', {
938
+ team_name: 'wr-mon',
939
+ snapshot: {
940
+ teamName: 'wr-mon',
941
+ phase: 'team-exec',
942
+ workers: [],
943
+ tasks: { total: 0, pending: 0, blocked: 0, in_progress: 0, completed: 0, failed: 0 },
944
+ deadWorkers: [],
945
+ nonReportingWorkers: [],
946
+ },
947
+ }, cwd);
948
+ assert.equal(result.ok, true);
949
+ }
950
+ finally {
951
+ await cleanup();
952
+ }
953
+ });
954
+ it('returns error when snapshot missing', async () => {
955
+ const result = await executeTeamApiOperation('write-monitor-snapshot', {
956
+ team_name: 'x',
957
+ }, '/tmp');
958
+ assert.equal(result.ok, false);
959
+ });
960
+ });
961
+ // ─── read-task-approval ───────────────────────────────────────────────────
962
+ describe('executeTeamApiOperation: read-task-approval', () => {
963
+ it('reads task approval (null when not set)', async () => {
964
+ const { cwd, cleanup } = await setupTeam('rd-appr');
965
+ try {
966
+ const task = await createTask('rd-appr', { subject: 'A', description: 'B', status: 'pending' }, cwd);
967
+ const result = await executeTeamApiOperation('read-task-approval', {
968
+ team_name: 'rd-appr', task_id: task.id,
969
+ }, cwd);
970
+ assert.equal(result.ok, true);
971
+ }
972
+ finally {
973
+ await cleanup();
974
+ }
975
+ });
976
+ it('returns error when required fields missing', async () => {
977
+ const result = await executeTeamApiOperation('read-task-approval', {
978
+ team_name: 'x',
979
+ }, '/tmp');
980
+ assert.equal(result.ok, false);
981
+ });
982
+ });
983
+ // ─── write-task-approval ──────────────────────────────────────────────────
984
+ describe('executeTeamApiOperation: write-task-approval', () => {
985
+ it('writes task approval successfully', async () => {
986
+ const { cwd, cleanup } = await setupTeam('wr-appr');
987
+ try {
988
+ const task = await createTask('wr-appr', { subject: 'A', description: 'B', status: 'pending' }, cwd);
989
+ const result = await executeTeamApiOperation('write-task-approval', {
990
+ team_name: 'wr-appr', task_id: task.id, status: 'approved',
991
+ reviewer: 'leader-fixed', decision_reason: 'Looks good',
992
+ }, cwd);
993
+ assert.equal(result.ok, true);
994
+ }
995
+ finally {
996
+ await cleanup();
997
+ }
998
+ });
999
+ it('rejects invalid approval status', async () => {
1000
+ const result = await executeTeamApiOperation('write-task-approval', {
1001
+ team_name: 'x', task_id: '1', status: 'maybe',
1002
+ reviewer: 'r', decision_reason: 'reason',
1003
+ }, '/tmp');
1004
+ assert.equal(result.ok, false);
1005
+ if (!result.ok)
1006
+ assert.match(result.error.message, /status must be one of/);
1007
+ });
1008
+ it('rejects non-boolean required field', async () => {
1009
+ const result = await executeTeamApiOperation('write-task-approval', {
1010
+ team_name: 'x', task_id: '1', status: 'approved',
1011
+ reviewer: 'r', decision_reason: 'reason', required: 'yes',
1012
+ }, '/tmp');
1013
+ assert.equal(result.ok, false);
1014
+ if (!result.ok)
1015
+ assert.match(result.error.message, /required must be a boolean/);
1016
+ });
1017
+ it('returns error when required fields missing', async () => {
1018
+ const result = await executeTeamApiOperation('write-task-approval', {
1019
+ team_name: 'x', task_id: '1',
1020
+ }, '/tmp');
1021
+ assert.equal(result.ok, false);
1022
+ });
1023
+ });
1024
+ // ─── error envelope (catch block) ─────────────────────────────────────────
1025
+ describe('executeTeamApiOperation: error handling', () => {
1026
+ it('wraps thrown errors in an error envelope', async () => {
1027
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-interop-err-'));
1028
+ try {
1029
+ await mkdir(join(cwd, '.omx', 'state', 'team', 'err-team'), { recursive: true });
1030
+ await writeFile(join(cwd, '.omx', 'state', 'team', 'err-team', 'config.json'), '{}', 'utf8');
1031
+ const result = await executeTeamApiOperation('claim-task', {
1032
+ team_name: 'err-team', task_id: '1', worker: 'w1',
1033
+ }, cwd);
1034
+ assert.ok(result.ok === true || result.ok === false);
1035
+ if (!result.ok) {
1036
+ assert.equal(result.operation, 'claim-task');
1037
+ assert.ok(result.error.code);
1038
+ assert.ok(result.error.message);
1039
+ }
1040
+ }
1041
+ finally {
1042
+ await rm(cwd, { recursive: true, force: true });
1043
+ }
1044
+ });
1045
+ it('resolves team cwd from empty team_name using fallback', async () => {
1046
+ const result = await executeTeamApiOperation('list-tasks', {
1047
+ team_name: '',
1048
+ }, '/tmp');
1049
+ assert.equal(result.ok, false);
1050
+ });
1051
+ });
1052
+ //# sourceMappingURL=api-interop.test.js.map