mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-13 03:33:15 +08:00
- suggest-compact hook now reads the latest usage record from the session transcript and suggests /compact at a window-scaled token threshold (160k/200k window, 250k/1M window; COMPACT_CONTEXT_THRESHOLD and COMPACT_CONTEXT_INTERVAL overridable), re-firing per 60k-token growth bucket; tool-call count stays as the secondary signal (#2155) - Codex repo marketplace now points at ./plugins/ecc instead of ./ — Codex never discovers plugins whose local marketplace source.path is the marketplace root (verified on Codex CLI 0.137.0); plugins/ecc is a thin folder referencing root skills/.mcp.json per maintainer direction on #2097; docs flag plugin mode as experimental with the upstream blocker openai/codex#26037 linked (#2128) - README badges for installs/stars/forks now use shields endpoint badges backed by api.ecc.tools (live install count 3,712 vs the stale static 150), which also eliminates shields' 'Unable to select next GitHub token from pool' render in the stars badge Closes #2155 Closes #2128
263 lines
8.4 KiB
JavaScript
263 lines
8.4 KiB
JavaScript
'use strict';
|
|
/**
|
|
* Tests for scripts/lib/transcript-context.js (#2155)
|
|
*
|
|
* Covers transcript usage extraction, context-window detection, threshold and
|
|
* interval resolution, and the bucket math the strategic-compact hook uses.
|
|
*
|
|
* Run with: node tests/lib/transcript-context.test.js
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const {
|
|
STANDARD_CONTEXT_WINDOW_TOKENS,
|
|
LARGE_CONTEXT_WINDOW_TOKENS,
|
|
DEFAULT_CONTEXT_THRESHOLD_STANDARD,
|
|
DEFAULT_CONTEXT_THRESHOLD_LARGE,
|
|
DEFAULT_CONTEXT_INTERVAL_TOKENS,
|
|
readLatestContextTokens,
|
|
resolveContextWindowTokens,
|
|
resolveContextThreshold,
|
|
resolveContextInterval,
|
|
computeContextBucket,
|
|
formatWindowLabel
|
|
} = require('../../scripts/lib/transcript-context');
|
|
|
|
console.log('=== Testing transcript-context.js ===\n');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function test(desc, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` ✓ ${desc}`);
|
|
passed++;
|
|
} catch (e) {
|
|
console.log(` ✗ ${desc}: ${e.message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
let fixtureSeq = 0;
|
|
|
|
function writeTranscript(lines) {
|
|
fixtureSeq += 1;
|
|
const filePath = path.join(os.tmpdir(), `transcript-context-test-${process.pid}-${fixtureSeq}.jsonl`);
|
|
fs.writeFileSync(filePath, lines.join('\n') + '\n');
|
|
return filePath;
|
|
}
|
|
|
|
function usageRecord(tokens, model = 'claude-sonnet-4-6', extra = {}) {
|
|
return JSON.stringify({
|
|
type: 'assistant',
|
|
message: {
|
|
model,
|
|
usage: {
|
|
input_tokens: tokens.input || 0,
|
|
cache_read_input_tokens: tokens.cacheRead || 0,
|
|
cache_creation_input_tokens: tokens.cacheCreation || 0,
|
|
output_tokens: tokens.output || 0
|
|
}
|
|
},
|
|
...extra
|
|
});
|
|
}
|
|
|
|
const cleanupPaths = [];
|
|
|
|
function tracked(filePath) {
|
|
cleanupPaths.push(filePath);
|
|
return filePath;
|
|
}
|
|
|
|
// ── readLatestContextTokens ──
|
|
console.log('readLatestContextTokens:');
|
|
|
|
test('sums input + cache_read + cache_creation from the latest usage record', () => {
|
|
const file = tracked(writeTranscript([
|
|
usageRecord({ input: 10, cacheRead: 20, cacheCreation: 5 }),
|
|
usageRecord({ input: 100, cacheRead: 150000, cacheCreation: 7000 })
|
|
]));
|
|
const result = readLatestContextTokens(file);
|
|
assert.ok(result, 'Expected a usage result');
|
|
assert.strictEqual(result.tokens, 157100);
|
|
});
|
|
|
|
test('returns the model id alongside the token count', () => {
|
|
const file = tracked(writeTranscript([
|
|
usageRecord({ input: 1000 }, 'claude-opus-4-5[1m]')
|
|
]));
|
|
const result = readLatestContextTokens(file);
|
|
assert.strictEqual(result.model, 'claude-opus-4-5[1m]');
|
|
});
|
|
|
|
test('skips trailing records without usage (e.g. tool results)', () => {
|
|
const file = tracked(writeTranscript([
|
|
usageRecord({ input: 5000 }),
|
|
JSON.stringify({ type: 'user', message: { content: 'tool result' } }),
|
|
JSON.stringify({ type: 'system', subtype: 'info' })
|
|
]));
|
|
const result = readLatestContextTokens(file);
|
|
assert.strictEqual(result.tokens, 5000);
|
|
});
|
|
|
|
test('skips malformed JSONL lines without throwing', () => {
|
|
const file = tracked(writeTranscript([
|
|
usageRecord({ input: 4200 }),
|
|
'{not json at all',
|
|
''
|
|
]));
|
|
const result = readLatestContextTokens(file);
|
|
assert.strictEqual(result.tokens, 4200);
|
|
});
|
|
|
|
test('returns null for a transcript with no usage records', () => {
|
|
const file = tracked(writeTranscript([
|
|
JSON.stringify({ type: 'user', message: { content: 'hello' } })
|
|
]));
|
|
assert.strictEqual(readLatestContextTokens(file), null);
|
|
});
|
|
|
|
test('returns null for a missing transcript file', () => {
|
|
assert.strictEqual(readLatestContextTokens(path.join(os.tmpdir(), 'definitely-missing.jsonl')), null);
|
|
});
|
|
|
|
test('returns null for empty or non-string paths', () => {
|
|
assert.strictEqual(readLatestContextTokens(''), null);
|
|
assert.strictEqual(readLatestContextTokens(undefined), null);
|
|
});
|
|
|
|
test('ignores zero-token usage records', () => {
|
|
const file = tracked(writeTranscript([
|
|
usageRecord({ input: 999 }),
|
|
usageRecord({ input: 0 })
|
|
]));
|
|
const result = readLatestContextTokens(file);
|
|
assert.strictEqual(result.tokens, 999);
|
|
});
|
|
|
|
test('only scans the transcript tail (latest records win on large files)', () => {
|
|
const filler = JSON.stringify({ type: 'system', note: 'x'.repeat(512) });
|
|
const lines = [usageRecord({ input: 11 })];
|
|
for (let i = 0; i < 50; i++) lines.push(filler);
|
|
lines.push(usageRecord({ input: 170000 }));
|
|
const file = tracked(writeTranscript(lines));
|
|
// Tail window smaller than the file forces the truncated-tail path.
|
|
const result = readLatestContextTokens(file, { tailBytes: 4096 });
|
|
assert.strictEqual(result.tokens, 170000);
|
|
});
|
|
|
|
// ── resolveContextWindowTokens ──
|
|
console.log('\nresolveContextWindowTokens:');
|
|
|
|
test('defaults to the standard 200k window', () => {
|
|
assert.strictEqual(resolveContextWindowTokens(50000, 'claude-sonnet-4-6'), STANDARD_CONTEXT_WINDOW_TOKENS);
|
|
});
|
|
|
|
test('detects a 1M window from the [1m] model marker', () => {
|
|
assert.strictEqual(resolveContextWindowTokens(50000, 'claude-opus-4-5[1m]'), LARGE_CONTEXT_WINDOW_TOKENS);
|
|
});
|
|
|
|
test('detects a 1M window when observed tokens exceed 200k (marker dropped)', () => {
|
|
assert.strictEqual(resolveContextWindowTokens(220000, 'claude-opus-4-5'), LARGE_CONTEXT_WINDOW_TOKENS);
|
|
});
|
|
|
|
test('treats an empty model id as standard window', () => {
|
|
assert.strictEqual(resolveContextWindowTokens(100000, ''), STANDARD_CONTEXT_WINDOW_TOKENS);
|
|
});
|
|
|
|
// ── resolveContextThreshold ──
|
|
console.log('\nresolveContextThreshold:');
|
|
|
|
test('defaults to 160k for the 200k window', () => {
|
|
assert.strictEqual(resolveContextThreshold({}, STANDARD_CONTEXT_WINDOW_TOKENS), DEFAULT_CONTEXT_THRESHOLD_STANDARD);
|
|
});
|
|
|
|
test('defaults to 250k for the 1M window', () => {
|
|
assert.strictEqual(resolveContextThreshold({}, LARGE_CONTEXT_WINDOW_TOKENS), DEFAULT_CONTEXT_THRESHOLD_LARGE);
|
|
});
|
|
|
|
test('honours COMPACT_CONTEXT_THRESHOLD override', () => {
|
|
assert.strictEqual(resolveContextThreshold({ COMPACT_CONTEXT_THRESHOLD: '1234' }, STANDARD_CONTEXT_WINDOW_TOKENS), 1234);
|
|
});
|
|
|
|
test('COMPACT_CONTEXT_THRESHOLD=0 disables the signal', () => {
|
|
assert.strictEqual(resolveContextThreshold({ COMPACT_CONTEXT_THRESHOLD: '0' }, STANDARD_CONTEXT_WINDOW_TOKENS), 0);
|
|
});
|
|
|
|
test('invalid COMPACT_CONTEXT_THRESHOLD falls back to the default', () => {
|
|
for (const bad of ['-5', 'abc', '99999999999']) {
|
|
assert.strictEqual(
|
|
resolveContextThreshold({ COMPACT_CONTEXT_THRESHOLD: bad }, STANDARD_CONTEXT_WINDOW_TOKENS),
|
|
DEFAULT_CONTEXT_THRESHOLD_STANDARD,
|
|
`Expected fallback for ${bad}`
|
|
);
|
|
}
|
|
});
|
|
|
|
// ── resolveContextInterval ──
|
|
console.log('\nresolveContextInterval:');
|
|
|
|
test('defaults to 60k tokens', () => {
|
|
assert.strictEqual(resolveContextInterval({}), DEFAULT_CONTEXT_INTERVAL_TOKENS);
|
|
});
|
|
|
|
test('honours COMPACT_CONTEXT_INTERVAL override', () => {
|
|
assert.strictEqual(resolveContextInterval({ COMPACT_CONTEXT_INTERVAL: '5000' }), 5000);
|
|
});
|
|
|
|
test('invalid COMPACT_CONTEXT_INTERVAL falls back to the default', () => {
|
|
for (const bad of ['0', '-1', 'abc']) {
|
|
assert.strictEqual(resolveContextInterval({ COMPACT_CONTEXT_INTERVAL: bad }), DEFAULT_CONTEXT_INTERVAL_TOKENS, `Expected fallback for ${bad}`);
|
|
}
|
|
});
|
|
|
|
// ── computeContextBucket ──
|
|
console.log('\ncomputeContextBucket:');
|
|
|
|
test('returns -1 below the threshold', () => {
|
|
assert.strictEqual(computeContextBucket(159999, 160000, 60000), -1);
|
|
});
|
|
|
|
test('returns bucket 0 at the threshold', () => {
|
|
assert.strictEqual(computeContextBucket(160000, 160000, 60000), 0);
|
|
});
|
|
|
|
test('increments the bucket after each interval of growth', () => {
|
|
assert.strictEqual(computeContextBucket(219999, 160000, 60000), 0);
|
|
assert.strictEqual(computeContextBucket(220000, 160000, 60000), 1);
|
|
assert.strictEqual(computeContextBucket(280000, 160000, 60000), 2);
|
|
});
|
|
|
|
test('returns -1 when the threshold is disabled (0)', () => {
|
|
assert.strictEqual(computeContextBucket(500000, 0, 60000), -1);
|
|
});
|
|
|
|
test('returns -1 for non-finite token counts', () => {
|
|
assert.strictEqual(computeContextBucket(NaN, 160000, 60000), -1);
|
|
});
|
|
|
|
// ── formatWindowLabel ──
|
|
console.log('\nformatWindowLabel:');
|
|
|
|
test('labels the standard and large windows', () => {
|
|
assert.strictEqual(formatWindowLabel(STANDARD_CONTEXT_WINDOW_TOKENS), '200k');
|
|
assert.strictEqual(formatWindowLabel(LARGE_CONTEXT_WINDOW_TOKENS), '1M');
|
|
});
|
|
|
|
// Cleanup
|
|
for (const filePath of cleanupPaths) {
|
|
try {
|
|
fs.unlinkSync(filePath);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
|
process.exit(failed > 0 ? 1 : 0);
|