251 lines
7.3 KiB
JavaScript
251 lines
7.3 KiB
JavaScript
import test from 'node:test';
|
|||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
||
|
|
import { tmpdir } from 'node:os';
|
||
|
|
import { dirname, join } from 'node:path';
|
||
|
|
|
||
|
|
import { createPiProvider } from '../packages/core/src/providers/pi.ts';
|
||
|
|
import {
|
||
|
|
buildCodingAgentContextEntries,
|
||
|
|
buildCodingAgentSessionPath,
|
||
|
|
buildAgentCoreSessionPath,
|
||
|
|
defaultAgentCoreContextEntryTransform,
|
||
|
|
PI_CONTEXT_ORACLE_VERSION,
|
||
|
|
pi083LeafId,
|
||
|
|
projectCanonicalEvidence,
|
||
|
|
} from './fixtures/pi/pi-0.83.0-context-oracle.mjs';
|
||
|
|
|
||
|
|
const CASES = 512;
|
||
|
|
const SEED = 0x5eedc0de;
|
||
|
|
|
||
|
|
function randomGenerator(seed) {
|
||
|
|
let state = seed >>> 0;
|
||
|
|
return () => {
|
||
|
|
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
|
||
|
|
return state / 0x1_0000_0000;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function integer(random, min, max) {
|
||
|
|
return min + Math.floor(random() * (max - min + 1));
|
||
|
|
}
|
||
|
|
|
||
|
|
function timestamp(caseIndex, entryIndex) {
|
||
|
|
return new Date(Date.UTC(2026, 7, 2, 12, caseIndex % 30, entryIndex)).toISOString();
|
||
|
|
}
|
||
|
|
|
||
|
|
function actualEvidence(records) {
|
||
|
|
return records.flatMap((record) => {
|
||
|
|
if (record.kind === 'message' && record.visibility === 'visible') {
|
||
|
|
return [JSON.stringify({
|
||
|
|
kind: 'message',
|
||
|
|
source: null,
|
||
|
|
role: record.role,
|
||
|
|
content: record.text,
|
||
|
|
})];
|
||
|
|
}
|
||
|
|
if (record.kind === 'summary' && record.visibility === 'visible') {
|
||
|
|
return [JSON.stringify({
|
||
|
|
kind: 'summary',
|
||
|
|
source: record.source,
|
||
|
|
role: null,
|
||
|
|
content: record.content,
|
||
|
|
})];
|
||
|
|
}
|
||
|
|
return [];
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function drain(generator) {
|
||
|
|
const records = [];
|
||
|
|
for (;;) {
|
||
|
|
const step = generator.next();
|
||
|
|
if (step.done) return records;
|
||
|
|
records.push(step.value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function createCase(random, caseIndex, totals) {
|
||
|
|
const entries = [];
|
||
|
|
const ids = [];
|
||
|
|
let headId = null;
|
||
|
|
const entryCount = integer(random, 8, 64);
|
||
|
|
for (let entryIndex = 0; entryIndex < entryCount; entryIndex++) {
|
||
|
|
const id = `case-${caseIndex}-entry-${entryIndex}`;
|
||
|
|
const time = timestamp(caseIndex, entryIndex);
|
||
|
|
const roll = random();
|
||
|
|
let entry;
|
||
|
|
|
||
|
|
if (roll < 0.08 && ids.length > 0) {
|
||
|
|
const targetId = random() < 0.12 ? null : ids[integer(random, 0, ids.length - 1)];
|
||
|
|
entry = { type: 'leaf', id, parentId: headId, timestamp: time, targetId };
|
||
|
|
headId = targetId;
|
||
|
|
totals.leaves++;
|
||
|
|
if (targetId === null) totals.nullLeaves++;
|
||
|
|
} else {
|
||
|
|
let parentId = headId;
|
||
|
|
if (random() < 0.06) {
|
||
|
|
parentId = `omitted-${caseIndex}-${entryIndex}`;
|
||
|
|
totals.orphanParents++;
|
||
|
|
} else if (ids.length > 0 && random() < 0.28) {
|
||
|
|
parentId = ids[integer(random, 0, ids.length - 1)];
|
||
|
|
}
|
||
|
|
|
||
|
|
if (roll < 0.72) {
|
||
|
|
entry = {
|
||
|
|
type: 'message',
|
||
|
|
id,
|
||
|
|
parentId,
|
||
|
|
timestamp: time,
|
||
|
|
message: {
|
||
|
|
role: 'user',
|
||
|
|
content: `entry:${id}`,
|
||
|
|
timestamp: Date.parse(time),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
totals.messages++;
|
||
|
|
} else if (roll < 0.84) {
|
||
|
|
entry = {
|
||
|
|
type: 'branch_summary',
|
||
|
|
id,
|
||
|
|
parentId,
|
||
|
|
timestamp: time,
|
||
|
|
fromId: parentId ?? id,
|
||
|
|
summary: `branch:${id}`,
|
||
|
|
};
|
||
|
|
totals.branchSummaries++;
|
||
|
|
} else if (roll < 0.96) {
|
||
|
|
const retained = random() < 0.5;
|
||
|
|
entry = {
|
||
|
|
type: 'compaction',
|
||
|
|
id,
|
||
|
|
parentId,
|
||
|
|
timestamp: time,
|
||
|
|
summary: `compaction:${id}`,
|
||
|
|
tokensBefore: integer(random, 0, 100_000),
|
||
|
|
...(retained
|
||
|
|
? {
|
||
|
|
retainedTail: [{
|
||
|
|
role: 'user',
|
||
|
|
content: `tail:${id}`,
|
||
|
|
timestamp: Date.parse(time),
|
||
|
|
}],
|
||
|
|
}
|
||
|
|
: { firstKeptEntryId: parentId }),
|
||
|
|
};
|
||
|
|
totals.compactions++;
|
||
|
|
if (retained) totals.retainedTailCompactions++;
|
||
|
|
} else {
|
||
|
|
entry = {
|
||
|
|
type: 'model_change',
|
||
|
|
id,
|
||
|
|
parentId,
|
||
|
|
timestamp: time,
|
||
|
|
provider: 'probe',
|
||
|
|
modelId: 'probe',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
headId = id;
|
||
|
|
}
|
||
|
|
|
||
|
|
entries.push(entry);
|
||
|
|
ids.push(id);
|
||
|
|
totals.entries++;
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
header: {
|
||
|
|
type: 'session',
|
||
|
|
version: 3,
|
||
|
|
id: `differential-${caseIndex}`,
|
||
|
|
timestamp: timestamp(caseIndex, 0),
|
||
|
|
cwd: `/tmp/obelisk-pi-differential/project-${caseIndex}`,
|
||
|
|
},
|
||
|
|
entries,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
test('fixed-seed randomized differential matches vendored Pi 0.83.0 context oracles', () => {
|
||
|
|
assert.equal(PI_CONTEXT_ORACLE_VERSION, '0.83.0');
|
||
|
|
const random = randomGenerator(SEED);
|
||
|
|
const root = mkdtempSync(join(tmpdir(), 'obelisk-pi-randomized-differential-'));
|
||
|
|
const generated = [];
|
||
|
|
const totals = {
|
||
|
|
cases: CASES,
|
||
|
|
entries: 0,
|
||
|
|
messages: 0,
|
||
|
|
leaves: 0,
|
||
|
|
nullLeaves: 0,
|
||
|
|
compactions: 0,
|
||
|
|
retainedTailCompactions: 0,
|
||
|
|
branchSummaries: 0,
|
||
|
|
orphanParents: 0,
|
||
|
|
retainedCheckpointPathCases: 0,
|
||
|
|
mixedCheckpointLegacyCases: 0,
|
||
|
|
};
|
||
|
|
|
||
|
|
for (let caseIndex = 0; caseIndex < CASES; caseIndex++) {
|
||
|
|
const generatedCase = createCase(random, caseIndex, totals);
|
||
|
|
generated.push(generatedCase);
|
||
|
|
const path = join(root, `case-${caseIndex}`, 'session.jsonl');
|
||
|
|
mkdirSync(dirname(path), { recursive: true });
|
||
|
|
writeFileSync(
|
||
|
|
path,
|
||
|
|
`${[generatedCase.header, ...generatedCase.entries].map(record => JSON.stringify(record)).join('\n')}\n`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const provider = createPiProvider({ rootDir: root });
|
||
|
|
const units = provider.discover({ lastCursor: () => null });
|
||
|
|
assert.equal(units.length, CASES);
|
||
|
|
for (const unit of units) {
|
||
|
|
const caseIndex = Number(/case-(\d+)/.exec(unit.key)?.[1]);
|
||
|
|
const entries = generated[caseIndex].entries;
|
||
|
|
const leafId = pi083LeafId(entries);
|
||
|
|
const fullPath = buildCodingAgentSessionPath(entries, leafId);
|
||
|
|
const codingAgentContext = buildCodingAgentContextEntries(entries, leafId);
|
||
|
|
const agentCoreContext = defaultAgentCoreContextEntryTransform(
|
||
|
|
buildAgentCoreSessionPath(entries, leafId),
|
||
|
|
);
|
||
|
|
// Pi 0.83's CLI owns legacy firstKeptEntryId semantics. retainedTail is the
|
||
|
|
// agent-core storage checkpoint format, so mixed/new chains use its bounded
|
||
|
|
// storage path before the context transform.
|
||
|
|
let checkpointIndex = -1;
|
||
|
|
for (let index = 0; index < fullPath.length; index++) {
|
||
|
|
const entry = fullPath[index];
|
||
|
|
if (entry.type === 'compaction' && entry.retainedTail !== undefined) {
|
||
|
|
checkpointIndex = index;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (checkpointIndex >= 0) {
|
||
|
|
totals.retainedCheckpointPathCases++;
|
||
|
|
if (fullPath.slice(checkpointIndex + 1).some(entry => (
|
||
|
|
entry.type === 'compaction' && entry.retainedTail === undefined
|
||
|
|
))) {
|
||
|
|
totals.mixedCheckpointLegacyCases++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const expectedContext = checkpointIndex >= 0
|
||
|
|
? agentCoreContext
|
||
|
|
: codingAgentContext;
|
||
|
|
assert.deepEqual(
|
||
|
|
actualEvidence(drain(provider.parse(unit, null))),
|
||
|
|
projectCanonicalEvidence(entries, expectedContext),
|
||
|
|
`seed 0x${SEED.toString(16)}, case ${caseIndex}`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
assert.deepEqual(totals, {
|
||
|
|
cases: 512,
|
||
|
|
entries: 18124,
|
||
|
|
messages: 11586,
|
||
|
|
leaves: 1457,
|
||
|
|
nullLeaves: 167,
|
||
|
|
compactions: 2230,
|
||
|
|
retainedTailCompactions: 1137,
|
||
|
|
branchSummaries: 2191,
|
||
|
|
orphanParents: 1060,
|
||
|
|
retainedCheckpointPathCases: 175,
|
||
|
|
mixedCheckpointLegacyCases: 32,
|
||
|
|
});
|
||
|
|
});
|