screenhand 0.1.1 → 0.3.0
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 +193 -109
- package/bin/darwin-arm64/macos-bridge +0 -0
- package/dist/mcp-desktop.js +5876 -0
- package/dist/scripts/codex-monitor-daemon.js +335 -0
- package/dist/scripts/export-help-center.js +112 -0
- package/dist/scripts/marketing-loop.js +117 -0
- package/dist/scripts/observer-daemon.js +288 -0
- package/dist/scripts/orchestrator-daemon.js +399 -0
- package/dist/scripts/supervisor-daemon.js +272 -0
- package/dist/scripts/threads-campaign.js +208 -0
- package/dist/scripts/worker-daemon.js +228 -0
- package/dist/src/agent/cli.js +82 -0
- package/dist/src/agent/loop.js +274 -0
- package/dist/src/community/fetcher.js +109 -0
- package/dist/src/community/index.js +6 -0
- package/dist/src/community/publisher.js +191 -0
- package/dist/src/community/remote-api.js +121 -0
- package/dist/src/community/types.js +3 -0
- package/dist/src/community/validator.js +95 -0
- package/{src/config.ts → dist/src/config.js} +5 -10
- package/dist/src/context-tracker.js +489 -0
- package/{src/index.ts → dist/src/index.js} +32 -52
- package/dist/src/ingestion/coverage-auditor.js +233 -0
- package/dist/src/ingestion/doc-parser.js +164 -0
- package/dist/src/ingestion/index.js +8 -0
- package/dist/src/ingestion/menu-scanner.js +152 -0
- package/dist/src/ingestion/reference-merger.js +186 -0
- package/dist/src/ingestion/shortcut-extractor.js +180 -0
- package/dist/src/ingestion/tutorial-extractor.js +170 -0
- package/dist/src/ingestion/types.js +3 -0
- package/dist/src/jobs/manager.js +305 -0
- package/dist/src/jobs/runner.js +806 -0
- package/dist/src/jobs/store.js +102 -0
- package/dist/src/jobs/types.js +30 -0
- package/dist/src/jobs/worker.js +97 -0
- package/dist/src/learning/engine.js +356 -0
- package/dist/src/learning/index.js +9 -0
- package/dist/src/learning/locator-policy.js +120 -0
- package/dist/src/learning/pattern-policy.js +89 -0
- package/dist/src/learning/recovery-policy.js +116 -0
- package/dist/src/learning/sensor-policy.js +115 -0
- package/dist/src/learning/timing-model.js +204 -0
- package/dist/src/learning/topology-policy.js +90 -0
- package/dist/src/learning/types.js +9 -0
- package/dist/src/logging/timeline-logger.js +48 -0
- package/dist/src/mcp/mcp-stdio-server.js +464 -0
- package/dist/src/mcp/server.js +363 -0
- package/dist/src/mcp-entry.js +60 -0
- package/dist/src/memory/playbook-seeds.js +200 -0
- package/dist/src/memory/recall.js +222 -0
- package/dist/src/memory/research.js +104 -0
- package/dist/src/memory/seeds.js +101 -0
- package/dist/src/memory/service.js +446 -0
- package/dist/src/memory/session.js +169 -0
- package/dist/src/memory/store.js +451 -0
- package/{src/runtime/locator-cache.ts → dist/src/memory/types.js} +1 -17
- package/dist/src/monitor/codex-monitor.js +382 -0
- package/dist/src/monitor/task-queue.js +97 -0
- package/dist/src/monitor/types.js +62 -0
- package/dist/src/native/bridge-client.js +412 -0
- package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
- package/dist/src/observer/state.js +199 -0
- package/dist/src/observer/types.js +43 -0
- package/dist/src/orchestrator/state.js +68 -0
- package/dist/src/orchestrator/types.js +22 -0
- package/dist/src/perception/ax-source.js +162 -0
- package/dist/src/perception/cdp-source.js +162 -0
- package/dist/src/perception/coordinator.js +771 -0
- package/dist/src/perception/frame-differ.js +287 -0
- package/dist/src/perception/index.js +22 -0
- package/dist/src/perception/manager.js +199 -0
- package/dist/src/perception/types.js +47 -0
- package/dist/src/perception/vision-source.js +399 -0
- package/dist/src/planner/deterministic.js +298 -0
- package/dist/src/planner/executor.js +870 -0
- package/dist/src/planner/goal-store.js +92 -0
- package/dist/src/planner/index.js +21 -0
- package/dist/src/planner/planner.js +520 -0
- package/dist/src/planner/tool-registry.js +71 -0
- package/dist/src/planner/types.js +22 -0
- package/dist/src/platform/explorer.js +213 -0
- package/dist/src/platform/help-center-markdown.js +527 -0
- package/dist/src/platform/learner.js +257 -0
- package/dist/src/playbook/engine.js +486 -0
- package/dist/src/playbook/index.js +20 -0
- package/dist/src/playbook/mcp-recorder.js +204 -0
- package/dist/src/playbook/recorder.js +536 -0
- package/dist/src/playbook/runner.js +408 -0
- package/dist/src/playbook/store.js +312 -0
- package/dist/src/playbook/types.js +17 -0
- package/dist/src/recovery/detectors.js +156 -0
- package/dist/src/recovery/engine.js +327 -0
- package/dist/src/recovery/index.js +20 -0
- package/dist/src/recovery/strategies.js +274 -0
- package/dist/src/recovery/types.js +20 -0
- package/dist/src/runtime/accessibility-adapter.js +430 -0
- package/dist/src/runtime/app-adapter.js +64 -0
- package/dist/src/runtime/applescript-adapter.js +305 -0
- package/dist/src/runtime/ax-role-map.js +96 -0
- package/dist/src/runtime/browser-adapter.js +52 -0
- package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
- package/dist/src/runtime/composite-adapter.js +221 -0
- package/dist/src/runtime/execution-contract.js +159 -0
- package/dist/src/runtime/executor.js +286 -0
- package/dist/src/runtime/locator-cache.js +50 -0
- package/dist/src/runtime/planning-loop.js +63 -0
- package/dist/src/runtime/service.js +432 -0
- package/dist/src/runtime/session-manager.js +63 -0
- package/dist/src/runtime/state-observer.js +121 -0
- package/dist/src/runtime/vision-adapter.js +225 -0
- package/dist/src/state/app-map-types.js +72 -0
- package/dist/src/state/app-map.js +1974 -0
- package/dist/src/state/entity-tracker.js +108 -0
- package/dist/src/state/fusion.js +96 -0
- package/dist/src/state/index.js +21 -0
- package/dist/src/state/ladder-generator.js +236 -0
- package/dist/src/state/persistence.js +156 -0
- package/dist/src/state/types.js +17 -0
- package/dist/src/state/world-model.js +1456 -0
- package/dist/src/supervisor/locks.js +186 -0
- package/dist/src/supervisor/supervisor.js +403 -0
- package/dist/src/supervisor/types.js +30 -0
- package/dist/src/test-mcp-protocol.js +154 -0
- package/dist/src/types.js +17 -0
- package/dist/src/util/atomic-write.js +133 -0
- package/dist/src/util/sanitize.js +146 -0
- package/dist-app-maps/com.figma.Desktop.json +959 -0
- package/dist-app-maps/com.hnc.Discord.json +1146 -0
- package/dist-app-maps/notion.id.json +2831 -0
- package/dist-playbooks/canva-screenhand-carousel.json +445 -0
- package/dist-playbooks/codex-desktop.json +76 -0
- package/dist-playbooks/competitor-research-stack.json +122 -0
- package/dist-playbooks/davinci-color-grade.json +153 -0
- package/dist-playbooks/davinci-edit-timeline.json +162 -0
- package/dist-playbooks/davinci-render.json +114 -0
- package/dist-playbooks/devto.json +52 -0
- package/dist-playbooks/discord.json +41 -0
- package/dist-playbooks/google-flow-create-project.json +59 -0
- package/dist-playbooks/google-flow-edit-image.json +90 -0
- package/dist-playbooks/google-flow-edit-video.json +90 -0
- package/dist-playbooks/google-flow-generate-image.json +68 -0
- package/dist-playbooks/google-flow-generate-video.json +191 -0
- package/dist-playbooks/google-flow-open-project.json +48 -0
- package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
- package/dist-playbooks/google-flow-search-assets.json +64 -0
- package/dist-playbooks/instagram.json +57 -0
- package/dist-playbooks/linkedin.json +52 -0
- package/dist-playbooks/n8n.json +43 -0
- package/dist-playbooks/reddit.json +52 -0
- package/dist-playbooks/threads.json +59 -0
- package/dist-playbooks/x-twitter.json +59 -0
- package/dist-playbooks/youtube.json +59 -0
- package/dist-references/canva.json +646 -0
- package/dist-references/codex-desktop.json +305 -0
- package/dist-references/davinci-resolve-keyboard.json +594 -0
- package/dist-references/davinci-resolve-menu-map.json +1139 -0
- package/dist-references/davinci-resolve-menus-batch1.json +116 -0
- package/dist-references/davinci-resolve-menus-batch2.json +372 -0
- package/dist-references/davinci-resolve-menus-batch3.json +330 -0
- package/dist-references/davinci-resolve-menus-batch4.json +297 -0
- package/dist-references/davinci-resolve-shortcuts.json +333 -0
- package/dist-references/devto.json +317 -0
- package/dist-references/discord.json +549 -0
- package/dist-references/figma.json +1186 -0
- package/dist-references/finder.json +146 -0
- package/dist-references/google-ads-transparency.json +95 -0
- package/dist-references/google-flow.json +649 -0
- package/dist-references/instagram.json +341 -0
- package/dist-references/linkedin.json +324 -0
- package/dist-references/meta-ad-library.json +86 -0
- package/dist-references/n8n.json +387 -0
- package/dist-references/notes.json +27 -0
- package/dist-references/notion.json +163 -0
- package/dist-references/reddit.json +341 -0
- package/dist-references/threads.json +337 -0
- package/dist-references/x-twitter.json +403 -0
- package/dist-references/youtube.json +373 -0
- package/native/macos-bridge/Package.swift +1 -0
- package/native/macos-bridge/Sources/AccessibilityBridge.swift +257 -36
- package/native/macos-bridge/Sources/AppManagement.swift +212 -2
- package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +348 -53
- package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
- package/native/macos-bridge/Sources/VisionBridge.swift +165 -7
- package/native/macos-bridge/Sources/main.swift +169 -16
- package/native/windows-bridge/Program.cs +5 -0
- package/native/windows-bridge/ScreenCapture.cs +124 -0
- package/package.json +29 -4
- package/scripts/postinstall.cjs +127 -0
- package/.claude/commands/automate.md +0 -28
- package/.claude/commands/debug-ui.md +0 -19
- package/.claude/commands/screenshot.md +0 -15
- package/.github/FUNDING.yml +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.mcp.json +0 -8
- package/DESKTOP_MCP_GUIDE.md +0 -92
- package/SECURITY.md +0 -44
- package/docs/architecture.md +0 -47
- package/install-skills.sh +0 -19
- package/mcp-bridge.ts +0 -271
- package/mcp-desktop.ts +0 -1221
- package/playbooks/instagram.json +0 -41
- package/playbooks/instagram_v2.json +0 -201
- package/playbooks/x_v1.json +0 -211
- package/scripts/devpost-live-loop.mjs +0 -421
- package/src/logging/timeline-logger.ts +0 -55
- package/src/mcp/server.ts +0 -449
- package/src/memory/recall.ts +0 -191
- package/src/memory/research.ts +0 -146
- package/src/memory/seeds.ts +0 -123
- package/src/memory/session.ts +0 -201
- package/src/memory/store.ts +0 -434
- package/src/memory/types.ts +0 -69
- package/src/native/bridge-client.ts +0 -239
- package/src/runtime/accessibility-adapter.ts +0 -487
- package/src/runtime/app-adapter.ts +0 -169
- package/src/runtime/applescript-adapter.ts +0 -376
- package/src/runtime/ax-role-map.ts +0 -102
- package/src/runtime/browser-adapter.ts +0 -129
- package/src/runtime/cdp-chrome-adapter.ts +0 -676
- package/src/runtime/composite-adapter.ts +0 -274
- package/src/runtime/executor.ts +0 -396
- package/src/runtime/planning-loop.ts +0 -81
- package/src/runtime/service.ts +0 -448
- package/src/runtime/session-manager.ts +0 -50
- package/src/runtime/state-observer.ts +0 -136
- package/src/runtime/vision-adapter.ts +0 -297
- package/src/types.ts +0 -297
- package/tests/bridge-client.test.ts +0 -176
- package/tests/browser-stealth.test.ts +0 -210
- package/tests/composite-adapter.test.ts +0 -64
- package/tests/mcp-server.test.ts +0 -151
- package/tests/memory-recall.test.ts +0 -339
- package/tests/memory-research.test.ts +0 -159
- package/tests/memory-seeds.test.ts +0 -120
- package/tests/memory-store.test.ts +0 -392
- package/tests/types.test.ts +0 -92
- package/tsconfig.check.json +0 -17
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -8
- /package/{playbooks → dist-references}/devpost.json +0 -0
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import { backgroundResearch } from "../src/memory/research.js";
|
|
6
|
-
import { MemoryStore } from "../src/memory/store.js";
|
|
7
|
-
|
|
8
|
-
let tmpDir: string;
|
|
9
|
-
let store: MemoryStore;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "research-test-"));
|
|
13
|
-
store = new MemoryStore(tmpDir);
|
|
14
|
-
store.init();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
vi.restoreAllMocks();
|
|
19
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe("backgroundResearch", () => {
|
|
23
|
-
it("returns immediately (non-blocking)", () => {
|
|
24
|
-
const start = Date.now();
|
|
25
|
-
// Mock fetch to prevent actual network calls
|
|
26
|
-
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("no network"));
|
|
27
|
-
backgroundResearch(store, "launch", { bundleId: "com.test" }, "app not found");
|
|
28
|
-
const elapsed = Date.now() - start;
|
|
29
|
-
expect(elapsed).toBeLessThan(5);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("does not throw on any input", () => {
|
|
33
|
-
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("no network"));
|
|
34
|
-
expect(() => backgroundResearch(store, "launch", {}, "error")).not.toThrow();
|
|
35
|
-
expect(() => backgroundResearch(store, "", {}, "")).not.toThrow();
|
|
36
|
-
expect(() => backgroundResearch(store, "key", { combo: "cmd+s" }, "failed")).not.toThrow();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("saves resolution to error cache (mock fetch)", async () => {
|
|
40
|
-
const mockResponse = {
|
|
41
|
-
ok: true,
|
|
42
|
-
json: async () => ({
|
|
43
|
-
AbstractText: "Try launching the app with the correct bundle ID from the Applications folder.",
|
|
44
|
-
}),
|
|
45
|
-
};
|
|
46
|
-
vi.spyOn(globalThis, "fetch").mockResolvedValue(mockResponse as Response);
|
|
47
|
-
|
|
48
|
-
// Delete API key so it falls through to DuckDuckGo path
|
|
49
|
-
const origKey = process.env["ANTHROPIC_API_KEY"];
|
|
50
|
-
delete process.env["ANTHROPIC_API_KEY"];
|
|
51
|
-
|
|
52
|
-
backgroundResearch(store, "launch", { bundleId: "com.test" }, "app not found");
|
|
53
|
-
|
|
54
|
-
// Wait for the async work to complete
|
|
55
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
56
|
-
|
|
57
|
-
const errors = store.readErrors();
|
|
58
|
-
// Should have the seed error (from backgroundResearch) plus any existing
|
|
59
|
-
const researchErrors = errors.filter((e) => e.resolution !== null);
|
|
60
|
-
expect(researchErrors.length).toBeGreaterThan(0);
|
|
61
|
-
expect(researchErrors[0]!.resolution).toContain("bundle ID");
|
|
62
|
-
|
|
63
|
-
// Restore
|
|
64
|
-
if (origKey !== undefined) process.env["ANTHROPIC_API_KEY"] = origKey;
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("saves resolution as strategy too (mock fetch)", async () => {
|
|
68
|
-
const mockResponse = {
|
|
69
|
-
ok: true,
|
|
70
|
-
json: async () => ({
|
|
71
|
-
AbstractText: "Use the correct app identifier to launch applications on macOS.",
|
|
72
|
-
}),
|
|
73
|
-
};
|
|
74
|
-
vi.spyOn(globalThis, "fetch").mockResolvedValue(mockResponse as Response);
|
|
75
|
-
|
|
76
|
-
const origKey = process.env["ANTHROPIC_API_KEY"];
|
|
77
|
-
delete process.env["ANTHROPIC_API_KEY"];
|
|
78
|
-
|
|
79
|
-
const initialCount = store.readStrategies().length;
|
|
80
|
-
backgroundResearch(store, "launch", { bundleId: "com.test" }, "app not found");
|
|
81
|
-
|
|
82
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
83
|
-
|
|
84
|
-
const strategies = store.readStrategies();
|
|
85
|
-
expect(strategies.length).toBeGreaterThan(initialCount);
|
|
86
|
-
const researchStrategy = strategies.find((s) => s.id.startsWith("str_research_"));
|
|
87
|
-
expect(researchStrategy).toBeDefined();
|
|
88
|
-
expect(researchStrategy!.tags).toContain("research");
|
|
89
|
-
|
|
90
|
-
if (origKey !== undefined) process.env["ANTHROPIC_API_KEY"] = origKey;
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("handles fetch failure gracefully", async () => {
|
|
94
|
-
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("network error"));
|
|
95
|
-
|
|
96
|
-
const origKey = process.env["ANTHROPIC_API_KEY"];
|
|
97
|
-
delete process.env["ANTHROPIC_API_KEY"];
|
|
98
|
-
|
|
99
|
-
const initialErrors = store.readErrors().length;
|
|
100
|
-
backgroundResearch(store, "launch", {}, "test error");
|
|
101
|
-
|
|
102
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
103
|
-
|
|
104
|
-
// No new errors should be added since fetch failed
|
|
105
|
-
const errors = store.readErrors();
|
|
106
|
-
expect(errors.length).toBe(initialErrors);
|
|
107
|
-
|
|
108
|
-
if (origKey !== undefined) process.env["ANTHROPIC_API_KEY"] = origKey;
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("falls back to DuckDuckGo when no API key", async () => {
|
|
112
|
-
const origKey = process.env["ANTHROPIC_API_KEY"];
|
|
113
|
-
delete process.env["ANTHROPIC_API_KEY"];
|
|
114
|
-
|
|
115
|
-
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
|
116
|
-
ok: true,
|
|
117
|
-
json: async () => ({
|
|
118
|
-
AbstractText: "DuckDuckGo answer for macOS automation fix.",
|
|
119
|
-
}),
|
|
120
|
-
} as Response);
|
|
121
|
-
|
|
122
|
-
backgroundResearch(store, "focus", { bundleId: "com.test" }, "app not running");
|
|
123
|
-
|
|
124
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
125
|
-
|
|
126
|
-
// Should have called DuckDuckGo (api.duckduckgo.com), not Anthropic
|
|
127
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
128
|
-
const calledUrl = fetchSpy.mock.calls[0]![0] as string;
|
|
129
|
-
expect(calledUrl).toContain("duckduckgo.com");
|
|
130
|
-
|
|
131
|
-
if (origKey !== undefined) process.env["ANTHROPIC_API_KEY"] = origKey;
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("tries Claude API first when API key is set", async () => {
|
|
135
|
-
const origKey = process.env["ANTHROPIC_API_KEY"];
|
|
136
|
-
process.env["ANTHROPIC_API_KEY"] = "test-key-123";
|
|
137
|
-
|
|
138
|
-
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
|
139
|
-
ok: true,
|
|
140
|
-
json: async () => ({
|
|
141
|
-
content: [{ type: "text", text: "Use the correct bundle identifier for the application." }],
|
|
142
|
-
}),
|
|
143
|
-
} as Response);
|
|
144
|
-
|
|
145
|
-
backgroundResearch(store, "launch", {}, "app not found");
|
|
146
|
-
|
|
147
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
148
|
-
|
|
149
|
-
expect(fetchSpy).toHaveBeenCalled();
|
|
150
|
-
const calledUrl = fetchSpy.mock.calls[0]![0] as string;
|
|
151
|
-
expect(calledUrl).toContain("anthropic.com");
|
|
152
|
-
|
|
153
|
-
if (origKey !== undefined) {
|
|
154
|
-
process.env["ANTHROPIC_API_KEY"] = origKey;
|
|
155
|
-
} else {
|
|
156
|
-
delete process.env["ANTHROPIC_API_KEY"];
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
});
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import { SEED_STRATEGIES } from "../src/memory/seeds.js";
|
|
6
|
-
import { MemoryStore } from "../src/memory/store.js";
|
|
7
|
-
import { RecallEngine } from "../src/memory/recall.js";
|
|
8
|
-
|
|
9
|
-
let tmpDir: string;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "seeds-test-"));
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe("SEED_STRATEGIES structure", () => {
|
|
20
|
-
it("all seeds have valid required fields", () => {
|
|
21
|
-
for (const s of SEED_STRATEGIES) {
|
|
22
|
-
expect(s.id).toBeTruthy();
|
|
23
|
-
expect(s.task).toBeTruthy();
|
|
24
|
-
expect(s.steps.length).toBeGreaterThan(0);
|
|
25
|
-
expect(s.tags.length).toBeGreaterThan(0);
|
|
26
|
-
expect(s.fingerprint).toBeTruthy();
|
|
27
|
-
expect(typeof s.successCount).toBe("number");
|
|
28
|
-
expect(typeof s.failCount).toBe("number");
|
|
29
|
-
expect(s.lastUsed).toBeTruthy();
|
|
30
|
-
for (const step of s.steps) {
|
|
31
|
-
expect(step.tool).toBeTruthy();
|
|
32
|
-
expect(step.params).toBeDefined();
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("all seeds have successCount of 10", () => {
|
|
38
|
-
for (const s of SEED_STRATEGIES) {
|
|
39
|
-
expect(s.successCount).toBe(10);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("has no duplicate fingerprints", () => {
|
|
44
|
-
const fingerprints = SEED_STRATEGIES.map((s) => s.fingerprint);
|
|
45
|
-
const unique = new Set(fingerprints);
|
|
46
|
-
// Some seeds may share tool sequences (e.g. focus→key), so check IDs are unique
|
|
47
|
-
const ids = SEED_STRATEGIES.map((s) => s.id);
|
|
48
|
-
expect(new Set(ids).size).toBe(ids.length);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("has approximately 12 seed strategies", () => {
|
|
52
|
-
expect(SEED_STRATEGIES.length).toBeGreaterThanOrEqual(10);
|
|
53
|
-
expect(SEED_STRATEGIES.length).toBeLessThanOrEqual(15);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe("Store seed loading", () => {
|
|
58
|
-
it("loads seeds on first init (empty directory)", () => {
|
|
59
|
-
const store = new MemoryStore(tmpDir);
|
|
60
|
-
store.init();
|
|
61
|
-
const strategies = store.readStrategies();
|
|
62
|
-
expect(strategies.length).toBe(SEED_STRATEGIES.length);
|
|
63
|
-
expect(strategies[0]!.id).toMatch(/^seed_/);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("skips seeds when strategies already exist on disk", async () => {
|
|
67
|
-
// First init — seeds are loaded and persisted
|
|
68
|
-
const store1 = new MemoryStore(tmpDir);
|
|
69
|
-
store1.init();
|
|
70
|
-
|
|
71
|
-
// Add a custom strategy
|
|
72
|
-
store1.appendStrategy({
|
|
73
|
-
id: "str_custom",
|
|
74
|
-
task: "custom task",
|
|
75
|
-
steps: [{ tool: "screenshot", params: {} }],
|
|
76
|
-
totalDurationMs: 100,
|
|
77
|
-
successCount: 5,
|
|
78
|
-
failCount: 0,
|
|
79
|
-
lastUsed: new Date().toISOString(),
|
|
80
|
-
tags: ["custom"],
|
|
81
|
-
fingerprint: "screenshot",
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// Wait for async write to persist
|
|
85
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
86
|
-
|
|
87
|
-
// Create a new store instance — should NOT re-add seeds
|
|
88
|
-
const store2 = new MemoryStore(tmpDir);
|
|
89
|
-
store2.init();
|
|
90
|
-
const strategies = store2.readStrategies();
|
|
91
|
-
// Should have seeds + custom, not double seeds
|
|
92
|
-
expect(strategies.length).toBe(SEED_STRATEGIES.length + 1);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("seeds are searchable via RecallEngine", () => {
|
|
96
|
-
const store = new MemoryStore(tmpDir);
|
|
97
|
-
store.init();
|
|
98
|
-
const engine = new RecallEngine(store);
|
|
99
|
-
|
|
100
|
-
const results = engine.recallStrategies("take photo", 5);
|
|
101
|
-
expect(results.length).toBeGreaterThan(0);
|
|
102
|
-
expect(results[0]!.task).toContain("Photo Booth");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("seeds survive reload (persisted to disk)", async () => {
|
|
106
|
-
const store1 = new MemoryStore(tmpDir);
|
|
107
|
-
store1.init();
|
|
108
|
-
|
|
109
|
-
// Give async write a moment
|
|
110
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
111
|
-
|
|
112
|
-
// Check the file was written
|
|
113
|
-
const memDir = path.join(tmpDir, ".screenhand", "memory");
|
|
114
|
-
const strategiesFile = path.join(memDir, "strategies.jsonl");
|
|
115
|
-
expect(fs.existsSync(strategiesFile)).toBe(true);
|
|
116
|
-
|
|
117
|
-
const lines = fs.readFileSync(strategiesFile, "utf-8").trim().split("\n");
|
|
118
|
-
expect(lines.length).toBe(SEED_STRATEGIES.length);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
@@ -1,392 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import { MemoryStore } from "../src/memory/store.js";
|
|
6
|
-
import type { ActionEntry, Strategy, ErrorPattern } from "../src/memory/types.js";
|
|
7
|
-
|
|
8
|
-
let tmpDir: string;
|
|
9
|
-
let store: MemoryStore;
|
|
10
|
-
|
|
11
|
-
function makeAction(overrides: Partial<ActionEntry> = {}): ActionEntry {
|
|
12
|
-
return {
|
|
13
|
-
id: "a_test" + Math.random().toString(36).slice(2, 6),
|
|
14
|
-
timestamp: new Date().toISOString(),
|
|
15
|
-
sessionId: "s_test",
|
|
16
|
-
tool: "apps",
|
|
17
|
-
params: {},
|
|
18
|
-
durationMs: 50,
|
|
19
|
-
success: true,
|
|
20
|
-
result: "ok",
|
|
21
|
-
error: null,
|
|
22
|
-
...overrides,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function makeStrategy(overrides: Partial<Strategy> = {}): Strategy {
|
|
27
|
-
const steps = overrides.steps ?? [{ tool: "apps", params: {} }];
|
|
28
|
-
return {
|
|
29
|
-
id: "str_test" + Math.random().toString(36).slice(2, 6),
|
|
30
|
-
task: "test task",
|
|
31
|
-
steps,
|
|
32
|
-
totalDurationMs: 50,
|
|
33
|
-
successCount: 1,
|
|
34
|
-
failCount: 0,
|
|
35
|
-
lastUsed: new Date().toISOString(),
|
|
36
|
-
tags: ["test"],
|
|
37
|
-
fingerprint: steps.map((s) => s.tool).join("→"),
|
|
38
|
-
...overrides,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function makeError(overrides: Partial<ErrorPattern> = {}): ErrorPattern {
|
|
43
|
-
return {
|
|
44
|
-
id: "err_test" + Math.random().toString(36).slice(2, 6),
|
|
45
|
-
tool: "launch",
|
|
46
|
-
params: { bundleId: "com.test.App" },
|
|
47
|
-
error: "timed out",
|
|
48
|
-
resolution: null,
|
|
49
|
-
occurrences: 1,
|
|
50
|
-
lastSeen: new Date().toISOString(),
|
|
51
|
-
...overrides,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Wait for async file writes to flush */
|
|
56
|
-
function waitForFlush(): Promise<void> {
|
|
57
|
-
return new Promise((r) => setTimeout(r, 50));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
beforeEach(() => {
|
|
61
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "screenhand-test-"));
|
|
62
|
-
// Pre-create memory dir so seeds don't auto-load (not first boot)
|
|
63
|
-
fs.mkdirSync(path.join(tmpDir, ".screenhand", "memory"), { recursive: true });
|
|
64
|
-
store = new MemoryStore(tmpDir);
|
|
65
|
-
store.init();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
afterEach(() => {
|
|
69
|
-
// Remove lock file first, then clean up
|
|
70
|
-
const lockPath = path.join(tmpDir, ".screenhand", "memory", ".lock");
|
|
71
|
-
try { fs.unlinkSync(lockPath); } catch { /* ignore */ }
|
|
72
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe("MemoryStore", () => {
|
|
76
|
-
describe("actions", () => {
|
|
77
|
-
it("appends actions and updates in-memory stats", () => {
|
|
78
|
-
store.appendAction(makeAction({ tool: "apps" }));
|
|
79
|
-
store.appendAction(makeAction({ tool: "focus" }));
|
|
80
|
-
|
|
81
|
-
const stats = store.getStats();
|
|
82
|
-
expect(stats.totalActions).toBe(2);
|
|
83
|
-
expect(stats.topTools).toContainEqual({ tool: "apps", count: 1 });
|
|
84
|
-
expect(stats.topTools).toContainEqual({ tool: "focus", count: 1 });
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("tracks success rate in-memory", () => {
|
|
88
|
-
store.appendAction(makeAction({ success: true }));
|
|
89
|
-
store.appendAction(makeAction({ success: true }));
|
|
90
|
-
store.appendAction(makeAction({ success: false }));
|
|
91
|
-
|
|
92
|
-
expect(store.getStats().successRate).toBeCloseTo(2 / 3);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("writes to disk asynchronously", async () => {
|
|
96
|
-
store.appendAction(makeAction({ tool: "apps" }));
|
|
97
|
-
// Wait for 100ms debounce + some buffer
|
|
98
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
99
|
-
|
|
100
|
-
const fp = path.join(tmpDir, ".screenhand", "memory", "actions.jsonl");
|
|
101
|
-
expect(fs.existsSync(fp)).toBe(true);
|
|
102
|
-
const content = fs.readFileSync(fp, "utf-8").trim();
|
|
103
|
-
expect(content.split("\n")).toHaveLength(1);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("returns empty stats when no actions", () => {
|
|
107
|
-
expect(store.getStats().totalActions).toBe(0);
|
|
108
|
-
expect(store.getStats().successRate).toBe(0);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe("strategies (cached)", () => {
|
|
113
|
-
it("appends and reads from cache", () => {
|
|
114
|
-
store.appendStrategy(makeStrategy({ task: "task A" }));
|
|
115
|
-
store.appendStrategy(makeStrategy({ task: "task B" }));
|
|
116
|
-
|
|
117
|
-
const strategies = store.readStrategies();
|
|
118
|
-
expect(strategies).toHaveLength(2);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("deduplicates by task name and increments successCount", () => {
|
|
122
|
-
store.appendStrategy(makeStrategy({ task: "take a photo", successCount: 1 }));
|
|
123
|
-
store.appendStrategy(makeStrategy({ task: "take a photo", successCount: 1 }));
|
|
124
|
-
|
|
125
|
-
const strategies = store.readStrategies();
|
|
126
|
-
expect(strategies).toHaveLength(1);
|
|
127
|
-
expect(strategies[0]!.successCount).toBe(2);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("persists to disk asynchronously", async () => {
|
|
131
|
-
store.appendStrategy(makeStrategy({ task: "persist test" }));
|
|
132
|
-
await waitForFlush();
|
|
133
|
-
|
|
134
|
-
const fp = path.join(tmpDir, ".screenhand", "memory", "strategies.jsonl");
|
|
135
|
-
const content = fs.readFileSync(fp, "utf-8").trim();
|
|
136
|
-
expect(content).toContain("persist test");
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("survives re-init (loads from disk)", async () => {
|
|
140
|
-
store.appendStrategy(makeStrategy({ task: "survive reload" }));
|
|
141
|
-
await waitForFlush();
|
|
142
|
-
|
|
143
|
-
// Create a new store pointing at the same dir
|
|
144
|
-
const store2 = new MemoryStore(tmpDir);
|
|
145
|
-
store2.init();
|
|
146
|
-
expect(store2.readStrategies()).toHaveLength(1);
|
|
147
|
-
expect(store2.readStrategies()[0]!.task).toBe("survive reload");
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe("errors (cached)", () => {
|
|
152
|
-
it("appends and reads from cache", () => {
|
|
153
|
-
store.appendError(makeError());
|
|
154
|
-
const errors = store.readErrors();
|
|
155
|
-
expect(errors).toHaveLength(1);
|
|
156
|
-
expect(errors[0]!.tool).toBe("launch");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("deduplicates by tool+error and increments occurrences", () => {
|
|
160
|
-
store.appendError(makeError({ tool: "launch", error: "timed out" }));
|
|
161
|
-
store.appendError(makeError({ tool: "launch", error: "timed out" }));
|
|
162
|
-
|
|
163
|
-
const errors = store.readErrors();
|
|
164
|
-
expect(errors).toHaveLength(1);
|
|
165
|
-
expect(errors[0]!.occurrences).toBe(2);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("preserves existing resolution when new one is null", () => {
|
|
169
|
-
store.appendError(makeError({ tool: "launch", error: "timeout", resolution: "use focus()" }));
|
|
170
|
-
store.appendError(makeError({ tool: "launch", error: "timeout", resolution: null }));
|
|
171
|
-
|
|
172
|
-
const errors = store.readErrors();
|
|
173
|
-
expect(errors[0]!.resolution).toBe("use focus()");
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
describe("stats", () => {
|
|
178
|
-
it("returns correct stats from in-memory counters", () => {
|
|
179
|
-
store.appendAction(makeAction({ tool: "apps", success: true }));
|
|
180
|
-
store.appendAction(makeAction({ tool: "apps", success: true }));
|
|
181
|
-
store.appendAction(makeAction({ tool: "focus", success: false }));
|
|
182
|
-
store.appendStrategy(makeStrategy());
|
|
183
|
-
store.appendError(makeError());
|
|
184
|
-
|
|
185
|
-
const stats = store.getStats();
|
|
186
|
-
expect(stats.totalActions).toBe(3);
|
|
187
|
-
expect(stats.totalStrategies).toBe(1);
|
|
188
|
-
expect(stats.totalErrors).toBe(1);
|
|
189
|
-
expect(stats.successRate).toBeCloseTo(2 / 3);
|
|
190
|
-
expect(stats.topTools[0]!.tool).toBe("apps");
|
|
191
|
-
expect(stats.topTools[0]!.count).toBe(2);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
describe("fingerprint index", () => {
|
|
196
|
-
it("looks up strategy by fingerprint in O(1)", () => {
|
|
197
|
-
const s = makeStrategy({ task: "photo", steps: [{ tool: "apps", params: {} }, { tool: "focus", params: {} }] });
|
|
198
|
-
store.appendStrategy(s);
|
|
199
|
-
|
|
200
|
-
const found = store.lookupByFingerprint("apps→focus");
|
|
201
|
-
expect(found).not.toBeUndefined();
|
|
202
|
-
expect(found!.task).toBe("photo");
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("returns undefined for unknown fingerprint", () => {
|
|
206
|
-
expect(store.lookupByFingerprint("nonexistent→tools")).toBeUndefined();
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("rebuilds index on init from disk", async () => {
|
|
210
|
-
store.appendStrategy(makeStrategy({ task: "persisted", steps: [{ tool: "launch", params: {} }, { tool: "ui_press", params: {} }] }));
|
|
211
|
-
await waitForFlush();
|
|
212
|
-
|
|
213
|
-
const store2 = new MemoryStore(tmpDir);
|
|
214
|
-
store2.init();
|
|
215
|
-
const found = store2.lookupByFingerprint("launch→ui_press");
|
|
216
|
-
expect(found).not.toBeUndefined();
|
|
217
|
-
expect(found!.task).toBe("persisted");
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
describe("feedback loop", () => {
|
|
222
|
-
it("increments successCount on positive outcome", () => {
|
|
223
|
-
const s = makeStrategy({ task: "test feedback", steps: [{ tool: "apps", params: {} }] });
|
|
224
|
-
store.appendStrategy(s);
|
|
225
|
-
|
|
226
|
-
store.recordStrategyOutcome("apps", true);
|
|
227
|
-
const strategies = store.readStrategies();
|
|
228
|
-
expect(strategies[0]!.successCount).toBe(2);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it("increments failCount on negative outcome", () => {
|
|
232
|
-
const s = makeStrategy({ task: "test feedback", steps: [{ tool: "apps", params: {} }] });
|
|
233
|
-
store.appendStrategy(s);
|
|
234
|
-
|
|
235
|
-
store.recordStrategyOutcome("apps", false);
|
|
236
|
-
const strategies = store.readStrategies();
|
|
237
|
-
expect(strategies[0]!.failCount).toBe(1);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("does nothing for unknown fingerprint", () => {
|
|
241
|
-
store.recordStrategyOutcome("unknown→fp", true);
|
|
242
|
-
expect(store.readStrategies()).toHaveLength(0);
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
describe("corrupted JSONL recovery", () => {
|
|
247
|
-
it("skips corrupted lines and parses valid ones", async () => {
|
|
248
|
-
// Manually write a file with a corrupted line
|
|
249
|
-
store.appendStrategy(makeStrategy({ task: "good entry" }));
|
|
250
|
-
await waitForFlush();
|
|
251
|
-
|
|
252
|
-
const fp = path.join(tmpDir, ".screenhand", "memory", "strategies.jsonl");
|
|
253
|
-
// Append a corrupted line
|
|
254
|
-
fs.appendFileSync(fp, "NOT VALID JSON\n");
|
|
255
|
-
// Append another valid line manually
|
|
256
|
-
const valid = makeStrategy({ task: "after corruption" });
|
|
257
|
-
fs.appendFileSync(fp, JSON.stringify(valid) + "\n");
|
|
258
|
-
|
|
259
|
-
// Re-init from disk
|
|
260
|
-
const store2 = new MemoryStore(tmpDir);
|
|
261
|
-
store2.init();
|
|
262
|
-
const strategies = store2.readStrategies();
|
|
263
|
-
// Should have both valid entries, skipping the corrupted one
|
|
264
|
-
expect(strategies.length).toBe(2);
|
|
265
|
-
expect(strategies[0]!.task).toBe("good entry");
|
|
266
|
-
expect(strategies[1]!.task).toBe("after corruption");
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it("handles completely empty file", () => {
|
|
270
|
-
const fp = path.join(tmpDir, ".screenhand", "memory", "strategies.jsonl");
|
|
271
|
-
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
272
|
-
fs.writeFileSync(fp, "");
|
|
273
|
-
|
|
274
|
-
const store2 = new MemoryStore(tmpDir);
|
|
275
|
-
store2.init();
|
|
276
|
-
expect(store2.readStrategies()).toEqual([]);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it("handles file with only whitespace/empty lines", () => {
|
|
280
|
-
const fp = path.join(tmpDir, ".screenhand", "memory", "strategies.jsonl");
|
|
281
|
-
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
282
|
-
fs.writeFileSync(fp, "\n\n \n\n");
|
|
283
|
-
|
|
284
|
-
const store2 = new MemoryStore(tmpDir);
|
|
285
|
-
store2.init();
|
|
286
|
-
expect(store2.readStrategies()).toEqual([]);
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
describe("LRU eviction", () => {
|
|
291
|
-
it("evicts oldest strategies when exceeding 500 limit", () => {
|
|
292
|
-
// Add 502 strategies with different timestamps
|
|
293
|
-
for (let i = 0; i < 502; i++) {
|
|
294
|
-
const date = new Date(Date.now() - (502 - i) * 1000); // oldest first
|
|
295
|
-
store.appendStrategy(makeStrategy({
|
|
296
|
-
task: `task_${i}`,
|
|
297
|
-
lastUsed: date.toISOString(),
|
|
298
|
-
steps: [{ tool: `tool_${i}`, params: {} }],
|
|
299
|
-
fingerprint: `tool_${i}`,
|
|
300
|
-
}));
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const strategies = store.readStrategies();
|
|
304
|
-
expect(strategies.length).toBeLessThanOrEqual(500);
|
|
305
|
-
// Oldest entries (task_0, task_1) should have been evicted
|
|
306
|
-
expect(strategies.find((s) => s.task === "task_0")).toBeUndefined();
|
|
307
|
-
expect(strategies.find((s) => s.task === "task_1")).toBeUndefined();
|
|
308
|
-
// Newest entries should remain
|
|
309
|
-
expect(strategies.find((s) => s.task === "task_501")).not.toBeUndefined();
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("evicts oldest errors when exceeding 200 limit", () => {
|
|
313
|
-
for (let i = 0; i < 202; i++) {
|
|
314
|
-
const date = new Date(Date.now() - (202 - i) * 1000);
|
|
315
|
-
store.appendError(makeError({
|
|
316
|
-
tool: `tool_${i}`,
|
|
317
|
-
error: `error_${i}`,
|
|
318
|
-
lastSeen: date.toISOString(),
|
|
319
|
-
}));
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const errors = store.readErrors();
|
|
323
|
-
expect(errors.length).toBeLessThanOrEqual(200);
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
describe("file locking", () => {
|
|
328
|
-
it("creates lock file on init", () => {
|
|
329
|
-
const lockPath = path.join(tmpDir, ".screenhand", "memory", ".lock");
|
|
330
|
-
expect(fs.existsSync(lockPath)).toBe(true);
|
|
331
|
-
const content = fs.readFileSync(lockPath, "utf-8").trim();
|
|
332
|
-
expect(parseInt(content, 10)).toBe(process.pid);
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
it("second store instance skips writes when locked", async () => {
|
|
336
|
-
// store already holds the lock
|
|
337
|
-
store.appendStrategy(makeStrategy({ task: "from first" }));
|
|
338
|
-
|
|
339
|
-
// Second instance can't get lock
|
|
340
|
-
const store2 = new MemoryStore(tmpDir);
|
|
341
|
-
store2.init();
|
|
342
|
-
store2.appendStrategy(makeStrategy({ task: "from second" }));
|
|
343
|
-
await waitForFlush();
|
|
344
|
-
|
|
345
|
-
// Second instance should have it in cache but not on disk
|
|
346
|
-
expect(store2.readStrategies().find((s) => s.task === "from second")).not.toBeUndefined();
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
describe("buffered writes", () => {
|
|
351
|
-
it("batches multiple action writes", async () => {
|
|
352
|
-
// Write 5 actions rapidly
|
|
353
|
-
for (let i = 0; i < 5; i++) {
|
|
354
|
-
store.appendAction(makeAction({ tool: `tool_${i}` }));
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Stats should be immediate (in-memory)
|
|
358
|
-
expect(store.getStats().totalActions).toBe(5);
|
|
359
|
-
|
|
360
|
-
// Wait for debounced flush
|
|
361
|
-
await new Promise((r) => setTimeout(r, 150));
|
|
362
|
-
|
|
363
|
-
const fp = path.join(tmpDir, ".screenhand", "memory", "actions.jsonl");
|
|
364
|
-
const lines = fs.readFileSync(fp, "utf-8").trim().split("\n");
|
|
365
|
-
expect(lines).toHaveLength(5);
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
describe("clear", () => {
|
|
370
|
-
it("clears specific category from cache and disk", () => {
|
|
371
|
-
store.appendAction(makeAction());
|
|
372
|
-
store.appendStrategy(makeStrategy());
|
|
373
|
-
store.appendError(makeError());
|
|
374
|
-
|
|
375
|
-
store.clear("actions");
|
|
376
|
-
expect(store.getStats().totalActions).toBe(0);
|
|
377
|
-
expect(store.readStrategies()).toHaveLength(1);
|
|
378
|
-
expect(store.readErrors()).toHaveLength(1);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it("clears all", () => {
|
|
382
|
-
store.appendAction(makeAction());
|
|
383
|
-
store.appendStrategy(makeStrategy());
|
|
384
|
-
store.appendError(makeError());
|
|
385
|
-
|
|
386
|
-
store.clear("all");
|
|
387
|
-
expect(store.getStats().totalActions).toBe(0);
|
|
388
|
-
expect(store.readStrategies()).toEqual([]);
|
|
389
|
-
expect(store.readErrors()).toEqual([]);
|
|
390
|
-
});
|
|
391
|
-
});
|
|
392
|
-
});
|