Repository navigation
feat(memory): project-scoped memory writes and .qwen/QWEN.local.md #4290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
83726cf
1fccf10
d6deb95
efca260
fce7655
bfe2476
1bce6c6
0f8ce08
a58cc78
4d7da07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,7 +12,10 @@ import { | |||||||||||||||
| WorkspaceMemoryFileTooLargeError, | ||||||||||||||||
| WorkspaceMemoryWriteTimeoutError, | ||||||||||||||||
| getAllGeminiMdFilenames, | ||||||||||||||||
| LOCAL_CONTEXT_FILENAME, | ||||||||||||||||
| QWEN_DIR, | ||||||||||||||||
| writeWorkspaceContextFile, | ||||||||||||||||
| type WriteContextFileScope, | ||||||||||||||||
| } from '@qwen-code/qwen-code-core'; | ||||||||||||||||
| import { writeStderrLine } from '../utils/stdioHelpers.js'; | ||||||||||||||||
| import { isServeDebugMode } from './debugMode.js'; | ||||||||||||||||
|
|
@@ -78,6 +81,13 @@ export interface WorkspaceMemoryRouteDeps { | |||||||||||||||
|
|
||||||||||||||||
| const MAX_MEMORY_CONTENT_BYTES = 1024 * 1024; | ||||||||||||||||
|
|
||||||||||||||||
| const VALID_SCOPES: ReadonlySet<string> = new Set([ | ||||||||||||||||
| 'workspace', | ||||||||||||||||
| 'global', | ||||||||||||||||
| 'local', | ||||||||||||||||
| 'auto', | ||||||||||||||||
| ]); | ||||||||||||||||
|
|
||||||||||||||||
| /** Mount the two memory routes on the supplied Express app. */ | ||||||||||||||||
| export function mountWorkspaceMemoryRoutes( | ||||||||||||||||
| app: Application, | ||||||||||||||||
|
|
@@ -116,9 +126,9 @@ export function mountWorkspaceMemoryRoutes( | |||||||||||||||
| const body = deps.safeBody(req); | ||||||||||||||||
|
|
||||||||||||||||
| const scope = body['scope']; | ||||||||||||||||
| if (scope !== 'workspace' && scope !== 'global') { | ||||||||||||||||
| if (!VALID_SCOPES.has(scope as string)) { | ||||||||||||||||
| res.status(400).json({ | ||||||||||||||||
| error: '`scope` must be "workspace" or "global"', | ||||||||||||||||
| error: `\`scope\` must be one of: ${[...VALID_SCOPES].map((s) => `"${s}"`).join(', ')}`, | ||||||||||||||||
| code: 'invalid_scope', | ||||||||||||||||
| }); | ||||||||||||||||
| return; | ||||||||||||||||
|
|
@@ -183,7 +193,7 @@ export function mountWorkspaceMemoryRoutes( | |||||||||||||||
|
|
||||||||||||||||
| try { | ||||||||||||||||
| const result = await writeWorkspaceContextFile({ | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] When Consider defining a typed error class (e.g. — qwen3.7-max via Qwen Code /review |
||||||||||||||||
| scope, | ||||||||||||||||
| scope: scope as WriteContextFileScope, | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] The POST handler now accepts Fix: Update — qwen-latest-series-invite-beta-v28 via Qwen Code /review |
||||||||||||||||
| mode, | ||||||||||||||||
| content, | ||||||||||||||||
| projectRoot: deps.boundWorkspace, | ||||||||||||||||
|
|
@@ -337,6 +347,29 @@ async function collectWorkspaceMemoryStatus( | |||||||||||||||
| ); | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] No test covers this new — qwen3.7-max via Qwen Code /review |
||||||||||||||||
| files.push(...workspaceFiles); | ||||||||||||||||
|
|
||||||||||||||||
| // Probe for .qwen/QWEN.local.md at the workspace root. | ||||||||||||||||
| const localPath = path.join(boundWorkspace, QWEN_DIR, LOCAL_CONTEXT_FILENAME); | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] Write path uses
Suggested change
— qwen-latest-series-invite-beta-v34 via Qwen Code /review |
||||||||||||||||
| try { | ||||||||||||||||
| const stat = await fs.stat(localPath); | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion]
Suggested change
— qwen3.7-max via Qwen Code /review |
||||||||||||||||
| if (stat.isFile()) { | ||||||||||||||||
| files.push({ | ||||||||||||||||
| absolutePath: localPath, | ||||||||||||||||
| scope: 'local', | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical]
Suggested change
— qwen3.7-max via Qwen Code /review |
||||||||||||||||
| bytes: stat.size, | ||||||||||||||||
| }); | ||||||||||||||||
| } | ||||||||||||||||
| } catch (err) { | ||||||||||||||||
| if (!isEnoent(err)) { | ||||||||||||||||
| errors.push({ | ||||||||||||||||
| kind: 'memory_file', | ||||||||||||||||
| status: 'error', | ||||||||||||||||
| error: err instanceof Error ? err.message : String(err), | ||||||||||||||||
| errorKind: 'stat_failed', | ||||||||||||||||
| hint: localPath, | ||||||||||||||||
| }); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| const globalDir = Storage.getGlobalQwenDir(); | ||||||||||||||||
| for (const filename of filenames) { | ||||||||||||||||
| const candidate = path.join(globalDir, filename); | ||||||||||||||||
|
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
|
|
||
| export const DEFAULT_CONTEXT_FILENAME = 'QWEN.md'; | ||
| export const AGENT_CONTEXT_FILENAME = 'AGENTS.md'; | ||
| export const LOCAL_CONTEXT_FILENAME = 'QWEN.local.md'; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Fix: Consolidate — have — qwen-latest-series-invite-beta-v28 via Qwen Code /review |
||
| export const MEMORY_SECTION_HEADER = '## Qwen Added Memories'; | ||
|
|
||
| // This variable will hold the currently configured filename for context files. | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,9 +9,11 @@ import * as os from 'node:os'; | |||||||||||||||||||||||||||||||||||||||
| import * as path from 'node:path'; | ||||||||||||||||||||||||||||||||||||||||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||||||||||||||||||||||||||||||||||||||||
| import { Storage } from '../config/storage.js'; | ||||||||||||||||||||||||||||||||||||||||
| import { QWEN_DIR } from '../utils/paths.js'; | ||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||
| AGENT_CONTEXT_FILENAME, | ||||||||||||||||||||||||||||||||||||||||
| DEFAULT_CONTEXT_FILENAME, | ||||||||||||||||||||||||||||||||||||||||
| LOCAL_CONTEXT_FILENAME, | ||||||||||||||||||||||||||||||||||||||||
| MEMORY_SECTION_HEADER, | ||||||||||||||||||||||||||||||||||||||||
| setGeminiMdFilename, | ||||||||||||||||||||||||||||||||||||||||
| } from './const.js'; | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -391,4 +393,206 @@ describe('writeWorkspaceContextFile', () => { | |||||||||||||||||||||||||||||||||||||||
| setGeminiMdFilename(DEFAULT_CONTEXT_FILENAME); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| describe('auto scope', () => { | ||||||||||||||||||||||||||||||||||||||||
| let savedFilenames: string | string[]; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| beforeEach(() => { | ||||||||||||||||||||||||||||||||||||||||
| // Capture whatever the filename list is before we touch it. | ||||||||||||||||||||||||||||||||||||||||
| savedFilenames = [DEFAULT_CONTEXT_FILENAME, AGENT_CONTEXT_FILENAME]; | ||||||||||||||||||||||||||||||||||||||||
| setGeminiMdFilename(savedFilenames); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| afterEach(() => { | ||||||||||||||||||||||||||||||||||||||||
| setGeminiMdFilename(savedFilenames); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| it('writes to project QWEN.md when one exists at the project root', async () => { | ||||||||||||||||||||||||||||||||||||||||
| const projectQwenPath = path.join(workspace, DEFAULT_CONTEXT_FILENAME); | ||||||||||||||||||||||||||||||||||||||||
| await fs.writeFile(projectQwenPath, '# project\n', 'utf8'); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const result = await writeWorkspaceContextFile({ | ||||||||||||||||||||||||||||||||||||||||
| scope: 'auto', | ||||||||||||||||||||||||||||||||||||||||
| mode: 'append', | ||||||||||||||||||||||||||||||||||||||||
| content: '- memory entry', | ||||||||||||||||||||||||||||||||||||||||
| projectRoot: workspace, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| expect(result.filePath).toBe(projectQwenPath); | ||||||||||||||||||||||||||||||||||||||||
| const written = await fs.readFile(projectQwenPath, 'utf8'); | ||||||||||||||||||||||||||||||||||||||||
| expect(written).toContain('- memory entry'); | ||||||||||||||||||||||||||||||||||||||||
| // Global dir must not have been touched. | ||||||||||||||||||||||||||||||||||||||||
| const globalPath = path.join(globalDir, DEFAULT_CONTEXT_FILENAME); | ||||||||||||||||||||||||||||||||||||||||
| await expect(fs.access(globalPath)).rejects.toMatchObject({ | ||||||||||||||||||||||||||||||||||||||||
| code: 'ENOENT', | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| it('writes to project AGENTS.md when that exists instead of QWEN.md', async () => { | ||||||||||||||||||||||||||||||||||||||||
| const agentsPath = path.join(workspace, AGENT_CONTEXT_FILENAME); | ||||||||||||||||||||||||||||||||||||||||
| await fs.writeFile(agentsPath, '# agents\n', 'utf8'); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const result = await writeWorkspaceContextFile({ | ||||||||||||||||||||||||||||||||||||||||
| scope: 'auto', | ||||||||||||||||||||||||||||||||||||||||
| mode: 'append', | ||||||||||||||||||||||||||||||||||||||||
| content: '- agents memory', | ||||||||||||||||||||||||||||||||||||||||
| projectRoot: workspace, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| expect(result.filePath).toBe(agentsPath); | ||||||||||||||||||||||||||||||||||||||||
| const written = await fs.readFile(agentsPath, 'utf8'); | ||||||||||||||||||||||||||||||||||||||||
| expect(written).toContain('- agents memory'); | ||||||||||||||||||||||||||||||||||||||||
| // QWEN.md must not be created. | ||||||||||||||||||||||||||||||||||||||||
| await expect( | ||||||||||||||||||||||||||||||||||||||||
| fs.access(path.join(workspace, DEFAULT_CONTEXT_FILENAME)), | ||||||||||||||||||||||||||||||||||||||||
| ).rejects.toMatchObject({ code: 'ENOENT' }); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| it('falls back to global when no project context file exists', async () => { | ||||||||||||||||||||||||||||||||||||||||
| const result = await writeWorkspaceContextFile({ | ||||||||||||||||||||||||||||||||||||||||
| scope: 'auto', | ||||||||||||||||||||||||||||||||||||||||
| mode: 'append', | ||||||||||||||||||||||||||||||||||||||||
| content: '- fallback entry', | ||||||||||||||||||||||||||||||||||||||||
| projectRoot: workspace, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| expect(result.filePath).toBe( | ||||||||||||||||||||||||||||||||||||||||
| path.join(globalDir, DEFAULT_CONTEXT_FILENAME), | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| expect(getGlobalQwenDirSpy).toHaveBeenCalled(); | ||||||||||||||||||||||||||||||||||||||||
| const written = await fs.readFile(result.filePath, 'utf8'); | ||||||||||||||||||||||||||||||||||||||||
| expect(written).toBe(MEMORY_SECTION_HEADER + '\n- fallback entry\n'); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| it('prefers QWEN.md over AGENTS.md when both exist (QWEN.md first in filenames list)', async () => { | ||||||||||||||||||||||||||||||||||||||||
| const qwenPath = path.join(workspace, DEFAULT_CONTEXT_FILENAME); | ||||||||||||||||||||||||||||||||||||||||
| const agentsPath = path.join(workspace, AGENT_CONTEXT_FILENAME); | ||||||||||||||||||||||||||||||||||||||||
| await fs.writeFile(qwenPath, '# qwen\n', 'utf8'); | ||||||||||||||||||||||||||||||||||||||||
| await fs.writeFile(agentsPath, '# agents\n', 'utf8'); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const result = await writeWorkspaceContextFile({ | ||||||||||||||||||||||||||||||||||||||||
| scope: 'auto', | ||||||||||||||||||||||||||||||||||||||||
| mode: 'append', | ||||||||||||||||||||||||||||||||||||||||
| content: '- which one?', | ||||||||||||||||||||||||||||||||||||||||
| projectRoot: workspace, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Should prefer QWEN.md since getAllGeminiMdFilenames returns it first. | ||||||||||||||||||||||||||||||||||||||||
| expect(result.filePath).toBe(qwenPath); | ||||||||||||||||||||||||||||||||||||||||
| const qwenContent = await fs.readFile(qwenPath, 'utf8'); | ||||||||||||||||||||||||||||||||||||||||
| expect(qwenContent).toContain('- which one?'); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| it('falls back to local file when no committed context exists', async () => { | ||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This test verifies
Suggested change
— qwen-latest-series-invite-beta-v34 via Qwen Code /review |
||||||||||||||||||||||||||||||||||||||||
| const localPath = path.join(workspace, QWEN_DIR, LOCAL_CONTEXT_FILENAME); | ||||||||||||||||||||||||||||||||||||||||
| await fs.mkdir(path.dirname(localPath), { recursive: true }); | ||||||||||||||||||||||||||||||||||||||||
| await fs.writeFile(localPath, '# local\n', 'utf8'); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const result = await writeWorkspaceContextFile({ | ||||||||||||||||||||||||||||||||||||||||
| scope: 'auto', | ||||||||||||||||||||||||||||||||||||||||
| mode: 'append', | ||||||||||||||||||||||||||||||||||||||||
| content: '- auto local entry', | ||||||||||||||||||||||||||||||||||||||||
| projectRoot: workspace, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| expect(result.filePath).toBe(localPath); | ||||||||||||||||||||||||||||||||||||||||
| const written = await fs.readFile(localPath, 'utf8'); | ||||||||||||||||||||||||||||||||||||||||
| expect(written).toContain('- auto local entry'); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| it('prefers committed QWEN.md over local file in auto scope', async () => { | ||||||||||||||||||||||||||||||||||||||||
| const qwenPath = path.join(workspace, DEFAULT_CONTEXT_FILENAME); | ||||||||||||||||||||||||||||||||||||||||
| const localPath = path.join(workspace, QWEN_DIR, LOCAL_CONTEXT_FILENAME); | ||||||||||||||||||||||||||||||||||||||||
| await fs.writeFile(qwenPath, '# qwen\n', 'utf8'); | ||||||||||||||||||||||||||||||||||||||||
| await fs.mkdir(path.dirname(localPath), { recursive: true }); | ||||||||||||||||||||||||||||||||||||||||
| await fs.writeFile(localPath, '# local\n', 'utf8'); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const result = await writeWorkspaceContextFile({ | ||||||||||||||||||||||||||||||||||||||||
| scope: 'auto', | ||||||||||||||||||||||||||||||||||||||||
| mode: 'append', | ||||||||||||||||||||||||||||||||||||||||
| content: '- which scope?', | ||||||||||||||||||||||||||||||||||||||||
| projectRoot: workspace, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| expect(result.filePath).toBe(qwenPath); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| describe('local scope', () => { | ||||||||||||||||||||||||||||||||||||||||
| it('writes to .qwen/QWEN.local.md', async () => { | ||||||||||||||||||||||||||||||||||||||||
| const result = await writeWorkspaceContextFile({ | ||||||||||||||||||||||||||||||||||||||||
| scope: 'local', | ||||||||||||||||||||||||||||||||||||||||
| mode: 'append', | ||||||||||||||||||||||||||||||||||||||||
| content: '- local entry', | ||||||||||||||||||||||||||||||||||||||||
| projectRoot: workspace, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const expectedPath = path.join( | ||||||||||||||||||||||||||||||||||||||||
| workspace, | ||||||||||||||||||||||||||||||||||||||||
| QWEN_DIR, | ||||||||||||||||||||||||||||||||||||||||
| LOCAL_CONTEXT_FILENAME, | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| expect(result.filePath).toBe(expectedPath); | ||||||||||||||||||||||||||||||||||||||||
| const written = await fs.readFile(expectedPath, 'utf8'); | ||||||||||||||||||||||||||||||||||||||||
| expect(written).toBe(`${MEMORY_SECTION_HEADER}\n- local entry\n`); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| it('creates the .qwen directory if missing', async () => { | ||||||||||||||||||||||||||||||||||||||||
| const qwenDir = path.join(workspace, QWEN_DIR); | ||||||||||||||||||||||||||||||||||||||||
| await expect(fs.access(qwenDir)).rejects.toMatchObject({ | ||||||||||||||||||||||||||||||||||||||||
| code: 'ENOENT', | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| await writeWorkspaceContextFile({ | ||||||||||||||||||||||||||||||||||||||||
| scope: 'local', | ||||||||||||||||||||||||||||||||||||||||
| mode: 'append', | ||||||||||||||||||||||||||||||||||||||||
| content: '- created', | ||||||||||||||||||||||||||||||||||||||||
| projectRoot: workspace, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const stat = await fs.stat(qwenDir); | ||||||||||||||||||||||||||||||||||||||||
| expect(stat.isDirectory()).toBe(true); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| it('adds .qwen/QWEN.local.md to .gitignore on first write', async () => { | ||||||||||||||||||||||||||||||||||||||||
| await writeWorkspaceContextFile({ | ||||||||||||||||||||||||||||||||||||||||
| scope: 'local', | ||||||||||||||||||||||||||||||||||||||||
| mode: 'append', | ||||||||||||||||||||||||||||||||||||||||
| content: '- local entry', | ||||||||||||||||||||||||||||||||||||||||
| projectRoot: workspace, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const gitignore = await fs.readFile( | ||||||||||||||||||||||||||||||||||||||||
| path.join(workspace, '.gitignore'), | ||||||||||||||||||||||||||||||||||||||||
| 'utf8', | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| expect(gitignore).toContain(`${QWEN_DIR}/${LOCAL_CONTEXT_FILENAME}`); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| it('does not duplicate the gitignore entry on subsequent writes', async () => { | ||||||||||||||||||||||||||||||||||||||||
| await writeWorkspaceContextFile({ | ||||||||||||||||||||||||||||||||||||||||
| scope: 'local', | ||||||||||||||||||||||||||||||||||||||||
| mode: 'append', | ||||||||||||||||||||||||||||||||||||||||
| content: '- first', | ||||||||||||||||||||||||||||||||||||||||
| projectRoot: workspace, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| await writeWorkspaceContextFile({ | ||||||||||||||||||||||||||||||||||||||||
| scope: 'local', | ||||||||||||||||||||||||||||||||||||||||
| mode: 'append', | ||||||||||||||||||||||||||||||||||||||||
| content: '- second', | ||||||||||||||||||||||||||||||||||||||||
| projectRoot: workspace, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const gitignore = await fs.readFile( | ||||||||||||||||||||||||||||||||||||||||
| path.join(workspace, '.gitignore'), | ||||||||||||||||||||||||||||||||||||||||
| 'utf8', | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| const count = gitignore | ||||||||||||||||||||||||||||||||||||||||
| .split('\n') | ||||||||||||||||||||||||||||||||||||||||
| .filter( | ||||||||||||||||||||||||||||||||||||||||
| (l) => l.trim() === `${QWEN_DIR}/${LOCAL_CONTEXT_FILENAME}`, | ||||||||||||||||||||||||||||||||||||||||
| ).length; | ||||||||||||||||||||||||||||||||||||||||
| expect(count).toBe(1); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion]
VALID_SCOPESis a runtimeSet<string>, whileWriteContextFileScopeis a compile-time type union. They are maintained independently — adding a scope to one without the other causes silent drift (route rejects type-valid scopes, or resolver falls through to global for unknown scopes).Derive the type from the set:
— qwen-latest-series-invite-beta-v34 via Qwen Code /review