basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7import { promises as fs } from 'node:fs';8import * as os from 'node:os';9import * as path from 'node:path';10import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';11import { Storage } from '../config/storage.js';12import {13 AGENT_CONTEXT_FILENAME,14 DEFAULT_CONTEXT_FILENAME,15 MEMORY_SECTION_HEADER,16 setGeminiMdFilename,17} from './const.js';18import { writeWorkspaceContextFile } from './writeContextFile.js';19 20describe('writeWorkspaceContextFile', () => {21 let tmpRoot: string;22 let workspace: string;23 let globalDir: string;24 let getGlobalQwenDirSpy: ReturnType<typeof vi.spyOn>;25 26 beforeEach(async () => {27 tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'qwen-write-context-'));28 workspace = path.join(tmpRoot, 'workspace');29 globalDir = path.join(tmpRoot, 'global');30 await fs.mkdir(workspace, { recursive: true });31 getGlobalQwenDirSpy = vi32 .spyOn(Storage, 'getGlobalQwenDir')33 .mockReturnValue(globalDir);34 });35 36 afterEach(async () => {37 getGlobalQwenDirSpy.mockRestore();38 await fs.rm(tmpRoot, { recursive: true, force: true });39 });40 41 it('creates QWEN.md with a fresh section header on first append', async () => {42 const result = await writeWorkspaceContextFile({43 scope: 'workspace',44 mode: 'append',45 content: '- first entry',46 projectRoot: workspace,47 });48 49 expect(result.filePath).toBe(50 path.join(workspace, DEFAULT_CONTEXT_FILENAME),51 );52 const written = await fs.readFile(result.filePath, 'utf8');53 expect(written).toBe(`${MEMORY_SECTION_HEADER}\n- first entry\n`);54 expect(result.bytesWritten).toBe(Buffer.byteLength(written, 'utf8'));55 });56 57 it('appends under existing section header', async () => {58 const initial = `# project notes\n\n${MEMORY_SECTION_HEADER}\n- first entry\n`;59 const filePath = path.join(workspace, DEFAULT_CONTEXT_FILENAME);60 await fs.writeFile(filePath, initial, 'utf8');61 62 await writeWorkspaceContextFile({63 scope: 'workspace',64 mode: 'append',65 content: '- second entry',66 projectRoot: workspace,67 });68 69 const written = await fs.readFile(filePath, 'utf8');70 expect(written).toBe(71 `# project notes\n\n${MEMORY_SECTION_HEADER}\n- first entry\n- second entry\n`,72 );73 });74 75 it('inserts a section header when file lacks one', async () => {76 const initial = '# project notes\n';77 const filePath = path.join(workspace, DEFAULT_CONTEXT_FILENAME);78 await fs.writeFile(filePath, initial, 'utf8');79 80 await writeWorkspaceContextFile({81 scope: 'workspace',82 mode: 'append',83 content: '- entry',84 projectRoot: workspace,85 });86 87 const written = await fs.readFile(filePath, 'utf8');88 expect(written).toBe(89 `# project notes\n\n${MEMORY_SECTION_HEADER}\n- entry\n`,90 );91 });92 93 it('replaces file contents in replace mode', async () => {94 const filePath = path.join(workspace, DEFAULT_CONTEXT_FILENAME);95 await fs.writeFile(filePath, 'old contents\n', 'utf8');96 97 const result = await writeWorkspaceContextFile({98 scope: 'workspace',99 mode: 'replace',100 content: 'replacement\n',101 projectRoot: workspace,102 });103 104 const written = await fs.readFile(filePath, 'utf8');105 expect(written).toBe('replacement\n');106 expect(result.bytesWritten).toBe(107 Buffer.byteLength('replacement\n', 'utf8'),108 );109 });110 111 it('writes to the global ~/.qwen directory when scope=global', async () => {112 const result = await writeWorkspaceContextFile({113 scope: 'global',114 mode: 'append',115 content: '- global entry',116 projectRoot: workspace,117 });118 119 expect(result.filePath).toBe(120 path.join(globalDir, DEFAULT_CONTEXT_FILENAME),121 );122 expect(getGlobalQwenDirSpy).toHaveBeenCalled();123 const written = await fs.readFile(result.filePath, 'utf8');124 expect(written).toBe(`${MEMORY_SECTION_HEADER}\n- global entry\n`);125 });126 127 it('creates the parent directory when missing', async () => {128 const nested = path.join(workspace, 'nested', 'deep');129 await writeWorkspaceContextFile({130 scope: 'workspace',131 mode: 'append',132 content: '- entry',133 projectRoot: nested,134 });135 136 const created = await fs.readFile(137 path.join(nested, DEFAULT_CONTEXT_FILENAME),138 'utf8',139 );140 expect(created).toContain('- entry');141 });142 143 it('rejects non-absolute projectRoot', async () => {144 await expect(145 writeWorkspaceContextFile({146 scope: 'workspace',147 mode: 'append',148 content: 'x',149 projectRoot: 'relative/path',150 }),151 ).rejects.toThrow(/projectRoot must be absolute/);152 });153 154 it('skips the write entirely when append content is whitespace only', async () => {155 const filePath = path.join(workspace, DEFAULT_CONTEXT_FILENAME);156 await fs.writeFile(filePath, 'preserved\n', 'utf8');157 158 // Spy on `fs.writeFile` rather than relying on filesystem mtime159 // resolution. macOS HFS+ has 1-second mtime resolution; a quick160 // re-write inside the same second would leave `mtimeMs` unchanged161 // and let a regression slip through. The spy makes the162 // "writeFile was never called" invariant explicit and platform-163 // independent.164 const writeFileSpy = vi.spyOn(fs, 'writeFile');165 try {166 const result = await writeWorkspaceContextFile({167 scope: 'workspace',168 mode: 'append',169 content: '\n\n',170 projectRoot: workspace,171 });172 173 const written = await fs.readFile(filePath, 'utf8');174 expect(written).toBe('preserved\n');175 // `bytesWritten: 0` because the no-op short-circuit wrote zero176 // bytes — NOT the existing file size. Earlier revisions returned177 // `stat.size` here, which conflated two semantics and let178 // clients accumulating `sum(bytesWritten)` count the existing179 // file every whitespace POST.180 expect(result.bytesWritten).toBe(0);181 expect(result.changed).toBe(false);182 // The no-op short-circuit must not call writeFile at all.183 expect(writeFileSpy).not.toHaveBeenCalled();184 } finally {185 writeFileSpy.mockRestore();186 }187 });188 189 it('serializes concurrent appends so no entry is lost', async () => {190 // Spawn 10 parallel appends with unique content. Without the191 // per-file mutex, the read-compose-write race in192 // `composeAppendedContent` lets later writes overwrite earlier193 // ones — at least one entry would be missing from the final file.194 const PARALLEL = 10;195 const writes = Array.from({ length: PARALLEL }, (_, i) =>196 writeWorkspaceContextFile({197 scope: 'workspace',198 mode: 'append',199 content: `- entry ${i}`,200 projectRoot: workspace,201 }),202 );203 const results = await Promise.all(writes);204 205 const filePath = path.join(workspace, DEFAULT_CONTEXT_FILENAME);206 const written = await fs.readFile(filePath, 'utf8');207 for (let i = 0; i < PARALLEL; i++) {208 expect(written).toContain(`- entry ${i}`);209 }210 // All N writes report changed; none short-circuited.211 expect(results.every((r) => r.changed)).toBe(true);212 // Exactly one section header — the lock keeps the213 // "is-section-present" check consistent across the group, so we214 // never insert duplicate headers.215 const headerCount = written.split(MEMORY_SECTION_HEADER).length - 1;216 expect(headerCount).toBe(1);217 });218 219 it('marks `changed: false` for a no-op append against a missing file', async () => {220 const result = await writeWorkspaceContextFile({221 scope: 'workspace',222 mode: 'append',223 content: ' ',224 projectRoot: workspace,225 });226 expect(result.changed).toBe(false);227 expect(result.bytesWritten).toBe(0);228 await expect(229 fs.access(path.join(workspace, DEFAULT_CONTEXT_FILENAME)),230 ).rejects.toMatchObject({ code: 'ENOENT' });231 });232 233 it('inserts new entries inside the MEMORY section, not past a later heading', async () => {234 // File where the MEMORY section is followed by other prose.235 // Without the section-boundary fix the new entry would be236 // appended to EOF, landing it inside the `## post` section.237 const filePath = path.join(workspace, DEFAULT_CONTEXT_FILENAME);238 const initial = `# pre\n\n${MEMORY_SECTION_HEADER}\n- first\n\n## post\nstuff\n`;239 await fs.writeFile(filePath, initial, 'utf8');240 241 await writeWorkspaceContextFile({242 scope: 'workspace',243 mode: 'append',244 content: '- second',245 projectRoot: workspace,246 });247 248 const written = await fs.readFile(filePath, 'utf8');249 expect(written).toBe(250 `# pre\n\n${MEMORY_SECTION_HEADER}\n- first\n- second\n\n## post\nstuff\n`,251 );252 // `- second` must be inside the memory block, not after `stuff`.253 const memorySection = written.indexOf(MEMORY_SECTION_HEADER);254 const postSection = written.indexOf('## post');255 const secondIdx = written.indexOf('- second');256 expect(secondIdx).toBeGreaterThan(memorySection);257 expect(secondIdx).toBeLessThan(postSection);258 });259 260 it('does not split a memory entry that contains `## ` inside a fenced code block', async () => {261 // Round-7 [Critical] glm-5.1: the `\n## ` boundary heuristic was262 // matching `## ` lines INSIDE user-authored fenced code blocks263 // (common in QWEN.md memory entries that quote API docs with264 // markdown headings). The old impl would insert the new entry265 // mid-fence, splitting the existing entry. Code-fence-aware266 // detection skips matches inside ``` ``` `` ` blocks.267 const filePath = path.join(workspace, DEFAULT_CONTEXT_FILENAME);268 const fencedEntry = [269 `${MEMORY_SECTION_HEADER}`,270 '- API example:',271 '```markdown',272 '## Request Body',273 'POST /api/thing',274 '```',275 '',276 ].join('\n');277 await fs.writeFile(filePath, fencedEntry, 'utf8');278 279 await writeWorkspaceContextFile({280 scope: 'workspace',281 mode: 'append',282 content: '- next entry',283 projectRoot: workspace,284 });285 286 const written = await fs.readFile(filePath, 'utf8');287 // The new entry must land AFTER the fence, not inside it.288 const fenceClose = written.lastIndexOf('```');289 const newEntry = written.indexOf('- next entry');290 expect(newEntry).toBeGreaterThan(fenceClose);291 // The fenced `## Request Body` must still be intact (no insert292 // before / inside the code block).293 expect(written).toContain(294 '```markdown\n## Request Body\nPOST /api/thing\n```',295 );296 });297 298 it('still respects real `## ` headings outside code fences', async () => {299 const filePath = path.join(workspace, DEFAULT_CONTEXT_FILENAME);300 // Memory section, then a fenced `## ` (must be skipped), then a301 // real `## post` heading (must be honored as the boundary).302 const initial = [303 `${MEMORY_SECTION_HEADER}`,304 '- existing',305 '```',306 '## fake heading inside fence',307 '```',308 '',309 '## post',310 'tail',311 '',312 ].join('\n');313 await fs.writeFile(filePath, initial, 'utf8');314 315 await writeWorkspaceContextFile({316 scope: 'workspace',317 mode: 'append',318 content: '- new',319 projectRoot: workspace,320 });321 322 const written = await fs.readFile(filePath, 'utf8');323 const realPost = written.indexOf('## post');324 const newEntry = written.indexOf('- new');325 expect(newEntry).toBeLessThan(realPost);326 expect(newEntry).toBeGreaterThan(written.indexOf('- existing'));327 });328 329 it('appends to EOF when the MEMORY section is the last block', async () => {330 // Sanity: when no later heading follows, behavior is the331 // pre-fix append-to-end path (still inside the section because332 // the section IS the tail).333 const filePath = path.join(workspace, DEFAULT_CONTEXT_FILENAME);334 const initial = `# pre\n\n${MEMORY_SECTION_HEADER}\n- a\n`;335 await fs.writeFile(filePath, initial, 'utf8');336 337 await writeWorkspaceContextFile({338 scope: 'workspace',339 mode: 'append',340 content: '- b',341 projectRoot: workspace,342 });343 344 const written = await fs.readFile(filePath, 'utf8');345 expect(written).toBe(`# pre\n\n${MEMORY_SECTION_HEADER}\n- a\n- b\n`);346 });347 348 it('does not create the parent directory on a no-op append', async () => {349 // Whitespace-only append targeting a non-existent nested path350 // must NOT call fs.mkdir — the no-op detection short-circuits351 // BEFORE acquiring the lock or touching the filesystem. Without352 // this, an empty POST would still bump the parent directory's353 // mtime even though the helper reports `changed: false`.354 const nested = path.join(workspace, 'never-exists');355 const result = await writeWorkspaceContextFile({356 scope: 'workspace',357 mode: 'append',358 content: '\n\n',359 projectRoot: nested,360 });361 expect(result.changed).toBe(false);362 await expect(fs.access(nested)).rejects.toMatchObject({ code: 'ENOENT' });363 });364 365 it('honors setGeminiMdFilename overrides so POST targets the same file GET surfaces', async () => {366 // Round-trip the `setGeminiMdFilename` override: with the prior367 // `DEFAULT_CONTEXT_FILENAME` hard-code, a deployment that switched368 // the context filename to `AGENTS.md` saw GET list the new file369 // but POST keep writing to `QWEN.md`. The fix routes370 // `resolveContextFilePath` through `getCurrentGeminiMdFilename()`371 // so both surfaces agree.372 try {373 setGeminiMdFilename(AGENT_CONTEXT_FILENAME);374 const result = await writeWorkspaceContextFile({375 scope: 'workspace',376 mode: 'append',377 content: '- entry',378 projectRoot: workspace,379 });380 expect(result.filePath).toBe(381 path.join(workspace, AGENT_CONTEXT_FILENAME),382 );383 const written = await fs.readFile(result.filePath, 'utf8');384 expect(written).toContain('- entry');385 // The legacy QWEN.md must NOT have been written — the prior386 // hard-coded behavior would have created it here.387 await expect(388 fs.access(path.join(workspace, DEFAULT_CONTEXT_FILENAME)),389 ).rejects.toMatchObject({ code: 'ENOENT' });390 } finally {391 setGeminiMdFilename(DEFAULT_CONTEXT_FILENAME);392 }393 });394});395 