@letta-ai/letta-code 0.28.1 → 0.28.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letta-ai/letta-code",
3
- "version": "0.28.1",
3
+ "version": "0.28.3",
4
4
  "description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.0",
@@ -2,13 +2,21 @@
2
2
 
3
3
  import { existsSync, readdirSync, readFileSync } from "node:fs";
4
4
  import { dirname, join, relative, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
5
6
 
6
7
  const TESTS_DIR_ENV = "LETTA_MOCK_ISOLATION_TESTS_DIR";
7
8
 
8
9
  const rootDir = process.cwd();
10
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
9
11
  const testsDir = process.env[TESTS_DIR_ENV]
10
12
  ? resolve(process.env[TESTS_DIR_ENV])
11
13
  : join(rootDir, "src");
14
+ const isolationManifest = JSON.parse(
15
+ readFileSync(join(scriptDir, "isolated-unit-tests.json"), "utf8"),
16
+ );
17
+ const ISOLATED_TEST_FILES = new Set(
18
+ isolationManifest.tests.map((entry) => entry.path),
19
+ );
12
20
 
13
21
  const FORBIDDEN_MOCK_MODULES = new Map([
14
22
  [
@@ -35,27 +43,6 @@ const COMPLETE_EXPORT_MOCK_MODULES = new Set([
35
43
  "/channels/discord/runtime",
36
44
  ]);
37
45
 
38
- // Existing top-level module mocks that predate this guard. New top-level
39
- // internal mocks are rejected by default because they are active while Bun loads
40
- // other test files in the same process. Prefer dependency injection or an
41
- // explicit test override helper. If a new top-level mock is truly unavoidable,
42
- // add a file+module entry here in the same PR with a clear explanation.
43
- const ALLOWED_TOP_LEVEL_MOCKS = new Set([
44
- "src/channels/discord-registry.test.ts::../backend/api/client",
45
- "src/cli/message-search-cache-warm.test.ts::../backend/api/search",
46
- "src/hooks/prompt-executor.test.ts::../backend/api/generate",
47
- "src/tools/memory-apply-patch.test.ts::../backend/api/client",
48
- "src/tools/memory-tool.test.ts::../backend/api/client",
49
- "src/tools/toolset-client-tool-rule-cleanup.test.ts::../backend/api/client",
50
- "src/tools/toolset-memfs-detach.test.ts::../backend/api/client",
51
- "src/websocket/listen-client-concurrency.test.ts::../agent/approval-execution",
52
- "src/websocket/listen-client-concurrency.test.ts::../agent/approval-recovery",
53
- "src/websocket/listen-client-concurrency.test.ts::../agent/message",
54
- "src/websocket/listen-client-concurrency.test.ts::../backend/api/client",
55
- "src/websocket/listen-client-concurrency.test.ts::../cli/helpers/approvalClassification",
56
- "src/websocket/listen-client-concurrency.test.ts::../cli/helpers/stream",
57
- ]);
58
-
59
46
  const mockModulePattern = /\bmock\.module\s*\(\s*(["'`])([^"'`]+)\1/g;
60
47
  const restoreHookPattern =
61
48
  /\bafter(?:All|Each)\s*\(\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>[\s\S]*?\bmock\.restore\s*\(/m;
@@ -390,8 +377,7 @@ for (const filePath of collectTestFiles(testsDir)) {
390
377
  isRelativeInternalModule(moduleSpecifier) &&
391
378
  isTopLevelMockCall(sourceText, match.index ?? 0)
392
379
  ) {
393
- const key = `${relativePath}::${moduleSpecifier}`;
394
- if (!ALLOWED_TOP_LEVEL_MOCKS.has(key)) {
380
+ if (!ISOLATED_TEST_FILES.has(relativePath)) {
395
381
  failures.push({
396
382
  type: "top-level-mock",
397
383
  filePath: relativePath,
@@ -457,7 +443,7 @@ if (failures.length > 0) {
457
443
  ` unsafe top-level internal module mock: ${failure.moduleSpecifier}`,
458
444
  );
459
445
  console.error(
460
- " Top-level mock.module() calls are active while Bun loads other test files. Move the mock into the test/beforeEach, use dependency injection, or add an explicit test override helper.",
446
+ " Top-level mock.module() calls are active while Bun loads other test files. Move the mock into the test/beforeEach, use dependency injection, or register a justified standalone process in scripts/isolated-unit-tests.json.",
461
447
  );
462
448
  break;
463
449
  case "partial-runtime-mock":
@@ -481,7 +467,7 @@ if (failures.length > 0) {
481
467
  "\nWhy this fails: Bun module mocks are process-global and can leak across files in the shared module cache.",
482
468
  );
483
469
  console.error(
484
- "Prefer explicit test override helpers or dependency injection. If a module mock is unavoidable, keep it scoped and restore it with afterEach().",
470
+ "Prefer explicit test override helpers or dependency injection. If a module mock is unavoidable, keep it scoped and restore it with afterEach(); top-level mocks must also run in an isolated process.",
485
471
  );
486
472
  process.exit(1);
487
473
  }
@@ -0,0 +1,64 @@
1
+ {
2
+ "tests": [
3
+ {
4
+ "path": "src/channels/discord-registry.test.ts",
5
+ "timeoutMs": 15000,
6
+ "reason": "Uses a top-level Bun module mock for the backend client."
7
+ },
8
+ {
9
+ "path": "src/channels/discord-service.test.ts",
10
+ "timeoutMs": 15000,
11
+ "reason": "Mutates shared channel account, route, pairing, and target stores."
12
+ },
13
+ {
14
+ "path": "src/channels/slack/media.test.ts",
15
+ "timeoutMs": 15000,
16
+ "reason": "Imports the real media module while Slack adapter tests replace it."
17
+ },
18
+ {
19
+ "path": "src/cli/message-search-cache-warm.test.ts",
20
+ "timeoutMs": 15000,
21
+ "reason": "Uses a top-level Bun module mock for backend search."
22
+ },
23
+ {
24
+ "path": "src/hooks/prompt-executor.test.ts",
25
+ "timeoutMs": 15000,
26
+ "reason": "Uses a top-level Bun module mock for backend generation."
27
+ },
28
+ {
29
+ "path": "src/tools/bash.test.ts",
30
+ "timeoutMs": 30000,
31
+ "reason": "Spawns real shells and temporarily mutates process platform and PATH state."
32
+ },
33
+ {
34
+ "path": "src/tools/enter-worktree.test.ts",
35
+ "timeoutMs": 30000,
36
+ "reason": "Spawns real Git processes and mutates HOME, USER_CWD, cwd, and runtime singletons."
37
+ },
38
+ {
39
+ "path": "src/tools/memory-apply-patch.test.ts",
40
+ "timeoutMs": 15000,
41
+ "reason": "Uses a top-level Bun module mock for the backend client."
42
+ },
43
+ {
44
+ "path": "src/tools/memory-tool.test.ts",
45
+ "timeoutMs": 15000,
46
+ "reason": "Uses a top-level Bun module mock for the backend client."
47
+ },
48
+ {
49
+ "path": "src/tools/toolset-client-tool-rule-cleanup.test.ts",
50
+ "timeoutMs": 15000,
51
+ "reason": "Uses a top-level Bun module mock for the backend client."
52
+ },
53
+ {
54
+ "path": "src/tools/toolset-memfs-detach.test.ts",
55
+ "timeoutMs": 15000,
56
+ "reason": "Uses a top-level Bun module mock for the backend client."
57
+ },
58
+ {
59
+ "path": "src/websocket/listen-client-concurrency.test.ts",
60
+ "timeoutMs": 30000,
61
+ "reason": "Uses several top-level Bun module mocks and exercises real concurrent listener state."
62
+ }
63
+ ]
64
+ }
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
- const { execSync } = require("node:child_process");
3
- const { readdirSync, statSync } = require("node:fs");
2
+ const { execFileSync } = require("node:child_process");
3
+ const { readFileSync, readdirSync } = require("node:fs");
4
4
  const path = require("node:path");
5
5
 
6
6
  // Unit test directories — bun discovers *.test.ts / *.test.tsx within each.
7
- // Listed explicitly so we skip src/integration-tests (API-gated) and avoid
8
- // shell expansion that can exceed the Windows command-line length limit.
7
+ // Listed explicitly so we skip src/integration-tests (API-gated).
9
8
  const dirs = [
10
9
  "src/agent",
11
10
  "src/auth",
@@ -30,23 +29,23 @@ const dirs = [
30
29
  "src/utils",
31
30
  "src/web",
32
31
  "src/websocket",
33
- // Root-level test files (not inside a subdirectory)
34
- "src/*.test.ts",
35
32
  ];
36
33
 
37
- // slack/media.test.ts imports the real media module. The adapter test harness
38
- // mocks that module, which in Bun 1.3.x poisons the shared module registry
39
- // across parallel workers. Run media tests in an isolated process first, then
40
- // run src/channels with every other test file.
41
- function findTestFiles(dir, exclude) {
34
+ const isolationManifest = JSON.parse(
35
+ readFileSync(path.join(__dirname, "isolated-unit-tests.json"), "utf8"),
36
+ );
37
+ const isolatedTests = isolationManifest.tests;
38
+ const isolatedPaths = new Set(isolatedTests.map((entry) => entry.path));
39
+
40
+ function findTestFiles(dir) {
42
41
  const results = [];
43
42
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
44
43
  const full = path.join(dir, entry.name);
45
44
  if (entry.isDirectory()) {
46
- results.push(...findTestFiles(full, exclude));
45
+ results.push(...findTestFiles(full));
47
46
  } else if (
48
- (entry.name.endsWith(".test.ts") || entry.name.endsWith(".test.tsx")) &&
49
- !exclude.includes(full.replace(/\\/g, "/"))
47
+ entry.name.endsWith(".test.ts") ||
48
+ entry.name.endsWith(".test.tsx")
50
49
  ) {
51
50
  results.push(full.replace(/\\/g, "/"));
52
51
  }
@@ -54,28 +53,87 @@ function findTestFiles(dir, exclude) {
54
53
  return results;
55
54
  }
56
55
 
57
- const channelTestFiles = findTestFiles("src/channels", [
58
- "src/channels/slack/media.test.ts",
59
- ]);
56
+ function findRootTestFiles(dir) {
57
+ return readdirSync(dir, { withFileTypes: true })
58
+ .filter(
59
+ (entry) =>
60
+ entry.isFile() &&
61
+ (entry.name.endsWith(".test.ts") || entry.name.endsWith(".test.tsx")),
62
+ )
63
+ .map((entry) => path.join(dir, entry.name).replace(/\\/g, "/"));
64
+ }
65
+
66
+ const allTestFiles = [
67
+ ...dirs.flatMap((dir) => findTestFiles(dir)),
68
+ ...findTestFiles("src/channels"),
69
+ ...findRootTestFiles("src"),
70
+ ].sort();
71
+ const discoveredPaths = new Set(allTestFiles);
72
+
73
+ for (const entry of isolatedTests) {
74
+ if (!discoveredPaths.has(entry.path)) {
75
+ throw new Error(
76
+ `Isolated unit test is missing from the unit-test roots: ${entry.path}`,
77
+ );
78
+ }
79
+ if (!Number.isInteger(entry.timeoutMs) || entry.timeoutMs <= 0) {
80
+ throw new Error(`Invalid timeout for isolated unit test: ${entry.path}`);
81
+ }
82
+ }
83
+
84
+ function runTests(files, timeoutMs) {
85
+ execFileSync("bun", ["test", ...files, "--timeout", String(timeoutMs)], {
86
+ stdio: "inherit",
87
+ });
88
+ }
89
+
90
+ function chunkByCommandLength(files, maxChars = 20000) {
91
+ const chunks = [];
92
+ let chunk = [];
93
+ let chars = 0;
94
+
95
+ for (const file of files) {
96
+ const nextChars = file.length + 3;
97
+ if (chunk.length > 0 && chars + nextChars > maxChars) {
98
+ chunks.push(chunk);
99
+ chunk = [];
100
+ chars = 0;
101
+ }
102
+ chunk.push(file);
103
+ chars += nextChars;
104
+ }
105
+ if (chunk.length > 0) {
106
+ chunks.push(chunk);
107
+ }
108
+ return chunks;
109
+ }
60
110
 
61
- const opts = { stdio: "inherit", shell: process.platform === "win32" };
62
111
  let exitCode = 0;
63
112
 
64
- // Run Slack media tests in isolation first (clean module registry)
65
- try {
66
- execSync("bun test src/channels/slack/media.test.ts --timeout 15000", opts);
67
- } catch (e) {
68
- exitCode = e.status ?? 1;
113
+ // Bun module mocks and process-global state are shared within one test process.
114
+ // Keep the explicitly stateful suites in fresh processes so they cannot poison
115
+ // the ordinary unit batch or inherit another suite's cwd/env/module registry.
116
+ for (const entry of isolatedTests) {
117
+ try {
118
+ runTests([entry.path], entry.timeoutMs);
119
+ } catch (error) {
120
+ exitCode = error.status ?? 1;
121
+ }
69
122
  }
70
123
 
71
- // Run everything else
72
- try {
73
- execSync(
74
- `bun test ${[...dirs, ...channelTestFiles].join(" ")} --timeout 15000`,
75
- opts,
76
- );
77
- } catch (e) {
78
- exitCode = e.status ?? 1;
124
+ const sharedProcessTests = allTestFiles.filter(
125
+ (file) => !isolatedPaths.has(file),
126
+ );
127
+
128
+ // Passing argv directly avoids cmd.exe's shorter shell command-line limit on
129
+ // Windows. Bounded batches also stay below CreateProcess's 32k limit as the
130
+ // suite grows.
131
+ for (const batch of chunkByCommandLength(sharedProcessTests)) {
132
+ try {
133
+ runTests(batch, 15000);
134
+ } catch (error) {
135
+ exitCode = error.status ?? 1;
136
+ }
79
137
  }
80
138
 
81
139
  process.exit(exitCode);
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "src/agent/client-skills.test.ts": 1196,
3
3
  "src/agent/memory-git.ts": 2324,
4
- "src/agent/subagents/manager.ts": 1577,
5
4
  "src/backend/local-backend.test.ts": 2589,
6
5
  "src/backend/local/local-backend.ts": 1058,
7
- "src/backend/local/local-store.ts": 3629,
6
+ "src/backend/local/local-store.ts": 3618,
8
7
  "src/backend/pi-stream-adapter.test.ts": 1442,
9
8
  "src/cli/app/AppCoordinator.tsx": 5198,
10
9
  "src/cli/app/AppView.tsx": 1750,
@@ -42,7 +41,7 @@
42
41
  "src/tools/manager.ts": 3293,
43
42
  "src/tools/tool-execution-context.test.ts": 1422,
44
43
  "src/types/protocol_v2.ts": 2932,
45
- "src/websocket/listen-client-concurrency.test.ts": 3021,
44
+ "src/websocket/listen-client-concurrency.test.ts": 3017,
46
45
  "src/websocket/listen-client-protocol.test.ts": 6721,
47
46
  "src/websocket/listener/commands/channels.ts": 1390,
48
47
  "src/websocket/listener/commands/memory.ts": 1114,