CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
workspace-memory.test.js419 linesDownload Raw Back to serve
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6import { promises as fs } from 'node:fs';7import * as os from 'node:os';8import * as path from 'node:path';9import express from 'express';10import request from 'supertest';11import { afterEach, beforeEach, describe, expect, it, vi, } from 'vitest';12import { AGENT_CONTEXT_FILENAME, DEFAULT_CONTEXT_FILENAME, Storage, setGeminiMdFilename, } from '@qwen-code/qwen-code-core';13import { createMutationGate } from './auth.js';14import { InvalidClientIdError, } from './acp-session-bridge.js';15import { mountWorkspaceMemoryRoutes } from './workspace-memory.js';16function buildBridgeStub(opts = {}) {17    const events = [];18    const known = new Set(opts.knownIds ?? []);19    return {20        events,21        publishWorkspaceEvent(event) {22            events.push(event);23        },24        knownClientIds() {25            return new Set(known);26        },27        // Methods below are not used by the memory routes; throw to keep28        // unrelated tests from accidentally relying on them.29        spawnOrAttach: () => {30            throw new Error('not implemented');31        },32        loadSession: () => {33            throw new Error('not implemented');34        },35        resumeSession: () => {36            throw new Error('not implemented');37        },38        sendPrompt: () => {39            throw new Error('not implemented');40        },41        cancelSession: () => {42            throw new Error('not implemented');43        },44        subscribeEvents: () => {45            throw new Error('not implemented');46        },47        closeSession: () => {48            throw new Error('not implemented');49        },50        updateSessionMetadata: () => {51            throw new Error('not implemented');52        },53        respondToPermission: () => {54            throw new Error('not implemented');55        },56        respondToSessionPermission: () => {57            throw new Error('not implemented');58        },59        listWorkspaceSessions: () => {60            throw new Error('not implemented');61        },62        recordHeartbeat: () => {63            throw new Error('not implemented');64        },65        getHeartbeatState: () => undefined,66        getWorkspaceMcpStatus: async () => {67            throw new Error('not implemented');68        },69        getWorkspaceSkillsStatus: async () => {70            throw new Error('not implemented');71        },72        getWorkspaceProvidersStatus: async () => {73            throw new Error('not implemented');74        },75        getSessionContextStatus: async () => {76            throw new Error('not implemented');77        },78        getSessionSupportedCommandsStatus: async () => {79            throw new Error('not implemented');80        },81        setSessionModel: async () => {82            throw new Error('not implemented');83        },84        killSession: async () => { },85        detachClient: async () => { },86        sessionCount: 0,87        pendingPermissionCount: 0,88        killAllSync: () => { },89        shutdown: async () => { },90        preheat: async () => { },91    };92}93function buildApp(opts) {94    const app = express();95    app.use(express.json({ limit: '10mb' }));96    const mutate = createMutationGate({97        tokenConfigured: opts.strictNoToken !== true,98        requireAuth: false,99    });100    mountWorkspaceMemoryRoutes(app, {101        bridge: opts.bridge,102        boundWorkspace: opts.boundWorkspace,103        ...(opts.collectStatus ? { collectStatus: opts.collectStatus } : {}),104        mutate,105        parseClientId: (req, res) => {106            const raw = req.get('x-qwen-client-id');107            if (raw === undefined || raw === '')108                return undefined;109            if (raw.length > 128 || !/^[A-Za-z0-9._:-]+$/.test(raw)) {110                res.status(400).json({111                    error: '`X-Qwen-Client-Id` must be a non-empty token',112                    code: 'invalid_client_id',113                });114                return null;115            }116            return raw;117        },118        safeBody: (req) => {119            const raw = req.body;120            if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {121                return Object.create(null);122            }123            const out = Object.create(null);124            for (const [k, v] of Object.entries(raw)) {125                if (k === '__proto__' || k === 'constructor' || k === 'prototype') {126                    continue;127                }128                out[k] = v;129            }130            return out;131        },132    });133    return app;134}135function resetContextFilenames() {136    setGeminiMdFilename([DEFAULT_CONTEXT_FILENAME, AGENT_CONTEXT_FILENAME]);137}138describe('workspace memory routes', () => {139    let tmp;140    let workspace;141    let globalDir;142    let getGlobalQwenDirSpy;143    beforeEach(async () => {144        tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'qwen-serve-memory-'));145        workspace = path.join(tmp, 'workspace');146        globalDir = path.join(tmp, 'global');147        await fs.mkdir(workspace, { recursive: true });148        getGlobalQwenDirSpy = vi149            .spyOn(Storage, 'getGlobalQwenDir')150            .mockReturnValue(globalDir);151        resetContextFilenames();152    });153    afterEach(async () => {154        resetContextFilenames();155        getGlobalQwenDirSpy.mockRestore();156        await fs.rm(tmp, { recursive: true, force: true });157    });158    describe('GET /workspace/memory', () => {159        it('returns idle status when no QWEN.md or AGENTS.md exists anywhere', async () => {160            const bridge = buildBridgeStub();161            const app = buildApp({ bridge, boundWorkspace: workspace });162            const res = await request(app).get('/workspace/memory');163            expect(res.status).toBe(200);164            expect(res.body).toEqual({165                v: 1,166                workspaceCwd: workspace,167                initialized: false,168                files: [],169                totalBytes: 0,170                fileCount: 0,171                ruleCount: 0,172            });173        });174        it('reports workspace and global QWEN.md files with byte counts', async () => {175            const wsFile = path.join(workspace, 'QWEN.md');176            const wsContent = 'workspace memory\n';177            await fs.writeFile(wsFile, wsContent, 'utf8');178            await fs.mkdir(globalDir, { recursive: true });179            const globalFile = path.join(globalDir, 'QWEN.md');180            const globalContent = 'global memory\n';181            await fs.writeFile(globalFile, globalContent, 'utf8');182            const bridge = buildBridgeStub();183            const app = buildApp({ bridge, boundWorkspace: workspace });184            const res = await request(app).get('/workspace/memory');185            expect(res.status).toBe(200);186            expect(res.body.initialized).toBe(true);187            expect(res.body.fileCount).toBe(2);188            expect(res.body.ruleCount).toBe(0);189            expect(res.body.totalBytes).toBe(Buffer.byteLength(wsContent) + Buffer.byteLength(globalContent));190            const paths = res.body.files.map((f) => f.path);191            expect(paths).toEqual(expect.arrayContaining([wsFile, globalFile]));192        });193    });194    describe('POST /workspace/memory', () => {195        it('appends to workspace QWEN.md and emits memory_changed', async () => {196            const bridge = buildBridgeStub();197            const app = buildApp({ bridge, boundWorkspace: workspace });198            const res = await request(app)199                .post('/workspace/memory')200                .send({ scope: 'workspace', mode: 'append', content: '- entry one' });201            expect(res.status).toBe(200);202            expect(res.body.ok).toBe(true);203            expect(res.body.mode).toBe('append');204            expect(res.body.filePath).toBe(path.join(workspace, 'QWEN.md'));205            const written = await fs.readFile(path.join(workspace, 'QWEN.md'), 'utf8');206            expect(written).toContain('- entry one');207            const events = bridge.events;208            expect(events).toHaveLength(1);209            expect(events[0]?.type).toBe('memory_changed');210            const data = events[0]?.data;211            expect(data['scope']).toBe('workspace');212            expect(data['mode']).toBe('append');213            expect(data['filePath']).toBe(path.join(workspace, 'QWEN.md'));214        });215        it('replaces workspace QWEN.md when mode=replace', async () => {216            const bridge = buildBridgeStub();217            const app = buildApp({ bridge, boundWorkspace: workspace });218            const filePath = path.join(workspace, 'QWEN.md');219            await fs.writeFile(filePath, 'old\n', 'utf8');220            const res = await request(app)221                .post('/workspace/memory')222                .send({ scope: 'workspace', mode: 'replace', content: 'new\n' });223            expect(res.status).toBe(200);224            const written = await fs.readFile(filePath, 'utf8');225            expect(written).toBe('new\n');226        });227        it('writes to the global ~/.qwen directory when scope=global', async () => {228            const bridge = buildBridgeStub();229            const app = buildApp({ bridge, boundWorkspace: workspace });230            const res = await request(app)231                .post('/workspace/memory')232                .send({ scope: 'global', mode: 'append', content: '- global note' });233            expect(res.status).toBe(200);234            expect(res.body.filePath).toBe(path.join(globalDir, 'QWEN.md'));235            const written = await fs.readFile(path.join(globalDir, 'QWEN.md'), 'utf8');236            expect(written).toContain('- global note');237        });238        it('rejects 400 invalid_scope on unknown scope value', async () => {239            const bridge = buildBridgeStub();240            const app = buildApp({ bridge, boundWorkspace: workspace });241            const res = await request(app)242                .post('/workspace/memory')243                .send({ scope: 'all', content: 'x' });244            expect(res.status).toBe(400);245            expect(res.body.code).toBe('invalid_scope');246        });247        it('rejects 400 invalid_mode on unknown mode value', async () => {248            const bridge = buildBridgeStub();249            const app = buildApp({ bridge, boundWorkspace: workspace });250            const res = await request(app)251                .post('/workspace/memory')252                .send({ scope: 'workspace', mode: 'merge', content: 'x' });253            expect(res.status).toBe(400);254            expect(res.body.code).toBe('invalid_mode');255        });256        it('rejects 400 invalid_content for non-string content', async () => {257            const bridge = buildBridgeStub();258            const app = buildApp({ bridge, boundWorkspace: workspace });259            const res = await request(app)260                .post('/workspace/memory')261                .send({ scope: 'workspace', content: 123 });262            expect(res.status).toBe(400);263            expect(res.body.code).toBe('invalid_content');264        });265        it('rejects 400 content_too_large above the 1 MB limit', async () => {266            const bridge = buildBridgeStub();267            const app = buildApp({ bridge, boundWorkspace: workspace });268            const big = 'x'.repeat(1024 * 1024 + 1);269            const res = await request(app)270                .post('/workspace/memory')271                .send({ scope: 'workspace', content: big });272            expect(res.status).toBe(400);273            expect(res.body.code).toBe('content_too_large');274        });275        it('returns 401 token_required when strict gate fires on no-token loopback', async () => {276            const bridge = buildBridgeStub();277            const app = buildApp({278                bridge,279                boundWorkspace: workspace,280                strictNoToken: true,281            });282            const res = await request(app)283                .post('/workspace/memory')284                .send({ scope: 'workspace', content: '- x' });285            expect(res.status).toBe(401);286            expect(res.body.code).toBe('token_required');287        });288        it('rejects 400 invalid_client_id when X-Qwen-Client-Id is unknown', async () => {289            const bridge = buildBridgeStub({ knownIds: ['client_known'] });290            const app = buildApp({ bridge, boundWorkspace: workspace });291            const res = await request(app)292                .post('/workspace/memory')293                .set('X-Qwen-Client-Id', 'client_unknown')294                .send({ scope: 'workspace', content: '- x' });295            expect(res.status).toBe(400);296            expect(res.body.code).toBe('invalid_client_id');297        });298        it('suppresses memory_changed event when append content is whitespace only', async () => {299            const bridge = buildBridgeStub();300            const app = buildApp({ bridge, boundWorkspace: workspace });301            const res = await request(app)302                .post('/workspace/memory')303                .send({ scope: 'workspace', mode: 'append', content: '\n\n  \n' });304            expect(res.status).toBe(200);305            expect(res.body.changed).toBe(false);306            const events = bridge.events;307            expect(events).toHaveLength(0);308        });309        it('returns 413 memory_file_too_large when existing QWEN.md exceeds the 16 MB cap', async () => {310            // Write a 17 MB existing QWEN.md, then attempt append. The311            // helper's pre-read `fs.stat` must refuse with the typed312            // error → the route maps it to 413.313            const filePath = path.join(workspace, 'QWEN.md');314            // 17 MB of `x` characters. Bypass the helper's mutex / cap by315            // writing directly via fs (simulating an externally-grown file316            // outside the daemon's control).317            const big = 'x'.repeat(17 * 1024 * 1024);318            await fs.writeFile(filePath, big, 'utf8');319            const bridge = buildBridgeStub();320            const app = buildApp({ bridge, boundWorkspace: workspace });321            const res = await request(app)322                .post('/workspace/memory')323                .send({ scope: 'workspace', mode: 'append', content: '- entry' });324            expect(res.status).toBe(413);325            expect(res.body.code).toBe('memory_file_too_large');326            expect(res.body.scope).toBe('workspace');327            expect(res.body.mode).toBe('append');328            expect(res.body.bytes).toBe(17 * 1024 * 1024);329            expect(res.body.limit).toBe(16 * 1024 * 1024);330            // Default response: no filePath, no path-embedding error message.331            expect(res.body.filePath).toBeUndefined();332            expect(res.body.error).not.toContain(filePath);333        });334        it('omits errorMessage + filePath in 500/413 responses unless QWEN_SERVE_DEBUG is on', async () => {335            // Windows ignores Unix-style permission bits passed to336            // `fs.chmod` — the directory stays writable, the POST succeeds337            // with 200, and the EACCES path this test exercises is338            // unreachable. The route logic itself is platform-agnostic; the339            // Ubuntu + macOS runs cover it. Mirrors the340            // `process.platform === 'win32'` early-return idiom already used341            // in `customBanner.test.ts:232`.342            if (process.platform === 'win32')343                return;344            // Default: production response carries no `errorMessage` or345            // `filePath` fields — operators read the daemon stderr log346            // for the path. Setting QWEN_SERVE_DEBUG=1 enables both.347            const bridge = buildBridgeStub();348            const app = buildApp({ bridge, boundWorkspace: workspace });349            // Force a 500 by making the workspace QWEN.md unwritable. We350            // chmod the WORKSPACE directory (not the file) so `mkdir` and351            // `writeFile` will fail with EACCES.352            const before = await fs.stat(workspace);353            await fs.chmod(workspace, 0o555);354            const prevDebug = process.env['QWEN_SERVE_DEBUG'];355            try {356                delete process.env['QWEN_SERVE_DEBUG'];357                const res = await request(app).post('/workspace/memory').send({358                    scope: 'workspace',359                    mode: 'append',360                    content: '- entry',361                });362                expect(res.status).toBe(500);363                expect(res.body.code).toBe('file_error');364                expect(res.body.scope).toBe('workspace');365                expect(res.body.mode).toBe('append');366                // Default response: no errorMessage, no filePath.367                expect(res.body.errorMessage).toBeUndefined();368                expect(res.body.filePath).toBeUndefined();369                // Toggle debug back on; the same payload now carries the370                // detail.371                process.env['QWEN_SERVE_DEBUG'] = '1';372                const debugRes = await request(app).post('/workspace/memory').send({373                    scope: 'workspace',374                    mode: 'append',375                    content: '- entry',376                });377                expect(debugRes.status).toBe(500);378                expect(typeof debugRes.body.errorMessage).toBe('string');379            }380            finally {381                if (prevDebug === undefined)382                    delete process.env['QWEN_SERVE_DEBUG'];383                else384                    process.env['QWEN_SERVE_DEBUG'] = prevDebug;385                await fs.chmod(workspace, before.mode);386            }387        });388        it('returns 500 memory_discovery_failed when GET helper throws unexpectedly', async () => {389            const bridge = buildBridgeStub();390            const app = buildApp({391                bridge,392                boundWorkspace: workspace,393                collectStatus: async () => {394                    throw new Error('boom');395                },396            });397            const res = await request(app).get('/workspace/memory');398            expect(res.status).toBe(500);399            expect(res.body.code).toBe('memory_discovery_failed');400        });401        it('stamps originatorClientId on the memory_changed event for known clients', async () => {402            const bridge = buildBridgeStub({ knownIds: ['client_a'] });403            const app = buildApp({ bridge, boundWorkspace: workspace });404            const res = await request(app)405                .post('/workspace/memory')406                .set('X-Qwen-Client-Id', 'client_a')407                .send({ scope: 'workspace', mode: 'append', content: '- x' });408            expect(res.status).toBe(200);409            const events = bridge.events;410            expect(events[0]?.originatorClientId).toBe('client_a');411        });412        // Reference InvalidClientIdError in case future refactors rename413        // it — keeps the import non-tree-shakeable surface a real symbol.414        it('exposes InvalidClientIdError from the bridge module', () => {415            expect(typeof InvalidClientIdError).toBe('function');416        });417    });418});419//# sourceMappingURL=workspace-memory.test.js.map
basant307/AI_Governance_Project · CoolFace