@diyor28/context 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +270 -0
- package/dist/__tests__/attachment-selector.test.d.ts +11 -0
- package/dist/__tests__/attachment-selector.test.d.ts.map +1 -0
- package/dist/__tests__/attachment-selector.test.js +449 -0
- package/dist/__tests__/attachment-selector.test.js.map +1 -0
- package/dist/__tests__/cache-breakpoints.test.d.ts +11 -0
- package/dist/__tests__/cache-breakpoints.test.d.ts.map +1 -0
- package/dist/__tests__/cache-breakpoints.test.js +398 -0
- package/dist/__tests__/cache-breakpoints.test.js.map +1 -0
- package/dist/__tests__/codecs.test.d.ts +7 -0
- package/dist/__tests__/codecs.test.d.ts.map +1 -0
- package/dist/__tests__/codecs.test.js +331 -0
- package/dist/__tests__/codecs.test.js.map +1 -0
- package/dist/__tests__/compactor.test.d.ts +11 -0
- package/dist/__tests__/compactor.test.d.ts.map +1 -0
- package/dist/__tests__/compactor.test.js +519 -0
- package/dist/__tests__/compactor.test.js.map +1 -0
- package/dist/__tests__/context-graph.test.d.ts +7 -0
- package/dist/__tests__/context-graph.test.d.ts.map +1 -0
- package/dist/__tests__/context-graph.test.js +262 -0
- package/dist/__tests__/context-graph.test.js.map +1 -0
- package/dist/__tests__/hash.test.d.ts +7 -0
- package/dist/__tests__/hash.test.d.ts.map +1 -0
- package/dist/__tests__/hash.test.js +228 -0
- package/dist/__tests__/hash.test.js.map +1 -0
- package/dist/__tests__/integration.test.d.ts +15 -0
- package/dist/__tests__/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration.test.js +728 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/__tests__/kind-order.test.d.ts +7 -0
- package/dist/__tests__/kind-order.test.d.ts.map +1 -0
- package/dist/__tests__/kind-order.test.js +243 -0
- package/dist/__tests__/kind-order.test.js.map +1 -0
- package/dist/__tests__/phase2-integration.test.d.ts +5 -0
- package/dist/__tests__/phase2-integration.test.d.ts.map +1 -0
- package/dist/__tests__/phase2-integration.test.js +222 -0
- package/dist/__tests__/phase2-integration.test.js.map +1 -0
- package/dist/__tests__/queries.test.d.ts +7 -0
- package/dist/__tests__/queries.test.d.ts.map +1 -0
- package/dist/__tests__/queries.test.js +254 -0
- package/dist/__tests__/queries.test.js.map +1 -0
- package/dist/__tests__/token-estimator.test.d.ts +7 -0
- package/dist/__tests__/token-estimator.test.d.ts.map +1 -0
- package/dist/__tests__/token-estimator.test.js +267 -0
- package/dist/__tests__/token-estimator.test.js.map +1 -0
- package/dist/adapters/anthropic-estimator.d.ts +38 -0
- package/dist/adapters/anthropic-estimator.d.ts.map +1 -0
- package/dist/adapters/anthropic-estimator.js +108 -0
- package/dist/adapters/anthropic-estimator.js.map +1 -0
- package/dist/adapters/attachment-resolver.d.ts +96 -0
- package/dist/adapters/attachment-resolver.d.ts.map +1 -0
- package/dist/adapters/attachment-resolver.js +176 -0
- package/dist/adapters/attachment-resolver.js.map +1 -0
- package/dist/adapters/attachment-selector.d.ts +59 -0
- package/dist/adapters/attachment-selector.d.ts.map +1 -0
- package/dist/adapters/attachment-selector.js +163 -0
- package/dist/adapters/attachment-selector.js.map +1 -0
- package/dist/adapters/gemini-estimator.d.ts +27 -0
- package/dist/adapters/gemini-estimator.d.ts.map +1 -0
- package/dist/adapters/gemini-estimator.js +80 -0
- package/dist/adapters/gemini-estimator.js.map +1 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +28 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/memory-store.d.ts +139 -0
- package/dist/adapters/memory-store.d.ts.map +1 -0
- package/dist/adapters/memory-store.js +187 -0
- package/dist/adapters/memory-store.js.map +1 -0
- package/dist/adapters/openai-estimator.d.ts +35 -0
- package/dist/adapters/openai-estimator.d.ts.map +1 -0
- package/dist/adapters/openai-estimator.js +89 -0
- package/dist/adapters/openai-estimator.js.map +1 -0
- package/dist/adapters/summarizer.d.ts +121 -0
- package/dist/adapters/summarizer.d.ts.map +1 -0
- package/dist/adapters/summarizer.js +121 -0
- package/dist/adapters/summarizer.js.map +1 -0
- package/dist/adapters/token-estimator.d.ts +63 -0
- package/dist/adapters/token-estimator.d.ts.map +1 -0
- package/dist/adapters/token-estimator.js +37 -0
- package/dist/adapters/token-estimator.js.map +1 -0
- package/dist/builder/context-builder.d.ts +186 -0
- package/dist/builder/context-builder.d.ts.map +1 -0
- package/dist/builder/context-builder.js +305 -0
- package/dist/builder/context-builder.js.map +1 -0
- package/dist/builder/context-fork.d.ts +166 -0
- package/dist/builder/context-fork.d.ts.map +1 -0
- package/dist/builder/context-fork.js +282 -0
- package/dist/builder/context-fork.js.map +1 -0
- package/dist/builder/index.d.ts +6 -0
- package/dist/builder/index.d.ts.map +1 -0
- package/dist/builder/index.js +22 -0
- package/dist/builder/index.js.map +1 -0
- package/dist/codecs/base.d.ts +18 -0
- package/dist/codecs/base.d.ts.map +1 -0
- package/dist/codecs/base.js +39 -0
- package/dist/codecs/base.js.map +1 -0
- package/dist/codecs/conversation-history.codec.d.ts +81 -0
- package/dist/codecs/conversation-history.codec.d.ts.map +1 -0
- package/dist/codecs/conversation-history.codec.js +89 -0
- package/dist/codecs/conversation-history.codec.js.map +1 -0
- package/dist/codecs/index.d.ts +31 -0
- package/dist/codecs/index.d.ts.map +1 -0
- package/dist/codecs/index.js +71 -0
- package/dist/codecs/index.js.map +1 -0
- package/dist/codecs/redacted-stub.codec.d.ts +32 -0
- package/dist/codecs/redacted-stub.codec.d.ts.map +1 -0
- package/dist/codecs/redacted-stub.codec.js +64 -0
- package/dist/codecs/redacted-stub.codec.js.map +1 -0
- package/dist/codecs/structured-reference.codec.d.ts +40 -0
- package/dist/codecs/structured-reference.codec.d.ts.map +1 -0
- package/dist/codecs/structured-reference.codec.js +81 -0
- package/dist/codecs/structured-reference.codec.js.map +1 -0
- package/dist/codecs/system-rules.codec.d.ts +32 -0
- package/dist/codecs/system-rules.codec.d.ts.map +1 -0
- package/dist/codecs/system-rules.codec.js +62 -0
- package/dist/codecs/system-rules.codec.js.map +1 -0
- package/dist/codecs/tool-output.codec.d.ts +66 -0
- package/dist/codecs/tool-output.codec.d.ts.map +1 -0
- package/dist/codecs/tool-output.codec.js +95 -0
- package/dist/codecs/tool-output.codec.js.map +1 -0
- package/dist/codecs/tool-schema.codec.d.ts +36 -0
- package/dist/codecs/tool-schema.codec.d.ts.map +1 -0
- package/dist/codecs/tool-schema.codec.js +74 -0
- package/dist/codecs/tool-schema.codec.js.map +1 -0
- package/dist/codecs/unsafe-text.codec.d.ts +28 -0
- package/dist/codecs/unsafe-text.codec.d.ts.map +1 -0
- package/dist/codecs/unsafe-text.codec.js +63 -0
- package/dist/codecs/unsafe-text.codec.js.map +1 -0
- package/dist/graph/context-graph.d.ts +121 -0
- package/dist/graph/context-graph.d.ts.map +1 -0
- package/dist/graph/context-graph.js +166 -0
- package/dist/graph/context-graph.js.map +1 -0
- package/dist/graph/index.d.ts +8 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +24 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/kind-order.d.ts +60 -0
- package/dist/graph/kind-order.d.ts.map +1 -0
- package/dist/graph/kind-order.js +113 -0
- package/dist/graph/kind-order.js.map +1 -0
- package/dist/graph/queries.d.ts +68 -0
- package/dist/graph/queries.d.ts.map +1 -0
- package/dist/graph/queries.js +240 -0
- package/dist/graph/queries.js.map +1 -0
- package/dist/graph/views.d.ts +90 -0
- package/dist/graph/views.d.ts.map +1 -0
- package/dist/graph/views.js +173 -0
- package/dist/graph/views.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/compactor.d.ts +128 -0
- package/dist/pipeline/compactor.d.ts.map +1 -0
- package/dist/pipeline/compactor.js +346 -0
- package/dist/pipeline/compactor.js.map +1 -0
- package/dist/pipeline/index.d.ts +6 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +22 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/summarizer.d.ts +18 -0
- package/dist/pipeline/summarizer.d.ts.map +1 -0
- package/dist/pipeline/summarizer.js +68 -0
- package/dist/pipeline/summarizer.js.map +1 -0
- package/dist/policies/default-policy.d.ts +29 -0
- package/dist/policies/default-policy.d.ts.map +1 -0
- package/dist/policies/default-policy.js +58 -0
- package/dist/policies/default-policy.js.map +1 -0
- package/dist/policies/index.d.ts +5 -0
- package/dist/policies/index.d.ts.map +1 -0
- package/dist/policies/index.js +21 -0
- package/dist/policies/index.js.map +1 -0
- package/dist/providers/anthropic-compiler.d.ts +58 -0
- package/dist/providers/anthropic-compiler.d.ts.map +1 -0
- package/dist/providers/anthropic-compiler.js +182 -0
- package/dist/providers/anthropic-compiler.js.map +1 -0
- package/dist/providers/capabilities.d.ts +54 -0
- package/dist/providers/capabilities.d.ts.map +1 -0
- package/dist/providers/capabilities.js +87 -0
- package/dist/providers/capabilities.js.map +1 -0
- package/dist/providers/gemini-compiler.d.ts +51 -0
- package/dist/providers/gemini-compiler.d.ts.map +1 -0
- package/dist/providers/gemini-compiler.js +206 -0
- package/dist/providers/gemini-compiler.js.map +1 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +24 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/openai-compiler.d.ts +46 -0
- package/dist/providers/openai-compiler.d.ts.map +1 -0
- package/dist/providers/openai-compiler.js +149 -0
- package/dist/providers/openai-compiler.js.map +1 -0
- package/dist/types/attachment.d.ts +62 -0
- package/dist/types/attachment.d.ts.map +1 -0
- package/dist/types/attachment.js +6 -0
- package/dist/types/attachment.js.map +1 -0
- package/dist/types/block.d.ts +61 -0
- package/dist/types/block.d.ts.map +1 -0
- package/dist/types/block.js +8 -0
- package/dist/types/block.js.map +1 -0
- package/dist/types/codec.d.ts +58 -0
- package/dist/types/codec.d.ts.map +1 -0
- package/dist/types/codec.js +6 -0
- package/dist/types/codec.js.map +1 -0
- package/dist/types/compiled.d.ts +91 -0
- package/dist/types/compiled.d.ts.map +1 -0
- package/dist/types/compiled.js +6 -0
- package/dist/types/compiled.js.map +1 -0
- package/dist/types/hash.d.ts +24 -0
- package/dist/types/hash.d.ts.map +1 -0
- package/dist/types/hash.js +49 -0
- package/dist/types/hash.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +26 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/policy.d.ts +128 -0
- package/dist/types/policy.d.ts.map +1 -0
- package/dist/types/policy.js +55 -0
- package/dist/types/policy.js.map +1 -0
- package/package.json +55 -0
- package/postcss.config.js +4 -0
- package/src/__tests__/attachment-selector.test.ts +559 -0
- package/src/__tests__/cache-breakpoints.test.ts +566 -0
- package/src/__tests__/codecs.test.ts +417 -0
- package/src/__tests__/compactor.test.ts +608 -0
- package/src/__tests__/context-graph.test.ts +383 -0
- package/src/__tests__/hash.test.ts +274 -0
- package/src/__tests__/integration.test.ts +866 -0
- package/src/__tests__/kind-order.test.ts +312 -0
- package/src/__tests__/phase2-integration.test.ts +253 -0
- package/src/__tests__/queries.test.ts +387 -0
- package/src/__tests__/token-estimator.test.ts +326 -0
- package/src/adapters/anthropic-estimator.ts +125 -0
- package/src/adapters/attachment-resolver.ts +295 -0
- package/src/adapters/attachment-selector.ts +218 -0
- package/src/adapters/gemini-estimator.ts +93 -0
- package/src/adapters/index.ts +12 -0
- package/src/adapters/memory-store.ts +299 -0
- package/src/adapters/openai-estimator.ts +105 -0
- package/src/adapters/summarizer.ts +250 -0
- package/src/adapters/token-estimator.ts +74 -0
- package/src/builder/context-builder.ts +467 -0
- package/src/builder/context-fork.ts +471 -0
- package/src/builder/index.ts +6 -0
- package/src/codecs/base.ts +36 -0
- package/src/codecs/conversation-history.codec.ts +108 -0
- package/src/codecs/index.ts +57 -0
- package/src/codecs/redacted-stub.codec.ts +76 -0
- package/src/codecs/structured-reference.codec.ts +96 -0
- package/src/codecs/system-rules.codec.ts +74 -0
- package/src/codecs/tool-output.codec.ts +109 -0
- package/src/codecs/tool-schema.codec.ts +87 -0
- package/src/codecs/unsafe-text.codec.ts +74 -0
- package/src/graph/context-graph.ts +205 -0
- package/src/graph/index.ts +8 -0
- package/src/graph/kind-order.ts +125 -0
- package/src/graph/queries.ts +306 -0
- package/src/graph/views.ts +255 -0
- package/src/index.ts +31 -0
- package/src/pipeline/compactor.ts +563 -0
- package/src/pipeline/index.ts +6 -0
- package/src/pipeline/summarizer.ts +76 -0
- package/src/policies/default-policy.ts +69 -0
- package/src/policies/index.ts +5 -0
- package/src/providers/anthropic-compiler.ts +294 -0
- package/src/providers/capabilities.ts +144 -0
- package/src/providers/gemini-compiler.ts +272 -0
- package/src/providers/index.ts +8 -0
- package/src/providers/openai-compiler.ts +191 -0
- package/src/types/attachment.ts +86 -0
- package/src/types/block.ts +84 -0
- package/src/types/codec.ts +68 -0
- package/src/types/compiled.ts +109 -0
- package/src/types/hash.ts +58 -0
- package/src/types/index.ts +10 -0
- package/src/types/policy.ts +194 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +21 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for AttachmentSelector: budget enforcement and ranking.
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* - Budget enforcement (token limit)
|
|
6
|
+
* - Ranking by purpose, user mention, recency
|
|
7
|
+
* - Deterministic ordering (same input → same selection)
|
|
8
|
+
* - Token estimation for different attachment types
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest';
|
|
12
|
+
import { AttachmentSelector } from '../adapters/attachment-selector.js';
|
|
13
|
+
import type { RankedAttachment } from '../adapters/attachment-selector.js';
|
|
14
|
+
import type { AttachmentPolicy } from '../types/policy.js';
|
|
15
|
+
import type { ResolvedAttachment } from '../types/attachment.js';
|
|
16
|
+
|
|
17
|
+
// Mock TokenEstimator
|
|
18
|
+
const mockEstimator = {
|
|
19
|
+
estimateTokens: async () => ({ tokens: 100 }),
|
|
20
|
+
} as any;
|
|
21
|
+
|
|
22
|
+
describe('AttachmentSelector', () => {
|
|
23
|
+
describe('Budget Enforcement', () => {
|
|
24
|
+
it('should select attachments within budget', async () => {
|
|
25
|
+
const policy: AttachmentPolicy = {
|
|
26
|
+
maxTokensTotal: 1000, // Budget for ~10 text attachments (100 tokens each)
|
|
27
|
+
selectionStrategy: {
|
|
28
|
+
rankBy: ['purpose'],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
33
|
+
|
|
34
|
+
const attachments: RankedAttachment[] = [
|
|
35
|
+
createRankedAttachment('att1', 'evidence', false, 'Short text', 400), // ~100 tokens
|
|
36
|
+
createRankedAttachment('att2', 'evidence', false, 'Short text', 400), // ~100 tokens
|
|
37
|
+
createRankedAttachment('att3', 'evidence', false, 'Short text', 400), // ~100 tokens
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const result = await selector.selectAttachments(attachments);
|
|
41
|
+
|
|
42
|
+
expect(result.selected).toHaveLength(3);
|
|
43
|
+
expect(result.tokensUsed).toBeLessThanOrEqual(1000);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should exclude attachments over budget', async () => {
|
|
47
|
+
const policy: AttachmentPolicy = {
|
|
48
|
+
maxTokensTotal: 500, // Small budget
|
|
49
|
+
selectionStrategy: {
|
|
50
|
+
rankBy: ['purpose'],
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
55
|
+
|
|
56
|
+
const largeText = 'x'.repeat(2000); // ~500 tokens
|
|
57
|
+
const attachments: RankedAttachment[] = [
|
|
58
|
+
createRankedAttachment('att1', 'evidence', false, largeText, 500), // ~500 tokens
|
|
59
|
+
createRankedAttachment('att2', 'evidence', false, largeText, 500), // ~500 tokens (over budget)
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const result = await selector.selectAttachments(attachments);
|
|
63
|
+
|
|
64
|
+
expect(result.selected).toHaveLength(1);
|
|
65
|
+
expect(result.excluded).toHaveLength(1);
|
|
66
|
+
expect(result.excluded[0].attachmentId).toBe('att2');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should track total tokens used', async () => {
|
|
70
|
+
const policy: AttachmentPolicy = {
|
|
71
|
+
maxTokensTotal: 2000,
|
|
72
|
+
selectionStrategy: {
|
|
73
|
+
rankBy: ['purpose'],
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
78
|
+
|
|
79
|
+
const attachments: RankedAttachment[] = [
|
|
80
|
+
createRankedAttachment('att1', 'evidence', false, 'Short text', 400), // ~100 tokens
|
|
81
|
+
createRankedAttachment('att2', 'evidence', false, 'Short text', 400), // ~100 tokens
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const result = await selector.selectAttachments(attachments);
|
|
85
|
+
|
|
86
|
+
expect(result.tokensUsed).toBeGreaterThan(0);
|
|
87
|
+
expect(result.tokensUsed).toBeLessThanOrEqual(2000);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle zero budget gracefully', async () => {
|
|
91
|
+
const policy: AttachmentPolicy = {
|
|
92
|
+
maxTokensTotal: 0,
|
|
93
|
+
selectionStrategy: {
|
|
94
|
+
rankBy: ['purpose'],
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
99
|
+
|
|
100
|
+
const attachments: RankedAttachment[] = [
|
|
101
|
+
createRankedAttachment('att1', 'evidence', false, 'text', 100),
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const result = await selector.selectAttachments(attachments);
|
|
105
|
+
|
|
106
|
+
expect(result.selected).toHaveLength(0);
|
|
107
|
+
expect(result.excluded).toHaveLength(1);
|
|
108
|
+
expect(result.tokensUsed).toBe(0);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('Purpose Ranking', () => {
|
|
113
|
+
it('should prioritize evidence > input > context > artifact', async () => {
|
|
114
|
+
const policy: AttachmentPolicy = {
|
|
115
|
+
maxTokensTotal: 200, // Only 2 attachments fit (100 tokens each)
|
|
116
|
+
selectionStrategy: {
|
|
117
|
+
rankBy: ['purpose'],
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
122
|
+
|
|
123
|
+
// Each attachment: 400 chars = ~100 tokens (text.length / 4)
|
|
124
|
+
const largeText = 'x'.repeat(400);
|
|
125
|
+
const attachments: RankedAttachment[] = [
|
|
126
|
+
createRankedAttachment('att1', 'artifact', false, largeText, 100), // Lowest priority
|
|
127
|
+
createRankedAttachment('att2', 'evidence', false, largeText, 100), // Highest priority
|
|
128
|
+
createRankedAttachment('att3', 'context', false, largeText, 100), // Medium priority
|
|
129
|
+
createRankedAttachment('att4', 'input', false, largeText, 100), // High priority
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const result = await selector.selectAttachments(attachments);
|
|
133
|
+
|
|
134
|
+
// Should select evidence and input (highest priority)
|
|
135
|
+
expect(result.selected).toHaveLength(2);
|
|
136
|
+
const selectedIds = result.selected.map((a) => a.attachmentId);
|
|
137
|
+
expect(selectedIds).toContain('att2'); // evidence
|
|
138
|
+
expect(selectedIds).toContain('att4'); // input
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should use custom purpose priority if provided', async () => {
|
|
142
|
+
const policy: AttachmentPolicy = {
|
|
143
|
+
maxTokensTotal: 200,
|
|
144
|
+
selectionStrategy: {
|
|
145
|
+
rankBy: ['purpose'],
|
|
146
|
+
purposePriority: {
|
|
147
|
+
artifact: 1, // Highest
|
|
148
|
+
context: 2,
|
|
149
|
+
input: 3,
|
|
150
|
+
evidence: 4, // Lowest
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
156
|
+
|
|
157
|
+
const attachments: RankedAttachment[] = [
|
|
158
|
+
createRankedAttachment('att1', 'artifact', false, 'text', 100),
|
|
159
|
+
createRankedAttachment('att2', 'evidence', false, 'text', 100),
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
const result = await selector.selectAttachments(attachments);
|
|
163
|
+
|
|
164
|
+
// Should prioritize artifact over evidence
|
|
165
|
+
expect(result.selected).toHaveLength(2);
|
|
166
|
+
expect(result.selected[0].attachmentId).toBe('att1'); // artifact first
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('User Mention Ranking', () => {
|
|
171
|
+
it('should prioritize user-mentioned attachments', async () => {
|
|
172
|
+
const policy: AttachmentPolicy = {
|
|
173
|
+
maxTokensTotal: 200,
|
|
174
|
+
selectionStrategy: {
|
|
175
|
+
rankBy: ['user_mention'],
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
180
|
+
|
|
181
|
+
// Each attachment: 400 chars = ~100 tokens
|
|
182
|
+
const largeText = 'x'.repeat(400);
|
|
183
|
+
const attachments: RankedAttachment[] = [
|
|
184
|
+
createRankedAttachment('att1', 'context', false, largeText, 100), // Not mentioned
|
|
185
|
+
createRankedAttachment('att2', 'context', true, largeText, 100), // User mentioned
|
|
186
|
+
createRankedAttachment('att3', 'context', true, largeText, 100), // User mentioned
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
const result = await selector.selectAttachments(attachments);
|
|
190
|
+
|
|
191
|
+
// Should select mentioned attachments first
|
|
192
|
+
expect(result.selected).toHaveLength(2);
|
|
193
|
+
const selectedIds = result.selected.map((a) => a.attachmentId);
|
|
194
|
+
expect(selectedIds).toContain('att2');
|
|
195
|
+
expect(selectedIds).toContain('att3');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('Recency Ranking', () => {
|
|
200
|
+
it('should prioritize recent attachments', async () => {
|
|
201
|
+
const now = Date.now() / 1000;
|
|
202
|
+
|
|
203
|
+
const policy: AttachmentPolicy = {
|
|
204
|
+
maxTokensTotal: 200,
|
|
205
|
+
selectionStrategy: {
|
|
206
|
+
rankBy: ['recency'],
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
211
|
+
|
|
212
|
+
// Each attachment: 400 chars = ~100 tokens
|
|
213
|
+
const largeText = 'x'.repeat(400);
|
|
214
|
+
const attachments: RankedAttachment[] = [
|
|
215
|
+
createRankedAttachment('att1', 'context', false, largeText, now - 1000), // Older
|
|
216
|
+
createRankedAttachment('att2', 'context', false, largeText, now - 100), // Recent
|
|
217
|
+
createRankedAttachment('att3', 'context', false, largeText, now - 10), // Most recent
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
const result = await selector.selectAttachments(attachments);
|
|
221
|
+
|
|
222
|
+
// Should select most recent
|
|
223
|
+
expect(result.selected).toHaveLength(2);
|
|
224
|
+
const selectedIds = result.selected.map((a) => a.attachmentId);
|
|
225
|
+
expect(selectedIds).toContain('att3'); // Most recent
|
|
226
|
+
expect(selectedIds).toContain('att2'); // Second most recent
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('Multi-Criterion Ranking', () => {
|
|
231
|
+
it('should rank by multiple criteria in priority order', async () => {
|
|
232
|
+
const policy: AttachmentPolicy = {
|
|
233
|
+
maxTokensTotal: 200,
|
|
234
|
+
selectionStrategy: {
|
|
235
|
+
rankBy: ['user_mention', 'purpose'], // Mention first, then purpose
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
240
|
+
|
|
241
|
+
// Each attachment: 400 chars = ~100 tokens
|
|
242
|
+
const largeText = 'x'.repeat(400);
|
|
243
|
+
const attachments: RankedAttachment[] = [
|
|
244
|
+
createRankedAttachment('att1', 'evidence', false, largeText, 100), // Not mentioned, high purpose
|
|
245
|
+
createRankedAttachment('att2', 'artifact', true, largeText, 100), // Mentioned, low purpose
|
|
246
|
+
createRankedAttachment('att3', 'input', true, largeText, 100), // Mentioned, medium purpose
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
const result = await selector.selectAttachments(attachments);
|
|
250
|
+
|
|
251
|
+
// Should select mentioned attachments (att2, att3), prioritize input over artifact
|
|
252
|
+
expect(result.selected).toHaveLength(2);
|
|
253
|
+
const selectedIds = result.selected.map((a) => a.attachmentId);
|
|
254
|
+
expect(selectedIds).toContain('att2');
|
|
255
|
+
expect(selectedIds).toContain('att3');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('Deterministic Ordering', () => {
|
|
260
|
+
it('should produce same selection for same input', async () => {
|
|
261
|
+
const policy: AttachmentPolicy = {
|
|
262
|
+
maxTokensTotal: 500,
|
|
263
|
+
selectionStrategy: {
|
|
264
|
+
rankBy: ['purpose', 'recency'],
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
269
|
+
|
|
270
|
+
const attachments: RankedAttachment[] = [
|
|
271
|
+
createRankedAttachment('att1', 'evidence', false, 'text', 100),
|
|
272
|
+
createRankedAttachment('att2', 'context', false, 'text', 200),
|
|
273
|
+
createRankedAttachment('att3', 'input', false, 'text', 150),
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
const result1 = await selector.selectAttachments([...attachments]);
|
|
277
|
+
const result2 = await selector.selectAttachments([...attachments]);
|
|
278
|
+
|
|
279
|
+
expect(result1.selected.map((a) => a.attachmentId)).toEqual(
|
|
280
|
+
result2.selected.map((a) => a.attachmentId)
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should use attachmentId as tiebreaker', async () => {
|
|
285
|
+
const policy: AttachmentPolicy = {
|
|
286
|
+
maxTokensTotal: 200,
|
|
287
|
+
selectionStrategy: {
|
|
288
|
+
rankBy: ['purpose'], // Same purpose = tie
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
293
|
+
|
|
294
|
+
// Each attachment: 400 chars = ~100 tokens
|
|
295
|
+
const largeText = 'x'.repeat(400);
|
|
296
|
+
const attachments: RankedAttachment[] = [
|
|
297
|
+
createRankedAttachment('bbb', 'evidence', false, largeText, 100),
|
|
298
|
+
createRankedAttachment('aaa', 'evidence', false, largeText, 100),
|
|
299
|
+
createRankedAttachment('ccc', 'evidence', false, largeText, 100),
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
const result = await selector.selectAttachments(attachments);
|
|
303
|
+
|
|
304
|
+
// Should select in alphabetical order (tiebreaker)
|
|
305
|
+
expect(result.selected).toHaveLength(2);
|
|
306
|
+
expect(result.selected[0].attachmentId).toBe('aaa');
|
|
307
|
+
expect(result.selected[1].attachmentId).toBe('bbb');
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('Token Estimation', () => {
|
|
312
|
+
it('should estimate tokens for text attachments', async () => {
|
|
313
|
+
const policy: AttachmentPolicy = {
|
|
314
|
+
maxTokensTotal: 200,
|
|
315
|
+
selectionStrategy: {
|
|
316
|
+
rankBy: ['purpose'],
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
321
|
+
|
|
322
|
+
// ~400 chars = ~100 tokens
|
|
323
|
+
const text = 'x'.repeat(400);
|
|
324
|
+
const attachments: RankedAttachment[] = [
|
|
325
|
+
createRankedAttachment('att1', 'evidence', false, text, 100),
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
const result = await selector.selectAttachments(attachments);
|
|
329
|
+
|
|
330
|
+
expect(result.tokensUsed).toBeGreaterThan(0);
|
|
331
|
+
expect(result.tokensUsed).toBeLessThanOrEqual(200);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should estimate tokens for image attachments', async () => {
|
|
335
|
+
const policy: AttachmentPolicy = {
|
|
336
|
+
maxTokensTotal: 1000,
|
|
337
|
+
selectionStrategy: {
|
|
338
|
+
rankBy: ['purpose'],
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
343
|
+
|
|
344
|
+
const imageAttachment: RankedAttachment = {
|
|
345
|
+
attachmentId: 'img1',
|
|
346
|
+
purpose: 'evidence',
|
|
347
|
+
userMention: false,
|
|
348
|
+
rankScore: 0,
|
|
349
|
+
filename: 'test.png',
|
|
350
|
+
mimeType: 'image/png',
|
|
351
|
+
sizeBytes: 1024,
|
|
352
|
+
storage: 'local',
|
|
353
|
+
storagePath: '/test/test.png',
|
|
354
|
+
createdAt: Date.now() / 1000,
|
|
355
|
+
content: 'base64data',
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const result = await selector.selectAttachments([imageAttachment]);
|
|
359
|
+
|
|
360
|
+
// Image should have conservative token estimate (~340 tokens)
|
|
361
|
+
expect(result.tokensUsed).toBeGreaterThan(0);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should estimate tokens for PDF attachments', async () => {
|
|
365
|
+
const policy: AttachmentPolicy = {
|
|
366
|
+
maxTokensTotal: 1000,
|
|
367
|
+
selectionStrategy: {
|
|
368
|
+
rankBy: ['purpose'],
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
373
|
+
|
|
374
|
+
const pdfAttachment: RankedAttachment = {
|
|
375
|
+
attachmentId: 'pdf1',
|
|
376
|
+
purpose: 'evidence',
|
|
377
|
+
userMention: false,
|
|
378
|
+
rankScore: 0,
|
|
379
|
+
filename: 'doc.pdf',
|
|
380
|
+
mimeType: 'application/pdf',
|
|
381
|
+
sizeBytes: 2048,
|
|
382
|
+
storage: 'local',
|
|
383
|
+
storagePath: '/test/doc.pdf',
|
|
384
|
+
createdAt: Date.now() / 1000,
|
|
385
|
+
content: 'base64pdfdata',
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const result = await selector.selectAttachments([pdfAttachment]);
|
|
389
|
+
|
|
390
|
+
expect(result.tokensUsed).toBeGreaterThan(0);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe('selectFromList convenience method', () => {
|
|
395
|
+
it('should build ranked attachments from list and metadata', async () => {
|
|
396
|
+
const policy: AttachmentPolicy = {
|
|
397
|
+
maxTokensTotal: 500,
|
|
398
|
+
selectionStrategy: {
|
|
399
|
+
rankBy: ['purpose'],
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
404
|
+
|
|
405
|
+
const attachments: ResolvedAttachment[] = [
|
|
406
|
+
{
|
|
407
|
+
attachmentId: 'att1',
|
|
408
|
+
filename: 'test.txt',
|
|
409
|
+
mimeType: 'text/plain',
|
|
410
|
+
sizeBytes: 12,
|
|
411
|
+
storage: 'local',
|
|
412
|
+
storagePath: '/test/test.txt',
|
|
413
|
+
createdAt: Date.now() / 1000,
|
|
414
|
+
text: 'Test content',
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
attachmentId: 'att2',
|
|
418
|
+
filename: 'test2.txt',
|
|
419
|
+
mimeType: 'text/plain',
|
|
420
|
+
sizeBytes: 14,
|
|
421
|
+
storage: 'local',
|
|
422
|
+
storagePath: '/test/test2.txt',
|
|
423
|
+
createdAt: Date.now() / 1000,
|
|
424
|
+
text: 'Test content 2',
|
|
425
|
+
},
|
|
426
|
+
];
|
|
427
|
+
|
|
428
|
+
const metadata = new Map([
|
|
429
|
+
['att1', { purpose: 'evidence' as const, userMention: true }],
|
|
430
|
+
['att2', { purpose: 'context' as const, userMention: false }],
|
|
431
|
+
]);
|
|
432
|
+
|
|
433
|
+
const result = await selector.selectFromList(attachments, metadata);
|
|
434
|
+
|
|
435
|
+
expect(result.selected).toHaveLength(2);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('should use default metadata when not provided', async () => {
|
|
439
|
+
const policy: AttachmentPolicy = {
|
|
440
|
+
maxTokensTotal: 500,
|
|
441
|
+
selectionStrategy: {
|
|
442
|
+
rankBy: ['purpose'],
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
447
|
+
|
|
448
|
+
const attachments: ResolvedAttachment[] = [
|
|
449
|
+
{
|
|
450
|
+
attachmentId: 'att1',
|
|
451
|
+
filename: 'test.txt',
|
|
452
|
+
mimeType: 'text/plain',
|
|
453
|
+
sizeBytes: 12,
|
|
454
|
+
storage: 'local',
|
|
455
|
+
storagePath: '/test/test.txt',
|
|
456
|
+
createdAt: Date.now() / 1000,
|
|
457
|
+
text: 'Test content',
|
|
458
|
+
},
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
const metadata = new Map(); // Empty metadata
|
|
462
|
+
|
|
463
|
+
const result = await selector.selectFromList(attachments, metadata);
|
|
464
|
+
|
|
465
|
+
// Should use default: purpose = 'context', userMention = false
|
|
466
|
+
expect(result.selected).toHaveLength(1);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
describe('Edge Cases', () => {
|
|
471
|
+
it('should handle empty attachment list', async () => {
|
|
472
|
+
const policy: AttachmentPolicy = {
|
|
473
|
+
maxTokensTotal: 1000,
|
|
474
|
+
selectionStrategy: {
|
|
475
|
+
rankBy: ['purpose'],
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
480
|
+
|
|
481
|
+
const result = await selector.selectAttachments([]);
|
|
482
|
+
|
|
483
|
+
expect(result.selected).toHaveLength(0);
|
|
484
|
+
expect(result.excluded).toHaveLength(0);
|
|
485
|
+
expect(result.tokensUsed).toBe(0);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('should handle single attachment', async () => {
|
|
489
|
+
const policy: AttachmentPolicy = {
|
|
490
|
+
maxTokensTotal: 1000,
|
|
491
|
+
selectionStrategy: {
|
|
492
|
+
rankBy: ['purpose'],
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
497
|
+
|
|
498
|
+
const attachments: RankedAttachment[] = [
|
|
499
|
+
createRankedAttachment('att1', 'evidence', false, 'text', 100),
|
|
500
|
+
];
|
|
501
|
+
|
|
502
|
+
const result = await selector.selectAttachments(attachments);
|
|
503
|
+
|
|
504
|
+
expect(result.selected).toHaveLength(1);
|
|
505
|
+
expect(result.excluded).toHaveLength(0);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('should handle all attachments over budget', async () => {
|
|
509
|
+
const policy: AttachmentPolicy = {
|
|
510
|
+
maxTokensTotal: 50, // Very small budget
|
|
511
|
+
selectionStrategy: {
|
|
512
|
+
rankBy: ['purpose'],
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const selector = new AttachmentSelector(policy, mockEstimator);
|
|
517
|
+
|
|
518
|
+
const largeText = 'x'.repeat(1000);
|
|
519
|
+
const attachments: RankedAttachment[] = [
|
|
520
|
+
createRankedAttachment('att1', 'evidence', false, largeText, 100),
|
|
521
|
+
createRankedAttachment('att2', 'evidence', false, largeText, 100),
|
|
522
|
+
];
|
|
523
|
+
|
|
524
|
+
const result = await selector.selectAttachments(attachments);
|
|
525
|
+
|
|
526
|
+
expect(result.selected).toHaveLength(0);
|
|
527
|
+
expect(result.excluded).toHaveLength(2);
|
|
528
|
+
expect(result.tokensUsed).toBe(0);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// Test helpers
|
|
534
|
+
|
|
535
|
+
function createRankedAttachment(
|
|
536
|
+
id: string,
|
|
537
|
+
purpose: 'evidence' | 'input' | 'context' | 'artifact',
|
|
538
|
+
userMention: boolean,
|
|
539
|
+
text: string,
|
|
540
|
+
createdAt: number
|
|
541
|
+
): RankedAttachment {
|
|
542
|
+
// If createdAt looks like a character count (small number), assume it's meant as a timestamp offset
|
|
543
|
+
// Otherwise, treat it as Unix timestamp in seconds
|
|
544
|
+
const normalizedCreatedAt = createdAt < 10000 ? Date.now() / 1000 - createdAt : createdAt;
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
attachmentId: id,
|
|
548
|
+
purpose,
|
|
549
|
+
userMention,
|
|
550
|
+
rankScore: 0, // Will be computed
|
|
551
|
+
filename: `${id}.txt`,
|
|
552
|
+
mimeType: 'text/plain',
|
|
553
|
+
sizeBytes: text.length,
|
|
554
|
+
storage: 'local',
|
|
555
|
+
storagePath: `/test/${id}.txt`,
|
|
556
|
+
createdAt: normalizedCreatedAt,
|
|
557
|
+
text,
|
|
558
|
+
};
|
|
559
|
+
}
|