Files
chat/api/server/services/Files/S3/tenantKey.spec.js
zeekay 7e26aa9653 feat(rag): finish the cutover — retrieval + vector cleanup on the one surface
The first pass moved ingestion; these are the paths that still read RAG_API_URL,
which the universe change removes. Left as-is they would have silently degraded:
file_search and the context handler would stop retrieving, and deleting an S3 object
would orphan its embeddings.

- file_search (api/app/clients/tools/util/fileSearch.js) queries through RagClient;
  `req` is threaded from handleTools (as `options.req`, the same value that file
  already passes to resolveTenantBearer for images) so retrieval runs on the user's
  own IAM bearer instead of a chat-minted short-lived token.
- createContextHandlers uses the client for both shapes (full context + top-k),
  normalised to { data } so its callers are untouched.
- deleting an S3 object now drops the file's chunks via the same client rather than
  the legacy deleteRagFile (which reads RAG_API_URL and would have no-op'd) —
  best-effort, so a retrieval-side failure never blocks the object delete.

Dead code removed with the paths they served: the axios import and createQueryBody
body-builder in fileSearch, the deleteRagFile import in S3 crud.

Tests 11/11 (7 client + 4 key convention; the S3 spec mocks the client). Lint clean
on every touched file — the one remaining handleTools prettier error is pre-existing
(3 on clean main, 1 after this).

packages/api/src/files/{text,rag}.ts and app/checks.ts still name RAG_API_URL; all
three already guard on it and degrade gracefully (native text parsing, no-op delete,
a startup log), so they are inert once the var is gone — they come out with the
package's own pass rather than being half-migrated here.
2026-07-26 14:13:26 -07:00

58 lines
2.5 KiB
JavaScript

// getS3Key is a pure function; stub the heavy transitive deps crud.js pulls in at
// require time (the SDK clients and the built @hanzochat/api bundle) so the key
// convention can be asserted without booting S3 or the agents runtime.
jest.mock('~/server/services/RagClient', () => ({ remove: jest.fn(), ragEnabled: () => true }), {
virtual: true,
});
jest.mock('@hanzochat/api', () => ({
initializeS3: jest.fn(),
deleteRagFile: jest.fn(),
isEnabled: () => false,
resolveRequestOrg: jest.fn(),
}));
jest.mock('@hanzochat/data-schemas', () => ({
logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
}));
jest.mock('@hanzochat/data-provider', () => ({ FileSources: { s3: 's3' } }));
jest.mock('@aws-sdk/s3-request-presigner', () => ({ getSignedUrl: jest.fn() }));
jest.mock('@aws-sdk/client-s3', () => ({
PutObjectCommand: jest.fn(),
GetObjectCommand: jest.fn(),
HeadObjectCommand: jest.fn(),
DeleteObjectCommand: jest.fn(),
}));
const { getS3Key } = require('./crud');
/**
* File storage is org-scoped in the shared bucket: the key carries the IAM org so
* one org's objects live in their own subtree. Pre-tenancy objects (and callers
* IAM gives no org) keep the historical flat key, which is why reads of existing
* files keep working.
*/
describe('getS3Key — IAM org scoping of the shared bucket', () => {
it('prefixes t/<org>/ when the request has an IAM org', () => {
expect(getS3Key('images', 'user1', 'a.png', 'acme')).toBe('t/acme/images/user1/a.png');
});
it('keeps the historical flat key when there is no org', () => {
expect(getS3Key('images', 'user1', 'a.png')).toBe('images/user1/a.png');
expect(getS3Key('images', 'user1', 'a.png', null)).toBe('images/user1/a.png');
expect(getS3Key('images', 'user1', 'a.png', '')).toBe('images/user1/a.png');
});
it('separates orgs — the whole point of the prefix', () => {
const a = getS3Key('images', 'user1', 'a.png', 'acme');
const b = getS3Key('images', 'user1', 'a.png', 'globex');
expect(a).not.toBe(b);
expect(a.startsWith('t/acme/')).toBe(true);
expect(b.startsWith('t/globex/')).toBe(true);
});
it('refuses an org that would escape its prefix', () => {
expect(() => getS3Key('images', 'user1', 'a.png', '../evil')).toThrow(/invalid tenantId/);
expect(() => getS3Key('images', 'user1', 'a.png', '..')).toThrow(/invalid tenantId/);
expect(() => getS3Key('images', 'user1', 'a.png', 'a/b')).toThrow(/invalid tenantId/);
});
});