@unerr-ai/unerr 0.1.6 → 0.1.7
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/README.md +70 -194
- package/dist/cli.js +39149 -36991
- package/package.json +9 -2
- package/dist/__tests__/architecture-guard.test.js +0 -122
- package/dist/__tests__/arg-validator.test.js +0 -205
- package/dist/__tests__/ast-extractor.test.js +0 -203
- package/dist/__tests__/auto-bootstrap.test.js +0 -280
- package/dist/__tests__/background-indexer.test.js +0 -228
- package/dist/__tests__/blast-radius-engine.test.js +0 -200
- package/dist/__tests__/bridge-isolation.test.js +0 -37
- package/dist/__tests__/budget-enforcer.test.js +0 -53
- package/dist/__tests__/cfg-test-detection-perf.test.js +0 -82
- package/dist/__tests__/change-narrative.test.js +0 -190
- package/dist/__tests__/check-commit.test.js +0 -258
- package/dist/__tests__/checksum.test.js +0 -34
- package/dist/__tests__/commit-watcher.test.js +0 -154
- package/dist/__tests__/community-detection.test.js +0 -179
- package/dist/__tests__/community-tools.test.js +0 -299
- package/dist/__tests__/components.test.js +0 -449
- package/dist/__tests__/compression-log.test.js +0 -174
- package/dist/__tests__/compression-quality-monitor.test.js +0 -40
- package/dist/__tests__/config-healer.test.js +0 -165
- package/dist/__tests__/context-ledger.test.js +0 -58
- package/dist/__tests__/convention-detector.test.js +0 -99
- package/dist/__tests__/convention-learner.test.js +0 -86
- package/dist/__tests__/correction-detector.test.js +0 -330
- package/dist/__tests__/daemon-autostart-install.test.js +0 -283
- package/dist/__tests__/daemon-bridge.test.js +0 -222
- package/dist/__tests__/daemon-dashboard.test.js +0 -202
- package/dist/__tests__/daemon-registry.test.js +0 -240
- package/dist/__tests__/daemon-supervisor.test.js +0 -318
- package/dist/__tests__/daemon-version-check.test.js +0 -275
- package/dist/__tests__/decision-point-detector.test.js +0 -98
- package/dist/__tests__/deep-link.test.js +0 -143
- package/dist/__tests__/disallowed-tools.test.js +0 -115
- package/dist/__tests__/drift-tracker.test.js +0 -582
- package/dist/__tests__/durability-scorer.test.js +0 -152
- package/dist/__tests__/efficiency-tracker.test.js +0 -65
- package/dist/__tests__/enrich.test.js +0 -144
- package/dist/__tests__/entity-rewind.test.js +0 -248
- package/dist/__tests__/ephemeral.test.js +0 -111
- package/dist/__tests__/exploration-cost.test.js +0 -93
- package/dist/__tests__/fact-generator.test.js +0 -197
- package/dist/__tests__/file-l0-graph.test.js +0 -244
- package/dist/__tests__/file-logger.test.js +0 -82
- package/dist/__tests__/file-outline.test.js +0 -141
- package/dist/__tests__/file-read-protocol.test.js +0 -188
- package/dist/__tests__/format-encoder.test.js +0 -233
- package/dist/__tests__/git-attribution.test.js +0 -259
- package/dist/__tests__/graph-temporal-joiner.test.js +0 -219
- package/dist/__tests__/health-grade-enhanced.test.js +0 -138
- package/dist/__tests__/health-map-data.test.js +0 -173
- package/dist/__tests__/helpers/mcp-harness.js +0 -45
- package/dist/__tests__/helpers/mcp-harness.test.js +0 -68
- package/dist/__tests__/hook-dedup.test.js +0 -112
- package/dist/__tests__/hook-runner.test.js +0 -253
- package/dist/__tests__/indexer-cfg.test.js +0 -185
- package/dist/__tests__/indexer-cross-file.test.js +0 -172
- package/dist/__tests__/indexer-extraction.test.js +0 -245
- package/dist/__tests__/indexer-incremental.test.js +0 -232
- package/dist/__tests__/indexer-language-expansion.test.js +0 -165
- package/dist/__tests__/init-push.test.js +0 -131
- package/dist/__tests__/instruction-writer.test.js +0 -179
- package/dist/__tests__/intelligence-integration.test.js +0 -217
- package/dist/__tests__/intent-correlator.test.js +0 -175
- package/dist/__tests__/intent-detector.test.js +0 -235
- package/dist/__tests__/intent-encoder.test.js +0 -167
- package/dist/__tests__/java-build-tool-detection.test.js +0 -174
- package/dist/__tests__/layer3-sprint-q.test.js +0 -160
- package/dist/__tests__/layer3-sprint-r.test.js +0 -91
- package/dist/__tests__/layer3-sprint-s.test.js +0 -183
- package/dist/__tests__/layer3-sprint-t.test.js +0 -201
- package/dist/__tests__/layer3-sprint-u.test.js +0 -174
- package/dist/__tests__/layer4-sprint-ba2.test.js +0 -354
- package/dist/__tests__/layer4-sprint-ba4.test.js +0 -84
- package/dist/__tests__/layer4-sprint-vs.test.js +0 -105
- package/dist/__tests__/ledger-chains.test.js +0 -162
- package/dist/__tests__/lifecycle-machine.test.js +0 -226
- package/dist/__tests__/local-chat-provider.test.js +0 -170
- package/dist/__tests__/local-convention-detector.test.js +0 -308
- package/dist/__tests__/local-embeddings.test.js +0 -422
- package/dist/__tests__/local-graph.test.js +0 -540
- package/dist/__tests__/local-indexer.test.js +0 -228
- package/dist/__tests__/local-intelligence-l3.test.js +0 -332
- package/dist/__tests__/local-llm.test.js +0 -253
- package/dist/__tests__/local-mode-offline.test.js +0 -187
- package/dist/__tests__/local-mode-stats.test.js +0 -273
- package/dist/__tests__/local-mode-tui.test.js +0 -343
- package/dist/__tests__/local-parse.test.js +0 -199
- package/dist/__tests__/log-tailer.test.js +0 -208
- package/dist/__tests__/loop-breaker.test.js +0 -276
- package/dist/__tests__/loop-miner.test.js +0 -226
- package/dist/__tests__/mcp-config.test.js +0 -126
- package/dist/__tests__/mcp-content-json.test.js +0 -10
- package/dist/__tests__/mcp-envelope.test.js +0 -124
- package/dist/__tests__/metrics-store.test.js +0 -223
- package/dist/__tests__/native-watcher.test.js +0 -191
- package/dist/__tests__/navigation-hooks-agent-aware.test.js +0 -145
- package/dist/__tests__/negative-knowledge.test.js +0 -116
- package/dist/__tests__/network-boundary.test.js +0 -190
- package/dist/__tests__/network-firewall.test.js +0 -112
- package/dist/__tests__/nudge-invariants.test.js +0 -160
- package/dist/__tests__/nudge-v2.test.js +0 -225
- package/dist/__tests__/offline-rewind.test.js +0 -251
- package/dist/__tests__/open-threads.test.js +0 -89
- package/dist/__tests__/output-compressor.test.js +0 -93
- package/dist/__tests__/pending-violations.test.js +0 -112
- package/dist/__tests__/persistence-effectiveness.test.js +0 -143
- package/dist/__tests__/provider-factory.test.js +0 -42
- package/dist/__tests__/providers.test.js +0 -24
- package/dist/__tests__/proxy.test.js +0 -314
- package/dist/__tests__/query-router.test.js +0 -1018
- package/dist/__tests__/reasoning-quality-route.test.js +0 -138
- package/dist/__tests__/redactor.test.js +0 -120
- package/dist/__tests__/resource-monitor.test.js +0 -57
- package/dist/__tests__/response-envelope.test.js +0 -100
- package/dist/__tests__/risk-classifier.test.js +0 -101
- package/dist/__tests__/risk-signal-scope.test.js +0 -75
- package/dist/__tests__/rule-evaluator.test.js +0 -280
- package/dist/__tests__/scip-decoder.test.js +0 -49
- package/dist/__tests__/scip-downloader.test.js +0 -201
- package/dist/__tests__/scip-merger.test.js +0 -103
- package/dist/__tests__/search-index.test.js +0 -422
- package/dist/__tests__/semantic-enrichment.test.js +0 -360
- package/dist/__tests__/session-brief-builder.test.js +0 -187
- package/dist/__tests__/session-context.test.js +0 -221
- package/dist/__tests__/session-continuity.test.js +0 -144
- package/dist/__tests__/session-dedup.test.js +0 -74
- package/dist/__tests__/session-event-wiring.test.js +0 -206
- package/dist/__tests__/session-events.test.js +0 -149
- package/dist/__tests__/session-legend.test.js +0 -20
- package/dist/__tests__/session-persistence.test.js +0 -131
- package/dist/__tests__/session-resume-block.test.js +0 -107
- package/dist/__tests__/session-resume.test.js +0 -97
- package/dist/__tests__/session-summary-writer.test.js +0 -134
- package/dist/__tests__/shadow-ledger.test.js +0 -203
- package/dist/__tests__/shell-classifier.test.js +0 -151
- package/dist/__tests__/shell-compression-floor.test.js +0 -189
- package/dist/__tests__/shell-compression-v2.test.js +0 -339
- package/dist/__tests__/shell-compressor.test.js +0 -35
- package/dist/__tests__/shell-hooks.test.js +0 -128
- package/dist/__tests__/shell-strategies.test.js +0 -644
- package/dist/__tests__/shell-tee.test.js +0 -133
- package/dist/__tests__/signal-dedup.test.js +0 -158
- package/dist/__tests__/signal-reinforcer.test.js +0 -77
- package/dist/__tests__/signal-scorer.test.js +0 -251
- package/dist/__tests__/signal-show-store.test.js +0 -108
- package/dist/__tests__/smart-truncate.test.js +0 -215
- package/dist/__tests__/snapshot-v2.test.js +0 -113
- package/dist/__tests__/sprint-l1-local-mode.test.js +0 -130
- package/dist/__tests__/sprint-l10-boot.test.js +0 -220
- package/dist/__tests__/sprint-l9-offline-commands.test.js +0 -189
- package/dist/__tests__/sprint-q-persistent-context.test.js +0 -198
- package/dist/__tests__/sprint-s1-wiring.test.js +0 -215
- package/dist/__tests__/sprint-s2-wiring.test.js +0 -256
- package/dist/__tests__/sprint-s3-wiring.test.js +0 -195
- package/dist/__tests__/sprint-s4-wiring.test.js +0 -213
- package/dist/__tests__/sprint-s6-hooks.test.js +0 -222
- package/dist/__tests__/sprint-s7-persistent.test.js +0 -263
- package/dist/__tests__/sprint-s8-value.test.js +0 -167
- package/dist/__tests__/sprint-s9-behavioral.test.js +0 -179
- package/dist/__tests__/sprint3-intelligence.test.js +0 -297
- package/dist/__tests__/sprint5-mcp-server.test.js +0 -136
- package/dist/__tests__/startup-display.test.js +0 -302
- package/dist/__tests__/startup-log-file.test.js +0 -97
- package/dist/__tests__/stash-manager.test.js +0 -229
- package/dist/__tests__/state-detector.test.js +0 -92
- package/dist/__tests__/status-dashboard.test.js +0 -142
- package/dist/__tests__/temporal-facts.test.js +0 -292
- package/dist/__tests__/temporal-routes.test.js +0 -142
- package/dist/__tests__/test-detector.test.js +0 -174
- package/dist/__tests__/theme.test.js +0 -72
- package/dist/__tests__/timeline-agents.test.js +0 -122
- package/dist/__tests__/timeline-bootstrap.test.js +0 -176
- package/dist/__tests__/timeline-filters.test.js +0 -193
- package/dist/__tests__/timeline-markers.test.js +0 -151
- package/dist/__tests__/timeline-routes.test.js +0 -156
- package/dist/__tests__/timeline-store.test.js +0 -171
- package/dist/__tests__/token-counter.test.js +0 -86
- package/dist/__tests__/token-estimator.test.js +0 -96
- package/dist/__tests__/token-flow-api.test.js +0 -239
- package/dist/__tests__/token-flow-instrumentation.test.js +0 -437
- package/dist/__tests__/token-flow-persistence.test.js +0 -356
- package/dist/__tests__/token-flow-routes.test.js +0 -199
- package/dist/__tests__/token-flow.test.js +0 -695
- package/dist/__tests__/tool-clusters.test.js +0 -177
- package/dist/__tests__/transport-mux.test.js +0 -283
- package/dist/__tests__/turn-segmenter.test.js +0 -166
- package/dist/__tests__/uninstall.test.js +0 -141
- package/dist/__tests__/warm-start-policy.test.js +0 -271
- package/dist/__tests__/wire-cap-nudge.test.js +0 -77
- package/dist/__tests__/worker-pool.test.js +0 -101
- package/dist/ui/assets/index-7gl3mIuY.css +0 -1
- package/dist/ui/assets/index-CX4FCWGT.js +0 -10
- package/dist/ui/assets/rolldown-runtime-S-ySWqyJ.js +0 -1
- package/dist/ui/assets/vis-network-NIJHUFI3.js +0 -908
- package/dist/ui/fonts/jetbrains-mono-latin-400-normal.woff +0 -0
- package/dist/ui/icon-wordmark.png +0 -0
- package/dist/ui/icon-wordmark.svg +0 -30
- package/dist/ui/icon.png +0 -0
- package/dist/ui/icon.svg +0 -25
- package/dist/ui/index.html +0 -15
- package/dist/ui/prototype-sandbox/index.html +0 -257
- package/dist/ui/screenshots/activity.png +0 -0
- package/dist/ui/screenshots/code-base-intelligence.png +0 -0
- package/dist/ui/screenshots/dashboard.png +0 -0
- package/dist/ui/screenshots/project-memory.png +0 -0
- package/dist/ui/screenshots/reasoning-quality.png +0 -0
- package/dist/ui/screenshots/reasoning-session.png +0 -0
- package/dist/ui/screenshots/token-session.png +0 -0
- package/dist/ui/screenshots/token-trace-main.png +0 -0
- package/dist/ui/screenshots/token-turn.png +0 -0
- package/dist/ui/unerr-wordmark.png +0 -0
- package/dist/ui/unerr-wordmark.svg +0 -9
- package/dist/ui/unerr.png +0 -0
- package/dist/ui/unerr.svg +0 -25
- package/dist/ui/web-app-manifest-192x192.png +0 -0
- package/dist/ui/web-app-manifest-512x512.png +0 -0
|
@@ -1,582 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P10-TEST-03: Drift tracker tests — drift computation, overlay management.
|
|
3
|
-
*/
|
|
4
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
8
|
-
import { entityKey } from "../intelligence/ast-extractor.js";
|
|
9
|
-
import { DRIFT_WATCHER_OPTIONS, DriftTracker, MtimeCache, determineOrigin, } from "../tracking/drift-tracker.js";
|
|
10
|
-
import { FileHashManager, contentSha256 } from "../tracking/file-hash-state.js";
|
|
11
|
-
let tempDir;
|
|
12
|
-
let projectRoot;
|
|
13
|
-
let unerrDir;
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
tempDir = join(tmpdir(), `unerr-drift-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
16
|
-
projectRoot = join(tempDir, "project");
|
|
17
|
-
unerrDir = join(projectRoot, ".unerr");
|
|
18
|
-
mkdirSync(join(unerrDir, "state"), { recursive: true });
|
|
19
|
-
mkdirSync(join(projectRoot, "src"), { recursive: true });
|
|
20
|
-
});
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
try {
|
|
23
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
/* ignore */
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
/** Create a mock CozoGraphStore with in-memory drift overlay */
|
|
30
|
-
function createMockGraph(baseEntities = []) {
|
|
31
|
-
const driftOverlay = new Map();
|
|
32
|
-
return {
|
|
33
|
-
driftOverlay,
|
|
34
|
-
getEntity: (key) => baseEntities.find((e) => e.key === key) ?? null,
|
|
35
|
-
getCallersOf: () => [],
|
|
36
|
-
getCalleesOf: () => [],
|
|
37
|
-
getEntitiesByFile: (fp) => baseEntities.filter((e) => e.file_path === fp),
|
|
38
|
-
searchEntities: () => [],
|
|
39
|
-
getImports: () => [],
|
|
40
|
-
healthCheck: () => ({ status: "up", latencyMs: 0 }),
|
|
41
|
-
isLoaded: () => true,
|
|
42
|
-
loadSnapshot: () => { },
|
|
43
|
-
hasRules: () => false,
|
|
44
|
-
getRules: () => [],
|
|
45
|
-
getPatterns: () => [],
|
|
46
|
-
loadRules: () => { },
|
|
47
|
-
loadPatterns: () => { },
|
|
48
|
-
hasJustifications: () => false,
|
|
49
|
-
getBusinessContext: () => null,
|
|
50
|
-
getConventions: () => [],
|
|
51
|
-
loadJustifications: () => { },
|
|
52
|
-
upsertDriftEntity: (entity) => {
|
|
53
|
-
driftOverlay.set(entity.key, entity);
|
|
54
|
-
},
|
|
55
|
-
removeDriftEntity: (key) => {
|
|
56
|
-
driftOverlay.delete(key);
|
|
57
|
-
},
|
|
58
|
-
getDriftEntitiesForFile: (fp) => {
|
|
59
|
-
const result = [];
|
|
60
|
-
for (const [, e] of driftOverlay) {
|
|
61
|
-
if (e.file_path === fp)
|
|
62
|
-
result.push(e);
|
|
63
|
-
}
|
|
64
|
-
return result;
|
|
65
|
-
},
|
|
66
|
-
clearDriftOverlay: () => {
|
|
67
|
-
driftOverlay.clear();
|
|
68
|
-
},
|
|
69
|
-
findEntityByName: (name) => baseEntities.find((e) => e.name === name) ?? null,
|
|
70
|
-
getAllDriftEdges: () => [],
|
|
71
|
-
upsertDriftEdge: () => { },
|
|
72
|
-
clearDriftEdges: () => { },
|
|
73
|
-
getDriftSummary: () => {
|
|
74
|
-
const summary = {
|
|
75
|
-
added: 0,
|
|
76
|
-
modified: 0,
|
|
77
|
-
deleted: 0,
|
|
78
|
-
dependency_changed: 0,
|
|
79
|
-
total: 0,
|
|
80
|
-
};
|
|
81
|
-
for (const [, e] of driftOverlay) {
|
|
82
|
-
if (e.drift_status === "added")
|
|
83
|
-
summary.added++;
|
|
84
|
-
else if (e.drift_status === "modified")
|
|
85
|
-
summary.modified++;
|
|
86
|
-
else if (e.drift_status === "deleted")
|
|
87
|
-
summary.deleted++;
|
|
88
|
-
else if (e.drift_status === "dependency_changed")
|
|
89
|
-
summary.dependency_changed++;
|
|
90
|
-
}
|
|
91
|
-
summary.total =
|
|
92
|
-
summary.added +
|
|
93
|
-
summary.modified +
|
|
94
|
-
summary.deleted +
|
|
95
|
-
summary.dependency_changed;
|
|
96
|
-
return summary;
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Create a mock graph with caller edge relationships.
|
|
102
|
-
* `callerMap` maps callee keys → array of caller LocalEntity objects.
|
|
103
|
-
*/
|
|
104
|
-
function createMockGraphWithCallers(baseEntities, callerMap) {
|
|
105
|
-
const base = createMockGraph(baseEntities);
|
|
106
|
-
// Override getCallersOf to return from the caller map
|
|
107
|
-
base.getCallersOf = (key) => callerMap.get(key) ?? [];
|
|
108
|
-
return base;
|
|
109
|
-
}
|
|
110
|
-
describe("DriftTracker", () => {
|
|
111
|
-
it("detects added entities (new file)", async () => {
|
|
112
|
-
const graph = createMockGraph();
|
|
113
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
114
|
-
const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
|
|
115
|
-
// Write a TypeScript file with a function
|
|
116
|
-
writeFileSync(join(projectRoot, "src/new-feature.ts"), `export function newFeature() {
|
|
117
|
-
return "hello"
|
|
118
|
-
}`);
|
|
119
|
-
const result = await tracker.processFile("src/new-feature.ts", "abc123");
|
|
120
|
-
expect(result.filesProcessed).toBe(1);
|
|
121
|
-
expect(result.entitiesAdded).toBe(1);
|
|
122
|
-
expect(result.entitiesModified).toBe(0);
|
|
123
|
-
expect(result.entitiesDeleted).toBe(0);
|
|
124
|
-
expect(graph.driftOverlay.size).toBe(1);
|
|
125
|
-
const driftEntity = [...graph.driftOverlay.values()][0];
|
|
126
|
-
expect(driftEntity.name).toBe("newFeature");
|
|
127
|
-
expect(driftEntity.drift_status).toBe("added");
|
|
128
|
-
expect(driftEntity.file_path).toBe("src/new-feature.ts");
|
|
129
|
-
});
|
|
130
|
-
it("detects modified entities (content changed)", async () => {
|
|
131
|
-
// Base entity
|
|
132
|
-
const baseEntities = [
|
|
133
|
-
{
|
|
134
|
-
key: "test-key-1",
|
|
135
|
-
kind: "function",
|
|
136
|
-
name: "existingFn",
|
|
137
|
-
file_path: "src/existing.ts",
|
|
138
|
-
start_line: 1,
|
|
139
|
-
end_line: 0,
|
|
140
|
-
signature: "()",
|
|
141
|
-
body: "function existingFn() {\n return 1\n}",
|
|
142
|
-
fan_in: 0,
|
|
143
|
-
fan_out: 0,
|
|
144
|
-
risk_level: "normal",
|
|
145
|
-
community: -1,
|
|
146
|
-
},
|
|
147
|
-
];
|
|
148
|
-
const graph = createMockGraph(baseEntities);
|
|
149
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
150
|
-
const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
|
|
151
|
-
// Write modified file
|
|
152
|
-
writeFileSync(join(projectRoot, "src/existing.ts"), `function existingFn() {
|
|
153
|
-
return 2
|
|
154
|
-
}`);
|
|
155
|
-
const result = await tracker.processFile("src/existing.ts", "abc123");
|
|
156
|
-
expect(result.filesProcessed).toBe(1);
|
|
157
|
-
// Either modified or added depending on key match
|
|
158
|
-
expect(result.entitiesModified + result.entitiesAdded).toBeGreaterThanOrEqual(1);
|
|
159
|
-
});
|
|
160
|
-
it("skips unchanged files", async () => {
|
|
161
|
-
const graph = createMockGraph();
|
|
162
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
163
|
-
const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
|
|
164
|
-
const content = "export function unchanged() { return 1 }";
|
|
165
|
-
writeFileSync(join(projectRoot, "src/unchanged.ts"), content);
|
|
166
|
-
// First process — should process
|
|
167
|
-
const r1 = await tracker.processFile("src/unchanged.ts", "abc123");
|
|
168
|
-
expect(r1.filesProcessed).toBe(1);
|
|
169
|
-
// Mark as processed manually
|
|
170
|
-
const sha = contentSha256(content);
|
|
171
|
-
fileHashManager.markProcessed("src/unchanged.ts", sha, "abc123");
|
|
172
|
-
fileHashManager.save();
|
|
173
|
-
// Second process — should skip
|
|
174
|
-
const r2 = await tracker.processFile("src/unchanged.ts", "abc123");
|
|
175
|
-
expect(r2.filesSkipped).toBe(1);
|
|
176
|
-
expect(r2.filesProcessed).toBe(0);
|
|
177
|
-
});
|
|
178
|
-
it("handles deleted files", async () => {
|
|
179
|
-
const baseEntities = [
|
|
180
|
-
{
|
|
181
|
-
key: "del-key-1",
|
|
182
|
-
kind: "function",
|
|
183
|
-
name: "deletedFn",
|
|
184
|
-
file_path: "src/deleted.ts",
|
|
185
|
-
start_line: 1,
|
|
186
|
-
end_line: 0,
|
|
187
|
-
signature: "",
|
|
188
|
-
body: "",
|
|
189
|
-
fan_in: 2,
|
|
190
|
-
fan_out: 1,
|
|
191
|
-
risk_level: "normal",
|
|
192
|
-
community: -1,
|
|
193
|
-
},
|
|
194
|
-
];
|
|
195
|
-
const graph = createMockGraph(baseEntities);
|
|
196
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
197
|
-
const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
|
|
198
|
-
// Don't create the file — it's "deleted"
|
|
199
|
-
const result = await tracker.processFile("src/deleted.ts", "abc123");
|
|
200
|
-
expect(result.filesProcessed).toBe(1);
|
|
201
|
-
expect(result.entitiesDeleted).toBe(1);
|
|
202
|
-
const deletedEntity = [...graph.driftOverlay.values()][0];
|
|
203
|
-
expect(deletedEntity.drift_status).toBe("deleted");
|
|
204
|
-
expect(deletedEntity.name).toBe("deletedFn");
|
|
205
|
-
});
|
|
206
|
-
it("skips unsupported languages", async () => {
|
|
207
|
-
const graph = createMockGraph();
|
|
208
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
209
|
-
const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
|
|
210
|
-
writeFileSync(join(projectRoot, "data.json"), '{"key": "value"}');
|
|
211
|
-
const result = await tracker.processFile("data.json", "abc123");
|
|
212
|
-
expect(result.filesProcessed).toBe(0);
|
|
213
|
-
expect(result.filesSkipped).toBe(0);
|
|
214
|
-
expect(graph.driftOverlay.size).toBe(0);
|
|
215
|
-
});
|
|
216
|
-
it("processes batch of files", async () => {
|
|
217
|
-
const graph = createMockGraph();
|
|
218
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
219
|
-
const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
|
|
220
|
-
writeFileSync(join(projectRoot, "src/a.ts"), "export function alpha() { return 1 }");
|
|
221
|
-
writeFileSync(join(projectRoot, "src/b.ts"), "export function beta() { return 2 }");
|
|
222
|
-
const result = await tracker.processFiles(["src/a.ts", "src/b.ts"], "abc123");
|
|
223
|
-
expect(result.filesProcessed).toBe(2);
|
|
224
|
-
expect(result.entitiesAdded).toBe(2);
|
|
225
|
-
});
|
|
226
|
-
it("drift summary aggregates correctly", async () => {
|
|
227
|
-
const baseEntities = [
|
|
228
|
-
{
|
|
229
|
-
key: "mod-key-1",
|
|
230
|
-
kind: "function",
|
|
231
|
-
name: "oldFn",
|
|
232
|
-
file_path: "src/old.ts",
|
|
233
|
-
start_line: 1,
|
|
234
|
-
end_line: 0,
|
|
235
|
-
signature: "",
|
|
236
|
-
body: "function oldFn() { return 1 }",
|
|
237
|
-
fan_in: 0,
|
|
238
|
-
fan_out: 0,
|
|
239
|
-
risk_level: "normal",
|
|
240
|
-
community: -1,
|
|
241
|
-
},
|
|
242
|
-
];
|
|
243
|
-
const graph = createMockGraph(baseEntities);
|
|
244
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
245
|
-
const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
|
|
246
|
-
// Add a new file
|
|
247
|
-
writeFileSync(join(projectRoot, "src/new.ts"), "export function newOne() { return 1 }");
|
|
248
|
-
await tracker.processFile("src/new.ts", "abc123");
|
|
249
|
-
// Delete an existing file (don't write it)
|
|
250
|
-
await tracker.processFile("src/old.ts", "abc123");
|
|
251
|
-
const summary = await tracker.getDriftSummary();
|
|
252
|
-
expect(summary.total).toBeGreaterThan(0);
|
|
253
|
-
expect(summary.added + summary.modified + summary.deleted).toBe(summary.total);
|
|
254
|
-
});
|
|
255
|
-
it("clears overlay on branch switch", async () => {
|
|
256
|
-
const graph = createMockGraph();
|
|
257
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
258
|
-
const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
|
|
259
|
-
writeFileSync(join(projectRoot, "src/a.ts"), "export function alpha() { return 1 }");
|
|
260
|
-
await tracker.processFile("src/a.ts", "abc123");
|
|
261
|
-
expect(graph.driftOverlay.size).toBe(1);
|
|
262
|
-
// Branch switch — clears and recomputes
|
|
263
|
-
writeFileSync(join(projectRoot, "src/b.ts"), "export function beta() { return 1 }");
|
|
264
|
-
await tracker.onBranchSwitch(["src/b.ts"], "def456");
|
|
265
|
-
// Old overlay should be cleared, new file processed
|
|
266
|
-
const summary = await tracker.getDriftSummary();
|
|
267
|
-
expect(summary.added).toBe(1);
|
|
268
|
-
});
|
|
269
|
-
it("saves drift summary to disk", async () => {
|
|
270
|
-
const graph = createMockGraph();
|
|
271
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
272
|
-
const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
|
|
273
|
-
writeFileSync(join(projectRoot, "src/a.ts"), "export function alpha() { return 1 }");
|
|
274
|
-
await tracker.processFiles(["src/a.ts"], "abc123");
|
|
275
|
-
const summaryPath = join(unerrDir, "drift", "drift_summary.json");
|
|
276
|
-
expect(existsSync(summaryPath)).toBe(true);
|
|
277
|
-
});
|
|
278
|
-
it("mtime cache skips file when mtime unchanged", async () => {
|
|
279
|
-
const graph = createMockGraph();
|
|
280
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
281
|
-
const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
|
|
282
|
-
const filePath = join(projectRoot, "src/stable.ts");
|
|
283
|
-
writeFileSync(filePath, "export function stable() { return 1 }");
|
|
284
|
-
// First call — processes the file (mtime is new)
|
|
285
|
-
const r1 = await tracker.processFile("src/stable.ts", "abc123");
|
|
286
|
-
expect(r1.filesProcessed).toBe(1);
|
|
287
|
-
// Second call — mtime unchanged, should skip via mtime cache
|
|
288
|
-
const r2 = await tracker.processFile("src/stable.ts", "abc123");
|
|
289
|
-
expect(r2.filesSkipped).toBe(1);
|
|
290
|
-
expect(r2.filesProcessed).toBe(0);
|
|
291
|
-
});
|
|
292
|
-
it("mtime cache processes file when content changes", async () => {
|
|
293
|
-
const graph = createMockGraph();
|
|
294
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
295
|
-
const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
|
|
296
|
-
const filePath = join(projectRoot, "src/changing.ts");
|
|
297
|
-
writeFileSync(filePath, "export function changing() { return 1 }");
|
|
298
|
-
// First call — processes
|
|
299
|
-
await tracker.processFile("src/changing.ts", "abc123");
|
|
300
|
-
// Modify file — mtime changes
|
|
301
|
-
writeFileSync(filePath, "export function changing() { return 2 }");
|
|
302
|
-
// Second call — mtime changed, should process
|
|
303
|
-
const r2 = await tracker.processFile("src/changing.ts", "abc123");
|
|
304
|
-
expect(r2.filesProcessed).toBe(1);
|
|
305
|
-
});
|
|
306
|
-
it("mtime cache clears on branch switch", async () => {
|
|
307
|
-
const graph = createMockGraph();
|
|
308
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
309
|
-
const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
|
|
310
|
-
const filePath = join(projectRoot, "src/branched.ts");
|
|
311
|
-
writeFileSync(filePath, "export function branched() { return 1 }");
|
|
312
|
-
// Process once to populate mtime cache
|
|
313
|
-
await tracker.processFile("src/branched.ts", "abc123");
|
|
314
|
-
// Branch switch clears mtime cache
|
|
315
|
-
await tracker.onBranchSwitch(["src/branched.ts"], "def456");
|
|
316
|
-
// After branch switch, same file should be reprocessed (mtime cache was cleared)
|
|
317
|
-
const r = await tracker.processFile("src/branched.ts", "def456");
|
|
318
|
-
// Will be processed (mtime cache cleared) but may be skipped by file hash manager
|
|
319
|
-
// since content hasn't changed. The point is mtime cache doesn't block it.
|
|
320
|
-
expect(r.filesProcessed + r.filesSkipped).toBe(1);
|
|
321
|
-
});
|
|
322
|
-
});
|
|
323
|
-
describe("MtimeCache", () => {
|
|
324
|
-
it("returns true for first check (new file)", () => {
|
|
325
|
-
const cache = new MtimeCache();
|
|
326
|
-
const filePath = join(projectRoot, "src/first.ts");
|
|
327
|
-
writeFileSync(filePath, "content");
|
|
328
|
-
expect(cache.check(filePath)).toBe(true);
|
|
329
|
-
});
|
|
330
|
-
it("returns false for unchanged file", () => {
|
|
331
|
-
const cache = new MtimeCache();
|
|
332
|
-
const filePath = join(projectRoot, "src/unchanged.ts");
|
|
333
|
-
writeFileSync(filePath, "content");
|
|
334
|
-
cache.check(filePath); // populate
|
|
335
|
-
expect(cache.check(filePath)).toBe(false);
|
|
336
|
-
});
|
|
337
|
-
it("returns true after file modification", () => {
|
|
338
|
-
const cache = new MtimeCache();
|
|
339
|
-
const filePath = join(projectRoot, "src/modified.ts");
|
|
340
|
-
writeFileSync(filePath, "content v1");
|
|
341
|
-
cache.check(filePath); // populate
|
|
342
|
-
// Modify — mtime changes
|
|
343
|
-
writeFileSync(filePath, "content v2");
|
|
344
|
-
expect(cache.check(filePath)).toBe(true);
|
|
345
|
-
});
|
|
346
|
-
it("returns true for non-existent file and evicts cache", () => {
|
|
347
|
-
const cache = new MtimeCache();
|
|
348
|
-
expect(cache.check("/nonexistent/file.ts")).toBe(true);
|
|
349
|
-
expect(cache.size).toBe(0);
|
|
350
|
-
});
|
|
351
|
-
it("evict removes file from cache", () => {
|
|
352
|
-
const cache = new MtimeCache();
|
|
353
|
-
const filePath = join(projectRoot, "src/evicted.ts");
|
|
354
|
-
writeFileSync(filePath, "content");
|
|
355
|
-
cache.check(filePath); // populate
|
|
356
|
-
expect(cache.size).toBe(1);
|
|
357
|
-
cache.evict(filePath);
|
|
358
|
-
expect(cache.size).toBe(0);
|
|
359
|
-
// Next check should return true (new entry)
|
|
360
|
-
expect(cache.check(filePath)).toBe(true);
|
|
361
|
-
});
|
|
362
|
-
it("clear removes all entries", () => {
|
|
363
|
-
const cache = new MtimeCache();
|
|
364
|
-
const file1 = join(projectRoot, "src/a.ts");
|
|
365
|
-
const file2 = join(projectRoot, "src/b.ts");
|
|
366
|
-
writeFileSync(file1, "a");
|
|
367
|
-
writeFileSync(file2, "b");
|
|
368
|
-
cache.check(file1);
|
|
369
|
-
cache.check(file2);
|
|
370
|
-
expect(cache.size).toBe(2);
|
|
371
|
-
cache.clear();
|
|
372
|
-
expect(cache.size).toBe(0);
|
|
373
|
-
});
|
|
374
|
-
});
|
|
375
|
-
describe("DRIFT_WATCHER_OPTIONS", () => {
|
|
376
|
-
it("has awaitWriteFinish with stabilityThreshold 300ms", () => {
|
|
377
|
-
expect(DRIFT_WATCHER_OPTIONS.awaitWriteFinish.stabilityThreshold).toBe(300);
|
|
378
|
-
expect(DRIFT_WATCHER_OPTIONS.awaitWriteFinish.pollInterval).toBe(100);
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
describe("determineOrigin", () => {
|
|
382
|
-
it("returns 'human' when lastSyncTimestamp is 0 (no sync)", () => {
|
|
383
|
-
expect(determineOrigin(0)).toBe("human");
|
|
384
|
-
});
|
|
385
|
-
it("returns 'ai' when <10s after sync", () => {
|
|
386
|
-
const recentSync = Date.now() - 5_000; // 5s ago
|
|
387
|
-
expect(determineOrigin(recentSync)).toBe("ai");
|
|
388
|
-
});
|
|
389
|
-
it("returns 'mixed' when 10-60s after sync", () => {
|
|
390
|
-
const mediumSync = Date.now() - 30_000; // 30s ago
|
|
391
|
-
expect(determineOrigin(mediumSync)).toBe("mixed");
|
|
392
|
-
});
|
|
393
|
-
it("returns 'human' when >60s after sync", () => {
|
|
394
|
-
const oldSync = Date.now() - 120_000; // 2min ago
|
|
395
|
-
expect(determineOrigin(oldSync)).toBe("human");
|
|
396
|
-
});
|
|
397
|
-
it("returns 'ai' at exactly 0ms after sync", () => {
|
|
398
|
-
const justNow = Date.now();
|
|
399
|
-
expect(determineOrigin(justNow)).toBe("ai");
|
|
400
|
-
});
|
|
401
|
-
});
|
|
402
|
-
describe("DriftTracker origin attribution", () => {
|
|
403
|
-
it("sets origin on added entities based on sync timestamp", async () => {
|
|
404
|
-
const graph = createMockGraph();
|
|
405
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
406
|
-
const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
|
|
407
|
-
// Simulate recent AI sync
|
|
408
|
-
tracker.setLastSyncTimestamp(Date.now() - 3_000);
|
|
409
|
-
writeFileSync(join(projectRoot, "src/ai-created.ts"), "export function aiCreated() { return 1 }");
|
|
410
|
-
await tracker.processFile("src/ai-created.ts", "abc123");
|
|
411
|
-
const entity = [...graph.driftOverlay.values()][0];
|
|
412
|
-
expect(entity?.origin).toBe("ai");
|
|
413
|
-
});
|
|
414
|
-
it("sets origin to 'human' when no sync has occurred", async () => {
|
|
415
|
-
const graph = createMockGraph();
|
|
416
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
417
|
-
const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
|
|
418
|
-
// No setLastSyncTimestamp call — default is 0
|
|
419
|
-
writeFileSync(join(projectRoot, "src/human-created.ts"), "export function humanCreated() { return 1 }");
|
|
420
|
-
await tracker.processFile("src/human-created.ts", "abc123");
|
|
421
|
-
const entity = [...graph.driftOverlay.values()][0];
|
|
422
|
-
expect(entity?.origin).toBe("human");
|
|
423
|
-
});
|
|
424
|
-
it("sets origin on deleted entities", async () => {
|
|
425
|
-
const baseEntities = [
|
|
426
|
-
{
|
|
427
|
-
key: "del-origin-key",
|
|
428
|
-
kind: "function",
|
|
429
|
-
name: "aboutToDelete",
|
|
430
|
-
file_path: "src/will-delete.ts",
|
|
431
|
-
start_line: 1,
|
|
432
|
-
end_line: 0,
|
|
433
|
-
signature: "",
|
|
434
|
-
body: "",
|
|
435
|
-
fan_in: 0,
|
|
436
|
-
fan_out: 0,
|
|
437
|
-
risk_level: "normal",
|
|
438
|
-
community: -1,
|
|
439
|
-
},
|
|
440
|
-
];
|
|
441
|
-
const graph = createMockGraph(baseEntities);
|
|
442
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
443
|
-
const tracker = new DriftTracker({ projectRoot, repoId: "repo1", unerrDir }, graph, fileHashManager);
|
|
444
|
-
// Simulate mixed timing
|
|
445
|
-
tracker.setLastSyncTimestamp(Date.now() - 25_000);
|
|
446
|
-
// File doesn't exist — deleted
|
|
447
|
-
await tracker.processFile("src/will-delete.ts", "abc123");
|
|
448
|
-
const entity = [...graph.driftOverlay.values()][0];
|
|
449
|
-
expect(entity?.origin).toBe("mixed");
|
|
450
|
-
expect(entity?.drift_status).toBe("deleted");
|
|
451
|
-
});
|
|
452
|
-
});
|
|
453
|
-
describe("Cross-file drift invalidation", () => {
|
|
454
|
-
const repoId = "repo1";
|
|
455
|
-
/** Build a base entity with a computable key */
|
|
456
|
-
function makeBaseEntity(filePath, name, kind, body) {
|
|
457
|
-
const key = entityKey(repoId, filePath, kind, name, "()");
|
|
458
|
-
return {
|
|
459
|
-
key,
|
|
460
|
-
kind,
|
|
461
|
-
name,
|
|
462
|
-
file_path: filePath,
|
|
463
|
-
start_line: 1,
|
|
464
|
-
end_line: 0,
|
|
465
|
-
signature: "()",
|
|
466
|
-
body,
|
|
467
|
-
fan_in: 0,
|
|
468
|
-
fan_out: 0,
|
|
469
|
-
risk_level: "normal",
|
|
470
|
-
community: -1,
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
it("propagates dependency_changed to callers in other files", async () => {
|
|
474
|
-
// File A has function `helperFn`, File B has function `callerFn` that calls it
|
|
475
|
-
const helperEntity = makeBaseEntity("src/helper.ts", "helperFn", "function", "function helperFn() {\n return 1\n}");
|
|
476
|
-
const callerEntity = makeBaseEntity("src/caller.ts", "callerFn", "function", "function callerFn() {\n return helperFn()\n}");
|
|
477
|
-
const callerMap = new Map();
|
|
478
|
-
callerMap.set(helperEntity.key, [callerEntity]);
|
|
479
|
-
const graph = createMockGraphWithCallers([helperEntity, callerEntity], callerMap);
|
|
480
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
481
|
-
const tracker = new DriftTracker({ projectRoot, repoId, unerrDir }, graph, fileHashManager);
|
|
482
|
-
// Write modified helper (body changed)
|
|
483
|
-
writeFileSync(join(projectRoot, "src/helper.ts"), "export function helperFn() {\n return 999\n}");
|
|
484
|
-
// Write caller file (unchanged, but needs to exist)
|
|
485
|
-
writeFileSync(join(projectRoot, "src/caller.ts"), "export function callerFn() {\n return helperFn()\n}");
|
|
486
|
-
const result = await tracker.processFile("src/helper.ts", "abc123");
|
|
487
|
-
expect(result.crossFileInvalidated).toBe(1);
|
|
488
|
-
// callerFn should have dependency_changed in the overlay
|
|
489
|
-
const callerDrift = graph.driftOverlay.get(callerEntity.key);
|
|
490
|
-
expect(callerDrift).toBeDefined();
|
|
491
|
-
expect(callerDrift?.drift_status).toBe("dependency_changed");
|
|
492
|
-
expect(callerDrift?.file_path).toBe("src/caller.ts");
|
|
493
|
-
});
|
|
494
|
-
it("does NOT invalidate callers in the same file", async () => {
|
|
495
|
-
// Both entities in the same file
|
|
496
|
-
const entity1 = makeBaseEntity("src/same-file.ts", "baseFunc", "function", "function baseFunc() {\n return 1\n}");
|
|
497
|
-
const entity2 = makeBaseEntity("src/same-file.ts", "callerFunc", "function", "function callerFunc() {\n return baseFunc()\n}");
|
|
498
|
-
const callerMap = new Map();
|
|
499
|
-
callerMap.set(entity1.key, [entity2]); // callerFunc calls baseFunc, same file
|
|
500
|
-
const graph = createMockGraphWithCallers([entity1, entity2], callerMap);
|
|
501
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
502
|
-
const tracker = new DriftTracker({ projectRoot, repoId, unerrDir }, graph, fileHashManager);
|
|
503
|
-
writeFileSync(join(projectRoot, "src/same-file.ts"), "export function baseFunc() {\n return 999\n}\nexport function callerFunc() {\n return baseFunc()\n}");
|
|
504
|
-
const result = await tracker.processFile("src/same-file.ts", "abc123");
|
|
505
|
-
// Same-file callers should NOT be cross-file invalidated
|
|
506
|
-
expect(result.crossFileInvalidated).toBe(0);
|
|
507
|
-
});
|
|
508
|
-
it("does NOT overwrite stronger drift status with dependency_changed", async () => {
|
|
509
|
-
const helperEntity = makeBaseEntity("src/dep.ts", "depFn", "function", "function depFn() {\n return 1\n}");
|
|
510
|
-
const callerEntity = makeBaseEntity("src/consumer.ts", "consumerFn", "function", "function consumerFn() {\n return depFn()\n}");
|
|
511
|
-
const callerMap = new Map();
|
|
512
|
-
callerMap.set(helperEntity.key, [callerEntity]);
|
|
513
|
-
const graph = createMockGraphWithCallers([helperEntity, callerEntity], callerMap);
|
|
514
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
515
|
-
const tracker = new DriftTracker({ projectRoot, repoId, unerrDir }, graph, fileHashManager);
|
|
516
|
-
// Pre-populate overlay with a "modified" status for the caller
|
|
517
|
-
graph.driftOverlay.set(callerEntity.key, {
|
|
518
|
-
key: callerEntity.key,
|
|
519
|
-
name: "consumerFn",
|
|
520
|
-
kind: "function",
|
|
521
|
-
signature: "()",
|
|
522
|
-
body: "function consumerFn() { return depFn() }",
|
|
523
|
-
file_path: "src/consumer.ts",
|
|
524
|
-
line_start: 1,
|
|
525
|
-
line_end: 3,
|
|
526
|
-
content_hash: "abc",
|
|
527
|
-
drift_status: "modified",
|
|
528
|
-
intent_id: "",
|
|
529
|
-
modified_at: new Date().toISOString(),
|
|
530
|
-
origin: "human",
|
|
531
|
-
previous_body: "",
|
|
532
|
-
previous_signature: "",
|
|
533
|
-
});
|
|
534
|
-
// Modify the dependency
|
|
535
|
-
writeFileSync(join(projectRoot, "src/dep.ts"), "export function depFn() {\n return 999\n}");
|
|
536
|
-
const result = await tracker.processFile("src/dep.ts", "abc123");
|
|
537
|
-
// Should NOT overwrite the "modified" status
|
|
538
|
-
expect(result.crossFileInvalidated).toBe(0);
|
|
539
|
-
const callerDrift = graph.driftOverlay.get(callerEntity.key);
|
|
540
|
-
expect(callerDrift?.drift_status).toBe("modified");
|
|
541
|
-
});
|
|
542
|
-
it("propagates dependency_changed when entity is deleted", async () => {
|
|
543
|
-
const deletedEntity = makeBaseEntity("src/removed.ts", "removedFn", "function", "function removedFn() {\n return 1\n}");
|
|
544
|
-
const callerEntity = makeBaseEntity("src/uses-removed.ts", "usesFn", "function", "function usesFn() {\n return removedFn()\n}");
|
|
545
|
-
const callerMap = new Map();
|
|
546
|
-
callerMap.set(deletedEntity.key, [callerEntity]);
|
|
547
|
-
const graph = createMockGraphWithCallers([deletedEntity, callerEntity], callerMap);
|
|
548
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
549
|
-
const tracker = new DriftTracker({ projectRoot, repoId, unerrDir }, graph, fileHashManager);
|
|
550
|
-
// Write the caller file (exists), but don't write removed.ts (deleted)
|
|
551
|
-
writeFileSync(join(projectRoot, "src/uses-removed.ts"), "export function usesFn() {\n return removedFn()\n}");
|
|
552
|
-
// Process the deleted file — entities should propagate dependency_changed
|
|
553
|
-
const result = await tracker.processFile("src/removed.ts", "abc123");
|
|
554
|
-
expect(result.entitiesDeleted).toBe(1);
|
|
555
|
-
// The caller in another file should get dependency_changed
|
|
556
|
-
const callerDrift = graph.driftOverlay.get(callerEntity.key);
|
|
557
|
-
expect(callerDrift).toBeDefined();
|
|
558
|
-
expect(callerDrift?.drift_status).toBe("dependency_changed");
|
|
559
|
-
});
|
|
560
|
-
it("counts crossFileInvalidated correctly in batch", async () => {
|
|
561
|
-
const entityA = makeBaseEntity("src/a.ts", "funcA", "function", "function funcA() {\n return 1\n}");
|
|
562
|
-
const callerB = makeBaseEntity("src/b.ts", "funcB", "function", "function funcB() {\n return funcA()\n}");
|
|
563
|
-
const callerC = makeBaseEntity("src/c.ts", "funcC", "function", "function funcC() {\n return funcA()\n}");
|
|
564
|
-
const callerMap = new Map();
|
|
565
|
-
callerMap.set(entityA.key, [callerB, callerC]);
|
|
566
|
-
const graph = createMockGraphWithCallers([entityA, callerB, callerC], callerMap);
|
|
567
|
-
const fileHashManager = new FileHashManager(unerrDir);
|
|
568
|
-
const tracker = new DriftTracker({ projectRoot, repoId, unerrDir }, graph, fileHashManager);
|
|
569
|
-
// Modify source entity
|
|
570
|
-
writeFileSync(join(projectRoot, "src/a.ts"), "export function funcA() {\n return 999\n}");
|
|
571
|
-
writeFileSync(join(projectRoot, "src/b.ts"), "export function funcB() {\n return funcA()\n}");
|
|
572
|
-
writeFileSync(join(projectRoot, "src/c.ts"), "export function funcC() {\n return funcA()\n}");
|
|
573
|
-
const result = await tracker.processFiles(["src/a.ts"], "abc123");
|
|
574
|
-
expect(result.crossFileInvalidated).toBe(2);
|
|
575
|
-
// Both callers should be in the overlay
|
|
576
|
-
expect(graph.driftOverlay.get(callerB.key)?.drift_status).toBe("dependency_changed");
|
|
577
|
-
expect(graph.driftOverlay.get(callerC.key)?.drift_status).toBe("dependency_changed");
|
|
578
|
-
// Summary should reflect it
|
|
579
|
-
const summary = await tracker.getDriftSummary();
|
|
580
|
-
expect(summary.dependency_changed).toBe(2);
|
|
581
|
-
});
|
|
582
|
-
});
|