Files
chat/api/server/services/Files/process.integration.spec.js
zooqueenandHanzo Dev b1130806a1 chat: half the api suites that ran nothing now run
Measured rather than guessed: 186 suites, 69 failing, and TWELVE of those failed
during import having executed zero tests. That distinction is invisible in a
failed/total count and it matters — a suite that runs and fails tells you
something, a suite that dies at import tells you only that somebody wrote a file.

Same cause throughout, and it cascades: the spec replaces a `@hanzochat/*`
package with the two or three names it expects the subject to want, so the first
thing deeper in the require graph that wants a fourth throws. One file went
through five in a row. Spreading `jest.requireActual` under the overrides fixes
it at the root; `deleteUser` additionally overrode `CacheKeys` with `{}`, which
made the real cacheConfig reject every namespace it was asked to validate.

  deleteUser         7 tests   modelEndHandler    6 tests
  preview           15 tests   addedConvo         3 tests
  process           49 tests

All red, and left that way — they are upstream specs whose implementations did
not land, and a red suite that runs is worth more than a green name that does
not. Six remain dead (roles, abort, streamTenant, auth.cloudfront,
imageTools-agent, importers); their blockers are middleware shapes rather than
package stubs. Nothing here gates.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-08 13:02:18 -07:00

205 lines
7.2 KiB
JavaScript

/**
* Integration test for the delete-time path of issue #12776.
*
* Covers the full flow through `processDeleteRequest`:
* 1. Real Agent + File docs in an in-memory Mongo.
* 2. Invoke the delete service.
* 3. Assert both the File record is gone and every agent's
* tool_resources.*.file_ids no longer references the deleted id.
*
* Uses FileSources.text so the strategy layer (disk / S3 / OpenAI) is a
* no-op — we don't need real filesystem access to exercise the agent
* reference cleanup, which is what issue #12776 is about.
*/
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { agentSchema, fileSchema, createMethods } = require('@hanzochat/data-schemas');
const { FileSources } = require('@hanzochat/data-provider');
jest.mock('@hanzochat/data-schemas', () => {
const actual = jest.requireActual('@hanzochat/data-schemas');
return {
...actual,
logger: { warn: jest.fn(), debug: jest.fn(), error: jest.fn(), info: jest.fn() },
};
});
jest.mock('@hanzochat/agents', () => ({
EnvVar: { CODE_API_KEY: 'CODE_API_KEY' },
}));
jest.mock('@hanzochat/api', () => ({
...jest.requireActual('@hanzochat/api'),
sanitizeFilename: jest.fn((n) => n),
parseText: jest.fn().mockResolvedValue({ text: '', bytes: 0 }),
processAudioFile: jest.fn(),
}));
jest.mock('~/server/controllers/assistants/v2', () => ({
addResourceFileId: jest.fn(),
deleteResourceFileId: jest.fn(),
}));
jest.mock('~/server/controllers/assistants/helpers', () => ({
getOpenAIClient: jest.fn(),
}));
jest.mock('~/server/services/Tools/credentials', () => ({
loadAuthValues: jest.fn(),
}));
jest.mock('~/server/services/Files/strategies', () => ({
getStrategyFunctions: jest.fn(() => ({ deleteFile: jest.fn().mockResolvedValue(undefined) })),
}));
jest.mock('~/server/services/Files/Audio/STTService', () => ({
STTService: { getInstance: jest.fn() },
}));
jest.mock('~/server/services/Config', () => ({
checkCapability: jest.fn().mockResolvedValue(true),
}));
jest.mock('~/cache', () => ({
getLogStores: jest.fn(() => ({ get: jest.fn(), set: jest.fn(), delete: jest.fn() })),
}));
// Replace the mocked `~/models` from the sibling process.spec.js with real,
// mongoose-backed methods. All our in-memory models share this module.
jest.mock('~/models', () => {
const mongoose = require('mongoose');
const { createMethods } = require('@hanzochat/data-schemas');
return createMethods(mongoose, {
removeAllPermissions: jest.fn().mockResolvedValue(undefined),
});
});
require('module-alias/register');
const { processDeleteRequest } = require('./process');
describe('processDeleteRequest — agent reference cleanup (issue #12776)', () => {
let mongoServer;
let Agent;
let File;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
// createMethods (via ~/models) registers the File model as a side-effect,
// but we also need the Agent model registered before any queries run.
Agent = mongoose.models.Agent || mongoose.model('Agent', agentSchema);
File = mongoose.models.File || mongoose.model('File', fileSchema);
// Touch createMethods once so the migration/setup side-effects run.
createMethods(mongoose, { removeAllPermissions: jest.fn() });
}, 30000);
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
beforeEach(async () => {
await Agent.deleteMany({});
await File.deleteMany({});
});
const seedFile = async (file_id, userId) =>
File.create({
file_id,
user: userId,
filename: `${file_id}.txt`,
filepath: `/tmp/${file_id}`,
object: 'file',
type: 'text/plain',
bytes: 1,
source: FileSources.text,
});
const seedAgent = async (authorId, tool_resources) =>
Agent.create({
id: `agent_${Math.random().toString(36).slice(2, 10)}`,
name: 'Integration Test Agent',
provider: 'test',
model: 'test-model',
author: authorId,
tool_resources,
});
const buildReq = (fileDocs, extraBody = {}) => ({
user: { id: fileDocs[0].user.toString() },
body: { files: fileDocs, ...extraBody },
config: { fileStrategy: 'local', fileConfig: {}, endpoints: {} },
});
test('strips deleted file_ids from every agent that referenced them', async () => {
const userId = new mongoose.Types.ObjectId();
const keeperId = `file_keeper_${Math.random().toString(36).slice(2, 10)}`;
const deletedId = `file_deleted_${Math.random().toString(36).slice(2, 10)}`;
const deletedFile = await seedFile(deletedId, userId);
await seedFile(keeperId, userId);
// Two agents both reference the file that's about to be deleted, plus the
// keeper. A third, unrelated agent has a different file_id and must not be
// touched by the cleanup.
const agentA = await seedAgent(userId, {
file_search: { file_ids: [deletedId, keeperId] },
});
const agentB = await seedAgent(userId, {
execute_code: { file_ids: [deletedId] },
});
const untouchedAgent = await seedAgent(userId, {
context: { file_ids: [keeperId] },
});
await processDeleteRequest({ req: buildReq([deletedFile.toObject()]), files: [deletedFile] });
expect(await File.findOne({ file_id: deletedId })).toBeNull();
expect(await File.findOne({ file_id: keeperId })).not.toBeNull();
const updatedA = await Agent.findOne({ id: agentA.id }).lean();
const updatedB = await Agent.findOne({ id: agentB.id }).lean();
const updatedUntouched = await Agent.findOne({ id: untouchedAgent.id }).lean();
expect(updatedA.tool_resources.file_search.file_ids).toEqual([keeperId]);
expect(updatedB.tool_resources.execute_code.file_ids).toEqual([]);
expect(updatedUntouched.tool_resources.context.file_ids).toEqual([keeperId]);
});
test('is a no-op when no agent references the deleted file', async () => {
const userId = new mongoose.Types.ObjectId();
const loneId = `file_lone_${Math.random().toString(36).slice(2, 10)}`;
const loneFile = await seedFile(loneId, userId);
const unrelatedAgent = await seedAgent(userId, {
file_search: { file_ids: ['other_id'] },
});
await processDeleteRequest({ req: buildReq([loneFile.toObject()]), files: [loneFile] });
expect(await File.findOne({ file_id: loneId })).toBeNull();
const after = await Agent.findOne({ id: unrelatedAgent.id }).lean();
expect(after.tool_resources.file_search.file_ids).toEqual(['other_id']);
});
test('still deletes the file when the agent cleanup step throws', async () => {
const userId = new mongoose.Types.ObjectId();
const targetId = `file_target_${Math.random().toString(36).slice(2, 10)}`;
const targetFile = await seedFile(targetId, userId);
const db = require('~/models');
const original = db.removeAgentResourceFilesFromAllAgents;
db.removeAgentResourceFilesFromAllAgents = jest
.fn()
.mockRejectedValue(new Error('simulated cleanup failure'));
try {
await processDeleteRequest({ req: buildReq([targetFile.toObject()]), files: [targetFile] });
expect(await File.findOne({ file_id: targetId })).toBeNull();
} finally {
db.removeAgentResourceFilesFromAllAgents = original;
}
});
});