@wangyaoshen/remux 0.3.8-dev.29e114b
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/.github/ISSUE_TEMPLATE/bug_report.md +47 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +38 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +28 -0
- package/.github/dependabot.yml +33 -0
- package/.github/workflows/ci.yml +65 -0
- package/.github/workflows/deploy.yml +65 -0
- package/.github/workflows/publish.yml +312 -0
- package/.github/workflows/release-please.yml +21 -0
- package/.gitmodules +3 -0
- package/.nvmrc +1 -0
- package/.release-please-manifest.json +3 -0
- package/CLAUDE.md +104 -0
- package/Dockerfile +23 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/apps/ios/Config/signing.xcconfig +4 -0
- package/apps/ios/Package.swift +26 -0
- package/apps/ios/Remux.xcodeproj/project.pbxproj +477 -0
- package/apps/ios/Remux.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/Contents.json +23 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_120x120.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_152x152.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_167x167.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_180x180.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_20x20.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_29x29.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_40x40.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_58x58.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_60x60.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_76x76.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_80x80.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_87x87.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/Contents.json +6 -0
- package/apps/ios/Sources/Remux/Extensions/FaceIDManager.swift +29 -0
- package/apps/ios/Sources/Remux/Extensions/InspectCache.swift +66 -0
- package/apps/ios/Sources/Remux/MainTabView.swift +32 -0
- package/apps/ios/Sources/Remux/Remux.entitlements +8 -0
- package/apps/ios/Sources/Remux/RemuxiOSApp.swift +14 -0
- package/apps/ios/Sources/Remux/RootView.swift +130 -0
- package/apps/ios/Sources/Remux/Views/Control/ControlView.swift +102 -0
- package/apps/ios/Sources/Remux/Views/Inspect/InspectView.swift +98 -0
- package/apps/ios/Sources/Remux/Views/Live/LiveTerminalView.swift +132 -0
- package/apps/ios/Sources/Remux/Views/Now/NowView.swift +173 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/ManualConnectView.swift +55 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/OnboardingView.swift +70 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/QRScannerView.swift +92 -0
- package/apps/ios/Sources/Remux/Views/Settings/MeView.swift +136 -0
- package/apps/macos/Package.swift +37 -0
- package/apps/macos/Resources/shell-integration/bash/bash-preexec.sh +382 -0
- package/apps/macos/Resources/shell-integration/bash/ghostty.bash +315 -0
- package/apps/macos/Resources/shell-integration/elvish/lib/ghostty-integration.elv +191 -0
- package/apps/macos/Resources/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +246 -0
- package/apps/macos/Resources/shell-integration/nushell/vendor/autoload/ghostty.nu +110 -0
- package/apps/macos/Resources/shell-integration/zsh/.zshenv +61 -0
- package/apps/macos/Resources/shell-integration/zsh/ghostty-integration +458 -0
- package/apps/macos/Resources/terminfo/67/ghostty +0 -0
- package/apps/macos/Resources/terminfo/78/xterm-ghostty +0 -0
- package/apps/macos/Sources/Remux/AppDelegate.swift +257 -0
- package/apps/macos/Sources/Remux/CrashReporter.swift +210 -0
- package/apps/macos/Sources/Remux/FinderIntegration.swift +117 -0
- package/apps/macos/Sources/Remux/GhosttyConfig.swift +311 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutAction.swift +115 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutSettingsView.swift +271 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/StoredShortcut.swift +149 -0
- package/apps/macos/Sources/Remux/MainContentView.swift +308 -0
- package/apps/macos/Sources/Remux/MenuBarManager.swift +275 -0
- package/apps/macos/Sources/Remux/NotificationManager.swift +145 -0
- package/apps/macos/Sources/Remux/PortScanner.swift +152 -0
- package/apps/macos/Sources/Remux/RemuxApp.swift +13 -0
- package/apps/macos/Sources/Remux/SSHDetector.swift +151 -0
- package/apps/macos/Sources/Remux/SessionPersistence.swift +226 -0
- package/apps/macos/Sources/Remux/SocketController.swift +258 -0
- package/apps/macos/Sources/Remux/UpdateChecker.swift +152 -0
- package/apps/macos/Sources/Remux/Views/CommandPalette.swift +198 -0
- package/apps/macos/Sources/Remux/Views/ConnectionView.swift +84 -0
- package/apps/macos/Sources/Remux/Views/InspectView.swift +127 -0
- package/apps/macos/Sources/Remux/Views/SettingsView.swift +77 -0
- package/apps/macos/Sources/Remux/Views/Sidebar/SidebarView.swift +410 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/BrowserPanel.swift +193 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/MarkdownPanel.swift +277 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/PanelProtocol.swift +14 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/SplitNode.swift +149 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/SplitView.swift +234 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/TerminalPanel.swift +26 -0
- package/apps/macos/Sources/Remux/Views/TabBarView.swift +94 -0
- package/apps/macos/Sources/Remux/Views/Terminal/ClipboardHelper.swift +101 -0
- package/apps/macos/Sources/Remux/Views/Terminal/CopyModeOverlay.swift +325 -0
- package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeTerminalView.swift +39 -0
- package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeView.swift +559 -0
- package/apps/macos/Sources/Remux/Views/Terminal/SurfaceSearchOverlay.swift +109 -0
- package/apps/macos/Sources/Remux/Views/Terminal/TerminalContainerView.swift +95 -0
- package/apps/macos/Sources/Remux/Views/Terminal/TerminalRelay.swift +117 -0
- package/build.mjs +33 -0
- package/native/android/DecodeGoldenPayloads.kt +487 -0
- package/native/android/ProtocolModels.kt +188 -0
- package/native/ios/DecodeGoldenPayloads.swift +711 -0
- package/native/ios/ProtocolModels.swift +200 -0
- package/package.json +45 -0
- package/packages/RemuxKit/Package.swift +27 -0
- package/packages/RemuxKit/Sources/RemuxKit/Device/DeviceManager.swift +27 -0
- package/packages/RemuxKit/Sources/RemuxKit/Models/ProtocolModels.swift +206 -0
- package/packages/RemuxKit/Sources/RemuxKit/Networking/MessageRouter.swift +108 -0
- package/packages/RemuxKit/Sources/RemuxKit/Networking/RemuxConnection.swift +395 -0
- package/packages/RemuxKit/Sources/RemuxKit/State/RemuxState.swift +188 -0
- package/packages/RemuxKit/Sources/RemuxKit/Storage/KeychainStore.swift +142 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyBridge.swift +145 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyTerminalView.swift +35 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/Resources/ghostty-terminal.html +91 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/ConnectionIntegrationTest.swift +74 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/KeychainStoreTests.swift +81 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/ProtocolModelsTests.swift +179 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/RemuxStateTests.swift +62 -0
- package/playwright.config.ts +17 -0
- package/pnpm-lock.yaml +1588 -0
- package/pty-daemon.js +303 -0
- package/release-please-config.json +14 -0
- package/scripts/auto-deploy.sh +46 -0
- package/scripts/build-dmg.sh +121 -0
- package/scripts/build-ghostty-kit.sh +43 -0
- package/scripts/check-active-terminology.mjs +132 -0
- package/scripts/setup-ci-secrets.sh +80 -0
- package/scripts/sync-ghostty-web.sh +28 -0
- package/scripts/upload-testflight.sh +100 -0
- package/server.js +7074 -0
- package/src/adapters/agent-events.ts +246 -0
- package/src/adapters/claude-code.ts +158 -0
- package/src/adapters/codex.ts +210 -0
- package/src/adapters/generic-shell.ts +58 -0
- package/src/adapters/index.ts +15 -0
- package/src/adapters/registry.ts +99 -0
- package/src/adapters/types.ts +41 -0
- package/src/auth.ts +174 -0
- package/src/e2ee.ts +236 -0
- package/src/git-service.ts +168 -0
- package/src/message-buffer.ts +137 -0
- package/src/pty-daemon.ts +357 -0
- package/src/push.ts +127 -0
- package/src/renderers.ts +455 -0
- package/src/server.ts +2407 -0
- package/src/service.ts +226 -0
- package/src/session.ts +978 -0
- package/src/store.ts +1422 -0
- package/src/team.ts +123 -0
- package/src/tunnel.ts +126 -0
- package/src/types.d.ts +50 -0
- package/src/vt-tracker.ts +188 -0
- package/src/workspace-head.ts +144 -0
- package/src/workspace.ts +153 -0
- package/src/ws-handler.ts +1526 -0
- package/start.ps1 +83 -0
- package/tests/adapters.test.js +171 -0
- package/tests/auth.test.js +243 -0
- package/tests/codex-adapter.test.js +535 -0
- package/tests/durable-stream.test.js +153 -0
- package/tests/e2e/app.spec.js +530 -0
- package/tests/e2ee.test.js +325 -0
- package/tests/message-buffer.test.js +245 -0
- package/tests/message-routing.test.js +305 -0
- package/tests/pty-daemon.test.js +346 -0
- package/tests/push.test.js +281 -0
- package/tests/renderers.test.js +391 -0
- package/tests/search-shell.test.js +499 -0
- package/tests/server.test.js +882 -0
- package/tests/service.test.js +267 -0
- package/tests/store.test.js +369 -0
- package/tests/tunnel.test.js +67 -0
- package/tests/workspace-head.test.js +116 -0
- package/tests/workspace.test.js +417 -0
- package/tsconfig.backend.json +11 -0
- package/tsconfig.json +15 -0
- package/tui/client/client_test.go +125 -0
- package/tui/client/connection.go +342 -0
- package/tui/client/host_manager.go +141 -0
- package/tui/config/cache.go +81 -0
- package/tui/config/config.go +53 -0
- package/tui/config/config_test.go +89 -0
- package/tui/go.mod +32 -0
- package/tui/go.sum +50 -0
- package/tui/main.go +261 -0
- package/tui/tests/integration_test.go +283 -0
- package/tui/ui/model.go +310 -0
- package/vitest.config.js +10 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for P5-B (Search / Memory / Handoff) and P5-C (Shell Integration).
|
|
3
|
+
* Covers FTS5 indexing, memory notes CRUD, handoff bundle, and command parsing.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
7
|
+
import Database from "better-sqlite3";
|
|
8
|
+
import {
|
|
9
|
+
_resetDbForTest,
|
|
10
|
+
closeDb,
|
|
11
|
+
createTopic,
|
|
12
|
+
createRun,
|
|
13
|
+
createArtifact,
|
|
14
|
+
indexEntity,
|
|
15
|
+
searchEntities,
|
|
16
|
+
removeFromIndex,
|
|
17
|
+
createNote,
|
|
18
|
+
listNotes,
|
|
19
|
+
updateNote,
|
|
20
|
+
deleteNote,
|
|
21
|
+
togglePinNote,
|
|
22
|
+
createCommand,
|
|
23
|
+
completeCommand,
|
|
24
|
+
listCommands,
|
|
25
|
+
listApprovals,
|
|
26
|
+
createApproval,
|
|
27
|
+
} from "../src/store.ts";
|
|
28
|
+
import { processShellIntegration } from "../src/session.ts";
|
|
29
|
+
|
|
30
|
+
/** Create an in-memory SQLite DB with full schema including new tables. */
|
|
31
|
+
function createTestDb() {
|
|
32
|
+
const db = new Database(":memory:");
|
|
33
|
+
db.pragma("journal_mode = WAL");
|
|
34
|
+
db.pragma("foreign_keys = ON");
|
|
35
|
+
db.exec(`
|
|
36
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
37
|
+
name TEXT PRIMARY KEY,
|
|
38
|
+
created_at INTEGER NOT NULL
|
|
39
|
+
);
|
|
40
|
+
CREATE TABLE IF NOT EXISTS tabs (
|
|
41
|
+
id INTEGER PRIMARY KEY,
|
|
42
|
+
session_name TEXT NOT NULL,
|
|
43
|
+
title TEXT NOT NULL DEFAULT 'Tab',
|
|
44
|
+
scrollback BLOB,
|
|
45
|
+
ended INTEGER NOT NULL DEFAULT 0,
|
|
46
|
+
FOREIGN KEY (session_name) REFERENCES sessions(name) ON DELETE CASCADE
|
|
47
|
+
);
|
|
48
|
+
CREATE TABLE IF NOT EXISTS devices (
|
|
49
|
+
id TEXT PRIMARY KEY,
|
|
50
|
+
name TEXT NOT NULL,
|
|
51
|
+
fingerprint TEXT NOT NULL,
|
|
52
|
+
trust TEXT NOT NULL DEFAULT 'untrusted',
|
|
53
|
+
created_at INTEGER NOT NULL,
|
|
54
|
+
last_seen INTEGER NOT NULL
|
|
55
|
+
);
|
|
56
|
+
CREATE TABLE IF NOT EXISTS pair_codes (
|
|
57
|
+
code TEXT PRIMARY KEY,
|
|
58
|
+
created_by TEXT NOT NULL,
|
|
59
|
+
expires_at INTEGER NOT NULL,
|
|
60
|
+
FOREIGN KEY (created_by) REFERENCES devices(id) ON DELETE CASCADE
|
|
61
|
+
);
|
|
62
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
63
|
+
key TEXT PRIMARY KEY,
|
|
64
|
+
value TEXT
|
|
65
|
+
);
|
|
66
|
+
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
|
67
|
+
device_id TEXT PRIMARY KEY,
|
|
68
|
+
endpoint TEXT NOT NULL,
|
|
69
|
+
p256dh TEXT NOT NULL,
|
|
70
|
+
auth TEXT NOT NULL,
|
|
71
|
+
created_at INTEGER NOT NULL
|
|
72
|
+
);
|
|
73
|
+
CREATE TABLE IF NOT EXISTS topics (
|
|
74
|
+
id TEXT PRIMARY KEY,
|
|
75
|
+
session_name TEXT NOT NULL,
|
|
76
|
+
title TEXT NOT NULL,
|
|
77
|
+
created_at INTEGER NOT NULL,
|
|
78
|
+
updated_at INTEGER NOT NULL
|
|
79
|
+
);
|
|
80
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
81
|
+
id TEXT PRIMARY KEY,
|
|
82
|
+
topic_id TEXT REFERENCES topics(id),
|
|
83
|
+
session_name TEXT NOT NULL,
|
|
84
|
+
tab_id INTEGER,
|
|
85
|
+
command TEXT,
|
|
86
|
+
exit_code INTEGER,
|
|
87
|
+
started_at INTEGER NOT NULL,
|
|
88
|
+
ended_at INTEGER,
|
|
89
|
+
status TEXT DEFAULT 'running'
|
|
90
|
+
);
|
|
91
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
92
|
+
id TEXT PRIMARY KEY,
|
|
93
|
+
run_id TEXT REFERENCES runs(id),
|
|
94
|
+
topic_id TEXT REFERENCES topics(id),
|
|
95
|
+
session_name TEXT,
|
|
96
|
+
type TEXT NOT NULL,
|
|
97
|
+
title TEXT,
|
|
98
|
+
content TEXT,
|
|
99
|
+
created_at INTEGER NOT NULL
|
|
100
|
+
);
|
|
101
|
+
CREATE TABLE IF NOT EXISTS approvals (
|
|
102
|
+
id TEXT PRIMARY KEY,
|
|
103
|
+
run_id TEXT REFERENCES runs(id),
|
|
104
|
+
topic_id TEXT REFERENCES topics(id),
|
|
105
|
+
title TEXT NOT NULL,
|
|
106
|
+
description TEXT,
|
|
107
|
+
status TEXT DEFAULT 'pending',
|
|
108
|
+
created_at INTEGER NOT NULL,
|
|
109
|
+
resolved_at INTEGER
|
|
110
|
+
);
|
|
111
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS fts_index USING fts5(
|
|
112
|
+
entity_type, entity_id, title, content,
|
|
113
|
+
tokenize='porter unicode61'
|
|
114
|
+
);
|
|
115
|
+
CREATE TABLE IF NOT EXISTS memory_notes (
|
|
116
|
+
id TEXT PRIMARY KEY,
|
|
117
|
+
content TEXT NOT NULL,
|
|
118
|
+
pinned INTEGER DEFAULT 0,
|
|
119
|
+
created_at INTEGER NOT NULL,
|
|
120
|
+
updated_at INTEGER NOT NULL,
|
|
121
|
+
session_name TEXT
|
|
122
|
+
);
|
|
123
|
+
CREATE TABLE IF NOT EXISTS commands (
|
|
124
|
+
id TEXT PRIMARY KEY,
|
|
125
|
+
session_name TEXT NOT NULL,
|
|
126
|
+
tab_id INTEGER NOT NULL,
|
|
127
|
+
command TEXT,
|
|
128
|
+
exit_code INTEGER,
|
|
129
|
+
cwd TEXT,
|
|
130
|
+
started_at INTEGER NOT NULL,
|
|
131
|
+
ended_at INTEGER
|
|
132
|
+
);
|
|
133
|
+
`);
|
|
134
|
+
return db;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── FTS5 Search ───────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
describe("FTS5: indexing and search", () => {
|
|
140
|
+
let db;
|
|
141
|
+
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
db = createTestDb();
|
|
144
|
+
_resetDbForTest(db);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
afterEach(() => {
|
|
148
|
+
closeDb();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("indexes and searches entities", () => {
|
|
152
|
+
indexEntity("topic", "t1", "Deploy Pipeline", "CI/CD deploy pipeline setup");
|
|
153
|
+
indexEntity("artifact", "a1", "Build Log", "npm run build output log");
|
|
154
|
+
|
|
155
|
+
const results = searchEntities("deploy");
|
|
156
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
157
|
+
expect(results[0].entityId).toBe("t1");
|
|
158
|
+
expect(results[0].entityType).toBe("topic");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("returns empty for empty query", () => {
|
|
162
|
+
indexEntity("topic", "t1", "Test", "content");
|
|
163
|
+
expect(searchEntities("")).toHaveLength(0);
|
|
164
|
+
expect(searchEntities(" ")).toHaveLength(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("respects limit parameter", () => {
|
|
168
|
+
for (let i = 0; i < 10; i++) {
|
|
169
|
+
indexEntity("topic", `t${i}`, `Topic ${i}`, `content about testing topic ${i}`);
|
|
170
|
+
}
|
|
171
|
+
const results = searchEntities("topic", 3);
|
|
172
|
+
expect(results).toHaveLength(3);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("removeFromIndex removes entity", () => {
|
|
176
|
+
indexEntity("topic", "t1", "Searchable", "findme content");
|
|
177
|
+
expect(searchEntities("findme").length).toBeGreaterThanOrEqual(1);
|
|
178
|
+
|
|
179
|
+
removeFromIndex("t1");
|
|
180
|
+
expect(searchEntities("findme")).toHaveLength(0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("handles special characters gracefully", () => {
|
|
184
|
+
indexEntity("topic", "t1", "Test", "content");
|
|
185
|
+
// Should not throw
|
|
186
|
+
const results = searchEntities('test "quoted"');
|
|
187
|
+
// May or may not find results, but should not crash
|
|
188
|
+
expect(Array.isArray(results)).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("createTopic auto-indexes into FTS", () => {
|
|
192
|
+
const topic = createTopic("main", "Database Migration Plan");
|
|
193
|
+
const results = searchEntities("migration");
|
|
194
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
195
|
+
expect(results[0].entityId).toBe(topic.id);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("createRun auto-indexes command into FTS", () => {
|
|
199
|
+
const run = createRun({ sessionName: "main", command: "npm test --coverage" });
|
|
200
|
+
const results = searchEntities("coverage");
|
|
201
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
202
|
+
expect(results[0].entityId).toBe(run.id);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("createArtifact auto-indexes into FTS", () => {
|
|
206
|
+
const artifact = createArtifact({
|
|
207
|
+
type: "note",
|
|
208
|
+
title: "Architecture Decision",
|
|
209
|
+
content: "Use SQLite for persistence layer",
|
|
210
|
+
});
|
|
211
|
+
const results = searchEntities("persistence");
|
|
212
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
213
|
+
expect(results[0].entityId).toBe(artifact.id);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ── Memory Notes ──────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
describe("Memory Notes CRUD", () => {
|
|
220
|
+
let db;
|
|
221
|
+
|
|
222
|
+
beforeEach(() => {
|
|
223
|
+
db = createTestDb();
|
|
224
|
+
_resetDbForTest(db);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
afterEach(() => {
|
|
228
|
+
closeDb();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("creates a note with generated ID and timestamps", () => {
|
|
232
|
+
const note = createNote("Remember to check logs");
|
|
233
|
+
expect(note.id).toBeTruthy();
|
|
234
|
+
expect(note.content).toBe("Remember to check logs");
|
|
235
|
+
expect(note.pinned).toBe(false);
|
|
236
|
+
expect(note.createdAt).toBeGreaterThan(0);
|
|
237
|
+
expect(note.updatedAt).toBe(note.createdAt);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("lists notes (pinned first)", () => {
|
|
241
|
+
const n1 = createNote("Note A");
|
|
242
|
+
const n2 = createNote("Note B");
|
|
243
|
+
togglePinNote(n1.id);
|
|
244
|
+
|
|
245
|
+
const notes = listNotes();
|
|
246
|
+
expect(notes).toHaveLength(2);
|
|
247
|
+
// Pinned note should be first
|
|
248
|
+
expect(notes[0].id).toBe(n1.id);
|
|
249
|
+
expect(notes[0].pinned).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("updates a note content", () => {
|
|
253
|
+
const note = createNote("Old content");
|
|
254
|
+
const ok = updateNote(note.id, "New content");
|
|
255
|
+
expect(ok).toBe(true);
|
|
256
|
+
|
|
257
|
+
const notes = listNotes();
|
|
258
|
+
expect(notes[0].content).toBe("New content");
|
|
259
|
+
expect(notes[0].updatedAt).toBeGreaterThanOrEqual(notes[0].createdAt);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("updateNote returns false for nonexistent ID", () => {
|
|
263
|
+
expect(updateNote("nonexistent", "x")).toBe(false);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("deletes a note", () => {
|
|
267
|
+
const note = createNote("To delete");
|
|
268
|
+
expect(deleteNote(note.id)).toBe(true);
|
|
269
|
+
expect(listNotes()).toHaveLength(0);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("deleteNote returns false for nonexistent ID", () => {
|
|
273
|
+
expect(deleteNote("nonexistent")).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("togglePinNote toggles pin state", () => {
|
|
277
|
+
const note = createNote("Pin me");
|
|
278
|
+
expect(listNotes()[0].pinned).toBe(false);
|
|
279
|
+
|
|
280
|
+
togglePinNote(note.id);
|
|
281
|
+
expect(listNotes()[0].pinned).toBe(true);
|
|
282
|
+
|
|
283
|
+
togglePinNote(note.id);
|
|
284
|
+
expect(listNotes()[0].pinned).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ── Commands (Shell Integration Store) ────────────────────────────
|
|
289
|
+
|
|
290
|
+
describe("Commands CRUD", () => {
|
|
291
|
+
let db;
|
|
292
|
+
|
|
293
|
+
beforeEach(() => {
|
|
294
|
+
db = createTestDb();
|
|
295
|
+
_resetDbForTest(db);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
afterEach(() => {
|
|
299
|
+
closeDb();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("creates a command record", () => {
|
|
303
|
+
const cmd = createCommand({
|
|
304
|
+
sessionName: "main",
|
|
305
|
+
tabId: 0,
|
|
306
|
+
command: "ls -la",
|
|
307
|
+
cwd: "/home/user",
|
|
308
|
+
});
|
|
309
|
+
expect(cmd.id).toBeTruthy();
|
|
310
|
+
expect(cmd.sessionName).toBe("main");
|
|
311
|
+
expect(cmd.tabId).toBe(0);
|
|
312
|
+
expect(cmd.command).toBe("ls -la");
|
|
313
|
+
expect(cmd.cwd).toBe("/home/user");
|
|
314
|
+
expect(cmd.exitCode).toBeNull();
|
|
315
|
+
expect(cmd.endedAt).toBeNull();
|
|
316
|
+
expect(cmd.startedAt).toBeGreaterThan(0);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("completes a command with exit code", () => {
|
|
320
|
+
const cmd = createCommand({ sessionName: "main", tabId: 0, command: "test" });
|
|
321
|
+
const ok = completeCommand(cmd.id, 0);
|
|
322
|
+
expect(ok).toBe(true);
|
|
323
|
+
|
|
324
|
+
const commands = listCommands(0);
|
|
325
|
+
expect(commands[0].exitCode).toBe(0);
|
|
326
|
+
expect(commands[0].endedAt).toBeGreaterThan(0);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("lists commands for a tab (most recent first)", () => {
|
|
330
|
+
createCommand({ sessionName: "main", tabId: 1, command: "first" });
|
|
331
|
+
createCommand({ sessionName: "main", tabId: 1, command: "second" });
|
|
332
|
+
createCommand({ sessionName: "main", tabId: 2, command: "other tab" });
|
|
333
|
+
|
|
334
|
+
const cmds = listCommands(1);
|
|
335
|
+
expect(cmds).toHaveLength(2);
|
|
336
|
+
expect(cmds[0].command).toBe("second"); // most recent first
|
|
337
|
+
expect(cmds[1].command).toBe("first");
|
|
338
|
+
|
|
339
|
+
const otherCmds = listCommands(2);
|
|
340
|
+
expect(otherCmds).toHaveLength(1);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("respects limit parameter", () => {
|
|
344
|
+
for (let i = 0; i < 10; i++) {
|
|
345
|
+
createCommand({ sessionName: "main", tabId: 0, command: `cmd${i}` });
|
|
346
|
+
}
|
|
347
|
+
const cmds = listCommands(0, 3);
|
|
348
|
+
expect(cmds).toHaveLength(3);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// ── Shell Integration: OSC 133 Parsing ────────────────────────────
|
|
353
|
+
|
|
354
|
+
describe("Shell Integration: OSC 133 parsing", () => {
|
|
355
|
+
let db;
|
|
356
|
+
|
|
357
|
+
beforeEach(() => {
|
|
358
|
+
db = createTestDb();
|
|
359
|
+
_resetDbForTest(db);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
afterEach(() => {
|
|
363
|
+
closeDb();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
/** Create a minimal mock tab for testing shell integration. */
|
|
367
|
+
function mockTab(id = 0) {
|
|
368
|
+
return {
|
|
369
|
+
id,
|
|
370
|
+
pty: null,
|
|
371
|
+
scrollback: { write: () => {}, read: () => Buffer.alloc(0) },
|
|
372
|
+
vt: null,
|
|
373
|
+
clients: new Set(),
|
|
374
|
+
cols: 80,
|
|
375
|
+
rows: 24,
|
|
376
|
+
ended: false,
|
|
377
|
+
title: "Test",
|
|
378
|
+
shellIntegration: {
|
|
379
|
+
phase: "idle",
|
|
380
|
+
commandBuffer: "",
|
|
381
|
+
cwd: null,
|
|
382
|
+
activeCommandId: null,
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
it("tracks CWD from OSC 7", () => {
|
|
388
|
+
const tab = mockTab();
|
|
389
|
+
processShellIntegration(
|
|
390
|
+
"\x1b]7;file://localhost/home/user/project\x07",
|
|
391
|
+
tab,
|
|
392
|
+
"main",
|
|
393
|
+
);
|
|
394
|
+
expect(tab.shellIntegration.cwd).toBe("/home/user/project");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("parses OSC 133;A (prompt start)", () => {
|
|
398
|
+
const tab = mockTab();
|
|
399
|
+
processShellIntegration("\x1b]133;A\x07", tab, "main");
|
|
400
|
+
expect(tab.shellIntegration.phase).toBe("prompt");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("parses full command lifecycle (B -> C -> D)", () => {
|
|
404
|
+
const tab = mockTab();
|
|
405
|
+
|
|
406
|
+
// Prompt start
|
|
407
|
+
processShellIntegration("\x1b]133;A\x07", tab, "main");
|
|
408
|
+
expect(tab.shellIntegration.phase).toBe("prompt");
|
|
409
|
+
|
|
410
|
+
// Command start + text + output start in one chunk
|
|
411
|
+
processShellIntegration(
|
|
412
|
+
"\x1b]133;B\x07ls -la\x1b]133;C\x07",
|
|
413
|
+
tab,
|
|
414
|
+
"main",
|
|
415
|
+
);
|
|
416
|
+
expect(tab.shellIntegration.phase).toBe("output");
|
|
417
|
+
expect(tab.shellIntegration.commandBuffer).toBe("ls -la");
|
|
418
|
+
expect(tab.shellIntegration.activeCommandId).toBeTruthy();
|
|
419
|
+
|
|
420
|
+
// Command end
|
|
421
|
+
const cmdId = tab.shellIntegration.activeCommandId;
|
|
422
|
+
processShellIntegration("\x1b]133;D;0\x07", tab, "main");
|
|
423
|
+
expect(tab.shellIntegration.phase).toBe("idle");
|
|
424
|
+
expect(tab.shellIntegration.activeCommandId).toBeNull();
|
|
425
|
+
|
|
426
|
+
// Verify command was persisted in DB
|
|
427
|
+
const commands = listCommands(tab.id);
|
|
428
|
+
expect(commands).toHaveLength(1);
|
|
429
|
+
expect(commands[0].command).toBe("ls -la");
|
|
430
|
+
expect(commands[0].exitCode).toBe(0);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("tracks non-zero exit codes", () => {
|
|
434
|
+
const tab = mockTab();
|
|
435
|
+
|
|
436
|
+
processShellIntegration(
|
|
437
|
+
"\x1b]133;B\x07bad-command\x1b]133;C\x07",
|
|
438
|
+
tab,
|
|
439
|
+
"main",
|
|
440
|
+
);
|
|
441
|
+
processShellIntegration("\x1b]133;D;127\x07", tab, "main");
|
|
442
|
+
|
|
443
|
+
const commands = listCommands(tab.id);
|
|
444
|
+
expect(commands).toHaveLength(1);
|
|
445
|
+
expect(commands[0].exitCode).toBe(127);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("handles OSC 7 with encoded path", () => {
|
|
449
|
+
const tab = mockTab();
|
|
450
|
+
processShellIntegration(
|
|
451
|
+
"\x1b]7;file://host/home/user/my%20project\x07",
|
|
452
|
+
tab,
|
|
453
|
+
"main",
|
|
454
|
+
);
|
|
455
|
+
expect(tab.shellIntegration.cwd).toBe("/home/user/my project");
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// ── Handoff Bundle ────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
describe("Handoff bundle generation", () => {
|
|
462
|
+
let db;
|
|
463
|
+
|
|
464
|
+
beforeEach(() => {
|
|
465
|
+
db = createTestDb();
|
|
466
|
+
_resetDbForTest(db);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
afterEach(() => {
|
|
470
|
+
closeDb();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("generates a bundle with basic structure", async () => {
|
|
474
|
+
// Need to import dynamically to avoid circular dependency issues
|
|
475
|
+
const { generateHandoffBundle } = await import("../src/workspace.ts");
|
|
476
|
+
|
|
477
|
+
const bundle = generateHandoffBundle();
|
|
478
|
+
expect(bundle.timestamp).toBeGreaterThan(0);
|
|
479
|
+
expect(Array.isArray(bundle.sessions)).toBe(true);
|
|
480
|
+
expect(Array.isArray(bundle.recentRuns)).toBe(true);
|
|
481
|
+
expect(Array.isArray(bundle.activeTopics)).toBe(true);
|
|
482
|
+
expect(Array.isArray(bundle.pendingApprovals)).toBe(true);
|
|
483
|
+
expect(Array.isArray(bundle.keyArtifacts)).toBe(true);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("includes recent runs and active topics", async () => {
|
|
487
|
+
const { generateHandoffBundle } = await import("../src/workspace.ts");
|
|
488
|
+
|
|
489
|
+
createTopic("main", "Active Topic");
|
|
490
|
+
createRun({ sessionName: "main", command: "npm test" });
|
|
491
|
+
createApproval({ title: "Deploy?" });
|
|
492
|
+
|
|
493
|
+
const bundle = generateHandoffBundle();
|
|
494
|
+
expect(bundle.activeTopics).toHaveLength(1);
|
|
495
|
+
expect(bundle.activeTopics[0].title).toBe("Active Topic");
|
|
496
|
+
expect(bundle.recentRuns).toHaveLength(1);
|
|
497
|
+
expect(bundle.pendingApprovals).toHaveLength(1);
|
|
498
|
+
});
|
|
499
|
+
});
|