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, '');
});
});
|