feat(observer): parser write-block v4.3 — embedding + reviewed + cost ext (phase 3 deferred #2)

This commit is contained in:
Дмитрий
2026-05-25 13:52:14 +03:00
parent e144879a08
commit a8aa94f180
2 changed files with 119 additions and 10 deletions
+102 -9
View File
@@ -233,7 +233,7 @@ describe('parseTranscript', () => {
expect(ep.events).toEqual([]);
expect(ep.outcome).toBe('unknown');
expect(ep.schema_version).toBe(4);
expect(ep.schema_minor).toBe(0);
expect(ep.schema_minor).toBe(3);
});
it('produces a complete 7-field primary_rationale', () => {
@@ -466,7 +466,7 @@ describe('parseTranscript — v4 episode (Phase 2 Task 15 bump)', () => {
]);
const ep = parseTranscript(t);
expect(ep.schema_version).toBe(4);
expect(ep.schema_minor).toBe(0);
expect(ep.schema_minor).toBe(3);
expect('classifier_output' in ep).toBe(true);
expect('degraded_mode' in ep).toBe(true);
expect('classifier_model' in ep.environment).toBe(true);
@@ -961,6 +961,10 @@ describe('extractTokenUsage (Task 2)', () => {
expect(extractTokenUsage(turn)).toEqual({
input_tokens: 18, output_tokens: 8, cache_read_input_tokens: 180,
cache_creation_input_tokens: 70, web_search_requests: 0, web_fetch_requests: 0, iterations: 0,
classifier_input_tokens: 0, classifier_output_tokens: 0,
self_assessment_input_tokens: 0, self_assessment_output_tokens: 0,
reviewer_input_tokens: 0, reviewer_output_tokens: 0,
reviewer_subagent_usd: 0, reviewer_direct_fallback_usd: 0,
});
});
it('captures server_tool_use bonus fields (web_search/web_fetch)', () => {
@@ -985,17 +989,23 @@ describe('extractTokenUsage (Task 2)', () => {
expect(extractTokenUsage(turn)).toEqual({
input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0,
cache_creation_input_tokens: 0, web_search_requests: 0, web_fetch_requests: 0, iterations: 0,
classifier_input_tokens: 0, classifier_output_tokens: 0,
self_assessment_input_tokens: 0, self_assessment_output_tokens: 0,
reviewer_input_tokens: 0, reviewer_output_tokens: 0,
reviewer_subagent_usd: 0, reviewer_direct_fallback_usd: 0,
});
});
it('handles empty/null turn safely', () => {
expect(extractTokenUsage([])).toEqual({
const zeroShape = {
input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0,
cache_creation_input_tokens: 0, web_search_requests: 0, web_fetch_requests: 0, iterations: 0,
});
expect(extractTokenUsage(null)).toEqual({
input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0,
cache_creation_input_tokens: 0, web_search_requests: 0, web_fetch_requests: 0, iterations: 0,
});
classifier_input_tokens: 0, classifier_output_tokens: 0,
self_assessment_input_tokens: 0, self_assessment_output_tokens: 0,
reviewer_input_tokens: 0, reviewer_output_tokens: 0,
reviewer_subagent_usd: 0, reviewer_direct_fallback_usd: 0,
};
expect(extractTokenUsage([])).toEqual(zeroShape);
expect(extractTokenUsage(null)).toEqual(zeroShape);
});
it('safely skips entries where usage is a non-object primitive (defensive guard)', () => {
const turn = [
@@ -1030,6 +1040,10 @@ describe('parseTranscript — task_cost integration (Task 2)', () => {
expect(result.task_cost).toEqual({
input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0,
cache_creation_input_tokens: 0, web_search_requests: 0, web_fetch_requests: 0, iterations: 0,
classifier_input_tokens: 0, classifier_output_tokens: 0,
self_assessment_input_tokens: 0, self_assessment_output_tokens: 0,
reviewer_input_tokens: 0, reviewer_output_tokens: 0,
reviewer_subagent_usd: 0, reviewer_direct_fallback_usd: 0,
});
});
});
@@ -1640,7 +1654,7 @@ describe('parseTranscript v3 fields', () => {
it('emits schema_version: 4', () => {
const ep = parseTranscript(transcriptDirectFeature(), 'sess-1');
expect(ep.schema_version).toBe(4);
expect(ep.schema_minor).toBe(0);
expect(ep.schema_minor).toBe(3);
});
it('sets recommended_node for direct feature-classified episode', () => {
@@ -1720,3 +1734,82 @@ describe('parseTranscript — router-state enrichment (Task 3)', () => {
}
});
});
// ─── Phase 3 deferred #2: parser write-block v4.3 ────────────────────────────
describe('parseTranscript — schema v4.3 write-block fields (phase 3 deferred #2)', () => {
function simpleTranscript(prompt = 'add a feature', ts = '2026-05-25T10:00:00Z', sid = 's-v43') {
return [
JSON.stringify({ type: 'user', message: { role: 'user', content: prompt }, timestamp: ts, sessionId: sid }),
JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: [{ type: 'text', text: 'done' }] }, timestamp: ts, sessionId: sid }),
].join('\n');
}
it('emits schema_minor 3', () => {
const ep = parseTranscript(simpleTranscript());
expect(ep.schema_minor).toBe(3);
});
it('emits outcome_reviewed: null', () => {
const ep = parseTranscript(simpleTranscript());
expect('outcome_reviewed' in ep).toBe(true);
expect(ep.outcome_reviewed).toBeNull();
});
it('emits outcome_reviewed_source: null', () => {
const ep = parseTranscript(simpleTranscript());
expect('outcome_reviewed_source' in ep).toBe(true);
expect(ep.outcome_reviewed_source).toBeNull();
});
it('emits prompt_embedding_base64 as null when embedding model unavailable', () => {
// parser is synchronous; embedding is null by design (filled async by stop-hook)
const ep = parseTranscript(simpleTranscript());
expect('prompt_embedding_base64' in ep).toBe(true);
expect(ep.prompt_embedding_base64).toBeNull();
});
it('does not throw when transcript has unusual content', () => {
// robustness guard: parser must never throw regardless of transcript shape
expect(() => parseTranscript(simpleTranscript('', '2026-05-25T10:00:00Z'))).not.toThrow();
expect(() => parseTranscript('')).not.toThrow();
expect(() => parseTranscript('{ broken json\nnot valid')).not.toThrow();
});
it('task_cost has 8 new zero-default LLM-cost fields', () => {
const ep = parseTranscript(simpleTranscript());
const cost = ep.task_cost;
expect(typeof cost.classifier_input_tokens).toBe('number');
expect(typeof cost.classifier_output_tokens).toBe('number');
expect(typeof cost.self_assessment_input_tokens).toBe('number');
expect(typeof cost.self_assessment_output_tokens).toBe('number');
expect(typeof cost.reviewer_input_tokens).toBe('number');
expect(typeof cost.reviewer_output_tokens).toBe('number');
expect(typeof cost.reviewer_subagent_usd).toBe('number');
expect(typeof cost.reviewer_direct_fallback_usd).toBe('number');
// all default to 0
expect(cost.classifier_input_tokens).toBe(0);
expect(cost.classifier_output_tokens).toBe(0);
expect(cost.self_assessment_input_tokens).toBe(0);
expect(cost.self_assessment_output_tokens).toBe(0);
expect(cost.reviewer_input_tokens).toBe(0);
expect(cost.reviewer_output_tokens).toBe(0);
expect(cost.reviewer_subagent_usd).toBe(0);
expect(cost.reviewer_direct_fallback_usd).toBe(0);
});
it('task_cost retains all existing fields alongside new ones', () => {
const lines = [
JSON.stringify({ type: 'user', message: { role: 'user', content: 'do it' } }),
JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: [{ type: 'text', text: 'ok' }], usage: { input_tokens: 100, output_tokens: 20, cache_read_input_tokens: 500, cache_creation_input_tokens: 50 } } }),
].join('\n');
const cost = parseTranscript(lines).task_cost;
expect(cost.input_tokens).toBe(100);
expect(cost.output_tokens).toBe(20);
expect(cost.cache_read_input_tokens).toBe(500);
expect(cost.cache_creation_input_tokens).toBe(50);
// new fields still 0 (populated retroactively by controller scripts)
expect(cost.classifier_input_tokens).toBe(0);
expect(cost.reviewer_subagent_usd).toBe(0);
});
});