agent-yes 1.287.0 → 1.288.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/dist/{SUPPORTED_CLIS-DKdFELZL.js → SUPPORTED_CLIS-BHowMgMY.js} +2 -2
- package/dist/{SUPPORTED_CLIS-5YPYQ8Iw.js → SUPPORTED_CLIS-ChhJyq4h.js} +3 -3
- package/dist/{agentShare-B525ly6v.js → agentShare-T3RskvDT.js} +2 -2
- package/dist/{callback-C-awM4qn.js → callback-CuohbCJA.js} +3 -3
- package/dist/{callback-BLTW4Mgp.js → callback-DeaETuHt.js} +2 -2
- package/dist/cli.js +5 -5
- package/dist/{cmdPs-BRtmnpsM.js → cmdPs-Bm4JG7hi.js} +2 -2
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-DshQvO-4.js → notifyDaemon-BORqPeAl.js} +2 -2
- package/dist/{parentPingLoop-Bjw6hKoZ.js → parentPingLoop-COePRYr9.js} +2 -2
- package/dist/{rustBinary-C57KWEtX.js → rustBinary-BmXzqj5z.js} +2 -2
- package/dist/{rustBinary-BXhp8yBp.js → rustBinary-D12k7iOD.js} +2 -2
- package/dist/{schedule-Boa0nE0V.js → schedule-DLoUmyJh.js} +4 -4
- package/dist/{serve-7mcHebep.js → serve-DaDlOtss.js} +17 -17
- package/dist/{setup-DXRL-Mdo.js → setup-Cqy8nRrq.js} +2 -2
- package/dist/{subcommands-BftthBK7.js → subcommands-BfiOyfz7.js} +15 -15
- package/dist/{subcommands-BQJ-Uqtl.js → subcommands-DKTa3E6O.js} +1 -1
- package/dist/{terminal-DrYf3HP0.js → terminal-C0tnGnrZ.js} +2 -2
- package/dist/trayApp-BrAMlCY7.js +5 -0
- package/dist/{trayApp-DG2fQnhx.js → trayApp-DvL9qW6V.js} +2 -2
- package/dist/{ts-LDnoqCV2.js → ts-DJLp2_bv.js} +3 -3
- package/dist/{versionChecker-CGmyt2qE.js → versionChecker-CkO7wYyV.js} +2 -2
- package/dist/{widget-B2SluyG1.js → widget-CJqpScCG.js} +3 -3
- package/dist/{ws-DWA90-uE.js → ws-DGENGNrr.js} +2 -2
- package/dist/{ws-CTOnVXg8.js → ws-DzCKvzWD.js} +2 -2
- package/lab/ui/index.html +95 -67
- package/package.json +2 -1
- package/ts/JsonlStore.bun.spec.ts +204 -0
- package/ts/ReadyManager.bun.spec.ts +72 -0
- package/ts/agentNice.bun.spec.ts +24 -0
- package/ts/agentPermissions.bun.spec.ts +117 -0
- package/ts/agentShare.bun.spec.ts +240 -0
- package/ts/agentTree.bun.spec.ts +92 -0
- package/ts/agentYesHome.bun.spec.ts +25 -0
- package/ts/askApi.bun.spec.ts +358 -0
- package/ts/askCli.bun.spec.ts +304 -0
- package/ts/autoRetry.bun.spec.ts +123 -0
- package/ts/badges.bun.spec.ts +187 -0
- package/ts/buildRustArgs.bun.spec.ts +351 -0
- package/ts/callbackCore.bun.spec.ts +139 -0
- package/ts/channels/hlc.bun.spec.ts +56 -0
- package/ts/channels/link.bun.spec.ts +72 -0
- package/ts/channels/op.bun.spec.ts +78 -0
- package/ts/channels/store.browser.bun.spec.ts +60 -0
- package/ts/channels/store.bun.spec.ts +145 -0
- package/ts/channels/store.node.bun.spec.ts +76 -0
- package/ts/channels/trust.bun.spec.ts +86 -0
- package/ts/channels.bun.spec.ts +212 -0
- package/ts/cli-idle.bun.spec.ts +20 -0
- package/ts/cmdDsh.bun.spec.ts +90 -0
- package/ts/configLoader.bun.spec.ts +273 -0
- package/ts/cwdConflictWarn.bun.spec.ts +60 -0
- package/ts/cwdPassthroughHint.bun.spec.ts +74 -0
- package/ts/e2e-crypto.bun.spec.ts +235 -0
- package/ts/forkNested.bun.spec.ts +240 -0
- package/ts/histStore.bun.spec.ts +355 -0
- package/ts/identity.bun.spec.ts +190 -0
- package/ts/idleWaiter.bun.spec.ts +91 -0
- package/ts/initMsg.bun.spec.ts +132 -0
- package/ts/invokedCli.bun.spec.ts +30 -0
- package/ts/ipcLock.bun.spec.ts +107 -0
- package/ts/logger.bun.spec.ts +27 -0
- package/ts/lsWatch.bun.spec.ts +61 -0
- package/ts/messageLog.bun.spec.ts +145 -0
- package/ts/needsInput.bun.spec.ts +157 -0
- package/ts/nodeRuntime.bun.spec.ts +99 -0
- package/ts/notifyDaemon.bun.spec.ts +297 -0
- package/ts/notifyInbox.bun.spec.ts +277 -0
- package/ts/notifyRouter.bun.spec.ts +351 -0
- package/ts/notifyStore.bun.spec.ts +403 -0
- package/ts/ownerHeartbeat.bun.spec.ts +175 -0
- package/ts/parentLink.bun.spec.ts +64 -0
- package/ts/parentPing.bun.spec.ts +176 -0
- package/ts/parentPingLoop.bun.spec.ts +79 -0
- package/ts/parentPingSend.bun.spec.ts +195 -0
- package/ts/parentWatching.bun.spec.ts +61 -0
- package/ts/pidStore.bun.spec.ts +372 -0
- package/ts/procStartTime.bun.spec.ts +117 -0
- package/ts/reaper.bun.spec.ts +86 -0
- package/ts/removeControlCharacters.bun.spec.ts +94 -0
- package/ts/resolveBinary.bun.spec.ts +133 -0
- package/ts/resultEnvelope.bun.spec.ts +46 -0
- package/ts/runningLock.bun.spec.ts +485 -0
- package/ts/rustBinary.bun.spec.ts +81 -0
- package/ts/schedule.bun.spec.ts +30 -0
- package/ts/serve.bun.spec.ts +166 -0
- package/ts/serveLock.bun.spec.ts +220 -0
- package/ts/session-integration.bun.spec.ts +93 -0
- package/ts/setup.bun.spec.ts +42 -0
- package/ts/share.bun.spec.ts +36 -0
- package/ts/sizeNego.bun.spec.ts +56 -0
- package/ts/statusText.bun.spec.ts +17 -0
- package/ts/strayServe.bun.spec.ts +50 -0
- package/ts/subcommandMirror.bun.spec.ts +76 -0
- package/ts/systemPathLink.bun.spec.ts +79 -0
- package/ts/termToken.bun.spec.ts +74 -0
- package/ts/terminal.bun.spec.ts +50 -0
- package/ts/terminalReply.bun.spec.ts +36 -0
- package/ts/titleScanner.bun.spec.ts +65 -0
- package/ts/todoAutomation.bun.spec.ts +253 -0
- package/ts/todoBlock.bun.spec.ts +73 -0
- package/ts/todoDigest.bun.spec.ts +184 -0
- package/ts/todoIdentity.bun.spec.ts +85 -0
- package/ts/todoLifecycle.bun.spec.ts +79 -0
- package/ts/todoParse.bun.spec.ts +98 -0
- package/ts/todoRoot.bun.spec.ts +117 -0
- package/ts/todoStore.bun.spec.ts +727 -0
- package/ts/tryCatch.bun.spec.ts +275 -0
- package/ts/webrtcLink.bun.spec.ts +45 -0
- package/ts/workspaceConfig.bun.spec.ts +319 -0
- package/dist/trayApp-1AQ6X2V9.js +0 -5
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { tryCatch } from "./tryCatch";
|
|
3
|
+
|
|
4
|
+
describe("tryCatch", () => {
|
|
5
|
+
describe("direct overload", () => {
|
|
6
|
+
it("should catch errors and call catchFn with error, attempts, robustFn, and args", () => {
|
|
7
|
+
let catchedError: unknown;
|
|
8
|
+
let catchedAttempts: unknown;
|
|
9
|
+
let catchedFn: unknown;
|
|
10
|
+
let catchedArgs!: unknown[];
|
|
11
|
+
const catchFn = (error: unknown, attempts: number, fn: unknown, ...args: unknown[]) => {
|
|
12
|
+
catchedError = error;
|
|
13
|
+
catchedAttempts = attempts;
|
|
14
|
+
catchedFn = fn;
|
|
15
|
+
catchedArgs = args;
|
|
16
|
+
return "caught";
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let calledArgs: unknown[] = [];
|
|
20
|
+
const errorFn = (...args: unknown[]) => {
|
|
21
|
+
calledArgs = args;
|
|
22
|
+
throw new Error("test error");
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const wrappedFn = tryCatch(catchFn, errorFn);
|
|
26
|
+
const result = wrappedFn("arg1", "arg2");
|
|
27
|
+
|
|
28
|
+
expect(result).toBe("caught");
|
|
29
|
+
expect(catchedError).toBeInstanceOf(Error);
|
|
30
|
+
expect(catchedAttempts).toBe(1);
|
|
31
|
+
expect(catchedFn).toBe(wrappedFn);
|
|
32
|
+
expect(catchedArgs).toEqual(["arg1", "arg2"]);
|
|
33
|
+
expect(calledArgs).toEqual(["arg1", "arg2"]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should return normal result when no error occurs directly", () => {
|
|
37
|
+
let catchCalled = false;
|
|
38
|
+
const catchFn = () => {
|
|
39
|
+
catchCalled = true;
|
|
40
|
+
return "error";
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let calledArgs: unknown[] = [];
|
|
44
|
+
const normalFn = (...args: unknown[]) => {
|
|
45
|
+
calledArgs = args;
|
|
46
|
+
return "success";
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const wrappedFn = tryCatch(catchFn, normalFn);
|
|
50
|
+
const result = wrappedFn("arg1", "arg2");
|
|
51
|
+
|
|
52
|
+
expect(result).toBe("success");
|
|
53
|
+
expect(catchCalled).toBe(false);
|
|
54
|
+
expect(calledArgs).toEqual(["arg1", "arg2"]);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("error handling", () => {
|
|
59
|
+
it("should handle different error types and pass function context", () => {
|
|
60
|
+
const results: unknown[] = [];
|
|
61
|
+
const functions: unknown[] = [];
|
|
62
|
+
const catchFn = (error: unknown, _attempts: number, fn: unknown, ..._args: unknown[]) => {
|
|
63
|
+
results.push(error);
|
|
64
|
+
functions.push(fn);
|
|
65
|
+
return "handled";
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// String error
|
|
69
|
+
const stringErrorFn = () => {
|
|
70
|
+
throw "string error";
|
|
71
|
+
};
|
|
72
|
+
const wrappedStringFn = tryCatch(catchFn, stringErrorFn);
|
|
73
|
+
expect(wrappedStringFn()).toBe("handled");
|
|
74
|
+
expect(results[0]).toBe("string error");
|
|
75
|
+
expect(functions[0]).toBe(wrappedStringFn);
|
|
76
|
+
|
|
77
|
+
// Object error
|
|
78
|
+
const objectError = { message: "object error" };
|
|
79
|
+
const objectErrorFn = () => {
|
|
80
|
+
throw objectError;
|
|
81
|
+
};
|
|
82
|
+
const wrappedObjectFn = tryCatch(catchFn, objectErrorFn);
|
|
83
|
+
expect(wrappedObjectFn()).toBe("handled");
|
|
84
|
+
expect(results[1]).toBe(objectError);
|
|
85
|
+
expect(functions[1]).toBe(wrappedObjectFn);
|
|
86
|
+
|
|
87
|
+
// null error
|
|
88
|
+
const nullErrorFn = () => {
|
|
89
|
+
throw null;
|
|
90
|
+
};
|
|
91
|
+
const wrappedNullFn = tryCatch(catchFn, nullErrorFn);
|
|
92
|
+
expect(wrappedNullFn()).toBe("handled");
|
|
93
|
+
expect(results[2]).toBe(null);
|
|
94
|
+
expect(functions[2]).toBe(wrappedNullFn);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should preserve function parameters and pass them to catchFn", () => {
|
|
98
|
+
let caughtError: unknown;
|
|
99
|
+
let caughtAttempts: unknown;
|
|
100
|
+
let caughtFn: unknown;
|
|
101
|
+
let caughtArgs!: unknown[];
|
|
102
|
+
const catchFn = (error: unknown, attempts: number, fn: unknown, ...args: unknown[]) => {
|
|
103
|
+
caughtError = error;
|
|
104
|
+
caughtAttempts = attempts;
|
|
105
|
+
caughtFn = fn;
|
|
106
|
+
caughtArgs = args;
|
|
107
|
+
return "caught";
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
let testArgs: [number, string, boolean] | undefined;
|
|
111
|
+
const testFn = (a: number, b: string, c: boolean) => {
|
|
112
|
+
testArgs = [a, b, c];
|
|
113
|
+
if (a > 5) throw new Error("too big");
|
|
114
|
+
return `${a}-${b}-${c}`;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const wrappedFn = tryCatch(catchFn, testFn);
|
|
118
|
+
|
|
119
|
+
// Normal execution
|
|
120
|
+
expect(wrappedFn(3, "test", true)).toBe("3-test-true");
|
|
121
|
+
expect(testArgs).toEqual([3, "test", true]);
|
|
122
|
+
|
|
123
|
+
// Error execution
|
|
124
|
+
expect(wrappedFn(10, "error", false)).toBe("caught");
|
|
125
|
+
expect(testArgs).toEqual([10, "error", false]);
|
|
126
|
+
expect(caughtError).toBeInstanceOf(Error);
|
|
127
|
+
expect(caughtAttempts).toBe(2);
|
|
128
|
+
expect(caughtFn).toBe(wrappedFn);
|
|
129
|
+
expect(caughtArgs).toEqual([10, "error", false]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should handle functions with no parameters", () => {
|
|
133
|
+
let caughtError: unknown;
|
|
134
|
+
let caughtAttempts: unknown;
|
|
135
|
+
let caughtFn: unknown;
|
|
136
|
+
let caughtArgs!: unknown[];
|
|
137
|
+
const catchFn = (error: unknown, attempts: number, fn: unknown, ...args: unknown[]) => {
|
|
138
|
+
caughtError = error;
|
|
139
|
+
caughtAttempts = attempts;
|
|
140
|
+
caughtFn = fn;
|
|
141
|
+
caughtArgs = args;
|
|
142
|
+
return "no params caught";
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
let called = false;
|
|
146
|
+
const noParamsFn = () => {
|
|
147
|
+
called = true;
|
|
148
|
+
throw new Error("no params error");
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const wrappedFn = tryCatch(catchFn, noParamsFn);
|
|
152
|
+
const result = wrappedFn();
|
|
153
|
+
|
|
154
|
+
expect(result).toBe("no params caught");
|
|
155
|
+
expect(called).toBe(true);
|
|
156
|
+
expect(caughtError).toBeInstanceOf(Error);
|
|
157
|
+
expect(caughtAttempts).toBe(1);
|
|
158
|
+
expect(caughtFn).toBe(wrappedFn);
|
|
159
|
+
expect(caughtArgs).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should handle functions returning different types", () => {
|
|
163
|
+
const catchFn = () => null;
|
|
164
|
+
|
|
165
|
+
// Function returning number
|
|
166
|
+
const numberFn = tryCatch(catchFn, () => 42);
|
|
167
|
+
expect(numberFn()).toBe(42);
|
|
168
|
+
|
|
169
|
+
// Function returning object
|
|
170
|
+
const obj = { key: "value" };
|
|
171
|
+
const objectFn = tryCatch(catchFn, () => obj);
|
|
172
|
+
expect(objectFn()).toBe(obj);
|
|
173
|
+
|
|
174
|
+
// Function returning undefined
|
|
175
|
+
const undefinedFn = tryCatch(catchFn, () => undefined);
|
|
176
|
+
expect(undefinedFn()).toBeUndefined();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("attempts tracking", () => {
|
|
181
|
+
it("should increment attempts on each call", () => {
|
|
182
|
+
const attemptsList: number[] = [];
|
|
183
|
+
const catchFn = (_error: unknown, attempts: number) => {
|
|
184
|
+
attemptsList.push(attempts);
|
|
185
|
+
return "caught";
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const errorFn = () => {
|
|
189
|
+
throw new Error("fail");
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const wrappedFn = tryCatch(catchFn, errorFn);
|
|
193
|
+
wrappedFn();
|
|
194
|
+
wrappedFn();
|
|
195
|
+
wrappedFn();
|
|
196
|
+
|
|
197
|
+
expect(attemptsList).toEqual([1, 2, 3]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should count attempts for both successful and failed calls", () => {
|
|
201
|
+
let lastAttempts = 0;
|
|
202
|
+
const catchFn = (_error: unknown, attempts: number) => {
|
|
203
|
+
lastAttempts = attempts;
|
|
204
|
+
return -1;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
let callCount = 0;
|
|
208
|
+
const sometimesFails = () => {
|
|
209
|
+
callCount++;
|
|
210
|
+
if (callCount % 2 === 0) throw new Error("even call");
|
|
211
|
+
return callCount;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const wrappedFn = tryCatch(catchFn, sometimesFails);
|
|
215
|
+
expect(wrappedFn()).toBe(1); // attempt 1, success
|
|
216
|
+
expect(wrappedFn()).toBe(-1); // attempt 2, fail
|
|
217
|
+
expect(lastAttempts).toBe(2);
|
|
218
|
+
expect(wrappedFn()).toBe(3); // attempt 3, success
|
|
219
|
+
expect(wrappedFn()).toBe(-1); // attempt 4, fail
|
|
220
|
+
expect(lastAttempts).toBe(4);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should allow retry via robustFn", () => {
|
|
224
|
+
let callCount = 0;
|
|
225
|
+
const catchFn = (_error: unknown, attempts: number, retry: () => number) => {
|
|
226
|
+
if (attempts < 3) return retry();
|
|
227
|
+
return -1;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const unreliableFn = () => {
|
|
231
|
+
callCount++;
|
|
232
|
+
if (callCount < 3) throw new Error("not yet");
|
|
233
|
+
return 42;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const wrappedFn = tryCatch(catchFn, unreliableFn);
|
|
237
|
+
expect(wrappedFn()).toBe(42);
|
|
238
|
+
expect(callCount).toBe(3);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("type safety", () => {
|
|
243
|
+
it("should maintain function signature", () => {
|
|
244
|
+
const catchFn = (_error: unknown, _attempts: number, _fn: unknown, ..._args: unknown[]) =>
|
|
245
|
+
"error";
|
|
246
|
+
const originalFn = (a: number, b: string): string => `${a}-${b}`;
|
|
247
|
+
|
|
248
|
+
const wrappedFn = tryCatch(catchFn, originalFn);
|
|
249
|
+
|
|
250
|
+
// This should be type-safe
|
|
251
|
+
const result: string = wrappedFn(1, "test");
|
|
252
|
+
expect(result).toBe("1-test");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should pass function reference and arguments to catchFn", () => {
|
|
256
|
+
let capturedFn: unknown;
|
|
257
|
+
let capturedArgs!: unknown[];
|
|
258
|
+
const catchFn = (_error: unknown, _attempts: number, fn: unknown, ...args: unknown[]) => {
|
|
259
|
+
capturedFn = fn;
|
|
260
|
+
capturedArgs = args;
|
|
261
|
+
return "handled";
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const testFn = (_x: number, _y: string) => {
|
|
265
|
+
throw new Error("test");
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const wrappedFn = tryCatch(catchFn, testFn);
|
|
269
|
+
wrappedFn(42, "hello");
|
|
270
|
+
|
|
271
|
+
expect(capturedFn).toBe(wrappedFn);
|
|
272
|
+
expect(capturedArgs).toEqual([42, "hello"]);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { DEFAULT_SIGHOST, isWebrtcSpec, parseWebrtcLink } from "./webrtcLink.ts";
|
|
3
|
+
|
|
4
|
+
// A representative v2 share secret (e1.<64 hex>); parseSecret keeps the hex `s`.
|
|
5
|
+
const SECRET = "e1.982610a3034f065bfe9700037b306a6afeb7dc48567064058e6c4bbc09e502c2";
|
|
6
|
+
|
|
7
|
+
describe("isWebrtcSpec", () => {
|
|
8
|
+
it("accepts webrtc:// links", () => {
|
|
9
|
+
expect(isWebrtcSpec(`webrtc://r1:${SECRET}@s.agent-yes.com`)).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
it("accepts https share links (have a # fragment)", () => {
|
|
12
|
+
expect(isWebrtcSpec(`https://agent-yes.com/w/#r1:${SECRET}`)).toBe(true);
|
|
13
|
+
expect(isWebrtcSpec(`http://localhost:8080/w/#r1:${SECRET}`)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
it("rejects http remotes and bare aliases", () => {
|
|
16
|
+
expect(isWebrtcSpec("token@192.168.1.5:7432")).toBe(false);
|
|
17
|
+
expect(isWebrtcSpec("work-mac")).toBe(false);
|
|
18
|
+
expect(isWebrtcSpec("work-mac:claude")).toBe(false);
|
|
19
|
+
expect(isWebrtcSpec("http://192.168.1.5:7432")).toBe(false); // no fragment
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("parseWebrtcLink", () => {
|
|
24
|
+
it("parses webrtc://room:token@host", () => {
|
|
25
|
+
const r = parseWebrtcLink(`webrtc://r223104:${SECRET}@example.com`);
|
|
26
|
+
expect(r).toEqual({ room: "r223104", s: expect.any(String), host: "example.com" });
|
|
27
|
+
expect(r!.s.length).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("parses an https share link and defaults the signaling host", () => {
|
|
31
|
+
const r = parseWebrtcLink(`https://agent-yes.com/w/#r223104:${SECRET}`);
|
|
32
|
+
expect(r).toMatchObject({ room: "r223104", host: DEFAULT_SIGHOST });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("honors an explicit @sighost in the fragment", () => {
|
|
36
|
+
const r = parseWebrtcLink(`https://agent-yes.com/w/#r1:${SECRET}@sig.example.com`);
|
|
37
|
+
expect(r).toMatchObject({ room: "r1", host: "sig.example.com" });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns null for non-share strings and malformed fragments", () => {
|
|
41
|
+
expect(parseWebrtcLink("token@host:7432")).toBeNull();
|
|
42
|
+
expect(parseWebrtcLink("just-an-alias")).toBeNull();
|
|
43
|
+
expect(parseWebrtcLink("https://agent-yes.com/w/#noColonHere")).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
|
+
import { homedir, tmpdir } from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import {
|
|
6
|
+
expandTilde,
|
|
7
|
+
getMaxAgents,
|
|
8
|
+
getMinFreeMb,
|
|
9
|
+
getSpawnWaitMs,
|
|
10
|
+
getProvisionAllowlist,
|
|
11
|
+
getProvisionHook,
|
|
12
|
+
getProvisionRoot,
|
|
13
|
+
getSpawnHook,
|
|
14
|
+
getWorkspaceRoot,
|
|
15
|
+
hasProvisionHook,
|
|
16
|
+
hasSpawnHook,
|
|
17
|
+
isProvisionAllowed,
|
|
18
|
+
resolveSpawnCwd,
|
|
19
|
+
setWorkspaceRoot,
|
|
20
|
+
} from "./workspaceConfig.ts";
|
|
21
|
+
|
|
22
|
+
describe("workspaceConfig", () => {
|
|
23
|
+
let original: string | undefined;
|
|
24
|
+
let tmp: string;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
original = process.env.AGENT_YES_HOME;
|
|
27
|
+
tmp = mkdtempSync(path.join(tmpdir(), "ay-cfg-"));
|
|
28
|
+
process.env.AGENT_YES_HOME = tmp;
|
|
29
|
+
});
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
if (original === undefined) delete process.env.AGENT_YES_HOME;
|
|
32
|
+
else process.env.AGENT_YES_HOME = original;
|
|
33
|
+
delete process.env.CODEHOST_WS_ROOT;
|
|
34
|
+
delete process.env.CODEHOST_PROVISION_ALLOWLIST;
|
|
35
|
+
delete process.env.AGENT_YES_SPAWN_HOOK;
|
|
36
|
+
delete process.env.AGENT_YES_PROVISION_HOOK;
|
|
37
|
+
delete process.env.AGENT_YES_MAX_AGENTS;
|
|
38
|
+
delete process.env.AGENT_YES_MIN_FREE_MB;
|
|
39
|
+
delete process.env.AGENT_YES_SPAWN_WAIT_MS;
|
|
40
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("defaults the workspace root to the home dir when unset", () => {
|
|
44
|
+
expect(getWorkspaceRoot()).toBe(homedir());
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("round-trips set/get and resolves to an absolute path", () => {
|
|
48
|
+
const saved = setWorkspaceRoot(path.join(tmp, "ws"));
|
|
49
|
+
expect(saved).toBe(path.join(tmp, "ws"));
|
|
50
|
+
expect(getWorkspaceRoot()).toBe(path.join(tmp, "ws"));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("expands a leading ~", () => {
|
|
54
|
+
expect(expandTilde("~")).toBe(homedir());
|
|
55
|
+
expect(expandTilde("~/projects")).toBe(path.join(homedir(), "projects"));
|
|
56
|
+
expect(expandTilde("/abs/path")).toBe("/abs/path");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("stores a tilde path as home-based absolute", () => {
|
|
60
|
+
const saved = setWorkspaceRoot("~/myws");
|
|
61
|
+
expect(saved).toBe(path.join(homedir(), "myws"));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("resolveSpawnCwd", () => {
|
|
65
|
+
beforeEach(() => setWorkspaceRoot(path.join(tmp, "ws")));
|
|
66
|
+
|
|
67
|
+
it("empty input → workspace root", () => {
|
|
68
|
+
expect(resolveSpawnCwd("")).toBe(path.join(tmp, "ws"));
|
|
69
|
+
expect(resolveSpawnCwd(undefined)).toBe(path.join(tmp, "ws"));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("bare name → <workspace>/<name>", () => {
|
|
73
|
+
expect(resolveSpawnCwd("myproject")).toBe(path.join(tmp, "ws", "myproject"));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("absolute path → used as-is", () => {
|
|
77
|
+
// path.resolve keeps a POSIX-absolute path on Unix but anchors it to the
|
|
78
|
+
// current drive on Windows (→ C:\tmp\elsewhere); compare against the same
|
|
79
|
+
// resolution the impl uses so the assertion holds on both (cf. "a/b" below).
|
|
80
|
+
expect(resolveSpawnCwd("/tmp/elsewhere")).toBe(path.resolve("/tmp/elsewhere"));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("tilde path → home-based", () => {
|
|
84
|
+
expect(resolveSpawnCwd("~/docs")).toBe(path.join(homedir(), "docs"));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("a relative path with a separator is resolved, not joined to the workspace", () => {
|
|
88
|
+
expect(resolveSpawnCwd("a/b")).toBe(path.resolve("a/b"));
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const writeConfig = (c: Record<string, unknown>) =>
|
|
93
|
+
writeFileSync(path.join(tmp, "config.json"), JSON.stringify(c));
|
|
94
|
+
|
|
95
|
+
describe("getProvisionRoot", () => {
|
|
96
|
+
it("is undefined when neither env nor config is set", () => {
|
|
97
|
+
expect(getProvisionRoot()).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("returns the configured provisionRoot, resolved", () => {
|
|
101
|
+
writeConfig({ provisionRoot: "/code" });
|
|
102
|
+
expect(getProvisionRoot()).toBe(path.resolve("/code"));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("env CODEHOST_WS_ROOT overrides the config and expands ~", () => {
|
|
106
|
+
writeConfig({ provisionRoot: "/code" });
|
|
107
|
+
process.env.CODEHOST_WS_ROOT = "~/ws";
|
|
108
|
+
expect(getProvisionRoot()).toBe(path.join(homedir(), "ws"));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("ignores a blank configured value", () => {
|
|
112
|
+
writeConfig({ provisionRoot: " " });
|
|
113
|
+
expect(getProvisionRoot()).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("getProvisionAllowlist", () => {
|
|
118
|
+
it("is empty when unset", () => {
|
|
119
|
+
expect(getProvisionAllowlist()).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("reads and normalizes the configured list (trim/lowercase/drop empties)", () => {
|
|
123
|
+
writeConfig({ provisionAllowlist: [" Snomiao ", "Acme/Repo", ""] });
|
|
124
|
+
expect(getProvisionAllowlist()).toEqual(["snomiao", "acme/repo"]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("env CODEHOST_PROVISION_ALLOWLIST (comma-separated) overrides config", () => {
|
|
128
|
+
writeConfig({ provisionAllowlist: ["snomiao"] });
|
|
129
|
+
process.env.CODEHOST_PROVISION_ALLOWLIST = "Foo, bar/baz ,";
|
|
130
|
+
expect(getProvisionAllowlist()).toEqual(["foo", "bar/baz"]);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("isProvisionAllowed", () => {
|
|
135
|
+
it("denies everything when the allowlist is empty (secure default)", () => {
|
|
136
|
+
expect(isProvisionAllowed("snomiao", "agent-yes")).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("'*' allows any owner/repo", () => {
|
|
140
|
+
writeConfig({ provisionAllowlist: ["*"] });
|
|
141
|
+
expect(isProvisionAllowed("anyone", "anything")).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("matches by owner case-insensitively and rejects others", () => {
|
|
145
|
+
writeConfig({ provisionAllowlist: ["snomiao"] });
|
|
146
|
+
expect(isProvisionAllowed("SNOMIAO", "x")).toBe(true);
|
|
147
|
+
expect(isProvisionAllowed("evil", "x")).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("matches an exact owner/repo and an owner/* glob", () => {
|
|
151
|
+
writeConfig({ provisionAllowlist: ["acme/widget", "org/*"] });
|
|
152
|
+
expect(isProvisionAllowed("acme", "widget")).toBe(true);
|
|
153
|
+
expect(isProvisionAllowed("acme", "other")).toBe(false);
|
|
154
|
+
expect(isProvisionAllowed("org", "anything")).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("getMaxAgents", () => {
|
|
159
|
+
it("is undefined (unlimited) when neither env nor config is set", () => {
|
|
160
|
+
expect(getMaxAgents()).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("reads a positive integer from config", () => {
|
|
164
|
+
writeConfig({ maxAgents: 8 });
|
|
165
|
+
expect(getMaxAgents()).toBe(8);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("env AGENT_YES_MAX_AGENTS overrides config", () => {
|
|
169
|
+
writeConfig({ maxAgents: 8 });
|
|
170
|
+
process.env.AGENT_YES_MAX_AGENTS = "3";
|
|
171
|
+
expect(getMaxAgents()).toBe(3);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("floors a fractional value", () => {
|
|
175
|
+
process.env.AGENT_YES_MAX_AGENTS = "4.9";
|
|
176
|
+
expect(getMaxAgents()).toBe(4);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("treats 0, negative, and garbage as unlimited (undefined)", () => {
|
|
180
|
+
writeConfig({ maxAgents: 0 });
|
|
181
|
+
expect(getMaxAgents()).toBeUndefined();
|
|
182
|
+
process.env.AGENT_YES_MAX_AGENTS = "-5";
|
|
183
|
+
expect(getMaxAgents()).toBeUndefined();
|
|
184
|
+
process.env.AGENT_YES_MAX_AGENTS = "lots";
|
|
185
|
+
expect(getMaxAgents()).toBeUndefined();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("treats a fractional value < 1 as unlimited, not a 0 hard-cap", () => {
|
|
189
|
+
// Regression: 0.5 must NOT floor to 0 (which would reject every spawn).
|
|
190
|
+
process.env.AGENT_YES_MAX_AGENTS = "0.5";
|
|
191
|
+
expect(getMaxAgents()).toBeUndefined();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("getMinFreeMb", () => {
|
|
196
|
+
it("is undefined (no floor) when unset", () => {
|
|
197
|
+
expect(getMinFreeMb()).toBeUndefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("reads config and lets env override", () => {
|
|
201
|
+
writeConfig({ minFreeMb: 1024 });
|
|
202
|
+
expect(getMinFreeMb()).toBe(1024);
|
|
203
|
+
process.env.AGENT_YES_MIN_FREE_MB = "2048";
|
|
204
|
+
expect(getMinFreeMb()).toBe(2048);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("treats non-positive/garbage/sub-1 as no floor", () => {
|
|
208
|
+
process.env.AGENT_YES_MIN_FREE_MB = "0";
|
|
209
|
+
expect(getMinFreeMb()).toBeUndefined();
|
|
210
|
+
process.env.AGENT_YES_MIN_FREE_MB = "nope";
|
|
211
|
+
expect(getMinFreeMb()).toBeUndefined();
|
|
212
|
+
process.env.AGENT_YES_MIN_FREE_MB = "0.5";
|
|
213
|
+
expect(getMinFreeMb()).toBeUndefined();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("getSpawnWaitMs", () => {
|
|
218
|
+
it("defaults to 10 minutes when unset", () => {
|
|
219
|
+
expect(getSpawnWaitMs()).toBe(600_000);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("reads config and lets env override; allows 0 (don't wait)", () => {
|
|
223
|
+
writeConfig({ spawnWaitMs: 5000 });
|
|
224
|
+
expect(getSpawnWaitMs()).toBe(5000);
|
|
225
|
+
process.env.AGENT_YES_SPAWN_WAIT_MS = "0";
|
|
226
|
+
expect(getSpawnWaitMs()).toBe(0);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("falls back to the default on negative/garbage", () => {
|
|
230
|
+
process.env.AGENT_YES_SPAWN_WAIT_MS = "-1";
|
|
231
|
+
expect(getSpawnWaitMs()).toBe(600_000);
|
|
232
|
+
process.env.AGENT_YES_SPAWN_WAIT_MS = "soon";
|
|
233
|
+
expect(getSpawnWaitMs()).toBe(600_000);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("getSpawnHook / hasSpawnHook", () => {
|
|
238
|
+
const isPosix = process.platform !== "win32";
|
|
239
|
+
|
|
240
|
+
it("is null/false when unset", () => {
|
|
241
|
+
expect(getSpawnHook()).toBeNull();
|
|
242
|
+
expect(hasSpawnHook()).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("returns the configured hook from a private (0600) config", () => {
|
|
246
|
+
writeConfig({ spawnHook: 'echo hi >&2\nexec "$@"' });
|
|
247
|
+
if (isPosix) chmodSync(path.join(tmp, "config.json"), 0o600);
|
|
248
|
+
expect(getSpawnHook()).toBe('echo hi >&2\nexec "$@"');
|
|
249
|
+
expect(hasSpawnHook()).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("ignores a blank hook", () => {
|
|
253
|
+
writeConfig({ spawnHook: " " });
|
|
254
|
+
expect(getSpawnHook()).toBeNull();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("env AGENT_YES_SPAWN_HOOK overrides the config", () => {
|
|
258
|
+
writeConfig({ spawnHook: "from-file" });
|
|
259
|
+
process.env.AGENT_YES_SPAWN_HOOK = "from-env";
|
|
260
|
+
expect(getSpawnHook()).toBe("from-env");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it.skipIf(!isPosix)(
|
|
264
|
+
"refuses a file-backed hook when the config is group/world-writable (tampering guard)",
|
|
265
|
+
() => {
|
|
266
|
+
writeConfig({ spawnHook: "echo pwned" });
|
|
267
|
+
chmodSync(path.join(tmp, "config.json"), 0o666);
|
|
268
|
+
expect(getSpawnHook()).toBeNull();
|
|
269
|
+
chmodSync(path.join(tmp, "config.json"), 0o600);
|
|
270
|
+
expect(getSpawnHook()).toBe("echo pwned");
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("getProvisionHook / hasProvisionHook", () => {
|
|
276
|
+
const isPosix = process.platform !== "win32";
|
|
277
|
+
|
|
278
|
+
it("is null/false when unset", () => {
|
|
279
|
+
expect(getProvisionHook()).toBeNull();
|
|
280
|
+
expect(hasProvisionHook()).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("returns the configured hook from a private (0600) config", () => {
|
|
284
|
+
writeConfig({ provisionHook: 'gh auth switch --user "$KOHO_OWNER"' });
|
|
285
|
+
if (isPosix) chmodSync(path.join(tmp, "config.json"), 0o600);
|
|
286
|
+
expect(getProvisionHook()).toBe('gh auth switch --user "$KOHO_OWNER"');
|
|
287
|
+
expect(hasProvisionHook()).toBe(true);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("ignores a blank hook", () => {
|
|
291
|
+
writeConfig({ provisionHook: " " });
|
|
292
|
+
expect(getProvisionHook()).toBeNull();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("env AGENT_YES_PROVISION_HOOK overrides the config", () => {
|
|
296
|
+
writeConfig({ provisionHook: "from-file" });
|
|
297
|
+
process.env.AGENT_YES_PROVISION_HOOK = "from-env";
|
|
298
|
+
expect(getProvisionHook()).toBe("from-env");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("is independent of the spawn hook", () => {
|
|
302
|
+
writeConfig({ spawnHook: "spawn-only" });
|
|
303
|
+
if (isPosix) chmodSync(path.join(tmp, "config.json"), 0o600);
|
|
304
|
+
expect(getSpawnHook()).toBe("spawn-only");
|
|
305
|
+
expect(getProvisionHook()).toBeNull();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it.skipIf(!isPosix)(
|
|
309
|
+
"refuses a file-backed hook when the config is group/world-writable (tampering guard)",
|
|
310
|
+
() => {
|
|
311
|
+
writeConfig({ provisionHook: "echo pwned" });
|
|
312
|
+
chmodSync(path.join(tmp, "config.json"), 0o666);
|
|
313
|
+
expect(getProvisionHook()).toBeNull();
|
|
314
|
+
chmodSync(path.join(tmp, "config.json"), 0o600);
|
|
315
|
+
expect(getProvisionHook()).toBe("echo pwned");
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
});
|
package/dist/trayApp-1AQ6X2V9.js
DELETED