|
|
@@ -12,7 +12,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
import * as fs from 'fs';
|
|
|
import * as os from 'os';
|
|
|
import * as path from 'path';
|
|
|
-import { planFrontload, findIndexedSubprojectRoots } from '../src/directory';
|
|
|
+import { planFrontload, findIndexedSubprojectRoots, isStructuralPrompt, hasStructuralKeyword, extractCodeTokens } from '../src/directory';
|
|
|
|
|
|
/** Make `dir` look indexed (isInitialized needs `.codegraph/codegraph.db`). */
|
|
|
function mkIndexed(dir: string): string {
|
|
|
@@ -128,3 +128,76 @@ describe('findIndexedSubprojectRoots', () => {
|
|
|
expect(findIndexedSubprojectRoots(tmp, { maxDepth: 2 })).toEqual([]);
|
|
|
});
|
|
|
});
|
|
|
+
|
|
|
+describe('hasStructuralKeyword — keyword signal fires the hook directly (#994)', () => {
|
|
|
+ it('English keywords match, with `\\b` so "flow" ≠ "flower"', () => {
|
|
|
+ expect(hasStructuralKeyword('how does article publish work')).toBe(true);
|
|
|
+ expect(hasStructuralKeyword('where is the token validated')).toBe(true);
|
|
|
+ expect(hasStructuralKeyword('trace the request flow')).toBe(true);
|
|
|
+ expect(hasStructuralKeyword('what calls parseToken')).toBe(true);
|
|
|
+ expect(hasStructuralKeyword('water the flower')).toBe(false); // "flow" in "flower"
|
|
|
+ });
|
|
|
+
|
|
|
+ it('Chinese keywords match WITHOUT `\\b` — the #994 fix (were silently dropped)', () => {
|
|
|
+ expect(hasStructuralKeyword('介绍文章发布流程')).toBe(true); // introduce / flow
|
|
|
+ expect(hasStructuralKeyword('登录是如何实现的')).toBe(true); // how / implement
|
|
|
+ expect(hasStructuralKeyword('这个函数的调用链')).toBe(true); // call (chain)
|
|
|
+ expect(hasStructuralKeyword('支付模块依赖哪些服务')).toBe(true); // depend
|
|
|
+ expect(hasStructuralKeyword('修复这个拼写错误')).toBe(false); // "fix this typo"
|
|
|
+ });
|
|
|
+
|
|
|
+ it('a bare code-token is NOT a keyword — it needs graph verification', () => {
|
|
|
+ expect(hasStructuralKeyword('看看 get_user 这段逻辑')).toBe(false);
|
|
|
+ expect(hasStructuralKeyword('I really love JavaScript')).toBe(false);
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+describe('extractCodeTokens — candidate symbols the hook verifies against the graph', () => {
|
|
|
+ it('pulls camelCase / PascalCase / snake_case / call / member tokens', () => {
|
|
|
+ expect(extractCodeTokens('prepareArticlePublish 的调用链')).toContain('prepareArticlePublish');
|
|
|
+ expect(extractCodeTokens('看看 get_user 这段逻辑')).toContain('get_user'); // snake_case
|
|
|
+ expect(extractCodeTokens('render() 在哪触发')).toContain('render'); // call form
|
|
|
+ expect(extractCodeTokens('user.login 做了什么').sort()).toEqual(['login', 'user']); // member access
|
|
|
+ expect(extractCodeTokens('看看 UserService')).toContain('UserService'); // PascalCase class kept
|
|
|
+ });
|
|
|
+
|
|
|
+ it('a tech brand is extracted as a CANDIDATE — the hook’s graph check is what rejects it', () => {
|
|
|
+ // This is the #994 follow-up: "JavaScript" is identifier-shaped, so it surfaces
|
|
|
+ // here as a candidate; the hook only fires if it's a real symbol in the index.
|
|
|
+ expect(extractCodeTokens('I really love JavaScript')).toEqual(['JavaScript']);
|
|
|
+ expect(extractCodeTokens('thoughts on GitHub vs GitLab').sort()).toEqual(['GitHub', 'GitLab']);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('ordinary prose and doc/data filenames yield no tokens', () => {
|
|
|
+ expect(extractCodeTokens('fix typo in readme')).toEqual([]);
|
|
|
+ expect(extractCodeTokens('fix the typo in README.md')).toEqual([]); // doc filename excluded
|
|
|
+ expect(extractCodeTokens('bump the version in package.json')).toEqual([]);
|
|
|
+ expect(extractCodeTokens('water the flower')).toEqual([]);
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+describe('isStructuralPrompt — cheap candidate gate (keyword OR code-token)', () => {
|
|
|
+ it('fires on a keyword prompt in any language', () => {
|
|
|
+ expect(isStructuralPrompt('how does article publish work')).toBe(true);
|
|
|
+ expect(isStructuralPrompt('介绍文章发布流程')).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('fires on a code-token prompt with no keyword', () => {
|
|
|
+ expect(isStructuralPrompt('看看 get_user 这段逻辑')).toBe(true);
|
|
|
+ expect(isStructuralPrompt('where is prepareArticlePublish 定义')).toBe(true);
|
|
|
+ expect(isStructuralPrompt('user.login 做了什么')).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('a tech brand passes the CHEAP gate as a candidate — the hook then graph-verifies it', () => {
|
|
|
+ // Layering, not a bug: isStructuralPrompt is shape-only, so a token-shaped brand
|
|
|
+ // is a candidate here; the hook rejects it as a non-symbol (proven by the CLI e2e).
|
|
|
+ expect(isStructuralPrompt('I really love JavaScript')).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('non-structural prose stays a no-op — in either language', () => {
|
|
|
+ expect(isStructuralPrompt('fix typo in readme')).toBe(false);
|
|
|
+ expect(isStructuralPrompt('修复这个拼写错误')).toBe(false);
|
|
|
+ expect(isStructuralPrompt('water the flower')).toBe(false);
|
|
|
+ expect(isStructuralPrompt('')).toBe(false);
|
|
|
+ });
|
|
|
+});
|