@moltzap/client 2026.511.1 → 2026.518.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.
Files changed (194) hide show
  1. package/dist/auth.d.ts +12 -6
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +67 -29
  4. package/dist/auth.js.map +1 -1
  5. package/dist/auth.test.js +113 -76
  6. package/dist/auth.test.js.map +1 -1
  7. package/dist/channel-core-context.test.d.ts +2 -0
  8. package/dist/channel-core-context.test.d.ts.map +1 -0
  9. package/dist/channel-core-context.test.js +334 -0
  10. package/dist/channel-core-context.test.js.map +1 -0
  11. package/dist/channel-core-dispatch.test.d.ts +2 -0
  12. package/dist/channel-core-dispatch.test.d.ts.map +1 -0
  13. package/dist/channel-core-dispatch.test.js +550 -0
  14. package/dist/channel-core-dispatch.test.js.map +1 -0
  15. package/dist/channel-core-enrichment.d.ts +8 -0
  16. package/dist/channel-core-enrichment.d.ts.map +1 -0
  17. package/dist/channel-core-enrichment.js +134 -0
  18. package/dist/channel-core-enrichment.js.map +1 -0
  19. package/dist/channel-core-errors.d.ts +20 -0
  20. package/dist/channel-core-errors.d.ts.map +1 -0
  21. package/dist/channel-core-errors.js +12 -0
  22. package/dist/channel-core-errors.js.map +1 -0
  23. package/dist/channel-core-test-support.d.ts +48 -0
  24. package/dist/channel-core-test-support.d.ts.map +1 -0
  25. package/dist/channel-core-test-support.js +111 -0
  26. package/dist/channel-core-test-support.js.map +1 -0
  27. package/dist/channel-core.d.ts +30 -7
  28. package/dist/channel-core.d.ts.map +1 -1
  29. package/dist/channel-core.js +214 -237
  30. package/dist/channel-core.js.map +1 -1
  31. package/dist/channel-core.test.js +216 -1082
  32. package/dist/channel-core.test.js.map +1 -1
  33. package/dist/cli/__tests__/cli-multi-agent.int.test.js +129 -23
  34. package/dist/cli/__tests__/cli-multi-agent.int.test.js.map +1 -1
  35. package/dist/cli/commands/conversations-v2.test.js +64 -54
  36. package/dist/cli/commands/conversations-v2.test.js.map +1 -1
  37. package/dist/cli/commands/conversations.d.ts +5 -5
  38. package/dist/cli/commands/conversations.d.ts.map +1 -1
  39. package/dist/cli/commands/conversations.js +43 -36
  40. package/dist/cli/commands/conversations.js.map +1 -1
  41. package/dist/cli/commands/messages.d.ts +4 -4
  42. package/dist/cli/commands/messages.d.ts.map +1 -1
  43. package/dist/cli/commands/messages.js +4 -4
  44. package/dist/cli/commands/messages.js.map +1 -1
  45. package/dist/cli/commands/messages.test.js +61 -49
  46. package/dist/cli/commands/messages.test.js.map +1 -1
  47. package/dist/cli/commands/ping.d.ts +0 -9
  48. package/dist/cli/commands/ping.d.ts.map +1 -1
  49. package/dist/cli/commands/ping.js +1 -1
  50. package/dist/cli/commands/ping.js.map +1 -1
  51. package/dist/cli/commands/presence.d.ts +1 -1
  52. package/dist/cli/commands/presence.js +1 -1
  53. package/dist/cli/commands/register.d.ts +2 -2
  54. package/dist/cli/commands/register.d.ts.map +1 -1
  55. package/dist/cli/commands/register.js +98 -77
  56. package/dist/cli/commands/register.js.map +1 -1
  57. package/dist/cli/commands/register.test.js +63 -39
  58. package/dist/cli/commands/register.test.js.map +1 -1
  59. package/dist/cli/commands/send.d.ts +6 -6
  60. package/dist/cli/commands/send.js +6 -6
  61. package/dist/cli/commands/send.test.js +32 -28
  62. package/dist/cli/commands/send.test.js.map +1 -1
  63. package/dist/cli/commands/status.d.ts.map +1 -1
  64. package/dist/cli/commands/status.js +3 -4
  65. package/dist/cli/commands/status.js.map +1 -1
  66. package/dist/cli/config.d.ts +0 -2
  67. package/dist/cli/config.d.ts.map +1 -1
  68. package/dist/cli/config.js +86 -76
  69. package/dist/cli/config.js.map +1 -1
  70. package/dist/cli/config.test.js +80 -42
  71. package/dist/cli/config.test.js.map +1 -1
  72. package/dist/cli/index.d.ts +1 -16
  73. package/dist/cli/index.d.ts.map +1 -1
  74. package/dist/cli/index.js +50 -113
  75. package/dist/cli/index.js.map +1 -1
  76. package/dist/cli/process-argv.d.ts +2 -0
  77. package/dist/cli/process-argv.d.ts.map +1 -0
  78. package/dist/cli/process-argv.js +2 -0
  79. package/dist/cli/process-argv.js.map +1 -0
  80. package/dist/cli/profile.d.ts +3 -10
  81. package/dist/cli/profile.d.ts.map +1 -1
  82. package/dist/cli/profile.js +105 -104
  83. package/dist/cli/profile.js.map +1 -1
  84. package/dist/cli/profile.test.js +338 -232
  85. package/dist/cli/profile.test.js.map +1 -1
  86. package/dist/cli/runtime.d.ts +5 -17
  87. package/dist/cli/runtime.d.ts.map +1 -1
  88. package/dist/cli/runtime.js +6 -59
  89. package/dist/cli/runtime.js.map +1 -1
  90. package/dist/cli/socket-client.d.ts +10 -15
  91. package/dist/cli/socket-client.d.ts.map +1 -1
  92. package/dist/cli/socket-client.js +74 -89
  93. package/dist/cli/socket-client.js.map +1 -1
  94. package/dist/cli/transport.d.ts +9 -5
  95. package/dist/cli/transport.d.ts.map +1 -1
  96. package/dist/cli/transport.js +45 -42
  97. package/dist/cli/transport.js.map +1 -1
  98. package/dist/cli/transport.test.js +227 -133
  99. package/dist/cli/transport.test.js.map +1 -1
  100. package/dist/index.d.ts +4 -1
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +3 -0
  103. package/dist/index.js.map +1 -1
  104. package/dist/local-daemon-rpc.d.ts +13 -0
  105. package/dist/local-daemon-rpc.d.ts.map +1 -0
  106. package/dist/local-daemon-rpc.js +17 -0
  107. package/dist/local-daemon-rpc.js.map +1 -0
  108. package/dist/local-paths.d.ts +7 -0
  109. package/dist/local-paths.d.ts.map +1 -0
  110. package/dist/local-paths.js +20 -0
  111. package/dist/local-paths.js.map +1 -0
  112. package/dist/runtime/close-info.d.ts +3 -12
  113. package/dist/runtime/close-info.d.ts.map +1 -1
  114. package/dist/runtime/close-info.js +28 -39
  115. package/dist/runtime/close-info.js.map +1 -1
  116. package/dist/runtime/close-info.test.js +77 -70
  117. package/dist/runtime/close-info.test.js.map +1 -1
  118. package/dist/runtime/errors.test.js +42 -28
  119. package/dist/runtime/errors.test.js.map +1 -1
  120. package/dist/runtime/frame.d.ts +4 -31
  121. package/dist/runtime/frame.d.ts.map +1 -1
  122. package/dist/runtime/frame.js +7 -116
  123. package/dist/runtime/frame.js.map +1 -1
  124. package/dist/runtime/frame.test.js +41 -54
  125. package/dist/runtime/frame.test.js.map +1 -1
  126. package/dist/runtime/index.d.ts +7 -0
  127. package/dist/runtime/index.d.ts.map +1 -1
  128. package/dist/runtime/index.js +7 -11
  129. package/dist/runtime/index.js.map +1 -1
  130. package/dist/runtime/local-history.d.ts +67 -0
  131. package/dist/runtime/local-history.d.ts.map +1 -0
  132. package/dist/runtime/local-history.js +77 -0
  133. package/dist/runtime/local-history.js.map +1 -0
  134. package/dist/runtime/local-service-commands.d.ts +19 -0
  135. package/dist/runtime/local-service-commands.d.ts.map +1 -1
  136. package/dist/runtime/local-service-commands.js +23 -0
  137. package/dist/runtime/local-service-commands.js.map +1 -1
  138. package/dist/runtime/local-socket-server.d.ts +20 -0
  139. package/dist/runtime/local-socket-server.d.ts.map +1 -0
  140. package/dist/runtime/local-socket-server.js +94 -0
  141. package/dist/runtime/local-socket-server.js.map +1 -0
  142. package/dist/runtime/refs.d.ts +1 -1
  143. package/dist/runtime/refs.js +1 -1
  144. package/dist/runtime/runtime.test.js +29 -21
  145. package/dist/runtime/runtime.test.js.map +1 -1
  146. package/dist/runtime/service-helpers.d.ts +32 -0
  147. package/dist/runtime/service-helpers.d.ts.map +1 -0
  148. package/dist/runtime/service-helpers.js +85 -0
  149. package/dist/runtime/service-helpers.js.map +1 -0
  150. package/dist/runtime/subscribers.d.ts +13 -17
  151. package/dist/runtime/subscribers.d.ts.map +1 -1
  152. package/dist/runtime/subscribers.js +37 -32
  153. package/dist/runtime/subscribers.js.map +1 -1
  154. package/dist/runtime/subscribers.test.js +139 -131
  155. package/dist/runtime/subscribers.test.js.map +1 -1
  156. package/dist/service-socket-path.test.d.ts +2 -0
  157. package/dist/service-socket-path.test.d.ts.map +1 -0
  158. package/dist/service-socket-path.test.js +87 -0
  159. package/dist/service-socket-path.test.js.map +1 -0
  160. package/dist/service.d.ts +23 -20
  161. package/dist/service.d.ts.map +1 -1
  162. package/dist/service.js +192 -340
  163. package/dist/service.js.map +1 -1
  164. package/dist/service.test.js +652 -653
  165. package/dist/service.test.js.map +1 -1
  166. package/dist/test/index.d.ts +30 -14
  167. package/dist/test/index.d.ts.map +1 -1
  168. package/dist/test/index.js +25 -21
  169. package/dist/test/index.js.map +1 -1
  170. package/dist/test-utils/channel-service-fixture.d.ts +1 -0
  171. package/dist/test-utils/channel-service-fixture.d.ts.map +1 -1
  172. package/dist/test-utils/channel-service-fixture.js +139 -98
  173. package/dist/test-utils/channel-service-fixture.js.map +1 -1
  174. package/dist/test-utils/conformance-adapter.d.ts.map +1 -1
  175. package/dist/test-utils/conformance-adapter.js +145 -124
  176. package/dist/test-utils/conformance-adapter.js.map +1 -1
  177. package/dist/test-utils/fake-service.d.ts.map +1 -1
  178. package/dist/test-utils/fake-service.js +3 -3
  179. package/dist/test-utils/fake-service.js.map +1 -1
  180. package/dist/test-utils/index.d.ts +25 -0
  181. package/dist/test-utils/index.d.ts.map +1 -1
  182. package/dist/test-utils/index.js +25 -8
  183. package/dist/test-utils/index.js.map +1 -1
  184. package/dist/ws-client-test-support.d.ts +180 -0
  185. package/dist/ws-client-test-support.d.ts.map +1 -0
  186. package/dist/ws-client-test-support.js +399 -0
  187. package/dist/ws-client-test-support.js.map +1 -0
  188. package/dist/ws-client.d.ts +59 -32
  189. package/dist/ws-client.d.ts.map +1 -1
  190. package/dist/ws-client.js +272 -321
  191. package/dist/ws-client.js.map +1 -1
  192. package/dist/ws-client.test.js +388 -800
  193. package/dist/ws-client.test.js.map +1 -1
  194. package/package.json +14 -10
@@ -1,263 +1,369 @@
1
1
  /**
2
- * Unit tests for the profile layer. Spec items §5.2 (`--profile`,
3
- * `--no-persist`), Invariants §4.3 (coexistence), §4.4 (no-disk-write).
2
+ * Unit tests for the profile layer. Spec items Section 5.2 (`--profile`,
3
+ * `--no-persist`), Invariants 4.3 (coexistence), 4.4 (no-disk-write).
4
4
  */
5
- import * as fs from "node:fs";
6
- import * as path from "node:path";
7
- import * as os from "node:os";
8
- import { Effect } from "effect";
9
- import { describe, expect, it } from "vitest";
5
+ import { FileSystem, Path } from "@effect/platform";
6
+ import { NodeContext } from "@effect/platform-node";
7
+ import { it as effectIt } from "@effect/vitest";
8
+ import { Effect, Exit, Schema } from "effect";
9
+ import * as fc from "fast-check";
10
+ import { afterEach, beforeEach, describe, expect, vi } from "vitest";
10
11
  import { emitNoPersist, loadLayeredConfig, parseProfileName, ProfileInvalidNameError, ProfileNotFoundError, resolveProfileAuth, writeProfile, } from "./profile.js";
11
- /**
12
- * Helper: create a temp dir, point MOLTZAP_CONFIG_HOME at it for the
13
- * duration of a test, seed with optional JSON, run fn, clean up.
14
- */
15
- const withTmpConfigHome = async (seed, fn) => {
16
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "moltzap-profile-"));
17
- const originalHome = process.env.MOLTZAP_CONFIG_HOME;
18
- process.env.MOLTZAP_CONFIG_HOME = tmp;
19
- try {
20
- if (seed !== undefined) {
21
- fs.writeFileSync(path.join(tmp, "config.json"), JSON.stringify(seed, null, 2));
22
- }
23
- return await fn(tmp);
24
- }
25
- finally {
26
- if (originalHome === undefined) {
27
- delete process.env.MOLTZAP_CONFIG_HOME;
28
- }
29
- else {
30
- process.env.MOLTZAP_CONFIG_HOME = originalHome;
31
- }
32
- fs.rmSync(tmp, { recursive: true, force: true });
33
- }
34
- };
35
- describe("parseProfileName", () => {
36
- it("accepts a valid lowercase-alphanumeric name", async () => {
37
- const name = await Effect.runPromise(parseProfileName("alice-bot"));
38
- expect(name).toBe("alice-bot");
12
+ const it = effectIt.scoped;
13
+ const CONFIG_FILE_NAME = "config.json";
14
+ const CONFIG_HOME_PREFIX = "moltzap-profile-";
15
+ const UTF8 = "utf-8";
16
+ const EMPTY_PROFILE_COUNT = 0;
17
+ const SINGLE_PROFILE_COUNT = 1;
18
+ const VALID_PROFILE_NAME = "alice-bot";
19
+ const ALICE_PROFILE_NAME = "alice";
20
+ const BOB_PROFILE_NAME = "bob";
21
+ const UNKNOWN_PROFILE_NAME = "nobody";
22
+ const GHOST_PROFILE_NAME = "ghost";
23
+ const DEFAULT_PROFILE_NAME = "default";
24
+ const TEST_SERVER_URL = "wss://x";
25
+ const DEFAULT_AGENT_NAME = "a";
26
+ const LEGACY_AGENT_NAME = "legacy";
27
+ const ALICE_AGENT_NAME = "alice";
28
+ const BOB_AGENT_NAME = "bob";
29
+ const DEFAULT_API_KEY = "k";
30
+ const LEGACY_API_KEY = "legacy-key";
31
+ const ALICE_API_KEY = "k-alice";
32
+ const BOB_API_KEY = "k-bob";
33
+ const BAD_PROFILE_NAME = "Bad";
34
+ const BAD_PROFILE_REASON = "upper";
35
+ const INVALID_JSON_TEXT = "{not json}";
36
+ const PROFILE_INVALID_NAME_ERROR = "ProfileInvalidNameError";
37
+ const PROFILE_CONFIG_READ_ERROR = "ProfileConfigReadError";
38
+ const PROFILE_NOT_FOUND_ERROR = "ProfileNotFoundError";
39
+ const WrittenProfileRecordSchema = Schema.Struct({
40
+ apiKey: Schema.String,
41
+ agentName: Schema.String,
42
+ serverUrl: Schema.String,
43
+ registeredAt: Schema.optional(Schema.String),
44
+ });
45
+ const WrittenConfigSchema = Schema.Struct({
46
+ apiKey: Schema.optional(Schema.String),
47
+ profiles: Schema.optional(Schema.Record({
48
+ key: Schema.String,
49
+ value: WrittenProfileRecordSchema,
50
+ })),
51
+ });
52
+ const decodeWrittenConfig = Schema.decodeUnknown(WrittenConfigSchema);
53
+ const profileNameArbitrary = fc.stringMatching(/^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$/);
54
+ beforeEach(() => {
55
+ vi.unstubAllEnvs();
56
+ });
57
+ afterEach(() => {
58
+ vi.unstubAllEnvs();
59
+ });
60
+ const withNodeContext = (effect) => effect.pipe(Effect.provide(NodeContext.layer));
61
+ const defaultRecord = (apiKey = DEFAULT_API_KEY) => ({
62
+ apiKey,
63
+ agentName: DEFAULT_AGENT_NAME,
64
+ serverUrl: TEST_SERVER_URL,
65
+ });
66
+ const namedRecord = (apiKey, agentName) => ({
67
+ apiKey,
68
+ agentName,
69
+ serverUrl: TEST_SERVER_URL,
70
+ });
71
+ const legacyConfig = (apiKey = DEFAULT_API_KEY) => ({
72
+ serverUrl: TEST_SERVER_URL,
73
+ apiKey,
74
+ agentName: DEFAULT_AGENT_NAME,
75
+ });
76
+ const namedProfilesConfig = () => ({
77
+ serverUrl: TEST_SERVER_URL,
78
+ profiles: {
79
+ [ALICE_PROFILE_NAME]: namedRecord(ALICE_API_KEY, ALICE_AGENT_NAME),
80
+ },
81
+ });
82
+ const makeConfigHome = Effect.gen(function* () {
83
+ const fileSystem = yield* FileSystem.FileSystem;
84
+ const configHome = yield* fileSystem.makeTempDirectoryScoped({
85
+ prefix: CONFIG_HOME_PREFIX,
39
86
  });
40
- it("rejects an empty string with ProfileInvalidNameError", async () => {
41
- const exit = await Effect.runPromiseExit(parseProfileName(""));
42
- expect(exit._tag).toBe("Failure");
87
+ vi.stubEnv("MOLTZAP_CONFIG_HOME", configHome);
88
+ return configHome;
89
+ });
90
+ const writeConfigText = (text) => Effect.gen(function* () {
91
+ const fileSystem = yield* FileSystem.FileSystem;
92
+ const path = yield* Path.Path;
93
+ const configHome = yield* makeConfigHome;
94
+ yield* fileSystem.writeFileString(path.join(configHome, CONFIG_FILE_NAME), text);
95
+ return configHome;
96
+ });
97
+ const writeConfigObject = (config) => writeConfigText(JSON.stringify(config));
98
+ const readConfigObject = (configHome) => Effect.gen(function* () {
99
+ const fileSystem = yield* FileSystem.FileSystem;
100
+ const path = yield* Path.Path;
101
+ const text = yield* fileSystem.readFileString(path.join(configHome, CONFIG_FILE_NAME), UTF8);
102
+ const parsed = yield* Effect.try({
103
+ try: () => JSON.parse(text),
104
+ catch: (error) => error,
43
105
  });
44
- it("rejects a name with uppercase letters", async () => {
45
- const exit = await Effect.runPromiseExit(parseProfileName("Alice"));
46
- expect(exit._tag).toBe("Failure");
106
+ return yield* decodeWrittenConfig(parsed);
107
+ });
108
+ const expectProfiles = (profiles) => {
109
+ expect(profiles).toBeDefined();
110
+ return profiles ?? {};
111
+ };
112
+ const expectFailure = (exit) => {
113
+ expect(Exit.isFailure(exit)).toBe(true);
114
+ };
115
+ const expectFailureContaining = (exit, marker) => {
116
+ expectFailure(exit);
117
+ expect(String(exit)).toContain(marker);
118
+ };
119
+ function expectGeneratedProfileNameParses(raw) {
120
+ const exit = Effect.runSyncExit(parseProfileName(raw));
121
+ expect(Exit.isSuccess(exit)).toBe(true);
122
+ if (Exit.isSuccess(exit)) {
123
+ expect(exit.value).toBe(raw);
124
+ }
125
+ }
126
+ function parsesLowercaseAlphanumericName() {
127
+ return Effect.gen(function* () {
128
+ const name = yield* parseProfileName(VALID_PROFILE_NAME);
129
+ expect(name).toBe(VALID_PROFILE_NAME);
47
130
  });
48
- it("rejects a name starting with a hyphen", async () => {
49
- const exit = await Effect.runPromiseExit(parseProfileName("-alice"));
50
- expect(exit._tag).toBe("Failure");
131
+ }
132
+ function generatedValidProfileNamesParse() {
133
+ return Effect.sync(() => {
134
+ fc.assert(fc.property(profileNameArbitrary, expectGeneratedProfileNameParses));
51
135
  });
52
- it("rejects a name ending with a hyphen", async () => {
53
- const exit = await Effect.runPromiseExit(parseProfileName("alice-"));
54
- expect(exit._tag).toBe("Failure");
136
+ }
137
+ function rejectsEmptyName() {
138
+ return Effect.gen(function* () {
139
+ const exit = yield* Effect.exit(parseProfileName(""));
140
+ expectFailure(exit);
55
141
  });
56
- it("fails with ProfileInvalidNameError type", async () => {
57
- const exit = await Effect.runPromiseExit(parseProfileName(""));
58
- if (exit._tag === "Failure") {
59
- const s = JSON.stringify(exit.cause);
60
- expect(s).toMatch(/ProfileInvalidNameError/);
61
- }
142
+ }
143
+ function rejectsUppercaseName() {
144
+ return Effect.gen(function* () {
145
+ const exit = yield* Effect.exit(parseProfileName(BAD_PROFILE_NAME));
146
+ expectFailure(exit);
62
147
  });
63
- });
64
- describe("loadLayeredConfig", () => {
65
- it("missing file resolves with empty view (default undefined, empty profiles)", async () => {
66
- await withTmpConfigHome(undefined, async () => {
67
- const view = await Effect.runPromise(loadLayeredConfig);
68
- expect(view.default).toBeUndefined();
69
- expect(view.profiles.size).toBe(0);
70
- });
148
+ }
149
+ function rejectsLeadingHyphen() {
150
+ return Effect.gen(function* () {
151
+ const exit = yield* Effect.exit(parseProfileName("-alice"));
152
+ expectFailure(exit);
71
153
  });
72
- it("legacy top-level { apiKey, agentName } populates default", async () => {
73
- await withTmpConfigHome({
74
- serverUrl: "wss://x",
75
- apiKey: "k1",
76
- agentName: "a1",
77
- }, async () => {
78
- const view = await Effect.runPromise(loadLayeredConfig);
79
- expect(view.default?.apiKey).toBe("k1");
80
- expect(view.default?.agentName).toBe("a1");
81
- expect(view.serverUrl).toBe("wss://x");
82
- });
154
+ }
155
+ function rejectsTrailingHyphen() {
156
+ return Effect.gen(function* () {
157
+ const exit = yield* Effect.exit(parseProfileName("alice-"));
158
+ expectFailure(exit);
83
159
  });
84
- it("{ profiles: { alice: ... } } populates the profiles map", async () => {
85
- await withTmpConfigHome({
86
- serverUrl: "wss://x",
87
- profiles: {
88
- alice: {
89
- apiKey: "k-alice",
90
- agentName: "alice",
91
- serverUrl: "wss://x",
92
- },
93
- },
94
- }, async () => {
95
- const view = await Effect.runPromise(loadLayeredConfig);
96
- expect(view.profiles.size).toBe(1);
97
- const rec = view.profiles.get("alice");
98
- expect(rec?.apiKey).toBe("k-alice");
99
- });
160
+ }
161
+ function invalidNameErrorTypeSurfaces() {
162
+ return Effect.gen(function* () {
163
+ const exit = yield* Effect.exit(parseProfileName(""));
164
+ expectFailureContaining(exit, PROFILE_INVALID_NAME_ERROR);
100
165
  });
101
- it("tolerates unknown top-level keys", async () => {
102
- await withTmpConfigHome({
103
- serverUrl: "wss://x",
104
- apiKey: "k",
105
- agentName: "a",
106
- // future-experimental key; should not cause a parse failure.
166
+ }
167
+ function missingFileLoadsEmptyView() {
168
+ return withNodeContext(Effect.gen(function* () {
169
+ yield* makeConfigHome;
170
+ const view = yield* loadLayeredConfig;
171
+ expect(view.default).toBeUndefined();
172
+ expect(view.profiles.size).toBe(EMPTY_PROFILE_COUNT);
173
+ }));
174
+ }
175
+ function malformedConfigFails() {
176
+ return withNodeContext(Effect.gen(function* () {
177
+ yield* writeConfigText(INVALID_JSON_TEXT);
178
+ const exit = yield* Effect.exit(loadLayeredConfig);
179
+ expectFailureContaining(exit, PROFILE_CONFIG_READ_ERROR);
180
+ }));
181
+ }
182
+ function legacyConfigPopulatesDefaultRecord() {
183
+ return withNodeContext(Effect.gen(function* () {
184
+ yield* writeConfigObject(legacyConfig());
185
+ const view = yield* loadLayeredConfig;
186
+ const record = view.default;
187
+ expect(record).toBeDefined();
188
+ if (record !== undefined) {
189
+ expect(record.apiKey).toBe(DEFAULT_API_KEY);
190
+ expect(record.agentName).toBe(DEFAULT_AGENT_NAME);
191
+ }
192
+ expect(view.serverUrl).toBe(TEST_SERVER_URL);
193
+ }));
194
+ }
195
+ function unknownTopLevelKeysAreTolerated() {
196
+ return withNodeContext(Effect.gen(function* () {
197
+ yield* writeConfigObject({
198
+ ...legacyConfig(),
107
199
  futureExperimentalField: { nested: true },
108
- }, async () => {
109
- const view = await Effect.runPromise(loadLayeredConfig);
110
- expect(view.default?.apiKey).toBe("k");
111
200
  });
112
- });
113
- it("missing registeredAt on legacy-shaped profile still decodes", async () => {
114
- await withTmpConfigHome({
115
- serverUrl: "wss://x",
201
+ const view = yield* loadLayeredConfig;
202
+ const record = view.default;
203
+ expect(record).toBeDefined();
204
+ if (record !== undefined) {
205
+ expect(record.apiKey).toBe(DEFAULT_API_KEY);
206
+ }
207
+ }));
208
+ }
209
+ function namedProfilesPopulateMap() {
210
+ return withNodeContext(Effect.gen(function* () {
211
+ yield* writeConfigObject(namedProfilesConfig());
212
+ const view = yield* loadLayeredConfig;
213
+ expect(view.profiles.size).toBe(SINGLE_PROFILE_COUNT);
214
+ const record = view.profiles.get(ALICE_PROFILE_NAME);
215
+ expect(record).toBeDefined();
216
+ if (record !== undefined) {
217
+ expect(record.apiKey).toBe(ALICE_API_KEY);
218
+ }
219
+ }));
220
+ }
221
+ function legacyProfileWithoutRegisteredAtDecodes() {
222
+ return withNodeContext(Effect.gen(function* () {
223
+ yield* writeConfigObject({
224
+ serverUrl: TEST_SERVER_URL,
116
225
  profiles: {
117
- bob: { apiKey: "k-bob", agentName: "bob", serverUrl: "wss://x" },
226
+ [BOB_PROFILE_NAME]: namedRecord(BOB_API_KEY, BOB_AGENT_NAME),
118
227
  },
119
- }, async () => {
120
- const view = await Effect.runPromise(loadLayeredConfig);
121
- const rec = view.profiles.get("bob");
122
- expect(rec?.registeredAt).toBeUndefined();
123
228
  });
124
- });
125
- it("parse error surfaces as ProfileConfigReadError (no silent defaults)", async () => {
126
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "moltzap-profile-"));
127
- const originalHome = process.env.MOLTZAP_CONFIG_HOME;
128
- process.env.MOLTZAP_CONFIG_HOME = tmp;
129
- try {
130
- fs.writeFileSync(path.join(tmp, "config.json"), "{not json}");
131
- const exit = await Effect.runPromiseExit(loadLayeredConfig);
132
- expect(exit._tag).toBe("Failure");
133
- if (exit._tag === "Failure") {
134
- const s = JSON.stringify(exit.cause);
135
- expect(s).toMatch(/ProfileConfigReadError/);
136
- }
229
+ const view = yield* loadLayeredConfig;
230
+ const record = view.profiles.get(BOB_PROFILE_NAME);
231
+ expect(record).toBeDefined();
232
+ if (record !== undefined) {
233
+ expect(record.registeredAt).toBeUndefined();
137
234
  }
138
- finally {
139
- if (originalHome === undefined) {
140
- delete process.env.MOLTZAP_CONFIG_HOME;
141
- }
142
- else {
143
- process.env.MOLTZAP_CONFIG_HOME = originalHome;
144
- }
145
- fs.rmSync(tmp, { recursive: true, force: true });
146
- }
147
- });
148
- });
149
- describe("resolveProfileAuth", () => {
150
- it("undefined name returns the default record", async () => {
151
- await withTmpConfigHome({ serverUrl: "wss://x", apiKey: "k", agentName: "a" }, async () => {
152
- const record = await Effect.runPromise(resolveProfileAuth(undefined));
153
- expect(record.apiKey).toBe("k");
154
- });
235
+ }));
236
+ }
237
+ function defaultProfileAuthResolves() {
238
+ return withNodeContext(Effect.gen(function* () {
239
+ yield* writeConfigObject(legacyConfig());
240
+ const record = yield* resolveProfileAuth(undefined);
241
+ expect(record.apiKey).toBe(DEFAULT_API_KEY);
242
+ }));
243
+ }
244
+ function unknownProfileFails() {
245
+ return withNodeContext(Effect.gen(function* () {
246
+ yield* writeConfigObject(legacyConfig());
247
+ const exit = yield* Effect.exit(resolveProfileAuth(GHOST_PROFILE_NAME));
248
+ expectFailureContaining(exit, PROFILE_NOT_FOUND_ERROR);
249
+ }));
250
+ }
251
+ function namedProfileAuthResolves() {
252
+ return withNodeContext(Effect.gen(function* () {
253
+ yield* writeConfigObject(namedProfilesConfig());
254
+ const record = yield* resolveProfileAuth(ALICE_PROFILE_NAME);
255
+ expect(record.apiKey).toBe(ALICE_API_KEY);
256
+ }));
257
+ }
258
+ function profileNotFoundErrorCarriesName() {
259
+ return Effect.sync(() => {
260
+ const err = new ProfileNotFoundError({ name: UNKNOWN_PROFILE_NAME });
261
+ expect(err.name).toBe(UNKNOWN_PROFILE_NAME);
155
262
  });
156
- it("unknown name returns ProfileNotFoundError (no fallback to default)", async () => {
157
- await withTmpConfigHome({ serverUrl: "wss://x", apiKey: "k", agentName: "a" }, async () => {
158
- const exit = await Effect.runPromiseExit(resolveProfileAuth("ghost"));
159
- expect(exit._tag).toBe("Failure");
160
- if (exit._tag === "Failure") {
161
- const s = JSON.stringify(exit.cause);
162
- expect(s).toMatch(/ProfileNotFoundError/);
163
- }
263
+ }
264
+ function writeNamedProfilePreservesLegacyTopLevelKeys() {
265
+ return withNodeContext(Effect.gen(function* () {
266
+ const configHome = yield* writeConfigObject({
267
+ serverUrl: TEST_SERVER_URL,
268
+ apiKey: LEGACY_API_KEY,
269
+ agentName: LEGACY_AGENT_NAME,
164
270
  });
271
+ yield* writeProfile(ALICE_PROFILE_NAME, namedRecord(ALICE_API_KEY, ALICE_AGENT_NAME));
272
+ const written = yield* readConfigObject(configHome);
273
+ const profiles = expectProfiles(written.profiles);
274
+ expect(written.apiKey).toBe(LEGACY_API_KEY);
275
+ expect(profiles[ALICE_PROFILE_NAME]?.apiKey).toBe(ALICE_API_KEY);
276
+ }));
277
+ }
278
+ function writeDefaultProfileUpdatesTopLevelKeys() {
279
+ return withNodeContext(Effect.gen(function* () {
280
+ const configHome = yield* makeConfigHome;
281
+ yield* writeProfile(DEFAULT_PROFILE_NAME, defaultRecord());
282
+ const written = yield* readConfigObject(configHome);
283
+ expect(written.apiKey).toBe(DEFAULT_API_KEY);
284
+ expect(written.profiles).toBeUndefined();
285
+ }));
286
+ }
287
+ function writeSecondProfilePreservesFirst() {
288
+ return withNodeContext(Effect.gen(function* () {
289
+ const configHome = yield* writeConfigObject(namedProfilesConfig());
290
+ yield* writeProfile(BOB_PROFILE_NAME, namedRecord(BOB_API_KEY, BOB_AGENT_NAME));
291
+ const written = yield* readConfigObject(configHome);
292
+ const profiles = expectProfiles(written.profiles);
293
+ expect(profiles[ALICE_PROFILE_NAME]?.apiKey).toBe(ALICE_API_KEY);
294
+ expect(profiles[BOB_PROFILE_NAME]?.apiKey).toBe(BOB_API_KEY);
295
+ }));
296
+ }
297
+ function emitNoPersistLeavesConfigDirUnchanged() {
298
+ return withNodeContext(Effect.gen(function* () {
299
+ const fileSystem = yield* FileSystem.FileSystem;
300
+ const configHome = yield* makeConfigHome;
301
+ const before = yield* fileSystem.readDirectory(configHome);
302
+ yield* emitNoPersist(defaultRecord());
303
+ const after = yield* fileSystem.readDirectory(configHome);
304
+ expect(after).toEqual(before);
305
+ }));
306
+ }
307
+ function emitNoPersistReturnsRecord() {
308
+ return Effect.gen(function* () {
309
+ const record = defaultRecord();
310
+ const result = yield* emitNoPersist(record);
311
+ expect(result.record).toEqual(record);
165
312
  });
166
- it("known name returns the named record", async () => {
167
- await withTmpConfigHome({
168
- serverUrl: "wss://x",
169
- profiles: {
170
- alice: {
171
- apiKey: "k-alice",
172
- agentName: "alice",
173
- serverUrl: "wss://x",
174
- },
175
- },
176
- }, async () => {
177
- const record = await Effect.runPromise(resolveProfileAuth("alice"));
178
- expect(record.apiKey).toBe("k-alice");
313
+ }
314
+ function profileInvalidNameErrorCarriesFields() {
315
+ return Effect.sync(() => {
316
+ const err = new ProfileInvalidNameError({
317
+ name: BAD_PROFILE_NAME,
318
+ reason: BAD_PROFILE_REASON,
179
319
  });
320
+ expect(err.name).toBe(BAD_PROFILE_NAME);
321
+ expect(err.reason).toBe(BAD_PROFILE_REASON);
180
322
  });
181
- it("ProfileNotFoundError carries the requested name", () => {
182
- const err = new ProfileNotFoundError({ name: "nobody" });
183
- expect(err.name).toBe("nobody");
184
- });
323
+ }
324
+ describe("parseProfileName valid input", () => {
325
+ it("accepts a valid lowercase-alphanumeric name", parsesLowercaseAlphanumericName);
326
+ it("accepts generated valid profile names", generatedValidProfileNamesParse);
327
+ });
328
+ describe("parseProfileName invalid input", () => {
329
+ it("rejects an empty string with ProfileInvalidNameError", rejectsEmptyName);
330
+ it("rejects a name with uppercase letters", rejectsUppercaseName);
331
+ it("rejects a name starting with a hyphen", rejectsLeadingHyphen);
332
+ });
333
+ describe("parseProfileName invalid boundary input", () => {
334
+ it("rejects a name ending with a hyphen", rejectsTrailingHyphen);
335
+ it("fails with ProfileInvalidNameError type", invalidNameErrorTypeSurfaces);
336
+ });
337
+ describe("loadLayeredConfig missing and malformed input", () => {
338
+ it("missing file resolves with empty view", missingFileLoadsEmptyView);
339
+ it("parse error surfaces as ProfileConfigReadError", malformedConfigFails);
340
+ });
341
+ describe("loadLayeredConfig legacy records", () => {
342
+ it("legacy top-level auth populates default", legacyConfigPopulatesDefaultRecord);
343
+ it("tolerates unknown top-level keys", unknownTopLevelKeysAreTolerated);
344
+ });
345
+ describe("loadLayeredConfig named profiles", () => {
346
+ it("profiles object populates the profiles map", namedProfilesPopulateMap);
347
+ it("missing registeredAt on legacy-shaped profile still decodes", legacyProfileWithoutRegisteredAtDecodes);
348
+ });
349
+ describe("resolveProfileAuth", () => {
350
+ it("undefined name returns the default record", defaultProfileAuthResolves);
351
+ it("unknown name returns ProfileNotFoundError", unknownProfileFails);
352
+ it("known name returns the named record", namedProfileAuthResolves);
353
+ });
354
+ describe("ProfileNotFoundError", () => {
355
+ it("carries the requested name", profileNotFoundErrorCarriesName);
185
356
  });
186
357
  describe("writeProfile", () => {
187
- it("writing under a named profile leaves top-level apiKey untouched", async () => {
188
- await withTmpConfigHome({ serverUrl: "wss://x", apiKey: "legacy-key", agentName: "legacy" }, async (tmp) => {
189
- await Effect.runPromise(writeProfile("alice", {
190
- apiKey: "k-alice",
191
- agentName: "alice",
192
- serverUrl: "wss://x",
193
- }));
194
- const written = JSON.parse(fs.readFileSync(path.join(tmp, "config.json"), "utf-8"));
195
- expect(written.apiKey).toBe("legacy-key");
196
- expect(written.profiles.alice.apiKey).toBe("k-alice");
197
- });
198
- });
199
- it("writing 'default' updates top-level keys (not under profiles)", async () => {
200
- await withTmpConfigHome(undefined, async (tmp) => {
201
- await Effect.runPromise(writeProfile("default", {
202
- apiKey: "k",
203
- agentName: "a",
204
- serverUrl: "wss://x",
205
- }));
206
- const written = JSON.parse(fs.readFileSync(path.join(tmp, "config.json"), "utf-8"));
207
- expect(written.apiKey).toBe("k");
208
- expect(written.profiles).toBeUndefined();
209
- });
210
- });
211
- it("adding a second profile preserves the first", async () => {
212
- await withTmpConfigHome({
213
- serverUrl: "wss://x",
214
- profiles: {
215
- alice: {
216
- apiKey: "k-alice",
217
- agentName: "alice",
218
- serverUrl: "wss://x",
219
- },
220
- },
221
- }, async (tmp) => {
222
- await Effect.runPromise(writeProfile("bob", {
223
- apiKey: "k-bob",
224
- agentName: "bob",
225
- serverUrl: "wss://x",
226
- }));
227
- const written = JSON.parse(fs.readFileSync(path.join(tmp, "config.json"), "utf-8"));
228
- expect(written.profiles.alice.apiKey).toBe("k-alice");
229
- expect(written.profiles.bob.apiKey).toBe("k-bob");
230
- });
231
- });
358
+ it("writing under a named profile leaves top-level apiKey untouched", writeNamedProfilePreservesLegacyTopLevelKeys);
359
+ it("writing default updates top-level keys", writeDefaultProfileUpdatesTopLevelKeys);
360
+ it("adding a second profile preserves the first", writeSecondProfilePreservesFirst);
232
361
  });
233
362
  describe("emitNoPersist", () => {
234
- it("never writes to ~/.moltzap/ (fs diff before/after is empty)", async () => {
235
- await withTmpConfigHome(undefined, async (tmp) => {
236
- const before = fs.readdirSync(tmp);
237
- await Effect.runPromise(emitNoPersist({
238
- apiKey: "k",
239
- agentName: "a",
240
- serverUrl: "wss://x",
241
- }));
242
- const after = fs.readdirSync(tmp);
243
- expect(after).toEqual(before);
244
- });
245
- });
246
- it("returns the record unchanged for the caller to print", async () => {
247
- const record = {
248
- apiKey: "k",
249
- agentName: "a",
250
- serverUrl: "wss://x",
251
- };
252
- const result = await Effect.runPromise(emitNoPersist(record));
253
- expect(result.record).toEqual(record);
254
- });
363
+ it("never writes to the moltzap config directory", emitNoPersistLeavesConfigDirUnchanged);
364
+ it("returns the record unchanged for the caller to print", emitNoPersistReturnsRecord);
255
365
  });
256
366
  describe("ProfileInvalidNameError", () => {
257
- it("carries name and reason", () => {
258
- const err = new ProfileInvalidNameError({ name: "Bad", reason: "upper" });
259
- expect(err.name).toBe("Bad");
260
- expect(err.reason).toBe("upper");
261
- });
367
+ it("carries name and reason", profileInvalidNameErrorCarriesFields);
262
368
  });
263
369
  //# sourceMappingURL=profile.test.js.map