* fix: convert dragged file paths to @mentions for attachment
When non-image files are dragged into the terminal, the file path was
inserted as plain text and never attached. Now detected absolute paths
are converted to @mentions so they get picked up by the attachment system.
* test: add tests for drag-and-drop file path detection
* fix: multi-image drag-and-drop only showing last image
insertTextAtCursor read input and cursorOffset from the React closure,
which is stale when called in a synchronous loop (e.g. onImagePaste for
multiple dragged images). Now uses refs so each insertion chains on the
previous one.
* fix: quote Windows absolute paths to avoid MCP mention collision
Paths containing ':' (e.g. Windows drive letters) are now emitted in
quoted @"..." form so they don't match the MCP resource mention regex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: decouple dragDropPaths from imagePaste and harden image checks
- Check image extension against the cleaned path (post quote/escape
stripping) so quoted or backslash-escaped image drops are reliably
routed to the image paste handler.
- Inline the image extension regex and drop the imagePaste/fsOperations
imports so the module (and its tests) no longer pull in `bun:bundle`
and the heavier fs wrapper chain. Use plain `fs.existsSync` for the
on-disk check.
- Add tests covering quoted image paths, uppercase extensions,
backslash-escaped image paths, escaped real files with spaces, mixed
segments containing an image, quoted-nonexistent paths, and leading
or trailing whitespace.
* test: verify dragged paths with an `@` segment are preserved
Adds a fixture under a scoped-package-style subdir (`@types/index.d.ts`)
so we exercise the realistic `node_modules/@types/...` drag case and
lock in that `extractDraggedFilePaths` returns the raw path unchanged —
the `@` inside the path must not collide with the mention prefix the
caller prepends downstream.
* test: parametrize dragDropPaths cases with test.each
Groups the 21 scenarios into four table-driven describes
(empty-result, single-path, multi-path, backslash-escaped) so that
adding a new case is a one-line row instead of a new `test()` block.
Fixture directories are now created synchronously at describe-load
time so their paths are available to the test.each tables, which are
built before any hook runs.
* test: add contract tests for @-mention extractor boundary
Pins the contract between `extractAtMentionedFiles` and
`extractMcpResourceMentions` so the MCP regex can't silently swallow
quoted file-path mentions.
These tests fail on current HEAD — 3 of 11 cases expose the regression
pointed out in the review on #382: `extractMcpResourceMentions`'s
trailing `\b` backtracks past the closing `"` of a quoted mention and
produces a ghost match for `@"C:\Users\..."`, `@C:\Users\...`, and
`@"/tmp/weird:name.txt"`. The remaining 8 cases lock in the behaviour
that must not change (legitimate `server:resource` mentions and plain
file-path mentions).
Committed failing on purpose as the first half of a test-then-fix
pair; the regex fix follows in a subsequent commit.
* fix: prevent MCP extractor from ghost-matching quoted/Windows paths
The MCP resource regex used `\b` as a trailing anchor with `[^\s]+`
character classes. On any quoted file mention containing a colon
(`@"C:\Users\me\file.txt"`, `@"/tmp/weird:name.txt"`), the engine
backtracked past the closing `"` to satisfy `\b`, producing a ghost
match that collided with `extractAtMentionedFiles`. Unquoted Windows
drive-letter paths (`@C:\Users\me\file.txt`) also matched because a
drive letter is structurally identical to an MCP `server:resource`
token.
Two guards:
1. `(?!")` right after `@` drops quoted tokens entirely, and adding
`"` to the character classes blocks any mid-match backtracking.
2. A post-match filter discards `^[A-Za-z]:[\\/]` — a single-letter
server followed by a path separator is always a Windows drive
prefix, never a real MCP resource.
Legitimate MCP forms (`@server:resource/path`, plugin-scoped like
`@asana-plugin:project-status/123`, inline prose mentions) remain
matched and are pinned by the contract tests added in 04998d5.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
86 lines
3.5 KiB
TypeScript
86 lines
3.5 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
import {
|
|
extractAtMentionedFiles,
|
|
extractMcpResourceMentions,
|
|
} from './attachments.js'
|
|
|
|
// Contract tests for the two @-mention extractors.
|
|
//
|
|
// Scope: the narrow contract between `extractAtMentionedFiles` and
|
|
// `extractMcpResourceMentions` where both are called on the same input
|
|
// and must not both claim the same token. The motivating bug is that
|
|
// `extractMcpResourceMentions`'s `\b` anchor lets it backtrack over the
|
|
// closing quote of a quoted file mention, producing a ghost match for
|
|
// `@"C:\Users\..."`. These tests pin the boundary so any regression in
|
|
// the MCP regex is caught immediately.
|
|
describe('extractor contract', () => {
|
|
describe('extractMcpResourceMentions must return empty for', () => {
|
|
const cases: Array<[string, string]> = [
|
|
// Primary bug: the quoted form that PromptInput emits for Windows
|
|
// paths today. `\b` backtracks past the trailing `"` and produces
|
|
// a ghost MCP match on current HEAD.
|
|
['a quoted Windows drive-letter path', '@"C:\\Users\\me\\file.txt"'],
|
|
// Even if the quote layer were stripped, a bare drive letter
|
|
// followed by a path separator is never an MCP resource.
|
|
['an unquoted Windows drive-letter path', '@C:\\Users\\me\\file.txt'],
|
|
// Sanity: quoted POSIX paths with no `:` at all never matched the
|
|
// MCP regex and must keep not matching after the fix.
|
|
['a quoted POSIX path with a space', '@"/Users/foo/my file.ts"'],
|
|
['an unquoted POSIX path', '@/Users/foo/bar.ts'],
|
|
// Quoted POSIX path that embeds a `:` in the filename — the quote
|
|
// layer must shield it from MCP matching, same as the Windows case.
|
|
['a quoted POSIX path with a colon in the name', '@"/tmp/weird:name.txt"'],
|
|
]
|
|
test.each(cases)('%s', (_label, input) => {
|
|
expect(extractMcpResourceMentions(input)).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('extractMcpResourceMentions still matches legitimate MCP mentions', () => {
|
|
// Regression guard for the fix. If someone tightens the MCP regex
|
|
// too aggressively, these break and the intent is clear.
|
|
const cases: Array<[string, string, string[]]> = [
|
|
[
|
|
'a simple server:resource token',
|
|
'@server:resource/path',
|
|
['server:resource/path'],
|
|
],
|
|
[
|
|
'a plugin-scoped server name with a dash',
|
|
'@asana-plugin:project-status/123',
|
|
['asana-plugin:project-status/123'],
|
|
],
|
|
[
|
|
'an MCP mention inline in prose',
|
|
'please check @server:res here',
|
|
['server:res'],
|
|
],
|
|
]
|
|
test.each(cases)('%s', (_label, input, expected) => {
|
|
expect(extractMcpResourceMentions(input)).toEqual(expected)
|
|
})
|
|
})
|
|
|
|
describe('extractAtMentionedFiles extracts the file paths it should', () => {
|
|
// Asserted separately from the MCP side: the bug is purely in the
|
|
// MCP extractor over-matching, so these assertions are the
|
|
// "baseline still works" half of the contract.
|
|
const cases: Array<[string, string, string[]]> = [
|
|
[
|
|
'a quoted Windows drive-letter path',
|
|
'@"C:\\Users\\me\\file.txt"',
|
|
['C:\\Users\\me\\file.txt'],
|
|
],
|
|
[
|
|
'a quoted POSIX path with a space',
|
|
'@"/Users/foo/my file.ts"',
|
|
['/Users/foo/my file.ts'],
|
|
],
|
|
['an unquoted POSIX path', '@/Users/foo/bar.ts', ['/Users/foo/bar.ts']],
|
|
]
|
|
test.each(cases)('%s', (_label, input, expected) => {
|
|
expect(extractAtMentionedFiles(input)).toEqual(expected)
|
|
})
|
|
})
|
|
})
|