@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,302 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for StartupDisplay + StartupRenderer (Task 1.3).
|
|
3
|
-
*
|
|
4
|
-
* Tests validate:
|
|
5
|
-
* - Three-Act structure: Banner (Act 1), HealthCard (Act 2), Invitation (Act 3)
|
|
6
|
-
* - Step status progression: pending → active → done
|
|
7
|
-
* - Health Shock display: full card on first boot, compact on subsequent
|
|
8
|
-
* - Invitation references specific entity from health data
|
|
9
|
-
* - Deep link included when repo ID available
|
|
10
|
-
* - Proxy mode displayed when not "full"
|
|
11
|
-
*/
|
|
12
|
-
import * as fs from "node:fs";
|
|
13
|
-
import * as os from "node:os";
|
|
14
|
-
import * as path from "node:path";
|
|
15
|
-
import { render } from "ink-testing-library";
|
|
16
|
-
import React from "react";
|
|
17
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
18
|
-
import { StartupDisplay, } from "../components/StartupDisplay.js";
|
|
19
|
-
import { ThemeProvider } from "../components/Theme.js";
|
|
20
|
-
function renderStartup(state) {
|
|
21
|
-
return render(React.createElement(ThemeProvider, null, React.createElement(StartupDisplay, { state })));
|
|
22
|
-
}
|
|
23
|
-
describe("StartupDisplay (1.3)", () => {
|
|
24
|
-
// ── Act 1: Instant Competence ──────────────────────────────────
|
|
25
|
-
describe("Act 1: Banner + Steps", () => {
|
|
26
|
-
it("renders brand banner", () => {
|
|
27
|
-
const { lastFrame } = renderStartup({
|
|
28
|
-
localMode: false,
|
|
29
|
-
steps: [],
|
|
30
|
-
firstBoot: false,
|
|
31
|
-
ready: false,
|
|
32
|
-
});
|
|
33
|
-
expect(lastFrame()).toContain("unerr");
|
|
34
|
-
expect(lastFrame()).toContain("▸");
|
|
35
|
-
});
|
|
36
|
-
it("renders steps with correct status icons", () => {
|
|
37
|
-
const { lastFrame } = renderStartup({
|
|
38
|
-
localMode: false,
|
|
39
|
-
steps: [
|
|
40
|
-
{ label: "Authenticated", value: "Org", status: "done" },
|
|
41
|
-
{ label: "Repository", value: "acme/repo", status: "done" },
|
|
42
|
-
{ label: "Graph loaded", status: "active" },
|
|
43
|
-
],
|
|
44
|
-
firstBoot: false,
|
|
45
|
-
ready: false,
|
|
46
|
-
});
|
|
47
|
-
const frame = lastFrame() ?? "";
|
|
48
|
-
expect(frame).toContain("✓");
|
|
49
|
-
expect(frame).toContain("Authenticated");
|
|
50
|
-
expect(frame).toContain("Org");
|
|
51
|
-
expect(frame).toContain("●");
|
|
52
|
-
expect(frame).toContain("Graph loaded");
|
|
53
|
-
});
|
|
54
|
-
it("shows step values alongside labels", () => {
|
|
55
|
-
const { lastFrame } = renderStartup({
|
|
56
|
-
localMode: false,
|
|
57
|
-
steps: [
|
|
58
|
-
{
|
|
59
|
-
label: "Graph loaded",
|
|
60
|
-
value: "2,341 entities · 1,892 edges",
|
|
61
|
-
status: "done",
|
|
62
|
-
},
|
|
63
|
-
],
|
|
64
|
-
firstBoot: false,
|
|
65
|
-
ready: false,
|
|
66
|
-
});
|
|
67
|
-
expect(lastFrame()).toContain("2,341 entities");
|
|
68
|
-
expect(lastFrame()).toContain("1,892 edges");
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
// ── Act 2: Revelation (Health Shock) ───────────────────────────
|
|
72
|
-
describe("Act 2: Health Shock", () => {
|
|
73
|
-
const health = {
|
|
74
|
-
grade: "C+",
|
|
75
|
-
totalEntities: 2341,
|
|
76
|
-
totalEdges: 1892,
|
|
77
|
-
totalRules: 12,
|
|
78
|
-
deadFunctionCount: 23,
|
|
79
|
-
highRiskEntities: [
|
|
80
|
-
{
|
|
81
|
-
name: "processPayment",
|
|
82
|
-
kind: "function",
|
|
83
|
-
file_path: "src/billing.ts",
|
|
84
|
-
fan_in: 14,
|
|
85
|
-
fan_out: 8,
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
score: 62,
|
|
89
|
-
};
|
|
90
|
-
it("renders full health card on first boot", () => {
|
|
91
|
-
const { lastFrame } = renderStartup({
|
|
92
|
-
localMode: false,
|
|
93
|
-
steps: [],
|
|
94
|
-
health,
|
|
95
|
-
firstBoot: true,
|
|
96
|
-
ready: false,
|
|
97
|
-
});
|
|
98
|
-
const frame = lastFrame() ?? "";
|
|
99
|
-
expect(frame).toContain("First Look");
|
|
100
|
-
expect(frame).toContain("C+");
|
|
101
|
-
expect(frame).toContain("62/100");
|
|
102
|
-
expect(frame).toContain("23 dead functions");
|
|
103
|
-
expect(frame).toContain("processPayment");
|
|
104
|
-
});
|
|
105
|
-
it("renders compact health line on subsequent boots", () => {
|
|
106
|
-
const { lastFrame } = renderStartup({
|
|
107
|
-
localMode: false,
|
|
108
|
-
steps: [],
|
|
109
|
-
health,
|
|
110
|
-
firstBoot: false,
|
|
111
|
-
ready: false,
|
|
112
|
-
});
|
|
113
|
-
const frame = lastFrame() ?? "";
|
|
114
|
-
expect(frame).toContain("C+");
|
|
115
|
-
expect(frame).toContain("2341 entities");
|
|
116
|
-
// Should NOT show full dead function detail in compact
|
|
117
|
-
expect(frame).not.toContain("23 dead functions");
|
|
118
|
-
});
|
|
119
|
-
it("does not render health section when no health data", () => {
|
|
120
|
-
const { lastFrame } = renderStartup({
|
|
121
|
-
localMode: false,
|
|
122
|
-
steps: [],
|
|
123
|
-
firstBoot: false,
|
|
124
|
-
ready: false,
|
|
125
|
-
});
|
|
126
|
-
expect(lastFrame()).not.toContain("First Look");
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
// ── Act 3: Invitation ──────────────────────────────────────────
|
|
130
|
-
describe("Act 3: Invitation", () => {
|
|
131
|
-
it("renders invitation with specific entity name", () => {
|
|
132
|
-
const { lastFrame } = renderStartup({
|
|
133
|
-
localMode: false,
|
|
134
|
-
steps: [],
|
|
135
|
-
firstBoot: false,
|
|
136
|
-
ready: true,
|
|
137
|
-
invitationEntity: "processPayment",
|
|
138
|
-
});
|
|
139
|
-
const frame = lastFrame() ?? "";
|
|
140
|
-
expect(frame).toContain("What depends on processPayment?");
|
|
141
|
-
expect(frame).toContain("blast radius");
|
|
142
|
-
});
|
|
143
|
-
it("renders proxy ready message", () => {
|
|
144
|
-
const { lastFrame } = renderStartup({
|
|
145
|
-
localMode: false,
|
|
146
|
-
steps: [],
|
|
147
|
-
firstBoot: false,
|
|
148
|
-
ready: true,
|
|
149
|
-
});
|
|
150
|
-
expect(lastFrame()).toContain("Proxy ready");
|
|
151
|
-
expect(lastFrame()).toContain("MCP on stdio");
|
|
152
|
-
});
|
|
153
|
-
it("shows proxy mode when not full", () => {
|
|
154
|
-
const { lastFrame } = renderStartup({
|
|
155
|
-
localMode: false,
|
|
156
|
-
steps: [],
|
|
157
|
-
firstBoot: false,
|
|
158
|
-
ready: true,
|
|
159
|
-
proxyMode: "parse",
|
|
160
|
-
});
|
|
161
|
-
expect(lastFrame()).toContain("parse mode");
|
|
162
|
-
});
|
|
163
|
-
it("shows local mode label when proxyMode is local", () => {
|
|
164
|
-
const { lastFrame } = renderStartup({
|
|
165
|
-
localMode: false,
|
|
166
|
-
steps: [],
|
|
167
|
-
firstBoot: false,
|
|
168
|
-
ready: true,
|
|
169
|
-
proxyMode: "local",
|
|
170
|
-
});
|
|
171
|
-
expect(lastFrame()).toContain("local mode");
|
|
172
|
-
});
|
|
173
|
-
it("renders deep link when available", () => {
|
|
174
|
-
const { lastFrame } = renderStartup({
|
|
175
|
-
localMode: false,
|
|
176
|
-
steps: [],
|
|
177
|
-
firstBoot: false,
|
|
178
|
-
ready: true,
|
|
179
|
-
deepLink: "https://app.unerr.dev/r/repo_123?utm_source=cli_startup",
|
|
180
|
-
});
|
|
181
|
-
expect(lastFrame()).toContain("https://app.unerr.dev/r/repo_123");
|
|
182
|
-
});
|
|
183
|
-
it("does not render invitation when not ready", () => {
|
|
184
|
-
const { lastFrame } = renderStartup({
|
|
185
|
-
localMode: false,
|
|
186
|
-
steps: [],
|
|
187
|
-
firstBoot: false,
|
|
188
|
-
ready: false,
|
|
189
|
-
invitationEntity: "processPayment",
|
|
190
|
-
});
|
|
191
|
-
expect(lastFrame()).not.toContain("What depends on");
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
// ── Full Three-Act integration ─────────────────────────────────
|
|
195
|
-
describe("Full Three-Act integration", () => {
|
|
196
|
-
it("renders all three acts together", () => {
|
|
197
|
-
const { lastFrame } = renderStartup({
|
|
198
|
-
localMode: false,
|
|
199
|
-
steps: [
|
|
200
|
-
{ label: "Authenticated", value: "Jaswanth's Org", status: "done" },
|
|
201
|
-
{ label: "Repository", value: "unerr-server (main)", status: "done" },
|
|
202
|
-
{
|
|
203
|
-
label: "Graph loaded",
|
|
204
|
-
value: "2,341 entities · 1,892 edges",
|
|
205
|
-
status: "done",
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
label: "MCP ready",
|
|
209
|
-
value: "15 tools (all local)",
|
|
210
|
-
status: "done",
|
|
211
|
-
},
|
|
212
|
-
],
|
|
213
|
-
health: {
|
|
214
|
-
grade: "C+",
|
|
215
|
-
totalEntities: 2341,
|
|
216
|
-
totalEdges: 1892,
|
|
217
|
-
totalRules: 12,
|
|
218
|
-
deadFunctionCount: 23,
|
|
219
|
-
highRiskEntities: [
|
|
220
|
-
{
|
|
221
|
-
name: "processPayment",
|
|
222
|
-
kind: "function",
|
|
223
|
-
file_path: "src/billing.ts",
|
|
224
|
-
fan_in: 14,
|
|
225
|
-
fan_out: 8,
|
|
226
|
-
},
|
|
227
|
-
],
|
|
228
|
-
score: 62,
|
|
229
|
-
},
|
|
230
|
-
firstBoot: true,
|
|
231
|
-
ready: true,
|
|
232
|
-
invitationEntity: "processPayment",
|
|
233
|
-
deepLink: "https://app.unerr.dev/r/repo_123?utm_source=cli_startup",
|
|
234
|
-
});
|
|
235
|
-
const frame = lastFrame() ?? "";
|
|
236
|
-
// Act 1
|
|
237
|
-
expect(frame).toContain("unerr");
|
|
238
|
-
expect(frame).toContain("Authenticated");
|
|
239
|
-
expect(frame).toContain("Jaswanth's Org");
|
|
240
|
-
// Act 2
|
|
241
|
-
expect(frame).toContain("First Look");
|
|
242
|
-
expect(frame).toContain("C+");
|
|
243
|
-
expect(frame).toContain("23 dead functions");
|
|
244
|
-
// Act 3
|
|
245
|
-
expect(frame).toContain("What depends on processPayment?");
|
|
246
|
-
expect(frame).toContain("Proxy ready");
|
|
247
|
-
expect(frame).toContain("https://app.unerr.dev/r/repo_123");
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
// ── StartupRenderer unit tests ───────────────────────────────────
|
|
252
|
-
describe("StartupRenderer", () => {
|
|
253
|
-
let tmpDir;
|
|
254
|
-
let origCwd;
|
|
255
|
-
beforeEach(() => {
|
|
256
|
-
tmpDir = path.join(os.tmpdir(), `unerr-startup-test-${Date.now()}`);
|
|
257
|
-
fs.mkdirSync(path.join(tmpDir, ".unerr", "state"), { recursive: true });
|
|
258
|
-
origCwd = process.cwd();
|
|
259
|
-
process.chdir(tmpDir);
|
|
260
|
-
});
|
|
261
|
-
afterEach(() => {
|
|
262
|
-
process.chdir(origCwd);
|
|
263
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
264
|
-
});
|
|
265
|
-
it("tracks first_boot_shown flag", async () => {
|
|
266
|
-
const { StartupRenderer } = await import("../proxy/startup-renderer.js");
|
|
267
|
-
const renderer = new StartupRenderer();
|
|
268
|
-
// No graph_version.json → first boot
|
|
269
|
-
renderer.setHealth({
|
|
270
|
-
grade: "C+",
|
|
271
|
-
totalEntities: 100,
|
|
272
|
-
totalEdges: 50,
|
|
273
|
-
totalRules: 5,
|
|
274
|
-
deadFunctionCount: 10,
|
|
275
|
-
highRiskEntities: [],
|
|
276
|
-
score: 62,
|
|
277
|
-
}, "repo_test");
|
|
278
|
-
// Should have written first_boot_shown = true
|
|
279
|
-
const versionPath = path.join(tmpDir, ".unerr", "state", "graph_version.json");
|
|
280
|
-
const data = JSON.parse(fs.readFileSync(versionPath, "utf-8"));
|
|
281
|
-
expect(data.first_boot_shown).toBe(true);
|
|
282
|
-
});
|
|
283
|
-
it("detects subsequent boot after first_boot_shown is set", async () => {
|
|
284
|
-
const versionPath = path.join(tmpDir, ".unerr", "state", "graph_version.json");
|
|
285
|
-
fs.writeFileSync(versionPath, JSON.stringify({ first_boot_shown: true }));
|
|
286
|
-
const { StartupRenderer } = await import("../proxy/startup-renderer.js");
|
|
287
|
-
const renderer = new StartupRenderer();
|
|
288
|
-
renderer.setHealth({
|
|
289
|
-
grade: "A",
|
|
290
|
-
totalEntities: 100,
|
|
291
|
-
totalEdges: 50,
|
|
292
|
-
totalRules: 5,
|
|
293
|
-
deadFunctionCount: 0,
|
|
294
|
-
highRiskEntities: [],
|
|
295
|
-
score: 95,
|
|
296
|
-
}, "repo_test");
|
|
297
|
-
// firstBoot should be false since flag was already set
|
|
298
|
-
// We verify indirectly — the renderer won't overwrite the flag
|
|
299
|
-
const data = JSON.parse(fs.readFileSync(versionPath, "utf-8"));
|
|
300
|
-
expect(data.first_boot_shown).toBe(true);
|
|
301
|
-
});
|
|
302
|
-
});
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { initFileLog, startupLog } from "../utils/startup-log.js";
|
|
6
|
-
describe("startup-log file logging", () => {
|
|
7
|
-
let tmpDir;
|
|
8
|
-
let logPath;
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
tmpDir = join(os.tmpdir(), `unerr-log-test-${Date.now()}`);
|
|
11
|
-
mkdirSync(tmpDir, { recursive: true });
|
|
12
|
-
initFileLog(tmpDir);
|
|
13
|
-
logPath = join(tmpDir, ".unerr", "logs", "unerr.jsonl");
|
|
14
|
-
});
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
17
|
-
});
|
|
18
|
-
it("creates .unerr/logs/unerr.jsonl on initFileLog", () => {
|
|
19
|
-
// initFileLog creates the directory; file created on first write
|
|
20
|
-
startupLog.step("test step");
|
|
21
|
-
expect(existsSync(logPath)).toBe(true);
|
|
22
|
-
});
|
|
23
|
-
it("step() writes a JSONL entry with level=step", () => {
|
|
24
|
-
startupLog.step("indexing files");
|
|
25
|
-
const lines = readFileSync(logPath, "utf-8").trim().split("\n");
|
|
26
|
-
const entry = JSON.parse(lines[lines.length - 1]);
|
|
27
|
-
expect(entry.level).toBe("step");
|
|
28
|
-
expect(entry.msg).toContain("indexing files");
|
|
29
|
-
expect(entry.ts).toBeDefined();
|
|
30
|
-
expect(entry.pid).toBe(process.pid);
|
|
31
|
-
});
|
|
32
|
-
it("done() includes ms metadata when provided", () => {
|
|
33
|
-
startupLog.done("graph loaded", 42);
|
|
34
|
-
const lines = readFileSync(logPath, "utf-8").trim().split("\n");
|
|
35
|
-
const entry = JSON.parse(lines[lines.length - 1]);
|
|
36
|
-
expect(entry.level).toBe("done");
|
|
37
|
-
expect(entry.ms).toBe(42);
|
|
38
|
-
});
|
|
39
|
-
it("metric() includes raw value and unit", () => {
|
|
40
|
-
startupLog.metric("entities", 1234, "nodes");
|
|
41
|
-
const lines = readFileSync(logPath, "utf-8").trim().split("\n");
|
|
42
|
-
const entry = JSON.parse(lines[lines.length - 1]);
|
|
43
|
-
expect(entry.level).toBe("metric");
|
|
44
|
-
expect(entry.value).toBe(1234);
|
|
45
|
-
expect(entry.unit).toBe("nodes");
|
|
46
|
-
});
|
|
47
|
-
it("warn() and error() write correct levels", () => {
|
|
48
|
-
startupLog.warn("something odd");
|
|
49
|
-
startupLog.error("something broke");
|
|
50
|
-
const lines = readFileSync(logPath, "utf-8").trim().split("\n");
|
|
51
|
-
const entries = lines.map((l) => JSON.parse(l));
|
|
52
|
-
const warn = entries.find((e) => e.level === "warn");
|
|
53
|
-
const err = entries.find((e) => e.level === "error");
|
|
54
|
-
expect(warn).toBeDefined();
|
|
55
|
-
expect(warn.msg).toContain("something odd");
|
|
56
|
-
expect(err).toBeDefined();
|
|
57
|
-
expect(err.msg).toContain("something broke");
|
|
58
|
-
});
|
|
59
|
-
it("graphLoaded() writes full stats object", () => {
|
|
60
|
-
startupLog.graphLoaded({
|
|
61
|
-
entities: 500,
|
|
62
|
-
edges: 1200,
|
|
63
|
-
files: 80,
|
|
64
|
-
communities: 5,
|
|
65
|
-
patterns: 12,
|
|
66
|
-
rules: 3,
|
|
67
|
-
ms: 150,
|
|
68
|
-
hottestFile: "src/main.ts",
|
|
69
|
-
hottestCount: 25,
|
|
70
|
-
});
|
|
71
|
-
const lines = readFileSync(logPath, "utf-8").trim().split("\n");
|
|
72
|
-
const entry = JSON.parse(lines[lines.length - 1]);
|
|
73
|
-
expect(entry.level).toBe("graph_loaded");
|
|
74
|
-
expect(entry.entities).toBe(500);
|
|
75
|
-
expect(entry.edges).toBe(1200);
|
|
76
|
-
expect(entry.files).toBe(80);
|
|
77
|
-
expect(entry.communities).toBe(5);
|
|
78
|
-
expect(entry.patterns).toBe(12);
|
|
79
|
-
expect(entry.rules).toBe(3);
|
|
80
|
-
expect(entry.ms).toBe(150);
|
|
81
|
-
expect(entry.hottestFile).toBe("src/main.ts");
|
|
82
|
-
expect(entry.hottestCount).toBe(25);
|
|
83
|
-
});
|
|
84
|
-
it("file entries have no ANSI escape codes", () => {
|
|
85
|
-
startupLog.step("test with colors");
|
|
86
|
-
const content = readFileSync(logPath, "utf-8");
|
|
87
|
-
expect(content).not.toContain("\x1b[");
|
|
88
|
-
});
|
|
89
|
-
it("ready() includes toolCount and mode", () => {
|
|
90
|
-
startupLog.ready(17, "local");
|
|
91
|
-
const lines = readFileSync(logPath, "utf-8").trim().split("\n");
|
|
92
|
-
const entry = JSON.parse(lines[lines.length - 1]);
|
|
93
|
-
expect(entry.level).toBe("ready");
|
|
94
|
-
expect(entry.toolCount).toBe(17);
|
|
95
|
-
expect(entry.mode).toBe("local");
|
|
96
|
-
});
|
|
97
|
-
});
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sprint 7.1: Git stash awareness tests.
|
|
3
|
-
*/
|
|
4
|
-
import { mkdirSync, mkdtempSync, writeFileSync, } from "node:fs";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import { describe, expect, it } from "vitest";
|
|
8
|
-
import { StashManager } from "../tracking/stash-manager.js";
|
|
9
|
-
/** Minimal mock CozoGraphStore backed by a Map for drift overlay. */
|
|
10
|
-
function createMockGraph(driftEntities = []) {
|
|
11
|
-
const entities = new Map();
|
|
12
|
-
for (const e of driftEntities) {
|
|
13
|
-
entities.set(e.key, e);
|
|
14
|
-
}
|
|
15
|
-
return {
|
|
16
|
-
entities,
|
|
17
|
-
getAllDriftEntities: () => [...entities.values()],
|
|
18
|
-
getAllDriftEdges: () => [],
|
|
19
|
-
upsertDriftEntity: (entity) => {
|
|
20
|
-
entities.set(entity.key, entity);
|
|
21
|
-
},
|
|
22
|
-
upsertDriftEdge: () => { },
|
|
23
|
-
removeDriftEntity: (key) => {
|
|
24
|
-
entities.delete(key);
|
|
25
|
-
},
|
|
26
|
-
clearDriftOverlay: () => {
|
|
27
|
-
entities.clear();
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
function makeDrift(overrides) {
|
|
32
|
-
return {
|
|
33
|
-
kind: "function",
|
|
34
|
-
signature: "()",
|
|
35
|
-
body: "function test() {}",
|
|
36
|
-
file_path: "src/test.ts",
|
|
37
|
-
line_start: 1,
|
|
38
|
-
line_end: 3,
|
|
39
|
-
content_hash: "abc123",
|
|
40
|
-
drift_status: "modified",
|
|
41
|
-
intent_id: "",
|
|
42
|
-
modified_at: new Date().toISOString(),
|
|
43
|
-
origin: "human",
|
|
44
|
-
previous_body: "",
|
|
45
|
-
previous_signature: "",
|
|
46
|
-
...overrides,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function setupProjectWithGit() {
|
|
50
|
-
const projectRoot = mkdtempSync(join(tmpdir(), "stash-"));
|
|
51
|
-
const unerrDir = join(projectRoot, ".unerr");
|
|
52
|
-
const gitDir = join(projectRoot, ".git");
|
|
53
|
-
mkdirSync(unerrDir, { recursive: true });
|
|
54
|
-
mkdirSync(join(gitDir, "refs"), { recursive: true });
|
|
55
|
-
mkdirSync(join(gitDir, "logs", "refs"), { recursive: true });
|
|
56
|
-
return { projectRoot, unerrDir, gitDir };
|
|
57
|
-
}
|
|
58
|
-
function writeStashRef(gitDir, ref) {
|
|
59
|
-
writeFileSync(join(gitDir, "refs", "stash"), `${ref}\n`, "utf-8");
|
|
60
|
-
}
|
|
61
|
-
function writeStashLog(gitDir, count) {
|
|
62
|
-
const lines = Array.from({ length: count }, (_, i) => `0000000 abcdef${i} Author <a@b.com> ${Date.now()} +0000\tstash@{${i}}: WIP`);
|
|
63
|
-
writeFileSync(join(gitDir, "logs", "refs", "stash"), `${lines.join("\n")}\n`, "utf-8");
|
|
64
|
-
}
|
|
65
|
-
const MOCK_FILE_HASHES = {
|
|
66
|
-
files: {
|
|
67
|
-
"src/test.ts": {
|
|
68
|
-
contentSha: "sha256abc",
|
|
69
|
-
headSha: "head123",
|
|
70
|
-
processedAt: new Date().toISOString(),
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
describe("StashManager", () => {
|
|
75
|
-
it("detects no change when stash ref unchanged", () => {
|
|
76
|
-
const { projectRoot, unerrDir, gitDir } = setupProjectWithGit();
|
|
77
|
-
writeStashRef(gitDir, "abc123def456");
|
|
78
|
-
writeStashLog(gitDir, 1);
|
|
79
|
-
const manager = new StashManager(unerrDir, projectRoot);
|
|
80
|
-
expect(manager.detectStashChange()).toBeNull();
|
|
81
|
-
});
|
|
82
|
-
it("detects stash push when count increases", () => {
|
|
83
|
-
const { projectRoot, unerrDir, gitDir } = setupProjectWithGit();
|
|
84
|
-
writeStashRef(gitDir, "ref1");
|
|
85
|
-
writeStashLog(gitDir, 1);
|
|
86
|
-
const manager = new StashManager(unerrDir, projectRoot);
|
|
87
|
-
// Simulate stash push
|
|
88
|
-
writeStashRef(gitDir, "ref2");
|
|
89
|
-
writeStashLog(gitDir, 2);
|
|
90
|
-
expect(manager.detectStashChange()).toBe("push");
|
|
91
|
-
});
|
|
92
|
-
it("detects stash pop when count decreases", () => {
|
|
93
|
-
const { projectRoot, unerrDir, gitDir } = setupProjectWithGit();
|
|
94
|
-
writeStashRef(gitDir, "ref1");
|
|
95
|
-
writeStashLog(gitDir, 2);
|
|
96
|
-
const manager = new StashManager(unerrDir, projectRoot);
|
|
97
|
-
// Simulate stash pop
|
|
98
|
-
writeStashRef(gitDir, "ref0");
|
|
99
|
-
writeStashLog(gitDir, 1);
|
|
100
|
-
expect(manager.detectStashChange()).toBe("pop");
|
|
101
|
-
});
|
|
102
|
-
it("saves and restores a stash snapshot", async () => {
|
|
103
|
-
const { projectRoot, unerrDir, gitDir } = setupProjectWithGit();
|
|
104
|
-
writeStashRef(gitDir, "abc123def456abc123def456abc123def456abc1");
|
|
105
|
-
writeStashLog(gitDir, 1);
|
|
106
|
-
const entities = [
|
|
107
|
-
makeDrift({ key: "k1", name: "fn1", drift_status: "modified" }),
|
|
108
|
-
makeDrift({ key: "k2", name: "fn2", drift_status: "added" }),
|
|
109
|
-
];
|
|
110
|
-
const graph = createMockGraph(entities);
|
|
111
|
-
const manager = new StashManager(unerrDir, projectRoot);
|
|
112
|
-
// Save snapshot
|
|
113
|
-
const snapshotId = await manager.saveSnapshot(graph, MOCK_FILE_HASHES);
|
|
114
|
-
expect(snapshotId).toBe("abc123def456");
|
|
115
|
-
// Clear overlay to simulate stash applying
|
|
116
|
-
graph.entities.clear();
|
|
117
|
-
expect(graph.getAllDriftEntities()).toHaveLength(0);
|
|
118
|
-
// Restore snapshot
|
|
119
|
-
const restored = await manager.restoreSnapshot(graph);
|
|
120
|
-
expect(restored).toBe(2);
|
|
121
|
-
expect(graph.entities.size).toBe(2);
|
|
122
|
-
expect(graph.entities.get("k1")?.name).toBe("fn1");
|
|
123
|
-
expect(graph.entities.get("k2")?.name).toBe("fn2");
|
|
124
|
-
});
|
|
125
|
-
it("returns null when saving with no drift entities", async () => {
|
|
126
|
-
const { projectRoot, unerrDir, gitDir } = setupProjectWithGit();
|
|
127
|
-
writeStashRef(gitDir, "abc123def456");
|
|
128
|
-
const graph = createMockGraph([]);
|
|
129
|
-
const manager = new StashManager(unerrDir, projectRoot);
|
|
130
|
-
const snapshotId = await manager.saveSnapshot(graph, MOCK_FILE_HASHES);
|
|
131
|
-
expect(snapshotId).toBeNull();
|
|
132
|
-
});
|
|
133
|
-
it("returns null when saving with no stash ref", async () => {
|
|
134
|
-
const { projectRoot, unerrDir } = setupProjectWithGit();
|
|
135
|
-
const graph = createMockGraph([makeDrift({ key: "k1", name: "fn1" })]);
|
|
136
|
-
const manager = new StashManager(unerrDir, projectRoot);
|
|
137
|
-
const snapshotId = await manager.saveSnapshot(graph, MOCK_FILE_HASHES);
|
|
138
|
-
expect(snapshotId).toBeNull();
|
|
139
|
-
});
|
|
140
|
-
it("restores 0 when no snapshots exist", async () => {
|
|
141
|
-
const { projectRoot, unerrDir } = setupProjectWithGit();
|
|
142
|
-
const graph = createMockGraph([]);
|
|
143
|
-
const manager = new StashManager(unerrDir, projectRoot);
|
|
144
|
-
expect(await manager.restoreSnapshot(graph)).toBe(0);
|
|
145
|
-
});
|
|
146
|
-
it("drops a specific snapshot", async () => {
|
|
147
|
-
const { projectRoot, unerrDir, gitDir } = setupProjectWithGit();
|
|
148
|
-
const fullRef = "abc123def456abc123def456abc123def456abc1";
|
|
149
|
-
writeStashRef(gitDir, fullRef);
|
|
150
|
-
const graph = createMockGraph([makeDrift({ key: "k1", name: "fn1" })]);
|
|
151
|
-
const manager = new StashManager(unerrDir, projectRoot);
|
|
152
|
-
await manager.saveSnapshot(graph, MOCK_FILE_HASHES);
|
|
153
|
-
expect(manager.listSnapshots()).toHaveLength(1);
|
|
154
|
-
expect(manager.dropSnapshot(fullRef)).toBe(true);
|
|
155
|
-
expect(manager.listSnapshots()).toHaveLength(0);
|
|
156
|
-
});
|
|
157
|
-
it("enforces LRU cap of 10 snapshots", async () => {
|
|
158
|
-
const { projectRoot, unerrDir, gitDir } = setupProjectWithGit();
|
|
159
|
-
const graph = createMockGraph([makeDrift({ key: "k1", name: "fn1" })]);
|
|
160
|
-
const manager = new StashManager(unerrDir, projectRoot);
|
|
161
|
-
// Create 12 snapshots
|
|
162
|
-
for (let i = 0; i < 12; i++) {
|
|
163
|
-
const ref = `ref${String(i).padStart(12, "0")}aaaaaaaaaaaaaaaa`;
|
|
164
|
-
writeStashRef(gitDir, ref);
|
|
165
|
-
await manager.saveSnapshot(graph, MOCK_FILE_HASHES);
|
|
166
|
-
}
|
|
167
|
-
// Should be capped at 10
|
|
168
|
-
expect(manager.listSnapshots().length).toBeLessThanOrEqual(10);
|
|
169
|
-
});
|
|
170
|
-
it("retrieves file hash state from snapshot", async () => {
|
|
171
|
-
const { projectRoot, unerrDir, gitDir } = setupProjectWithGit();
|
|
172
|
-
writeStashRef(gitDir, "abc123def456abc1");
|
|
173
|
-
const graph = createMockGraph([makeDrift({ key: "k1", name: "fn1" })]);
|
|
174
|
-
const manager = new StashManager(unerrDir, projectRoot);
|
|
175
|
-
await manager.saveSnapshot(graph, MOCK_FILE_HASHES);
|
|
176
|
-
const hashes = manager.getSnapshotFileHashes();
|
|
177
|
-
expect(hashes).not.toBeNull();
|
|
178
|
-
expect(hashes?.files["src/test.ts"]?.contentSha).toBe("sha256abc");
|
|
179
|
-
});
|
|
180
|
-
it("cleans up snapshot after restore", async () => {
|
|
181
|
-
const { projectRoot, unerrDir, gitDir } = setupProjectWithGit();
|
|
182
|
-
writeStashRef(gitDir, "abc123def456abc1");
|
|
183
|
-
const graph = createMockGraph([makeDrift({ key: "k1", name: "fn1" })]);
|
|
184
|
-
const manager = new StashManager(unerrDir, projectRoot);
|
|
185
|
-
await manager.saveSnapshot(graph, MOCK_FILE_HASHES);
|
|
186
|
-
expect(manager.listSnapshots()).toHaveLength(1);
|
|
187
|
-
graph.entities.clear();
|
|
188
|
-
await manager.restoreSnapshot(graph);
|
|
189
|
-
// Snapshot should be cleaned up after restore
|
|
190
|
-
expect(manager.listSnapshots()).toHaveLength(0);
|
|
191
|
-
});
|
|
192
|
-
it("handles stash push → pop cycle preserving overlay", async () => {
|
|
193
|
-
const { projectRoot, unerrDir, gitDir } = setupProjectWithGit();
|
|
194
|
-
const originalEntities = [
|
|
195
|
-
makeDrift({
|
|
196
|
-
key: "k1",
|
|
197
|
-
name: "handler",
|
|
198
|
-
drift_status: "modified",
|
|
199
|
-
body: "function handler() { return 42; }",
|
|
200
|
-
previous_body: "function handler() { return 0; }",
|
|
201
|
-
}),
|
|
202
|
-
];
|
|
203
|
-
const graph = createMockGraph(originalEntities);
|
|
204
|
-
// Initial state: 1 stash
|
|
205
|
-
writeStashRef(gitDir, "stashref1aaa");
|
|
206
|
-
writeStashLog(gitDir, 0);
|
|
207
|
-
const manager = new StashManager(unerrDir, projectRoot);
|
|
208
|
-
// Simulate stash push
|
|
209
|
-
writeStashRef(gitDir, "stashref2bbb");
|
|
210
|
-
writeStashLog(gitDir, 1);
|
|
211
|
-
expect(manager.detectStashChange()).toBe("push");
|
|
212
|
-
await manager.saveSnapshot(graph, MOCK_FILE_HASHES);
|
|
213
|
-
// Clear overlay (simulating git stash restoring working tree)
|
|
214
|
-
graph.entities.clear();
|
|
215
|
-
expect(graph.getAllDriftEntities()).toHaveLength(0);
|
|
216
|
-
// Simulate stash pop
|
|
217
|
-
writeStashRef(gitDir, "stashref1aaa");
|
|
218
|
-
writeStashLog(gitDir, 0);
|
|
219
|
-
expect(manager.detectStashChange()).toBe("pop");
|
|
220
|
-
const restored = await manager.restoreSnapshot(graph);
|
|
221
|
-
expect(restored).toBe(1);
|
|
222
|
-
const entity = graph.entities.get("k1");
|
|
223
|
-
expect(entity).toBeDefined();
|
|
224
|
-
expect(entity?.name).toBe("handler");
|
|
225
|
-
expect(entity?.drift_status).toBe("modified");
|
|
226
|
-
expect(entity?.body).toBe("function handler() { return 42; }");
|
|
227
|
-
expect(entity?.previous_body).toBe("function handler() { return 0; }");
|
|
228
|
-
});
|
|
229
|
-
});
|