W
File size: 19,004 Bytes
2b64d42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { extractCallerEnvironment } from '../src/handlers/chat.js';
import { buildToolPreambleForProto } from '../src/handlers/tool-emulation.js';

// Why these tests exist:
//
// Without environment lifting Opus on Cascade believes its workspace is
// /tmp/windsurf-workspace (the planner's authoritative prior) and issues
// LS / Read tool calls against that path even when Claude Code's `<env>`
// block in the request says cwd is /Users/<user>/IdeaProjects/<project>.
// The model then narrates the contents of an empty scratch dir back as
// if it were the user's project.
//
// extractCallerEnvironment lifts the canonical Claude Code `<env>` keys
// (Working directory, Is directory a git repo, Platform, OS Version) so
// buildToolPreambleForProto can emit them as authoritative environment
// facts at the very top of the proto-level tool_calling_section
// override โ€” which IS authoritative to the upstream model and overrides
// the Cascade planner's workspace prior.

describe('extractCallerEnvironment', () => {
  it('lifts Claude Code <env> block from a system message', () => {
    const messages = [
      { role: 'system', content: 'You are Claude Code...\n\n<env>\nWorking directory: /Users/jaxyu/IdeaProjects/flux-panel\nIs directory a git repo: Yes\nPlatform: darwin\nOS Version: Darwin 24.0.0\nToday\'s date: 2026-04-25\n</env>\n\nMore instructions.' },
      { role: 'user', content: 'check the branches' },
    ];
    const result = extractCallerEnvironment(messages);
    assert.match(result, /- Working directory: \/Users\/jaxyu\/IdeaProjects\/flux-panel/);
    assert.match(result, /- Is the directory a git repo: Yes/);
    assert.match(result, /- Platform: darwin/);
    assert.match(result, /- OS version: Darwin 24\.0\.0/);
  });

  it('lifts cwd from a <system-reminder> embedded in a user message (Claude Code 2.x layout)', () => {
    const messages = [
      { role: 'system', content: 'You are Claude Code...' },
      { role: 'user', content: '<system-reminder>\nSkills available...\n\n<env>\nWorking directory: /home/dev/proj\n</env>\n</system-reminder>\n\nactual question here' },
    ];
    const result = extractCallerEnvironment(messages);
    assert.match(result, /- Working directory: \/home\/dev\/proj/);
  });

  it('handles content-block arrays (Anthropic-format text blocks)', () => {
    const messages = [
      { role: 'user', content: [
        { type: 'text', text: 'Working directory: /var/app' },
        { type: 'text', text: 'Platform: linux' },
      ]},
    ];
    const result = extractCallerEnvironment(messages);
    assert.match(result, /- Working directory: \/var\/app/);
    assert.match(result, /- Platform: linux/);
  });

  it('lifts Windows CWD from Claude Code 2.1.120 system-reminder form', () => {
    const messages = [
      { role: 'user', content: [
        { type: 'text', text: '<system-reminder>\nAs you answer the user, use this context:\n# currentWorkingDirectory\nCWD: D:\\Project\\WindsurfAPI\n</system-reminder>' },
        { type: 'text', text: 'Read package.json' },
      ] },
    ];
    const result = extractCallerEnvironment(messages);
    assert.match(result, /- Working directory: D:\\Project\\WindsurfAPI/);
  });

  it('returns empty string when no env hints are present', () => {
    const messages = [
      { role: 'system', content: 'You are a helpful assistant.' },
      { role: 'user', content: 'hello' },
    ];
    assert.equal(extractCallerEnvironment(messages), '');
  });

  it('lifts cwd from Claude Code 2.1+ prose form (no key/value separator)', () => {
    // Real Claude Code v2.1.114 system prompt embeds cwd in prose, e.g.:
    //   "You are an interactive agent that helps users with software
    //    engineering tasks and the current working directory is /Users/.../proj."
    // The line-anchored key/value form does not match; we fall back to a
    // looser "current working directory is /path" pattern that requires the
    // captured slot to actually look like a path (`[/~]โ€ฆ`).
    const messages = [
      { role: 'system', content: 'You are an interactive agent that helps users with software engineering tasks and the current working directory is /Users/jaxyu/IdeaProjects/flux-panel. Match the user\'s language.' },
      { role: 'user', content: 'check the project' },
    ];
    const result = extractCallerEnvironment(messages);
    assert.match(result, /- Working directory: \/Users\/jaxyu\/IdeaProjects\/flux-panel/);
  });

  it('lifts cwd from prose form with backticks around the path', () => {
    const messages = [
      { role: 'system', content: 'The current working directory is `/Users/dev/proj`.' },
    ];
    assert.match(extractCallerEnvironment(messages), /- Working directory: \/Users\/dev\/proj/);
  });

  it('does not match abstract prose without an actual path', () => {
    // "the working directory you choose" / "the working directory in the
    // docs" never has a `/` or `~` in the captured slot, so the path-tail
    // guard rejects it.
    const messages = [
      { role: 'user', content: 'Note: the working directory you choose is up to you.' },
      { role: 'user', content: 'See the working directory in the docs.' },
    ];
    assert.equal(extractCallerEnvironment(messages), '');
  });

  it('takes the first occurrence per key (closest to system / earliest message)', () => {
    const messages = [
      { role: 'system', content: 'Working directory: /first' },
      { role: 'user', content: 'Working directory: /second' },
    ];
    assert.match(extractCallerEnvironment(messages), /\/first/);
  });

  it('rejects values that are control-character noise or our own redaction marker', () => {
    const messages = [
      { role: 'system', content: 'Working directory: <workspace>' },
    ];
    assert.equal(extractCallerEnvironment(messages), '');
  });

  it('handles non-array input safely', () => {
    assert.equal(extractCallerEnvironment(null), '');
    assert.equal(extractCallerEnvironment(undefined), '');
    assert.equal(extractCallerEnvironment('not an array'), '');
  });

  // โ”€โ”€โ”€โ”€โ”€ #100 follow-up: bare-path fallback when no <env> block โ”€โ”€โ”€โ”€โ”€
  describe('bare-path cwd fallback (#100)', () => {
    it('lifts a Windows path glued to Chinese text in the first user message', () => {
      // Real yunduobaba prompt โ€” no <env>, no separator between path and CJK.
      const messages = [
        { role: 'user', content: 'C:\\Users\\renfei\\Downloads\\WindsurfAPI-master\\WindsurfAPI-masterๅˆ†ๆžไธ‹่ฟ™ไธช้กน็›ฎ' },
      ];
      const out = extractCallerEnvironment(messages);
      assert.equal(out, '- Working directory: C:\\Users\\renfei\\Downloads\\WindsurfAPI-master\\WindsurfAPI-master');
    });

    it('lifts a Unix path at the start of a user prompt', () => {
      const messages = [
        { role: 'user', content: '/home/user/projects/myproj ๅธฎๆˆ‘ๅˆ†ๆž' },
      ];
      assert.match(extractCallerEnvironment(messages), /\/home\/user\/projects\/myproj/);
    });

    it('lifts a Mac /Users path with no separator', () => {
      const messages = [
        { role: 'user', content: '/Users/jane/code/app please review' },
      ];
      assert.match(extractCallerEnvironment(messages), /\/Users\/jane\/code\/app/);
    });

    it('lifts a tilde path', () => {
      const messages = [
        { role: 'user', content: '~/dotfiles ็œ‹็œ‹่ฟ™ไธช' },
      ];
      assert.match(extractCallerEnvironment(messages), /~\/dotfiles/);
    });

    it('rejects a path that ends in a common file extension (single-file reference)', () => {
      const messages = [
        { role: 'user', content: 'C:\\Users\\me\\notes.md ่งฃ้‡Š่ฟ™ไธชๆ–‡ไปถ' },
      ];
      // The file path is a target, not a cwd. Should not lift.
      assert.equal(extractCallerEnvironment(messages), '');
    });

    it('does NOT trigger when the canonical extractor already found cwd', () => {
      // Bare-path fallback is a last-resort. If <env> already gave us cwd we use that.
      const messages = [
        { role: 'system', content: 'Working directory: /Users/dev/proj' },
        { role: 'user', content: 'C:\\some\\windows\\path ๅˆ†ๆž' },
      ];
      const out = extractCallerEnvironment(messages);
      assert.match(out, /\/Users\/dev\/proj/);
      assert.doesNotMatch(out, /windows\\path/);
    });

    it('only scans the first user message (later assistant/tool replies do not count)', () => {
      const messages = [
        { role: 'user', content: 'no path here' },
        { role: 'assistant', content: 'I see C:\\some\\path in some logs' },
      ];
      assert.equal(extractCallerEnvironment(messages), '');
    });

    it('only scans the leading 200 chars of a user message (mid-prose paths skipped)', () => {
      const head = 'I have been wondering for a long time about a thing in the project that lives somewhere in my filesystem and I think it might be useful to look there. The path I have in mind is /home/user/proj but please confirm.';
      assert.ok(head.length > 200);
      const messages = [{ role: 'user', content: head }];
      // The path appears past char 200 โ†’ fallback should NOT trigger.
      assert.equal(extractCallerEnvironment(messages), '');
    });

    it('rejects too-short fragments like /a or C:\\', () => {
      assert.equal(extractCallerEnvironment([{ role: 'user', content: '/a please look' }]), '');
      assert.equal(extractCallerEnvironment([{ role: 'user', content: 'C:\\ open this' }]), '');
    });

    it('handles content-block array with a path in the first text block', () => {
      const messages = [
        { role: 'user', content: [
          { type: 'text', text: 'D:\\Project\\WindsurfAPI ไฝ ็œ‹ไธ€ไธ‹' },
        ]},
      ];
      assert.match(extractCallerEnvironment(messages), /D:\\Project\\WindsurfAPI/);
    });

    // โ”€โ”€โ”€โ”€โ”€ #100 follow-up #2 (yunduobaba): Claude Code <system-reminder> wrappers โ”€โ”€โ”€โ”€โ”€
    //
    // Claude Code's hooks inject one or more <system-reminder>...</system-reminder>
    // blocks at the very top of every user turn โ€” frequently 1โ€“5 KB
    // (skills list, available tools, MCP server hints, todo state). That
    // pushes the path the user actually typed past the 300-char head and
    // the original pass-1 scan misses it. Real reproduction from the
    // user's debug log: lastUser=len=14095 with the path at the very
    // start of the *user's* prose but buried under reminder wrappers.

    it('lifts a path from after a 1KB <system-reminder> block (the #100 follow-up bug)', () => {
      const reminder = '<system-reminder>' + 'x'.repeat(1000) + '</system-reminder>\n\n';
      const messages = [
        { role: 'user', content: reminder + 'C:\\Users\\renfei\\Downloads\\WindsurfAPI-master ๅˆ†ๆžไธ‹่ฟ™ไธช้กน็›ฎ' },
      ];
      const out = extractCallerEnvironment(messages);
      assert.match(out, /C:\\Users\\renfei\\Downloads\\WindsurfAPI-master/,
        'path past 300 chars but at start of post-reminder content must lift');
    });

    it('lifts a path from after multiple stacked <system-reminder> blocks', () => {
      const r1 = '<system-reminder>' + 'a'.repeat(800) + '</system-reminder>';
      const r2 = '<system-reminder>' + 'b'.repeat(800) + '</system-reminder>';
      const r3 = '<system-reminder>' + 'c'.repeat(800) + '</system-reminder>';
      const messages = [
        { role: 'user', content: `${r1}\n${r2}\n${r3}\n\n/home/dev/myproj ๅธฎๆˆ‘็œ‹ไธ‹` },
      ];
      assert.match(extractCallerEnvironment(messages), /\/home\/dev\/myproj/);
    });

    it('does NOT match a path buried in prose after stripping reminders', () => {
      // Pass 2 must remain anchored โ€” only paths at the start of the
      // unwrapped content count. A reminder followed by prose followed
      // by a path is still a mid-prose mention, not a cwd hint.
      const reminder = '<system-reminder>' + 'x'.repeat(500) + '</system-reminder>\n\n';
      const messages = [
        { role: 'user', content: reminder + 'I was wondering about /home/user/proj because something something' },
      ];
      assert.equal(extractCallerEnvironment(messages), '');
    });

    it('skips pass 2 entirely when no <system-reminder> wrapper is present', () => {
      // Cheap-out: if there's no reminder wrapper there's nothing to strip,
      // and the original pass-1 result already covers the case.
      const messages = [
        { role: 'user', content: 'just a question with no path and no reminder' },
      ];
      assert.equal(extractCallerEnvironment(messages), '');
    });
  });

  // โ”€โ”€โ”€โ”€โ”€ #106 / #107 (zhangzhang-bit): adjective-prefixed cwd + bullet fallback โ”€โ”€โ”€โ”€โ”€
  //
  // Two real-world failure modes from a Claude Code 2.x system prompt:
  //
  //   (A) The canonical key is preceded by an adjective:
  //         "- Primary working directory: D:\Project\foo"
  //       The old regex only matched bare "Working directory" so this
  //       lifted as nothing.
  //
  //   (B) The system prompt mentions "current working directory" in
  //       prose first (no path adjacent), and the actual cwd appears
  //       later as a standalone bullet line. Old regex stopped at the
  //       first textual hit and returned empty.

  describe('Claude Code 2.x adjective + bullet cwd (#106 / #107)', () => {
    it('lifts cwd from "Primary working directory: ..." (Claude Code 2.x phrasing)', () => {
      const messages = [
        { role: 'system', content: '# Environment\nYou have been invoked in the following environment:\n - Primary working directory: D:\\Project\\WindsurfAPI\n - Is a git repository: true\n - Platform: win32\n' },
        { role: 'user', content: 'hi' },
      ];
      const out = extractCallerEnvironment(messages);
      assert.match(out, /- Working directory: D:\\Project\\WindsurfAPI/,
        'adjective-prefixed "Primary working directory" must lift');
      assert.match(out, /- Is the directory a git repo: true/,
        'Claude Code 2.x "Is a git repository" must also lift');
      assert.match(out, /- Platform: win32/);
    });

    it('lifts cwd via prose-then-bullet pattern (#107 zhangzhang-bit symptom)', () => {
      // 26 KB system prompt that says "...current working directory."
      // mid-prose with NO adjacent path, then has the actual cwd in a
      // bullet much later. Old regex would match the prose form first,
      // capture nothing, and return empty.
      const filler = 'lorem ipsum dolor sit amet '.repeat(200); // ~5 KB filler
      const sys = [
        'You are an interactive agent that helps users with software engineering tasks and the current working directory.',
        '',
        filler,
        '',
        '# Environment',
        ' - Primary working directory: D:\\Project\\foo',
        ' - Platform: win32',
      ].join('\n');
      const messages = [
        { role: 'system', content: sys },
        { role: 'user', content: 'analyze this' },
      ];
      assert.match(extractCallerEnvironment(messages), /- Working directory: D:\\Project\\foo/,
        'must skip the keyword-only prose mention and find the bulleted cwd later');
    });

    it('falls back to a standalone bullet path when no key/value pair exists', () => {
      // Custom agent emitting just a bullet list of paths with no
      // explicit "Working directory:" key. Last-resort scanForBulletCwdInSystem
      // should pick the first absolute-looking path.
      const messages = [
        { role: 'system', content: 'Environment facts:\n - D:\\Project\\foo\n - some other note' },
        { role: 'user', content: 'hi' },
      ];
      assert.match(extractCallerEnvironment(messages), /- Working directory: D:\\Project\\foo/);
    });

    it('bullet-fallback ignores file-extension paths and our redaction marker', () => {
      // A bullet pointing at a single file is not a cwd hint.
      const messages = [
        { role: 'system', content: 'Files of interest:\n - D:\\Project\\foo\\readme.md\n - <workspace>' },
        { role: 'user', content: 'hi' },
      ];
      assert.equal(extractCallerEnvironment(messages), '',
        'file-target bullets and the <workspace> redaction marker must not lift as cwd');
    });

    it('bullet-fallback only scans system messages (chat-mention paths do not count)', () => {
      const messages = [
        { role: 'system', content: 'no env here' },
        { role: 'user', content: 'I was browsing /home/dev/random earlier โ€” unrelated' },
      ];
      assert.equal(extractCallerEnvironment(messages), '',
        'a path mentioned in a user chat message must not be promoted to cwd via the system-bullet fallback');
    });

    it('matches the canonical "Working directory" form as before (back-compat)', () => {
      const messages = [
        { role: 'system', content: '<env>\nWorking directory: /Users/jane/proj\n</env>' },
        { role: 'user', content: 'hi' },
      ];
      assert.match(extractCallerEnvironment(messages), /- Working directory: \/Users\/jane\/proj/);
    });
  });
});

describe('buildToolPreambleForProto with environment override', () => {
  const tools = [{ type: 'function', function: { name: 'Bash', description: 'Run shell', parameters: { type: 'object' } } }];

  it('emits an authoritative environment block before the protocol header when env is provided', () => {
    const env = '- Working directory: /Users/jaxyu/IdeaProjects/flux-panel\n- Platform: darwin';
    const out = buildToolPreambleForProto(tools, 'auto', env);
    // Env block must come BEFORE the protocol header
    const envIdx = out.indexOf('## Environment facts');
    const headerIdx = out.indexOf('You have access to the following functions');
    assert.ok(envIdx >= 0, 'env header must be present');
    assert.ok(headerIdx >= 0, 'protocol header must be present');
    assert.ok(envIdx < headerIdx, 'env block must come BEFORE the protocol header');
    assert.match(out, /\/Users\/jaxyu\/IdeaProjects\/flux-panel/);
    assert.match(out, /active execution context/i);
    assert.doesNotMatch(out, /ignore|for this request only|---/i);
  });

  it('omits the environment block when env is empty (back-compat with PR #54 shape)', () => {
    const out = buildToolPreambleForProto(tools, 'auto', '');
    assert.ok(!out.includes('Authoritative environment'));
    // Tool protocol still rendered as before
    assert.match(out, /You have access to the following functions/);
    assert.match(out, /### Bash/);
  });

  it('omits the environment block when env is missing', () => {
    const out = buildToolPreambleForProto(tools, 'auto');
    assert.ok(!out.includes('Authoritative environment'));
    assert.match(out, /You have access to the following functions/);
  });

  it('still returns empty string when there are no tools (env alone is not enough to render)', () => {
    const out = buildToolPreambleForProto([], 'auto', '- Working directory: /x');
    assert.equal(out, '');
  });
});