mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-25 00:14:40 +00:00
ci(upgrade-path): guard durable history across a bump on an extant source
The corpus had no durable SQLite provider, so the one never-lose direction it could not reach was the one this change breaks: a source that keeps its file while the provider prunes rows out of it. gen-corpus now writes copilot's OTel span store at its real default path (80 spans over 40 days), and the run caches it with the published CLI, deletes half the spans from a DB that stays present, valid and populated, then upgrades through the parse-version bump. Copilot cost must not shrink across that, and must still not shrink on the warm run that reads the rewritten cache — a carry-forward that only survived in memory fails the second check. Verified to bite: with the carry-forward reverted the step reports LOST $0.361746 (10.0%) and fails. Also: the expected daily-cache filename moves to v21.
This commit is contained in:
parent
fb1bde94b7
commit
aeaa11f73f
2 changed files with 147 additions and 0 deletions
|
|
@ -363,6 +363,79 @@ function genCursor() {
|
|||
return existsSync(dbPath + '-wal') ? 3 : 2
|
||||
}
|
||||
|
||||
// ── copilot ─────────────────────────────────────────────────────────────────
|
||||
// The OTel span store VS Code's Copilot Chat writes. It is here for one thing
|
||||
// the transcript providers cannot exercise: a DURABLE source that keeps its
|
||||
// file forever while the extension prunes rows out of it. run.mjs caches this
|
||||
// with the published CLI, prunes conversations, then upgrades — the cache is
|
||||
// the only remaining record of the pruned days, and a parse-version bump must
|
||||
// not take them with it. Platform-specific default path, no override var,
|
||||
// matching the rest of this corpus.
|
||||
|
||||
const COPILOT_MODELS = ['gpt-4.1', 'claude-sonnet-4-5']
|
||||
|
||||
function copilotOtelDbPath() {
|
||||
if (process.platform === 'darwin') {
|
||||
return join(HOME, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'github.copilot-chat', 'agent-traces.db')
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
const appdata = process.env['APPDATA'] ?? join(HOME, 'AppData', 'Roaming')
|
||||
return join(appdata, 'Code', 'User', 'globalStorage', 'github.copilot-chat', 'agent-traces.db')
|
||||
}
|
||||
return join(HOME, '.config', 'Code', 'User', 'globalStorage', 'github.copilot-chat', 'agent-traces.db')
|
||||
}
|
||||
|
||||
function genCopilot() {
|
||||
const { DatabaseSync } = require_('node:sqlite')
|
||||
const dbPath = copilotOtelDbPath()
|
||||
mkdirSync(join(dbPath, '..'), { recursive: true })
|
||||
for (const suffix of ['', '-wal', '-shm']) rmSync(dbPath + suffix, { force: true })
|
||||
|
||||
const db = new DatabaseSync(dbPath)
|
||||
db.exec(`
|
||||
CREATE TABLE spans (
|
||||
span_id TEXT PRIMARY KEY NOT NULL,
|
||||
trace_id TEXT NOT NULL,
|
||||
operation_name TEXT,
|
||||
start_time_ms INTEGER NOT NULL DEFAULT 0,
|
||||
response_model TEXT
|
||||
);
|
||||
CREATE TABLE span_attributes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
span_id TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT
|
||||
);
|
||||
`)
|
||||
const insSpan = db.prepare('INSERT INTO spans (span_id, trace_id, operation_name, start_time_ms, response_model) VALUES (?, ?, ?, ?, ?)')
|
||||
const insAttr = db.prepare('INSERT INTO span_attributes (span_id, key, value) VALUES (?, ?, ?)')
|
||||
|
||||
// Two spans a day over the last 40 days, so pruning by day leaves a valid,
|
||||
// still-populated DB rather than an empty one — "extant but pruned" is the
|
||||
// shape that matters, and an empty DB would also pass a naive carry-forward.
|
||||
let n = 0
|
||||
for (let day = SPAN_DAYS - 40; day < SPAN_DAYS; day++) {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const spanId = `cp-span-${String(n).padStart(4, '0')}`
|
||||
const model = COPILOT_MODELS[n % COPILOT_MODELS.length]
|
||||
const ts = at(day, 9 + i * 4, between(0, 59))
|
||||
insSpan.run(spanId, `cp-trace-${day}-${i}`, 'chat', ts.getTime(), model)
|
||||
const attrs = {
|
||||
'gen_ai.conversation.id': `cp-conv-${day}-${i}`,
|
||||
'gen_ai.response.model': model,
|
||||
'gen_ai.usage.input_tokens': between(2000, 20000),
|
||||
'gen_ai.usage.output_tokens': between(200, 3000),
|
||||
'gen_ai.usage.cache_read.input_tokens': between(0, 8000),
|
||||
'gen_ai.usage.cache_creation.input_tokens': 0,
|
||||
}
|
||||
for (const [k, v] of Object.entries(attrs)) insAttr.run(spanId, k, String(v))
|
||||
n++
|
||||
}
|
||||
}
|
||||
db.close()
|
||||
return n
|
||||
}
|
||||
|
||||
// ── run ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
const counts = {
|
||||
|
|
@ -373,5 +446,6 @@ const counts = {
|
|||
dsh: genDsh(),
|
||||
grok: genGrok(),
|
||||
cursor: genCursor(),
|
||||
copilot: genCopilot(),
|
||||
}
|
||||
console.log(JSON.stringify(counts))
|
||||
|
|
|
|||
|
|
@ -414,6 +414,79 @@ else {
|
|||
}
|
||||
}
|
||||
|
||||
// ── 9. durable history across a bump on an extant, pruned source ─────────────
|
||||
//
|
||||
// The other never-lose direction. A durable SQLite source keeps its file
|
||||
// forever while the provider prunes rows out of it, so "the source path is
|
||||
// gone" is not the test for whether the cache is the last remaining record.
|
||||
// A parse-version bump rebuilds the provider section, and if it drops entries
|
||||
// whose file still exists it deletes exactly the history nothing can rebuild.
|
||||
// This build bumps PROVIDER_PARSE_VERSIONS.copilot, so it takes that path for
|
||||
// real. (#946 review.)
|
||||
|
||||
step('durable history across a bump on an extant, pruned source')
|
||||
const prunedCache = join(CACHES, 'pruned')
|
||||
mkdirSync(prunedCache, { recursive: true })
|
||||
|
||||
const copilotDb = process.platform === 'darwin'
|
||||
? join(HOME, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'github.copilot-chat', 'agent-traces.db')
|
||||
: process.platform === 'win32'
|
||||
? join(process.env['APPDATA'] ?? join(HOME, 'AppData', 'Roaming'), 'Code', 'User', 'globalStorage', 'github.copilot-chat', 'agent-traces.db')
|
||||
: join(HOME, '.config', 'Code', 'User', 'globalStorage', 'github.copilot-chat', 'agent-traces.db')
|
||||
|
||||
// providerDetails carries the internal provider id; the `providers` map is
|
||||
// keyed by lowercased DISPLAY name, which is not stable to match on.
|
||||
const copilotOf = payload =>
|
||||
(payload.menubar?.current?.providerDetails ?? []).find(d => d.id === 'copilot')
|
||||
const prunedBase = capture(oldBin, prunedCache, join(PAYLOADS, 'pruned-baseline'))
|
||||
const beforeSlice = copilotOf(prunedBase)
|
||||
|
||||
if (!beforeSlice || !(beforeSlice.cost > 0)) {
|
||||
skip(`${OLD_VERSION} reported no copilot usage to prune (nothing to protect)`)
|
||||
} else {
|
||||
ok(`baseline copilot: $${beforeSlice.cost.toFixed(6)}`)
|
||||
|
||||
// Prune HALF the conversations, leaving the DB present, valid and still
|
||||
// populated: the surviving rows are what the bump re-parses, the pruned ones
|
||||
// exist only in the cache from here on.
|
||||
let pruned = 0
|
||||
try {
|
||||
const { DatabaseSync } = await import('node:sqlite')
|
||||
const db = new DatabaseSync(copilotDb)
|
||||
const ids = db.prepare('SELECT span_id FROM spans ORDER BY span_id').all().map(r => r.span_id)
|
||||
const doomed = ids.filter((_, i) => i % 2 === 0)
|
||||
const delSpan = db.prepare('DELETE FROM spans WHERE span_id = ?')
|
||||
const delAttr = db.prepare('DELETE FROM span_attributes WHERE span_id = ?')
|
||||
for (const id of doomed) { delAttr.run(id); delSpan.run(id) }
|
||||
pruned = doomed.length
|
||||
const left = db.prepare('SELECT COUNT(*) AS n FROM spans').get().n
|
||||
db.close()
|
||||
ok(`pruned ${pruned} of ${ids.length} spans; ${left} remain in a DB that still exists`)
|
||||
} catch (err) {
|
||||
skip(`could not prune the copilot store (${err.message})`)
|
||||
}
|
||||
|
||||
if (pruned > 0) {
|
||||
const prunedUp = capture(newBin, prunedCache, join(PAYLOADS, 'pruned-upgraded'))
|
||||
const afterSlice = copilotOf(prunedUp)
|
||||
if (!afterSlice) {
|
||||
fail(`the copilot slice is gone entirely after the bump; baseline had $${beforeSlice.cost.toFixed(6)}`)
|
||||
} else {
|
||||
// The bump may legitimately ADD (this build reads per-request rows the
|
||||
// baseline never had). It must never subtract.
|
||||
const costOk = afterSlice.cost >= beforeSlice.cost - 1e-9
|
||||
const lost = costOk ? '' : ` — LOST $${(beforeSlice.cost - afterSlice.cost).toFixed(6)} (${(100 * (beforeSlice.cost - afterSlice.cost) / beforeSlice.cost).toFixed(1)}%)`
|
||||
check(costOk, `copilot cost across the bump: $${beforeSlice.cost.toFixed(6)} -> $${afterSlice.cost.toFixed(6)}${lost}`)
|
||||
|
||||
// And persisted, not merely served: a second run reads the cache the
|
||||
// bump rewrote, so a carry-forward that only survived in memory fails.
|
||||
const warm = copilotOf(capture(newBin, prunedCache, join(PAYLOADS, 'pruned-warm')))
|
||||
check(!!warm && warm.cost >= beforeSlice.cost - 1e-9,
|
||||
`and again from the rewritten cache: $${(warm?.cost ?? 0).toFixed(6)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── done ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
console.log(`\n${failures ? `FAILED: ${failures} check(s)` : 'PASSED'}${skipped ? ` (${skipped} skipped)` : ''}`)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue