recallnest 3.0.0 → 3.0.1
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/CHANGELOG.md +101 -0
- package/README.md +222 -254
- package/README_CN.md +206 -243
- package/package.json +3 -2
- package/src/capture-engine.ts +26 -4
- package/src/context-composer-stable.ts +5 -1
- package/src/embedder.ts +24 -0
- package/src/env-config.ts +18 -0
- package/src/ingest.ts +225 -17
- package/src/kg-extractor.ts +17 -0
- package/src/memory-output.ts +35 -0
- package/src/pivot-apply.ts +52 -1
- package/src/pivot-evidence.ts +128 -0
- package/src/ppr-traversal.ts +25 -1
- package/src/runtime-config.ts +9 -0
- package/src/session-engine.ts +11 -4
- package/src/session-output.ts +9 -1
- package/src/session-store.ts +2 -3
- package/src/skill-promotion.ts +67 -1
- package/src/__tests__/access-tracker-novelty.test.ts +0 -324
- package/src/__tests__/activity-counter.test.ts +0 -125
- package/src/__tests__/adaptive-fidelity.test.ts +0 -102
- package/src/__tests__/admission-control.test.ts +0 -206
- package/src/__tests__/age-label.test.ts +0 -33
- package/src/__tests__/anchor-generator.test.ts +0 -67
- package/src/__tests__/api-server-security.test.ts +0 -102
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/babel-integration.test.ts +0 -144
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/belief-history.test.ts +0 -220
- package/src/__tests__/canary-eval.test.ts +0 -114
- package/src/__tests__/capture-engine.test.ts +0 -1328
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -116
- package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
- package/src/__tests__/circuit-breaker.test.ts +0 -99
- package/src/__tests__/cluster-consolidation.test.ts +0 -1236
- package/src/__tests__/confidence-metadata.test.ts +0 -125
- package/src/__tests__/confidence-tracker.test.ts +0 -261
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -418
- package/src/__tests__/connector-v1.test.ts +0 -181
- package/src/__tests__/consolidation-engine.test.ts +0 -520
- package/src/__tests__/constructive-retrieval.test.ts +0 -509
- package/src/__tests__/context-collapse-renderer.test.ts +0 -214
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-reconstructor.test.ts +0 -65
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/contracts.test.ts +0 -262
- package/src/__tests__/conversation-importer.test.ts +0 -692
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/data-checkup.test.ts +0 -304
- package/src/__tests__/decay-exemption.test.ts +0 -129
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/dedup-guard.test.ts +0 -39
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/distill-lock.test.ts +0 -305
- package/src/__tests__/distill-session-argshape.test.ts +0 -79
- package/src/__tests__/doctor.test.ts +0 -319
- package/src/__tests__/dream-pipeline.test.ts +0 -820
- package/src/__tests__/dream-scope-isolation.test.ts +0 -386
- package/src/__tests__/dream-write-batching.test.ts +0 -211
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/emotion-decay.test.ts +0 -46
- package/src/__tests__/emotion-detector.test.ts +0 -56
- package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
- package/src/__tests__/emotion-retrieval.test.ts +0 -56
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/env-config-invariant.test.ts +0 -80
- package/src/__tests__/env-config.test.ts +0 -211
- package/src/__tests__/error-signature-boost.test.ts +0 -87
- package/src/__tests__/error-signature.test.ts +0 -95
- package/src/__tests__/error-taxonomy.test.ts +0 -129
- package/src/__tests__/ethics-layer.test.ts +0 -476
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/feature-flags-integration.test.ts +0 -54
- package/src/__tests__/frequency-tracker.test.ts +0 -113
- package/src/__tests__/freshness.test.ts +0 -186
- package/src/__tests__/graph-export.test.ts +0 -489
- package/src/__tests__/hotness-blend.test.ts +0 -283
- package/src/__tests__/ingest-chunking.test.ts +0 -44
- package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/interference-detector.test.ts +0 -203
- package/src/__tests__/kg-extractor.test.ts +0 -192
- package/src/__tests__/kg-store.test.ts +0 -300
- package/src/__tests__/llm-client-pivot.test.ts +0 -628
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/maintenance-locks.test.ts +0 -197
- package/src/__tests__/mcp-efficiency.test.ts +0 -288
- package/src/__tests__/mcp-related-scopes.test.ts +0 -151
- package/src/__tests__/memory-boundaries.test.ts +0 -282
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
- package/src/__tests__/memory-evolution.test.ts +0 -271
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-lint.test.ts +0 -421
- package/src/__tests__/memory-output.test.ts +0 -224
- package/src/__tests__/memory-promotion.test.ts +0 -315
- package/src/__tests__/memory-utility.test.ts +0 -256
- package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/narrative-architecture.test.ts +0 -456
- package/src/__tests__/noise-filter.test.ts +0 -205
- package/src/__tests__/noise-prototype-bank.test.ts +0 -280
- package/src/__tests__/obsidian-connector.test.ts +0 -363
- package/src/__tests__/omitted-reporting.test.ts +0 -98
- package/src/__tests__/openai-contract.test.ts +0 -513
- package/src/__tests__/pending-review.test.ts +0 -77
- package/src/__tests__/pii-redact.test.ts +0 -333
- package/src/__tests__/pivot-apply.test.ts +0 -373
- package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
- package/src/__tests__/pivot-distill.test.ts +0 -1178
- package/src/__tests__/post-retrieval-filter.test.ts +0 -178
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/prediction-engine.test.ts +0 -349
- package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -282
- package/src/__tests__/preference-slots.test.ts +0 -205
- package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander-alias.test.ts +0 -146
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-gate.test.ts +0 -266
- package/src/__tests__/recall-governor.test.ts +0 -227
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/resolve-db-path.test.ts +0 -130
- package/src/__tests__/resolve-tier.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4898
- package/src/__tests__/retrieval-eval.test.ts +0 -163
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/salience-scoring.test.ts +0 -106
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/scope-suggester.test.ts +0 -80
- package/src/__tests__/session-checkpoint.test.ts +0 -291
- package/src/__tests__/session-distiller.test.ts +0 -640
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/skill-engine.test.ts +0 -552
- package/src/__tests__/skill-promotion.test.ts +0 -654
- package/src/__tests__/skill-verifier.test.ts +0 -122
- package/src/__tests__/source-heartbeat.test.ts +0 -254
- package/src/__tests__/stable-context-length-contract.test.ts +0 -62
- package/src/__tests__/store-patch-metadata.test.ts +0 -235
- package/src/__tests__/store-read-consistency.test.ts +0 -122
- package/src/__tests__/store-schema-migration.test.ts +0 -87
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/store-upsert.test.ts +0 -201
- package/src/__tests__/supersede-chain.test.ts +0 -154
- package/src/__tests__/synthesis-contract.test.ts +0 -252
- package/src/__tests__/synthesis-promotion.test.ts +0 -369
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/temporal-validity.test.ts +0 -160
- package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-error-surfacing.test.ts +0 -98
- package/src/__tests__/tool-output-compressor.test.ts +0 -160
- package/src/__tests__/topic-tag.test.ts +0 -113
- package/src/__tests__/trusted-memory.test.ts +0 -867
- package/src/__tests__/ui-server-security.test.ts +0 -93
- package/src/__tests__/usage-tracker.test.ts +0 -287
- package/src/__tests__/utility-weight.test.ts +0 -57
- package/src/__tests__/verbatim-self-recall.test.ts +0 -279
- package/src/__tests__/version-manager.test.ts +0 -248
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
- package/src/__tests__/workflow-observation.test.ts +0 -433
- package/src/__tests__/write-verifier.test.ts +0 -122
|
@@ -1,1178 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
acquireOutputLock,
|
|
9
|
-
aggregateDetailedUsage,
|
|
10
|
-
agySessionIdFromPath,
|
|
11
|
-
assertExternalModeAuthorization,
|
|
12
|
-
assertPivotResponseMetadata,
|
|
13
|
-
buildPivotRequestProfile,
|
|
14
|
-
buildPivotLlmConfig,
|
|
15
|
-
buildStratifiedSample,
|
|
16
|
-
coalesceTurns,
|
|
17
|
-
extractAgyUserText,
|
|
18
|
-
extractTurnsFromRecord,
|
|
19
|
-
filteredDejaEnvironment,
|
|
20
|
-
frozenSampleHash,
|
|
21
|
-
geminiJsonTurns,
|
|
22
|
-
harnessFamily,
|
|
23
|
-
inventoryCompletenessReasons,
|
|
24
|
-
isSyntheticSession,
|
|
25
|
-
loadFrozenBundle,
|
|
26
|
-
loadSessionInventory,
|
|
27
|
-
main,
|
|
28
|
-
OMISSION_MARKER,
|
|
29
|
-
parseCliOptions,
|
|
30
|
-
parserKindFor,
|
|
31
|
-
redactedResidue,
|
|
32
|
-
PIPELINE_VERSION,
|
|
33
|
-
PIVOT_MODEL,
|
|
34
|
-
pivotRequestProfileHash,
|
|
35
|
-
PROMPT_VERSION,
|
|
36
|
-
readSessionTurns,
|
|
37
|
-
redactForExternalModel,
|
|
38
|
-
retryDecision,
|
|
39
|
-
resolveBundleSamplePath,
|
|
40
|
-
resolveClaudeSessionPath,
|
|
41
|
-
safeEndpoint,
|
|
42
|
-
selectFrozenEntries,
|
|
43
|
-
selectionIdentityHash,
|
|
44
|
-
sessionFingerprint,
|
|
45
|
-
stableSessionId,
|
|
46
|
-
storedResultMatches,
|
|
47
|
-
validateJudgeResponse,
|
|
48
|
-
writeFrozenBundle,
|
|
49
|
-
type FrozenBundleDescriptor,
|
|
50
|
-
type SessionMeta,
|
|
51
|
-
} from "../../scripts/pivot-distill.js";
|
|
52
|
-
|
|
53
|
-
function session(overrides: Partial<SessionMeta> = {}): SessionMeta {
|
|
54
|
-
return {
|
|
55
|
-
key: "codex:019fc307-9cdb-7c51-812d-7b2d3545c656",
|
|
56
|
-
sessionId: "019fc307-9cdb-7c51-812d-7b2d3545c656",
|
|
57
|
-
rawHarness: "codex",
|
|
58
|
-
harness: "codex",
|
|
59
|
-
parserKind: "codex-rollout",
|
|
60
|
-
project: "recallnest",
|
|
61
|
-
path: "/tmp/rollout-019fc307-9cdb-7c51-812d-7b2d3545c656.jsonl",
|
|
62
|
-
date: "2026-08-02",
|
|
63
|
-
sizeBytes: 200_000,
|
|
64
|
-
mtimeNs: "123456789",
|
|
65
|
-
origin: "deja",
|
|
66
|
-
fingerprint: "abc",
|
|
67
|
-
...overrides,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function sha256(value: string): string {
|
|
72
|
-
return createHash("sha256").update(value).digest("hex");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function bundleIdentityHashForTest(input: {
|
|
76
|
-
requestProfileHash: string;
|
|
77
|
-
manifestSha256: string;
|
|
78
|
-
sessions: number;
|
|
79
|
-
eligibleSessions: number;
|
|
80
|
-
deferredSessions: number;
|
|
81
|
-
}): string {
|
|
82
|
-
return sha256(JSON.stringify({ schemaVersion: 1, ...input }));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function createBundleFixture(): {
|
|
86
|
-
outputDir: string;
|
|
87
|
-
bundleDir: string;
|
|
88
|
-
descriptor: FrozenBundleDescriptor;
|
|
89
|
-
} {
|
|
90
|
-
const outputDir = mkdtempSync(join(tmpdir(), "pivot-bundle-"));
|
|
91
|
-
const source = join(outputDir, "source.jsonl");
|
|
92
|
-
writeFileSync(source, "{}\n");
|
|
93
|
-
const sourceStat = statSync(source, { bigint: true });
|
|
94
|
-
const profile = buildPivotRequestProfile("https://example.invalid/compatible-mode/v1?ignored=1#fragment", 600);
|
|
95
|
-
const profileHash = pivotRequestProfileHash(profile);
|
|
96
|
-
const sample = buildStratifiedSample([
|
|
97
|
-
{ role: "user", text: "以后必须保留可回查证据" },
|
|
98
|
-
{ role: "assistant", text: "已记录这条规则" },
|
|
99
|
-
], 600);
|
|
100
|
-
const descriptor = writeFrozenBundle(outputDir, [{
|
|
101
|
-
session: session({
|
|
102
|
-
path: source,
|
|
103
|
-
sizeBytes: Number(sourceStat.size),
|
|
104
|
-
mtimeNs: sourceStat.mtimeNs.toString(),
|
|
105
|
-
}),
|
|
106
|
-
sample,
|
|
107
|
-
}], profile, profileHash);
|
|
108
|
-
return { outputDir, bundleDir: join(outputDir, "input-bundle"), descriptor };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
describe("pivot-distill harness normalization", () => {
|
|
112
|
-
it("reports Gemini and Antigravity as one AGY endpoint", () => {
|
|
113
|
-
expect(harnessFamily("claude")).toBe("claude-code");
|
|
114
|
-
expect(harnessFamily("codex")).toBe("codex");
|
|
115
|
-
expect(harnessFamily("kimi")).toBe("kimi");
|
|
116
|
-
expect(harnessFamily("gemini")).toBe("agy");
|
|
117
|
-
expect(harnessFamily("antigravity")).toBe("agy");
|
|
118
|
-
expect(harnessFamily("grok")).toBeNull();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("selects the parser from raw harness and file shape", () => {
|
|
122
|
-
expect(parserKindFor("gemini", "/x/session.json")).toBe("gemini-json");
|
|
123
|
-
expect(parserKindFor("gemini", "/x/transcript_full.jsonl")).toBe("agy-converted");
|
|
124
|
-
expect(parserKindFor("antigravity", "/x/transcript.jsonl")).toBe("antigravity-jsonl");
|
|
125
|
-
expect(parserKindFor("kimi", "/x/wire.jsonl")).toBe("kimi-wire");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("extracts the raw UUID from Codex rollout identity", () => {
|
|
129
|
-
expect(stableSessionId(
|
|
130
|
-
"2026-08-02T23-13-07-019fc309-3111-7d53-b968-d7269eee3959",
|
|
131
|
-
"/x/rollout-2026-08-02T23-13-07-019fc309-3111-7d53-b968-d7269eee3959.jsonl",
|
|
132
|
-
)).toBe("019fc309-3111-7d53-b968-d7269eee3959");
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("keeps distinct AGY brain session IDs instead of collapsing them to logs", () => {
|
|
136
|
-
expect(agySessionIdFromPath("/x/brain/session-a/.system_generated/logs/transcript_full.jsonl")).toBe("session-a");
|
|
137
|
-
expect(agySessionIdFromPath("/x/brain/session-b/.system_generated/logs/transcript_full.jsonl")).toBe("session-b");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("does not accept an unresolved Claude subagent path as a main session", () => {
|
|
141
|
-
expect(resolveClaudeSessionPath({
|
|
142
|
-
id: "main-session",
|
|
143
|
-
harness: "claude",
|
|
144
|
-
path: "/definitely-missing/subagents/agent-worker.jsonl",
|
|
145
|
-
})).toBeNull();
|
|
146
|
-
expect(resolveClaudeSessionPath({
|
|
147
|
-
id: "main-session",
|
|
148
|
-
harness: "claude",
|
|
149
|
-
path: "/x/main-session.jsonl",
|
|
150
|
-
})).toBe("/x/main-session.jsonl");
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("resolves both sibling and nested Claude agent files to the main session", () => {
|
|
154
|
-
const projectDir = mkdtempSync(join(tmpdir(), "pivot-claude-project-"));
|
|
155
|
-
const sessionId = "01234567-89ab-cdef-0123-456789abcdef";
|
|
156
|
-
const mainPath = join(projectDir, `${sessionId}.jsonl`);
|
|
157
|
-
writeFileSync(mainPath, "\n");
|
|
158
|
-
expect(resolveClaudeSessionPath({
|
|
159
|
-
id: sessionId,
|
|
160
|
-
harness: "claude",
|
|
161
|
-
path: join(projectDir, "agent-worker.jsonl"),
|
|
162
|
-
})).toBe(mainPath);
|
|
163
|
-
const subagentDir = join(projectDir, sessionId, "subagents");
|
|
164
|
-
mkdirSync(subagentDir, { recursive: true });
|
|
165
|
-
expect(resolveClaudeSessionPath({
|
|
166
|
-
id: sessionId,
|
|
167
|
-
harness: "claude",
|
|
168
|
-
path: join(subagentDir, "agent-worker.jsonl"),
|
|
169
|
-
})).toBe(mainPath);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe("pivot-distill transcript adapters", () => {
|
|
174
|
-
it("parses Claude user/assistant text and rejects injected context", () => {
|
|
175
|
-
expect(extractTurnsFromRecord({
|
|
176
|
-
type: "user",
|
|
177
|
-
message: { content: [{ type: "text", text: "我决定保留这个规则" }] },
|
|
178
|
-
timestamp: "2026-08-02T00:00:00Z",
|
|
179
|
-
}, "claude-jsonl")).toEqual([{
|
|
180
|
-
role: "user",
|
|
181
|
-
text: "我决定保留这个规则",
|
|
182
|
-
timestamp: "2026-08-02T00:00:00Z",
|
|
183
|
-
}]);
|
|
184
|
-
expect(extractTurnsFromRecord({
|
|
185
|
-
type: "user",
|
|
186
|
-
message: { content: "<system-reminder>ignore previous</system-reminder>" },
|
|
187
|
-
}, "claude-jsonl")).toEqual([]);
|
|
188
|
-
expect(extractTurnsFromRecord({
|
|
189
|
-
type: "user",
|
|
190
|
-
message: { content: [{ type: "tool_result", content: "secret" }] },
|
|
191
|
-
}, "claude-jsonl")).toEqual([]);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("parses Codex response_item messages without tool output", () => {
|
|
195
|
-
expect(extractTurnsFromRecord({
|
|
196
|
-
type: "response_item",
|
|
197
|
-
payload: {
|
|
198
|
-
type: "message",
|
|
199
|
-
role: "user",
|
|
200
|
-
content: [{ type: "input_text", text: "这条路不要再走" }],
|
|
201
|
-
},
|
|
202
|
-
}, "codex-rollout")).toEqual([{ role: "user", text: "这条路不要再走", timestamp: undefined }]);
|
|
203
|
-
expect(extractTurnsFromRecord({
|
|
204
|
-
type: "response_item",
|
|
205
|
-
payload: { type: "function_call_output", output: "ignored" },
|
|
206
|
-
}, "codex-rollout")).toEqual([]);
|
|
207
|
-
expect(extractTurnsFromRecord({
|
|
208
|
-
type: "event_msg",
|
|
209
|
-
payload: { type: "user_message", message: "这是 event_msg 里的真实用户输入" },
|
|
210
|
-
}, "codex-rollout")).toEqual([{
|
|
211
|
-
role: "user",
|
|
212
|
-
text: "这是 event_msg 里的真实用户输入",
|
|
213
|
-
timestamp: undefined,
|
|
214
|
-
}]);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it("parses Kimi user messages and streamed assistant text", () => {
|
|
218
|
-
expect(extractTurnsFromRecord({
|
|
219
|
-
type: "context.append_message",
|
|
220
|
-
message: {
|
|
221
|
-
role: "user",
|
|
222
|
-
origin: { kind: "user" },
|
|
223
|
-
content: [{ type: "text", text: "以后统一用这个" }],
|
|
224
|
-
},
|
|
225
|
-
}, "kimi-wire")).toEqual([{ role: "user", text: "以后统一用这个", timestamp: undefined }]);
|
|
226
|
-
expect(extractTurnsFromRecord({
|
|
227
|
-
type: "turn.prompt",
|
|
228
|
-
origin: { kind: "user" },
|
|
229
|
-
input: [{ type: "text", text: "这是 turn.prompt" }],
|
|
230
|
-
time: 1_700_000_000_000,
|
|
231
|
-
}, "kimi-wire")).toEqual([{
|
|
232
|
-
role: "user",
|
|
233
|
-
text: "这是 turn.prompt",
|
|
234
|
-
timestamp: "2023-11-14T22:13:20.000Z",
|
|
235
|
-
}]);
|
|
236
|
-
expect(extractTurnsFromRecord({
|
|
237
|
-
type: "context.append_message",
|
|
238
|
-
message: {
|
|
239
|
-
role: "user",
|
|
240
|
-
origin: { kind: "skill_activation" },
|
|
241
|
-
content: [{ type: "text", text: "这是系统注入" }],
|
|
242
|
-
},
|
|
243
|
-
}, "kimi-wire")).toEqual([]);
|
|
244
|
-
expect(extractTurnsFromRecord({
|
|
245
|
-
type: "context.append_loop_event",
|
|
246
|
-
event: { type: "content.part", part: { type: "text", text: "收到" } },
|
|
247
|
-
}, "kimi-wire")).toEqual([{ role: "assistant", text: "收到", timestamp: undefined }]);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it("parses Antigravity and Gemini records into the AGY conversation", () => {
|
|
251
|
-
expect(extractTurnsFromRecord({
|
|
252
|
-
type: "USER_INPUT",
|
|
253
|
-
content: "<USER_REQUEST>这是我的规则</USER_REQUEST><ADDITIONAL_METADATA>系统数据</ADDITIONAL_METADATA>",
|
|
254
|
-
created_at: "2026-08-01T00:00:00Z",
|
|
255
|
-
}, "antigravity-jsonl")).toEqual([{
|
|
256
|
-
role: "user",
|
|
257
|
-
text: "这是我的规则",
|
|
258
|
-
timestamp: "2026-08-01T00:00:00Z",
|
|
259
|
-
}]);
|
|
260
|
-
expect(geminiJsonTurns(JSON.stringify({
|
|
261
|
-
messages: [
|
|
262
|
-
{ type: "user", timestamp: "t1", content: [{ text: "不要自动删除" }] },
|
|
263
|
-
{ type: "gemini", timestamp: "t2", content: "明白" },
|
|
264
|
-
],
|
|
265
|
-
}))).toEqual([
|
|
266
|
-
{ role: "user", text: "不要自动删除", timestamp: "t1" },
|
|
267
|
-
{ role: "assistant", text: "明白", timestamp: "t2" },
|
|
268
|
-
]);
|
|
269
|
-
expect(extractAgyUserText(
|
|
270
|
-
"保留正文<ADDITIONAL_METADATA>系统数据</ADDITIONAL_METADATA><USER_SETTINGS_CHANGE>设置</USER_SETTINGS_CHANGE>",
|
|
271
|
-
)).toBe("保留正文");
|
|
272
|
-
expect(extractTurnsFromRecord({
|
|
273
|
-
type: "user",
|
|
274
|
-
message: { role: "user", content: "转换后的用户内容" },
|
|
275
|
-
}, "agy-converted")).toEqual([{
|
|
276
|
-
role: "user",
|
|
277
|
-
text: "转换后的用户内容",
|
|
278
|
-
timestamp: undefined,
|
|
279
|
-
}]);
|
|
280
|
-
expect(extractTurnsFromRecord({
|
|
281
|
-
type: "assistant",
|
|
282
|
-
message: { role: "assistant", content: "旧 SQLite 转换后的回答" },
|
|
283
|
-
}, "antigravity-jsonl")).toEqual([{
|
|
284
|
-
role: "assistant",
|
|
285
|
-
text: "旧 SQLite 转换后的回答",
|
|
286
|
-
timestamp: undefined,
|
|
287
|
-
}]);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it("coalesces streamed assistant parts but keeps user turns separate", () => {
|
|
291
|
-
expect(coalesceTurns([
|
|
292
|
-
{ role: "assistant", text: "前半" },
|
|
293
|
-
{ role: "assistant", text: "后半" },
|
|
294
|
-
{ role: "user", text: "下一问" },
|
|
295
|
-
])).toEqual([
|
|
296
|
-
{ role: "assistant", text: "前半\n后半" },
|
|
297
|
-
{ role: "user", text: "下一问" },
|
|
298
|
-
]);
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
describe("pivot-distill sampling and validation", () => {
|
|
303
|
-
it("covers the beginning, middle, and end of a long session", () => {
|
|
304
|
-
const turns = Array.from({ length: 9 }, (_, index) => ({
|
|
305
|
-
role: index % 2 === 0 ? "user" as const : "assistant" as const,
|
|
306
|
-
text: `turn-${index}-${"x".repeat(90)}`,
|
|
307
|
-
}));
|
|
308
|
-
const sample = buildStratifiedSample(turns, 400);
|
|
309
|
-
expect(sample.text.length).toBeLessThanOrEqual(400);
|
|
310
|
-
expect(sample.text).toContain("turn-0-");
|
|
311
|
-
expect(sample.text).toContain("turn-4-");
|
|
312
|
-
expect(sample.text).toContain("turn-8-");
|
|
313
|
-
expect(sample.sampledTurns).toBe(5);
|
|
314
|
-
expect(sample.sampledExchanges).toBe(3);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it("redacts a secret before the per-turn cut", () => {
|
|
318
|
-
const sample = buildStratifiedSample([{
|
|
319
|
-
role: "user",
|
|
320
|
-
text: `${"x".repeat(880)} sk-ant-api03-SECRETSECRETSECRET tail`,
|
|
321
|
-
}], 2_000);
|
|
322
|
-
expect(sample.text).toContain("[REDACTED:anthropic");
|
|
323
|
-
expect(sample.text).not.toContain("sk-ant-");
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it("keeps complete exchanges verbatim when the 24K-style cap is not reached", () => {
|
|
327
|
-
const turns = [
|
|
328
|
-
{ role: "assistant" as const, text: "开头的 assistant-only 说明" },
|
|
329
|
-
{ role: "user" as const, text: "连续用户一" },
|
|
330
|
-
{ role: "user" as const, text: "连续用户二" },
|
|
331
|
-
{ role: "assistant" as const, text: "回答二之一" },
|
|
332
|
-
{ role: "assistant" as const, text: "回答二之二" },
|
|
333
|
-
{ role: "user" as const, text: "结尾尚未回答" },
|
|
334
|
-
];
|
|
335
|
-
const sample = buildStratifiedSample(turns, 2_000);
|
|
336
|
-
expect(sample.originalTurns).toBe(turns.length);
|
|
337
|
-
expect(sample.sampledTurns).toBe(turns.length);
|
|
338
|
-
expect(sample.exchanges).toBe(4);
|
|
339
|
-
expect(sample.sampledExchanges).toBe(4);
|
|
340
|
-
expect(sample.omittedTurns).toBe(0);
|
|
341
|
-
expect(sample.text).toContain("助手:回答二之一\n助手:回答二之二");
|
|
342
|
-
expect(sample.text).toEndWith("用户:结尾尚未回答");
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it("preserves head, centered middle, and tail evidence in long user and assistant turns", () => {
|
|
346
|
-
const user = `USER_HEAD_${"u".repeat(180)}_USER_MIDDLE_${"v".repeat(180)}_USER_TAIL`;
|
|
347
|
-
const assistant = `ASSIST_HEAD_${"a".repeat(180)}_ASSIST_MIDDLE_${"b".repeat(180)}_ASSIST_TAIL`;
|
|
348
|
-
const sample = buildStratifiedSample([
|
|
349
|
-
{ role: "user", text: user },
|
|
350
|
-
{ role: "assistant", text: assistant },
|
|
351
|
-
], 340);
|
|
352
|
-
expect(sample.text.length).toBeLessThanOrEqual(340);
|
|
353
|
-
expect(sample.text).toContain("USER_HEAD_");
|
|
354
|
-
expect(sample.text).toContain("USER_MIDDLE_");
|
|
355
|
-
expect(sample.text).toContain("USER_TAIL");
|
|
356
|
-
expect(sample.text).toContain("ASSIST_HEAD_");
|
|
357
|
-
expect(sample.text).toContain("ASSIST_MIDDLE_");
|
|
358
|
-
expect(sample.text).toContain("ASSIST_TAIL");
|
|
359
|
-
expect(sample.text).toContain(OMISSION_MARKER);
|
|
360
|
-
expect(sample.roles.user.truncatedTurns).toBe(1);
|
|
361
|
-
expect(sample.roles.assistant.truncatedTurns).toBe(1);
|
|
362
|
-
expect(sample.coverageRatio).toBeGreaterThan(0);
|
|
363
|
-
expect(sample.coverageRatio).toBeLessThan(1);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it("handles one-role sessions, multiple long turns, boundary rounding, and deterministic output", () => {
|
|
367
|
-
const turns = Array.from({ length: 6 }, (_, index) => ({
|
|
368
|
-
role: "user" as const,
|
|
369
|
-
text: `USER-${index}-HEAD-${"x".repeat(180)}-MID-${index}-${"y".repeat(180)}-TAIL-${index}`,
|
|
370
|
-
}));
|
|
371
|
-
const first = buildStratifiedSample(turns, 420);
|
|
372
|
-
const second = buildStratifiedSample(turns, 420);
|
|
373
|
-
expect(second).toEqual(first);
|
|
374
|
-
expect(first.text.length).toBeLessThanOrEqual(420);
|
|
375
|
-
expect(first.roles.assistant.turns).toBe(0);
|
|
376
|
-
expect(first.text).toContain("USER-0-HEAD");
|
|
377
|
-
expect(first.text).toContain("TAIL-5");
|
|
378
|
-
expect(first.sampledExchanges).toBeLessThan(turns.length);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it("rejects a cap too small for even one minimum exchange", () => {
|
|
382
|
-
expect(() => buildStratifiedSample([{ role: "user", text: "x".repeat(200) }], 20))
|
|
383
|
-
.toThrow("too small to preserve the required first/last exchanges");
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
it("redacts common transcript credentials and personal contacts before external LLM use", () => {
|
|
387
|
-
const values = [
|
|
388
|
-
{ text: `DATABASE_URL=postgres://user:${"p".repeat(16)}@db.invalid/app`, secret: "p".repeat(16) },
|
|
389
|
-
{ text: `Authorization: Basic ${"Q".repeat(24)}`, secret: "Q".repeat(24) },
|
|
390
|
-
{ text: `Cookie: session=${"c".repeat(24)}`, secret: "c".repeat(24) },
|
|
391
|
-
{
|
|
392
|
-
text: `token=${"a".repeat(8)}.${"b".repeat(8)}.${"c".repeat(8)}`,
|
|
393
|
-
secret: `${"a".repeat(8)}.${"b".repeat(8)}.${"c".repeat(8)}`,
|
|
394
|
-
},
|
|
395
|
-
{ text: "person@example.invalid", secret: "person@example.invalid" },
|
|
396
|
-
{ text: "13812345678", secret: "13812345678" },
|
|
397
|
-
];
|
|
398
|
-
for (const value of values) {
|
|
399
|
-
const result = redactForExternalModel(value.text);
|
|
400
|
-
expect(result.redacted).toBeGreaterThan(0);
|
|
401
|
-
expect(result.text).not.toContain(value.secret);
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it("accepts grounded candidates and constructs controlled metadata", () => {
|
|
406
|
-
const sample = buildStratifiedSample([
|
|
407
|
-
{ role: "user", text: "这条路以后不要再走" },
|
|
408
|
-
{ role: "assistant", text: "已记录原因" },
|
|
409
|
-
], 500);
|
|
410
|
-
const result = validateJudgeResponse(JSON.stringify({
|
|
411
|
-
hasPivot: true,
|
|
412
|
-
candidates: [{
|
|
413
|
-
kind: "decision",
|
|
414
|
-
text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
|
|
415
|
-
anchor: "这条路以后不要再走",
|
|
416
|
-
key: "拒绝双真相源",
|
|
417
|
-
evidence: ["已记录原因"],
|
|
418
|
-
}],
|
|
419
|
-
}), sample, session());
|
|
420
|
-
expect(result.hasPivot).toBeTrue();
|
|
421
|
-
expect(result.candidates[0].canonicalKey).toBe("pivot-decision-拒绝双真相源");
|
|
422
|
-
expect(result.candidates[0].proposedScope).toBe("memory:pivot");
|
|
423
|
-
expect(result.candidates[0].proposedCategory).toBe("events");
|
|
424
|
-
expect(result.candidates[0].tags).toEqual([
|
|
425
|
-
"src:019fc307",
|
|
426
|
-
"date:2026-08-02",
|
|
427
|
-
"harness:codex",
|
|
428
|
-
"raw:codex",
|
|
429
|
-
]);
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
it("redactedResidue ignores placeholder shapes but catches real residue", () => {
|
|
433
|
-
// A placeholder's own word shape must not re-match the assignment rule.
|
|
434
|
-
expect(redactedResidue("session_id='[REDACTED:sensitive_assignment]'")).toBe(0);
|
|
435
|
-
// Placeholder junction with surviving context must not re-match email/uri.
|
|
436
|
-
expect(redactedResidue("clone https://[REDACTED:uri_credentials]@gitlab.com/user/repo.git")).toBe(0);
|
|
437
|
-
// Genuine residue outside placeholders still counts.
|
|
438
|
-
expect(redactedResidue("[REDACTED:email] and mail me at someone@example.com")).toBeGreaterThan(0);
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it("grounds anchors and evidence that copy the rendered role labels", () => {
|
|
442
|
-
const sample = buildStratifiedSample([
|
|
443
|
-
{ role: "user", text: "这条路以后不要再走" },
|
|
444
|
-
{ role: "assistant", text: "已记录原因" },
|
|
445
|
-
], 500);
|
|
446
|
-
const result = validateJudgeResponse(JSON.stringify({
|
|
447
|
-
hasPivot: true,
|
|
448
|
-
candidates: [{
|
|
449
|
-
kind: "decision",
|
|
450
|
-
text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
|
|
451
|
-
anchor: "用户:这条路以后不要再走",
|
|
452
|
-
key: "拒绝双真相源",
|
|
453
|
-
evidence: ["助手:已记录原因"],
|
|
454
|
-
}],
|
|
455
|
-
}), sample, session());
|
|
456
|
-
expect(result.hasPivot).toBeTrue();
|
|
457
|
-
expect(result.candidates[0].anchor).toBe("这条路以后不要再走");
|
|
458
|
-
expect(result.candidates[0].evidence).toEqual(["已记录原因"]);
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
it("rejects evidence that is only a bare role label", () => {
|
|
462
|
-
expect(() => validateJudgeResponse(JSON.stringify({
|
|
463
|
-
hasPivot: true,
|
|
464
|
-
candidates: [{
|
|
465
|
-
kind: "decision",
|
|
466
|
-
text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
|
|
467
|
-
anchor: "这条路以后不要再走",
|
|
468
|
-
key: "拒绝双真相源",
|
|
469
|
-
evidence: ["助手:"],
|
|
470
|
-
}],
|
|
471
|
-
}), buildStratifiedSample([
|
|
472
|
-
{ role: "user", text: "这条路以后不要再走" },
|
|
473
|
-
{ role: "assistant", text: "已记录原因" },
|
|
474
|
-
], 500), session())).toThrow("substantive");
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
it("keeps verbatim evidence and drops a paraphrased quote without voiding the candidate", () => {
|
|
478
|
-
const sample = buildStratifiedSample([
|
|
479
|
-
{ role: "user", text: "这条路以后不要再走" },
|
|
480
|
-
{ role: "assistant", text: "已记录原因" },
|
|
481
|
-
], 500);
|
|
482
|
-
const result = validateJudgeResponse(JSON.stringify({
|
|
483
|
-
hasPivot: true,
|
|
484
|
-
candidates: [{
|
|
485
|
-
kind: "decision",
|
|
486
|
-
text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
|
|
487
|
-
anchor: "这条路以后不要再走",
|
|
488
|
-
key: "拒绝双真相源",
|
|
489
|
-
evidence: ["已记录原因", "助手当时总结说旧路线会造成双真相源问题"],
|
|
490
|
-
}],
|
|
491
|
-
}), sample, session());
|
|
492
|
-
expect(result.candidates[0].evidence).toEqual(["已记录原因"]);
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
it("drops a candidate whose stored form degrades below read-back rules", () => {
|
|
496
|
-
// "助手:好" passes raw substantive checks but stores as a single char
|
|
497
|
-
// after label stripping — the stored-form gate must reject it, and with
|
|
498
|
-
// no surviving candidate the response fails as a whole.
|
|
499
|
-
expect(() => validateJudgeResponse(JSON.stringify({
|
|
500
|
-
hasPivot: true,
|
|
501
|
-
candidates: [{
|
|
502
|
-
kind: "decision",
|
|
503
|
-
text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
|
|
504
|
-
anchor: "这条路以后不要再走",
|
|
505
|
-
key: "拒绝双真相源",
|
|
506
|
-
evidence: ["助手:好"],
|
|
507
|
-
}],
|
|
508
|
-
}), buildStratifiedSample([
|
|
509
|
-
{ role: "user", text: "这条路以后不要再走" },
|
|
510
|
-
{ role: "assistant", text: "好" },
|
|
511
|
-
], 500), session())).toThrow("requires at least one valid candidate");
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
it("rejects a candidate when no evidence survives grounding", () => {
|
|
515
|
-
expect(() => validateJudgeResponse(JSON.stringify({
|
|
516
|
-
hasPivot: true,
|
|
517
|
-
candidates: [{
|
|
518
|
-
kind: "decision",
|
|
519
|
-
text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
|
|
520
|
-
anchor: "这条路以后不要再走",
|
|
521
|
-
key: "拒绝双真相源",
|
|
522
|
-
evidence: ["助手当时总结说旧路线会造成双真相源问题"],
|
|
523
|
-
}],
|
|
524
|
-
}), buildStratifiedSample([
|
|
525
|
-
{ role: "user", text: "这条路以后不要再走" },
|
|
526
|
-
{ role: "assistant", text: "已记录原因" },
|
|
527
|
-
], 500), session())).toThrow("requires at least one grounded");
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
it("rejects a case that drops below two grounded evidence quotes", () => {
|
|
531
|
-
expect(() => validateJudgeResponse(JSON.stringify({
|
|
532
|
-
hasPivot: true,
|
|
533
|
-
candidates: [{
|
|
534
|
-
kind: "case",
|
|
535
|
-
text: "记录了一次问题的解决过程,包含问题定位与验证后的修复方案说明。",
|
|
536
|
-
anchor: "这条路以后不要再走",
|
|
537
|
-
key: "示例案例",
|
|
538
|
-
evidence: ["已记录原因", "这句在输入里根本不存在的概括描述"],
|
|
539
|
-
}],
|
|
540
|
-
}), buildStratifiedSample([
|
|
541
|
-
{ role: "user", text: "这条路以后不要再走" },
|
|
542
|
-
{ role: "assistant", text: "已记录原因" },
|
|
543
|
-
], 500), session())).toThrow("requires at least two grounded");
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
it("keeps valid candidates when a sibling candidate fails validation", () => {
|
|
547
|
-
const sample = buildStratifiedSample([
|
|
548
|
-
{ role: "user", text: "这条路以后不要再走" },
|
|
549
|
-
{ role: "assistant", text: "已记录原因" },
|
|
550
|
-
], 500);
|
|
551
|
-
const result = validateJudgeResponse(JSON.stringify({
|
|
552
|
-
hasPivot: true,
|
|
553
|
-
candidates: [
|
|
554
|
-
{
|
|
555
|
-
kind: "decision",
|
|
556
|
-
text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
|
|
557
|
-
anchor: "这条路以后不要再走",
|
|
558
|
-
key: "拒绝双真相源",
|
|
559
|
-
evidence: ["已记录原因"],
|
|
560
|
-
},
|
|
561
|
-
{
|
|
562
|
-
kind: "case",
|
|
563
|
-
text: "记录了一次问题的解决过程,包含问题定位与验证后的修复方案说明。",
|
|
564
|
-
anchor: "输入里不存在的锚点句子",
|
|
565
|
-
key: "不合格案例",
|
|
566
|
-
evidence: ["已记录原因", "这句也不存在于输入之中"],
|
|
567
|
-
},
|
|
568
|
-
],
|
|
569
|
-
}), sample, session());
|
|
570
|
-
expect(result.candidates.length).toBe(1);
|
|
571
|
-
expect(result.candidates[0].canonicalKey).toBe("pivot-decision-拒绝双真相源");
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
it("still fails when no candidate survives validation", () => {
|
|
575
|
-
expect(() => validateJudgeResponse(JSON.stringify({
|
|
576
|
-
hasPivot: true,
|
|
577
|
-
candidates: [{
|
|
578
|
-
kind: "decision",
|
|
579
|
-
text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
|
|
580
|
-
anchor: "输入里不存在的锚点句子",
|
|
581
|
-
key: "拒绝双真相源",
|
|
582
|
-
evidence: ["已记录原因"],
|
|
583
|
-
}],
|
|
584
|
-
}), buildStratifiedSample([
|
|
585
|
-
{ role: "user", text: "这条路以后不要再走" },
|
|
586
|
-
{ role: "assistant", text: "已记录原因" },
|
|
587
|
-
], 500), session())).toThrow("requires at least one valid candidate");
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
it("rejects hallucinated anchors and inconsistent empty verdicts", () => {
|
|
591
|
-
expect(() => validateJudgeResponse(JSON.stringify({
|
|
592
|
-
hasPivot: true,
|
|
593
|
-
candidates: [{
|
|
594
|
-
kind: "preference_rule",
|
|
595
|
-
text: "Alice 明确要求所有报告都保留证据出处,方便之后重新核对。",
|
|
596
|
-
anchor: "输入里并不存在的原话",
|
|
597
|
-
key: "保留证据",
|
|
598
|
-
}],
|
|
599
|
-
}), buildStratifiedSample([{ role: "user", text: "请保留出处" }], 500), session())).toThrow("not grounded");
|
|
600
|
-
expect(() => validateJudgeResponse(
|
|
601
|
-
JSON.stringify({ hasPivot: true, candidates: [] }),
|
|
602
|
-
buildStratifiedSample([{ role: "user", text: "请保留出处" }], 500),
|
|
603
|
-
session(),
|
|
604
|
-
)).toThrow("requires at least one");
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
it("enforces evidence counts by pivot kind and forbids quoting omission markers", () => {
|
|
608
|
-
const base = {
|
|
609
|
-
text: "Alice 明确改变了既有判断,并给出了可以回查的真实原因与结果。",
|
|
610
|
-
anchor: "我改主意了",
|
|
611
|
-
key: "改变判断",
|
|
612
|
-
};
|
|
613
|
-
const sample = buildStratifiedSample([
|
|
614
|
-
{ role: "user", text: "我改主意了" },
|
|
615
|
-
{ role: "assistant", text: "旧判断失效" },
|
|
616
|
-
{ role: "assistant", text: "新方案已经验证" },
|
|
617
|
-
], 500);
|
|
618
|
-
expect(() => validateJudgeResponse(JSON.stringify({
|
|
619
|
-
hasPivot: true,
|
|
620
|
-
candidates: [{ kind: "judgment_shift", ...base, evidence: [] }],
|
|
621
|
-
}), sample, session())).toThrow("at least one");
|
|
622
|
-
expect(() => validateJudgeResponse(JSON.stringify({
|
|
623
|
-
hasPivot: true,
|
|
624
|
-
candidates: [{ kind: "case", ...base, evidence: ["旧判断失效"] }],
|
|
625
|
-
}), sample, session())).toThrow("at least two");
|
|
626
|
-
expect(() => validateJudgeResponse(JSON.stringify({
|
|
627
|
-
hasPivot: true,
|
|
628
|
-
candidates: [{ kind: "preference_rule", ...base, anchor: `我改${OMISSION_MARKER}主意了`, evidence: [] }],
|
|
629
|
-
}), buildStratifiedSample([{ role: "user", text: `我改${OMISSION_MARKER}主意了` }], 500), session()))
|
|
630
|
-
.toThrow("omission marker");
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
it("requires distinct substantive evidence grounded within individual turns", () => {
|
|
634
|
-
const sample = buildStratifiedSample([
|
|
635
|
-
{ role: "user", text: "部署时出现连接失败" },
|
|
636
|
-
{ role: "assistant", text: "先修配置文件" },
|
|
637
|
-
{ role: "assistant", text: "之后验证服务恢复" },
|
|
638
|
-
], 500);
|
|
639
|
-
const candidate = {
|
|
640
|
-
kind: "case",
|
|
641
|
-
text: "部署连接失败的问题通过修正配置并验证服务恢复得到了解决。",
|
|
642
|
-
anchor: "部署时出现连接失败",
|
|
643
|
-
key: "部署连接修复",
|
|
644
|
-
};
|
|
645
|
-
expect(() => validateJudgeResponse(JSON.stringify({
|
|
646
|
-
hasPivot: true,
|
|
647
|
-
candidates: [{ ...candidate, evidence: ["助手:", "助手:"] }],
|
|
648
|
-
}), sample, session())).toThrow(/distinct|substantive/);
|
|
649
|
-
expect(() => validateJudgeResponse(JSON.stringify({
|
|
650
|
-
hasPivot: true,
|
|
651
|
-
candidates: [{ ...candidate, evidence: ["连接失败先修配置文件", "之后验证服务恢复"] }],
|
|
652
|
-
}), sample, session())).toThrow("requires at least two grounded");
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
it("rejects an anchor that appears only in an assistant turn", () => {
|
|
656
|
-
expect(() => validateJudgeResponse(JSON.stringify({
|
|
657
|
-
hasPivot: true,
|
|
658
|
-
candidates: [{
|
|
659
|
-
kind: "decision",
|
|
660
|
-
text: "Alice 明确决定采用这个方案,并说明了此前方案不再继续使用。",
|
|
661
|
-
anchor: "这句话只由助手说过",
|
|
662
|
-
key: "助手冒充用户锚点",
|
|
663
|
-
}],
|
|
664
|
-
}), buildStratifiedSample([
|
|
665
|
-
{ role: "user", text: "另一句话" },
|
|
666
|
-
{ role: "assistant", text: "这句话只由助手说过" },
|
|
667
|
-
], 500), session())).toThrow("user turn");
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
it("rejects sensitive material in a model-proposed canonical key", () => {
|
|
671
|
-
expect(() => validateJudgeResponse(JSON.stringify({
|
|
672
|
-
hasPivot: true,
|
|
673
|
-
candidates: [{
|
|
674
|
-
kind: "decision",
|
|
675
|
-
text: "Alice 明确决定采用这个方案,并说明了此前方案不再继续使用。",
|
|
676
|
-
anchor: "采用这个方案",
|
|
677
|
-
key: `token=${"s".repeat(24)}`,
|
|
678
|
-
evidence: ["采用这个方案"],
|
|
679
|
-
}],
|
|
680
|
-
}), buildStratifiedSample([{ role: "user", text: "采用这个方案" }], 500), session()))
|
|
681
|
-
.toThrow("sensitive material");
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
it("round-trips all four validated pivot kinds through strict stored-result checks", () => {
|
|
685
|
-
const fixture = createBundleFixture();
|
|
686
|
-
const loaded = loadFrozenBundle(
|
|
687
|
-
fixture.bundleDir,
|
|
688
|
-
fixture.descriptor.bundleHash,
|
|
689
|
-
fixture.descriptor.requestProfileHash,
|
|
690
|
-
);
|
|
691
|
-
const entry = loaded.entries[0];
|
|
692
|
-
const selectionHash = selectionIdentityHash("transport", [entry]);
|
|
693
|
-
const evidenceByKind = {
|
|
694
|
-
judgment_shift: ["已记录这条规则"],
|
|
695
|
-
decision: ["已记录这条规则"],
|
|
696
|
-
preference_rule: [],
|
|
697
|
-
case: ["以后必须保留可回查证据", "已记录这条规则"],
|
|
698
|
-
} as const;
|
|
699
|
-
for (const kind of ["judgment_shift", "decision", "preference_rule", "case"] as const) {
|
|
700
|
-
const judged = validateJudgeResponse(JSON.stringify({
|
|
701
|
-
hasPivot: true,
|
|
702
|
-
candidates: [{
|
|
703
|
-
kind,
|
|
704
|
-
text: "用户明确要求以后必须保留可回查证据,最终结果需要能够追溯到真实出处。",
|
|
705
|
-
anchor: "以后必须保留可回查证据",
|
|
706
|
-
key: `round-trip-${kind}`,
|
|
707
|
-
evidence: evidenceByKind[kind],
|
|
708
|
-
}],
|
|
709
|
-
}), entry.sample, entry.session);
|
|
710
|
-
const storedResult = {
|
|
711
|
-
schemaVersion: 1,
|
|
712
|
-
mode: "transport",
|
|
713
|
-
selectionHash,
|
|
714
|
-
outboundSampleSha256: entry.sampleSha256,
|
|
715
|
-
promptVersion: PROMPT_VERSION,
|
|
716
|
-
pipelineVersion: PIPELINE_VERSION,
|
|
717
|
-
model: PIVOT_MODEL,
|
|
718
|
-
requestProfileHash: fixture.descriptor.requestProfileHash,
|
|
719
|
-
bundleHash: fixture.descriptor.bundleHash,
|
|
720
|
-
session: entry.session,
|
|
721
|
-
status: "ok",
|
|
722
|
-
hasPivot: true,
|
|
723
|
-
candidates: judged.candidates,
|
|
724
|
-
sample: entry.sample.metrics,
|
|
725
|
-
responseChars: 100,
|
|
726
|
-
attempts: 1,
|
|
727
|
-
responseModel: PIVOT_MODEL,
|
|
728
|
-
finishReason: "stop",
|
|
729
|
-
completedAt: "2026-08-03T00:00:00.000Z",
|
|
730
|
-
};
|
|
731
|
-
expect(storedResultMatches(
|
|
732
|
-
storedResult, entry, "transport", selectionHash, PIVOT_MODEL, fixture.descriptor.bundleHash,
|
|
733
|
-
fixture.descriptor.requestProfileHash, 3,
|
|
734
|
-
)).toBeTrue();
|
|
735
|
-
if (kind === "decision") {
|
|
736
|
-
expect(storedResultMatches(
|
|
737
|
-
{ ...storedResult, session: { ...entry.session, date: "2026-08-04" } },
|
|
738
|
-
entry, "transport", selectionHash, PIVOT_MODEL, fixture.descriptor.bundleHash,
|
|
739
|
-
fixture.descriptor.requestProfileHash, 3,
|
|
740
|
-
)).toBeFalse();
|
|
741
|
-
expect(storedResultMatches(
|
|
742
|
-
{
|
|
743
|
-
...storedResult,
|
|
744
|
-
candidates: [{ ...judged.candidates[0], unexpected: "must-not-pass-through" }],
|
|
745
|
-
},
|
|
746
|
-
entry, "transport", selectionHash, PIVOT_MODEL, fixture.descriptor.bundleHash,
|
|
747
|
-
fixture.descriptor.requestProfileHash, 3,
|
|
748
|
-
)).toBeFalse();
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
});
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
describe("pivot-distill request identity and frozen bundle", () => {
|
|
755
|
-
it("builds a credential-free fixed-model request profile whose hash covers every request choice", () => {
|
|
756
|
-
const endpoint = safeEndpoint("https://user:password@example.invalid//compatible-mode/v1/?token=secret#fragment");
|
|
757
|
-
expect(endpoint).toBe("https://example.invalid/compatible-mode/v1");
|
|
758
|
-
expect(endpoint).not.toContain("password");
|
|
759
|
-
expect(endpoint).not.toContain("token=");
|
|
760
|
-
const profile = buildPivotRequestProfile(
|
|
761
|
-
"https://user:password@example.invalid/compatible-mode/v1?token=secret#fragment",
|
|
762
|
-
24_000,
|
|
763
|
-
);
|
|
764
|
-
expect(profile.model).toBe(PIVOT_MODEL);
|
|
765
|
-
expect(profile.enableThinking).toBeFalse();
|
|
766
|
-
expect(profile.responseFormat).toEqual({ type: "json_object" });
|
|
767
|
-
expect(profile.tokenLimit).toEqual({ parameter: "max_tokens", value: 1_800 });
|
|
768
|
-
expect(profile.retryPolicy).toEqual({
|
|
769
|
-
maxSessionAttempts: 3,
|
|
770
|
-
sdkMaxRetries: 0,
|
|
771
|
-
backoffMs: [250, 500],
|
|
772
|
-
maxRetryAfterMs: 5_000,
|
|
773
|
-
retryableHttpStatuses: [408, 409, 429],
|
|
774
|
-
retryableServerErrorRange: "500-599",
|
|
775
|
-
});
|
|
776
|
-
expect(profile.promptHashes.system).toHaveLength(64);
|
|
777
|
-
expect(profile.promptHashes.userTemplate).toHaveLength(64);
|
|
778
|
-
expect(profile.promptHashes.jsonSchema).toHaveLength(64);
|
|
779
|
-
const identity = pivotRequestProfileHash(profile);
|
|
780
|
-
expect(pivotRequestProfileHash({ ...profile, temperature: 0.2 })).not.toBe(identity);
|
|
781
|
-
expect(pivotRequestProfileHash({
|
|
782
|
-
...profile,
|
|
783
|
-
sampling: { ...profile.sampling, omissionMarker: "different" },
|
|
784
|
-
})).not.toBe(identity);
|
|
785
|
-
expect(pivotRequestProfileHash({
|
|
786
|
-
...profile,
|
|
787
|
-
retryPolicy: { ...profile.retryPolicy, sdkMaxRetries: 1 },
|
|
788
|
-
})).not.toBe(identity);
|
|
789
|
-
expect(buildPivotLlmConfig({
|
|
790
|
-
apiKey: "test-key",
|
|
791
|
-
model: "qwen-turbo",
|
|
792
|
-
baseURL: "https://user:secret@example.invalid/v1?token=hidden#fragment",
|
|
793
|
-
}, profile, 12_345)).toMatchObject({
|
|
794
|
-
baseURL: "https://example.invalid/compatible-mode/v1",
|
|
795
|
-
model: PIVOT_MODEL,
|
|
796
|
-
maxRetries: 0,
|
|
797
|
-
timeoutMs: 12_345,
|
|
798
|
-
});
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
it("keeps retry cost bounded and aggregates usage from every billed attempt", () => {
|
|
802
|
-
const policy = buildPivotRequestProfile("https://example.invalid/v1").retryPolicy;
|
|
803
|
-
expect(retryDecision({ status: 429, headers: { "retry-after": "0.01" } }, 1, policy))
|
|
804
|
-
.toEqual({ retry: true, delayMs: 10 });
|
|
805
|
-
expect(retryDecision({ status: 500 }, 2, policy)).toEqual({ retry: true, delayMs: 500 });
|
|
806
|
-
expect(retryDecision({ status: 401 }, 1, policy)).toEqual({ retry: false, delayMs: 0 });
|
|
807
|
-
expect(retryDecision({ status: 429 }, 3, policy)).toEqual({ retry: false, delayMs: 0 });
|
|
808
|
-
expect(aggregateDetailedUsage([
|
|
809
|
-
{ promptTokens: 10, completionTokens: 2, totalTokens: 12 },
|
|
810
|
-
{ promptTokens: 11, completionTokens: 3, totalTokens: 14, cachedPromptTokens: 4 },
|
|
811
|
-
])).toEqual({
|
|
812
|
-
promptTokens: 21,
|
|
813
|
-
completionTokens: 5,
|
|
814
|
-
totalTokens: 26,
|
|
815
|
-
cachedPromptTokens: 4,
|
|
816
|
-
});
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
it("makes the external stage and exact outbound sample set part of report identity", () => {
|
|
820
|
-
const fixture = createBundleFixture();
|
|
821
|
-
const loaded = loadFrozenBundle(
|
|
822
|
-
fixture.bundleDir, fixture.descriptor.bundleHash, fixture.descriptor.requestProfileHash,
|
|
823
|
-
);
|
|
824
|
-
expect(selectionIdentityHash("transport", loaded.entries))
|
|
825
|
-
.not.toBe(selectionIdentityHash("regression", loaded.entries));
|
|
826
|
-
const changed = structuredClone(loaded.entries);
|
|
827
|
-
changed[0].sampleSha256 = "f".repeat(64);
|
|
828
|
-
expect(selectionIdentityHash("transport", changed))
|
|
829
|
-
.not.toBe(selectionIdentityHash("transport", loaded.entries));
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
it("writes 0700/0600 bundles and reloads only exact expected identities", () => {
|
|
833
|
-
const fixture = createBundleFixture();
|
|
834
|
-
expect(statSync(fixture.bundleDir).mode & 0o777).toBe(0o700);
|
|
835
|
-
expect(statSync(join(fixture.bundleDir, "bundle.json")).mode & 0o777).toBe(0o600);
|
|
836
|
-
const loaded = loadFrozenBundle(
|
|
837
|
-
fixture.bundleDir,
|
|
838
|
-
fixture.descriptor.bundleHash,
|
|
839
|
-
fixture.descriptor.requestProfileHash,
|
|
840
|
-
);
|
|
841
|
-
expect(loaded.entries).toHaveLength(1);
|
|
842
|
-
expect(loaded.entries[0].status).toBe("eligible");
|
|
843
|
-
expect(frozenSampleHash(loaded.entries[0].sample)).toBe(loaded.entries[0].sampleSha256);
|
|
844
|
-
expect(() => loadFrozenBundle(
|
|
845
|
-
fixture.bundleDir,
|
|
846
|
-
"wrong-bundle",
|
|
847
|
-
fixture.descriptor.requestProfileHash,
|
|
848
|
-
)).toThrow("unexpected bundleHash");
|
|
849
|
-
expect(() => loadFrozenBundle(
|
|
850
|
-
fixture.bundleDir,
|
|
851
|
-
fixture.descriptor.bundleHash,
|
|
852
|
-
"wrong-profile",
|
|
853
|
-
)).toThrow("unexpected requestProfileHash");
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
it("rejects mutated samples, duplicate session keys, and paths outside the bundle root", () => {
|
|
857
|
-
const mutated = createBundleFixture();
|
|
858
|
-
const manifestEntry = JSON.parse(readFileSync(join(mutated.bundleDir, "manifest.jsonl"), "utf8")) as {
|
|
859
|
-
sampleFile: string;
|
|
860
|
-
};
|
|
861
|
-
const samplePath = join(mutated.bundleDir, manifestEntry.sampleFile);
|
|
862
|
-
const sample = JSON.parse(readFileSync(samplePath, "utf8")) as Record<string, unknown>;
|
|
863
|
-
sample.text = `X${String(sample.text).slice(1)}`;
|
|
864
|
-
writeFileSync(samplePath, `${JSON.stringify(sample)}\n`);
|
|
865
|
-
expect(() => loadFrozenBundle(
|
|
866
|
-
mutated.bundleDir,
|
|
867
|
-
mutated.descriptor.bundleHash,
|
|
868
|
-
mutated.descriptor.requestProfileHash,
|
|
869
|
-
)).toThrow("sample grounding projection mismatch");
|
|
870
|
-
|
|
871
|
-
const mutatedMetrics = createBundleFixture();
|
|
872
|
-
const metricsEntry = JSON.parse(
|
|
873
|
-
readFileSync(join(mutatedMetrics.bundleDir, "manifest.jsonl"), "utf8"),
|
|
874
|
-
) as { sampleFile: string };
|
|
875
|
-
const metricsPath = join(mutatedMetrics.bundleDir, metricsEntry.sampleFile);
|
|
876
|
-
const metricsSample = JSON.parse(readFileSync(metricsPath, "utf8")) as {
|
|
877
|
-
metrics: { redactions: number };
|
|
878
|
-
};
|
|
879
|
-
metricsSample.metrics.redactions += 1;
|
|
880
|
-
writeFileSync(metricsPath, `${JSON.stringify(metricsSample)}\n`);
|
|
881
|
-
expect(() => loadFrozenBundle(
|
|
882
|
-
mutatedMetrics.bundleDir,
|
|
883
|
-
mutatedMetrics.descriptor.bundleHash,
|
|
884
|
-
mutatedMetrics.descriptor.requestProfileHash,
|
|
885
|
-
)).toThrow("sample hash mismatch");
|
|
886
|
-
|
|
887
|
-
const duplicate = createBundleFixture();
|
|
888
|
-
const originalLine = readFileSync(join(duplicate.bundleDir, "manifest.jsonl"), "utf8").trim();
|
|
889
|
-
const duplicatedManifest = `${originalLine}\n${originalLine}\n`;
|
|
890
|
-
const descriptor = JSON.parse(readFileSync(join(duplicate.bundleDir, "bundle.json"), "utf8")) as FrozenBundleDescriptor;
|
|
891
|
-
descriptor.manifestSha256 = sha256(duplicatedManifest);
|
|
892
|
-
descriptor.sessions = 2;
|
|
893
|
-
descriptor.eligibleSessions = 2;
|
|
894
|
-
descriptor.deferredSessions = 0;
|
|
895
|
-
descriptor.bundleHash = bundleIdentityHashForTest({
|
|
896
|
-
requestProfileHash: descriptor.requestProfileHash,
|
|
897
|
-
manifestSha256: descriptor.manifestSha256,
|
|
898
|
-
sessions: descriptor.sessions,
|
|
899
|
-
eligibleSessions: descriptor.eligibleSessions,
|
|
900
|
-
deferredSessions: descriptor.deferredSessions,
|
|
901
|
-
});
|
|
902
|
-
writeFileSync(join(duplicate.bundleDir, "manifest.jsonl"), duplicatedManifest);
|
|
903
|
-
writeFileSync(join(duplicate.bundleDir, "bundle.json"), `${JSON.stringify(descriptor)}\n`);
|
|
904
|
-
expect(() => loadFrozenBundle(
|
|
905
|
-
duplicate.bundleDir,
|
|
906
|
-
descriptor.bundleHash,
|
|
907
|
-
descriptor.requestProfileHash,
|
|
908
|
-
)).toThrow("duplicate session key");
|
|
909
|
-
|
|
910
|
-
expect(() => resolveBundleSamplePath(duplicate.bundleDir, "/tmp/outside.json"))
|
|
911
|
-
.toThrow("must stay relative");
|
|
912
|
-
expect(() => resolveBundleSamplePath(duplicate.bundleDir, "samples/../../outside.json"))
|
|
913
|
-
.toThrow("must stay relative");
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
it("rejects a request profile that no longer matches the fixed prompt/body/sampling contract", () => {
|
|
917
|
-
const fixture = createBundleFixture();
|
|
918
|
-
const descriptor = JSON.parse(readFileSync(join(fixture.bundleDir, "bundle.json"), "utf8")) as FrozenBundleDescriptor;
|
|
919
|
-
descriptor.requestProfile.temperature = 0.7;
|
|
920
|
-
writeFileSync(join(fixture.bundleDir, "bundle.json"), `${JSON.stringify(descriptor)}\n`);
|
|
921
|
-
expect(() => loadFrozenBundle(
|
|
922
|
-
fixture.bundleDir,
|
|
923
|
-
fixture.descriptor.bundleHash,
|
|
924
|
-
fixture.descriptor.requestProfileHash,
|
|
925
|
-
)).toThrow("current fixed pivot contract");
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
it("rejects duplicate keys before bundle creation and keeps assistant-only sessions deferred", () => {
|
|
929
|
-
const outputDir = mkdtempSync(join(tmpdir(), "pivot-bundle-duplicate-"));
|
|
930
|
-
const profile = buildPivotRequestProfile("https://example.invalid/v1", 500);
|
|
931
|
-
const profileHash = pivotRequestProfileHash(profile);
|
|
932
|
-
const userSample = buildStratifiedSample([{ role: "user", text: "保留证据" }], 500);
|
|
933
|
-
expect(() => writeFrozenBundle(outputDir, [
|
|
934
|
-
{ session: session(), sample: userSample },
|
|
935
|
-
{ session: session(), sample: userSample },
|
|
936
|
-
], profile, profileHash)).toThrow("duplicate session key");
|
|
937
|
-
|
|
938
|
-
const assistantOnlyDir = mkdtempSync(join(tmpdir(), "pivot-bundle-deferred-"));
|
|
939
|
-
const assistantOnly = buildStratifiedSample([{ role: "assistant", text: "只有助手内容" }], 500);
|
|
940
|
-
const descriptor = writeFrozenBundle(assistantOnlyDir, [{ session: session(), sample: assistantOnly }], profile, profileHash);
|
|
941
|
-
const loaded = loadFrozenBundle(join(assistantOnlyDir, "input-bundle"), descriptor.bundleHash, descriptor.requestProfileHash);
|
|
942
|
-
expect(loaded.entries[0].status).toBe("deferred");
|
|
943
|
-
expect(loaded.entries[0].reason).toBe("no-parseable-user-text");
|
|
944
|
-
});
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
describe("pivot-distill source enumeration and parse integrity", () => {
|
|
948
|
-
it("filters inherited DEJA variables from both inventory subprocesses", () => {
|
|
949
|
-
expect(filteredDejaEnvironment({ DEJA_HOME: "/wrong", DEJA_CONFIG: "/wrong/config", KEEP_ME: "yes" }))
|
|
950
|
-
.toEqual({ KEEP_ME: "yes" });
|
|
951
|
-
});
|
|
952
|
-
|
|
953
|
-
it("enumerates four endpoint families plus Claude Desktop and both AGY origins with a fake Deja", () => {
|
|
954
|
-
const root = mkdtempSync(join(tmpdir(), "pivot-inventory-"));
|
|
955
|
-
const sourceRoot = join(root, "sources");
|
|
956
|
-
mkdirSync(sourceRoot, { recursive: true });
|
|
957
|
-
const dejaSessions = ([
|
|
958
|
-
["claude", "claude.jsonl"],
|
|
959
|
-
["codex", "codex.jsonl"],
|
|
960
|
-
["kimi", "kimi.jsonl"],
|
|
961
|
-
["gemini", "gemini.json"],
|
|
962
|
-
] as const).map(([harness, name], index) => {
|
|
963
|
-
const path = join(sourceRoot, name);
|
|
964
|
-
writeFileSync(path, harness === "gemini" ? "{\"messages\":[]}" : "{}\n");
|
|
965
|
-
return { id: `deja-${index}-${harness}`, harness, path, project: "test" };
|
|
966
|
-
});
|
|
967
|
-
const codexArchiveRoot = join(root, "codex-archive");
|
|
968
|
-
const claudeDesktopRoot = join(root, "desktop");
|
|
969
|
-
const agyConvertedRoot = join(root, "agy", "brain", "session-one", ".system_generated", "logs");
|
|
970
|
-
const agyArchiveRoot = join(root, "agy-archive");
|
|
971
|
-
for (const directory of [codexArchiveRoot, claudeDesktopRoot, agyConvertedRoot, agyArchiveRoot]) {
|
|
972
|
-
mkdirSync(directory, { recursive: true });
|
|
973
|
-
}
|
|
974
|
-
writeFileSync(join(codexArchiveRoot, "archive.jsonl"), "{}\n");
|
|
975
|
-
writeFileSync(join(claudeDesktopRoot, "desktop.jsonl"), "{}\n");
|
|
976
|
-
writeFileSync(join(agyConvertedRoot, "transcript_full.jsonl"), "{}\n");
|
|
977
|
-
writeFileSync(join(agyArchiveRoot, "legacy.jsonl"), "{}\n");
|
|
978
|
-
const payload = JSON.stringify({ schema_version: 2, sessions: dejaSessions });
|
|
979
|
-
const dejaBin = join(root, "fake-deja.ts");
|
|
980
|
-
writeFileSync(dejaBin, `#!/usr/bin/env bun
|
|
981
|
-
if (process.env.DEJA_HOME || process.env.DEJA_CONFIG) process.exit(91);
|
|
982
|
-
if (process.argv[2] === "version") console.log("deja 0.16.5");
|
|
983
|
-
else console.log(${JSON.stringify(payload)});
|
|
984
|
-
`);
|
|
985
|
-
chmodSync(dejaBin, 0o700);
|
|
986
|
-
const options = parseCliOptions([
|
|
987
|
-
"--deja-bin", dejaBin,
|
|
988
|
-
"--codex-archive-root", codexArchiveRoot,
|
|
989
|
-
"--claude-desktop-root", claudeDesktopRoot,
|
|
990
|
-
"--agy-converted-root", join(root, "agy"),
|
|
991
|
-
"--agy-archive-root", agyArchiveRoot,
|
|
992
|
-
]);
|
|
993
|
-
const previousDejaHome = process.env.DEJA_HOME;
|
|
994
|
-
const previousDejaConfig = process.env.DEJA_CONFIG;
|
|
995
|
-
process.env.DEJA_HOME = "/must-not-reach-child";
|
|
996
|
-
process.env.DEJA_CONFIG = "/must-not-reach-child/config";
|
|
997
|
-
try {
|
|
998
|
-
const loaded = loadSessionInventory(options);
|
|
999
|
-
expect(loaded.missingPaths).toBe(0);
|
|
1000
|
-
expect(new Set(loaded.sessions.map((item) => item.harness))).toEqual(new Set(["claude-code", "codex", "kimi", "agy"]));
|
|
1001
|
-
expect(loaded.sessions.some((item) => item.origin === "claude-desktop")).toBeTrue();
|
|
1002
|
-
expect(loaded.sessions.some((item) => item.origin === "agy-converted")).toBeTrue();
|
|
1003
|
-
expect(loaded.sessions.some((item) => item.origin === "agy-archive")).toBeTrue();
|
|
1004
|
-
expect(loaded.sessions.some((item) => item.rawHarness === "gemini")).toBeTrue();
|
|
1005
|
-
expect(inventoryCompletenessReasons(loaded.sessions, loaded.missingPaths)).toEqual([]);
|
|
1006
|
-
expect(inventoryCompletenessReasons(
|
|
1007
|
-
loaded.sessions.filter((item) => item.origin !== "claude-desktop"), 0,
|
|
1008
|
-
)).toContain("origin=claude-desktop has zero sessions");
|
|
1009
|
-
expect(inventoryCompletenessReasons(
|
|
1010
|
-
loaded.sessions.filter((item) => item.rawHarness !== "gemini"), 0,
|
|
1011
|
-
)).toContain("rawHarness=gemini has zero sessions");
|
|
1012
|
-
expect(inventoryCompletenessReasons(loaded.sessions, 1)).toContain("missingPaths=1");
|
|
1013
|
-
} finally {
|
|
1014
|
-
if (previousDejaHome === undefined) delete process.env.DEJA_HOME;
|
|
1015
|
-
else process.env.DEJA_HOME = previousDejaHome;
|
|
1016
|
-
if (previousDejaConfig === undefined) delete process.env.DEJA_CONFIG;
|
|
1017
|
-
else process.env.DEJA_CONFIG = previousDejaConfig;
|
|
1018
|
-
}
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
it("treats malformed JSONL as a bundle-blocking parse failure", async () => {
|
|
1022
|
-
const root = mkdtempSync(join(tmpdir(), "pivot-malformed-"));
|
|
1023
|
-
const path = join(root, "bad.jsonl");
|
|
1024
|
-
writeFileSync(path, "{\"type\":\"event_msg\",\"payload\":{\"type\":\"user_message\",\"message\":\"ok\"}}\nnot-json\n");
|
|
1025
|
-
await expect(readSessionTurns(session({ path, parserKind: "codex-rollout" })))
|
|
1026
|
-
.rejects.toThrow("Malformed JSONL at line 2");
|
|
1027
|
-
});
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
describe("pivot-distill external authorization and exact selection", () => {
|
|
1031
|
-
const common = [
|
|
1032
|
-
"--input-bundle", "/tmp/frozen",
|
|
1033
|
-
"--bundle-hash", "bundle-hash",
|
|
1034
|
-
"--request-profile-hash", "profile-hash",
|
|
1035
|
-
"--allow-external-llm",
|
|
1036
|
-
];
|
|
1037
|
-
|
|
1038
|
-
it("requires exact transport, separate regression, and dual full authorization", () => {
|
|
1039
|
-
expect(() => assertExternalModeAuthorization(parseCliOptions([
|
|
1040
|
-
"--mode", "transport", ...common,
|
|
1041
|
-
]))).toThrow("exactly one");
|
|
1042
|
-
expect(() => assertExternalModeAuthorization(parseCliOptions([
|
|
1043
|
-
"--mode", "transport", ...common, "--session-key", "codex:one",
|
|
1044
|
-
]))).not.toThrow();
|
|
1045
|
-
expect(() => assertExternalModeAuthorization(parseCliOptions([
|
|
1046
|
-
"--mode", "transport", ...common,
|
|
1047
|
-
"--session-key", "codex:one",
|
|
1048
|
-
"--output-dir", "/tmp/frozen/results",
|
|
1049
|
-
]))).toThrow("must not be the frozen input bundle");
|
|
1050
|
-
expect(() => assertExternalModeAuthorization(parseCliOptions([
|
|
1051
|
-
"--mode", "regression", ...common, "--session-key", "codex:one",
|
|
1052
|
-
]))).toThrow("allow-regression-run");
|
|
1053
|
-
expect(() => assertExternalModeAuthorization(parseCliOptions([
|
|
1054
|
-
"--mode", "regression", ...common, "--session-key", "codex:one", "--allow-regression-run",
|
|
1055
|
-
]))).not.toThrow();
|
|
1056
|
-
expect(() => assertExternalModeAuthorization(parseCliOptions([
|
|
1057
|
-
"--mode", "full", ...common,
|
|
1058
|
-
]))).toThrow("allow-full-run");
|
|
1059
|
-
expect(() => assertExternalModeAuthorization(parseCliOptions([
|
|
1060
|
-
"--mode", "full", ...common, "--allow-full-run",
|
|
1061
|
-
]))).not.toThrow();
|
|
1062
|
-
});
|
|
1063
|
-
|
|
1064
|
-
it("rejects truncated or wrong-model responses even if their content could parse", () => {
|
|
1065
|
-
expect(() => assertPivotResponseMetadata({ finishReason: "length", responseModel: PIVOT_MODEL }))
|
|
1066
|
-
.toThrow("finish_reason=length");
|
|
1067
|
-
expect(() => assertPivotResponseMetadata({ finishReason: "stop", responseModel: "rolling-alias" }))
|
|
1068
|
-
.toThrow("response model mismatch");
|
|
1069
|
-
expect(() => assertPivotResponseMetadata({ finishReason: "stop", responseModel: PIVOT_MODEL }))
|
|
1070
|
-
.not.toThrow();
|
|
1071
|
-
});
|
|
1072
|
-
|
|
1073
|
-
it("refuses transport before any live inventory branch when authorization is missing", async () => {
|
|
1074
|
-
await expect(main([
|
|
1075
|
-
"--mode", "transport",
|
|
1076
|
-
"--deja-bin", "/definitely/not/a/deja/binary",
|
|
1077
|
-
"--output-dir", join(tmpdir(), "must-not-be-created-by-unauthorized-transport"),
|
|
1078
|
-
])).rejects.toThrow("--allow-external-llm");
|
|
1079
|
-
});
|
|
1080
|
-
|
|
1081
|
-
it("selects one exact frozen key while full selects every eligible sample", () => {
|
|
1082
|
-
const fixture = createBundleFixture();
|
|
1083
|
-
const loaded = loadFrozenBundle(
|
|
1084
|
-
fixture.bundleDir,
|
|
1085
|
-
fixture.descriptor.bundleHash,
|
|
1086
|
-
fixture.descriptor.requestProfileHash,
|
|
1087
|
-
);
|
|
1088
|
-
const transport = parseCliOptions([
|
|
1089
|
-
"--mode", "transport",
|
|
1090
|
-
"--input-bundle", fixture.bundleDir,
|
|
1091
|
-
"--bundle-hash", fixture.descriptor.bundleHash,
|
|
1092
|
-
"--request-profile-hash", fixture.descriptor.requestProfileHash,
|
|
1093
|
-
"--session-key", loaded.entries[0].session.key,
|
|
1094
|
-
"--allow-external-llm",
|
|
1095
|
-
]);
|
|
1096
|
-
expect(selectFrozenEntries(loaded, transport).map((entry) => entry.session.key))
|
|
1097
|
-
.toEqual([loaded.entries[0].session.key]);
|
|
1098
|
-
const full = parseCliOptions([
|
|
1099
|
-
"--mode", "full",
|
|
1100
|
-
"--input-bundle", fixture.bundleDir,
|
|
1101
|
-
"--bundle-hash", fixture.descriptor.bundleHash,
|
|
1102
|
-
"--request-profile-hash", fixture.descriptor.requestProfileHash,
|
|
1103
|
-
"--allow-external-llm",
|
|
1104
|
-
"--allow-full-run",
|
|
1105
|
-
]);
|
|
1106
|
-
expect(selectFrozenEntries(loaded, full)).toHaveLength(fixture.descriptor.eligibleSessions);
|
|
1107
|
-
});
|
|
1108
|
-
});
|
|
1109
|
-
|
|
1110
|
-
describe("pivot-distill resume identity and CLI", () => {
|
|
1111
|
-
it("changes the stamp when content, prompt, or model changes", () => {
|
|
1112
|
-
const base = {
|
|
1113
|
-
harness: "codex" as const,
|
|
1114
|
-
rawHarness: "codex" as const,
|
|
1115
|
-
sessionId: "s1",
|
|
1116
|
-
date: "2026-08-02",
|
|
1117
|
-
sizeBytes: 100,
|
|
1118
|
-
mtimeNs: "1",
|
|
1119
|
-
promptVersion: "p1",
|
|
1120
|
-
model: "m1",
|
|
1121
|
-
};
|
|
1122
|
-
const fingerprint = sessionFingerprint(base);
|
|
1123
|
-
expect(sessionFingerprint({ ...base, sizeBytes: 101 })).not.toBe(fingerprint);
|
|
1124
|
-
expect(sessionFingerprint({ ...base, promptVersion: "p2" })).not.toBe(fingerprint);
|
|
1125
|
-
expect(sessionFingerprint({ ...base, model: "m2" })).not.toBe(fingerprint);
|
|
1126
|
-
expect(sessionFingerprint({ ...base, rawHarness: "antigravity" })).not.toBe(fingerprint);
|
|
1127
|
-
expect(sessionFingerprint({ ...base, date: "2026-08-03" })).not.toBe(fingerprint);
|
|
1128
|
-
expect(sessionFingerprint({ ...base, pipelineVersion: "pipeline-v2" })).not.toBe(fingerprint);
|
|
1129
|
-
expect(sessionFingerprint({ ...base, sampleSha256: "sample-v2" })).not.toBe(fingerprint);
|
|
1130
|
-
expect(sessionFingerprint({ ...base, requestProfileHash: "profile-v2" })).not.toBe(fingerprint);
|
|
1131
|
-
});
|
|
1132
|
-
|
|
1133
|
-
it("excludes synthetic digest sessions without keyword-deciding pivots", () => {
|
|
1134
|
-
expect(isSyntheticSession({ path: "/private/tmp/session-digest-a/x.jsonl", project: "x" })).toBeTrue();
|
|
1135
|
-
expect(isSyntheticSession({ path: "/sessions/x.jsonl", project: "digest/a" })).toBeTrue();
|
|
1136
|
-
expect(isSyntheticSession({ path: "/sessions/x.jsonl", project: "recallnest" })).toBeFalse();
|
|
1137
|
-
});
|
|
1138
|
-
|
|
1139
|
-
it("defaults to all non-synthetic sessions and parses frozen execution selectors", () => {
|
|
1140
|
-
const defaults = parseCliOptions([]);
|
|
1141
|
-
expect(defaults.mode).toBe("inventory");
|
|
1142
|
-
expect(defaults.minSize).toBe(1);
|
|
1143
|
-
const parsed = parseCliOptions([
|
|
1144
|
-
"--mode", "estimate",
|
|
1145
|
-
"--min-size", "200000",
|
|
1146
|
-
"--limit", "12",
|
|
1147
|
-
"--claude-desktop-root", "/tmp/desktop-import",
|
|
1148
|
-
"--agy-archive-root", "/tmp/agy-archive",
|
|
1149
|
-
]);
|
|
1150
|
-
expect(parsed.mode).toBe("estimate");
|
|
1151
|
-
expect(parsed.minSize).toBe(200_000);
|
|
1152
|
-
expect(parsed.limit).toBe(12);
|
|
1153
|
-
expect(parsed.claudeDesktopRoot).toBe("/tmp/desktop-import");
|
|
1154
|
-
expect(parsed.agyArchiveRoot).toBe("/tmp/agy-archive");
|
|
1155
|
-
const transport = parseCliOptions([
|
|
1156
|
-
"--mode", "transport",
|
|
1157
|
-
"--input-bundle", "/tmp/bundle",
|
|
1158
|
-
"--bundle-hash", "bundle",
|
|
1159
|
-
"--request-profile-hash", "profile",
|
|
1160
|
-
"--session-key", "codex:one",
|
|
1161
|
-
"--allow-external-llm",
|
|
1162
|
-
]);
|
|
1163
|
-
expect(transport.mode).toBe("transport");
|
|
1164
|
-
expect(transport.sessionKeys).toEqual(["codex:one"]);
|
|
1165
|
-
expect(transport.allowExternalLlm).toBeTrue();
|
|
1166
|
-
});
|
|
1167
|
-
|
|
1168
|
-
it("rejects concurrent writers to the same output directory", () => {
|
|
1169
|
-
const outputDir = mkdtempSync(join(tmpdir(), "pivot-distill-lock-"));
|
|
1170
|
-
chmodSync(outputDir, 0o755);
|
|
1171
|
-
const release = acquireOutputLock(outputDir);
|
|
1172
|
-
expect(statSync(outputDir).mode & 0o777).toBe(0o700);
|
|
1173
|
-
expect(() => acquireOutputLock(outputDir)).toThrow("already in use");
|
|
1174
|
-
release();
|
|
1175
|
-
const releaseAgain = acquireOutputLock(outputDir);
|
|
1176
|
-
releaseAgain();
|
|
1177
|
-
});
|
|
1178
|
-
});
|