@indigoai-us/hq-cloud 6.14.43 → 6.14.45

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.
@@ -0,0 +1,212 @@
1
+ /**
2
+ * The emit filter must run BEFORE handleEvent pays for a stat.
3
+ *
4
+ * The recursive backend hands every OS event to handleEvent, and a `rename`
5
+ * used to be lstat'd before the exclusion filter was consulted. On a real HQ
6
+ * that is one synchronous syscall for every file touched anywhere under
7
+ * `repos/` or `workspace/worktrees/` — build, install, and checkout churn the
8
+ * watcher immediately throws away. Scoped watch roots (see watch-roots.ts) keep
9
+ * most of that traffic from being delivered at all; this gate makes whatever
10
+ * still arrives cost a string compare instead of a syscall.
11
+ *
12
+ * This lives in its own file because it mocks `fs` module-wide, which would
13
+ * otherwise perturb the 53 tests in watcher.test.ts.
14
+ */
15
+
16
+ import * as os from "os";
17
+ import * as path from "path";
18
+
19
+ import { describe, expect, it, vi } from "vitest";
20
+
21
+ const { lstatCalls } = vi.hoisted(() => ({ lstatCalls: vi.fn() }));
22
+
23
+ vi.mock("fs", async (importOriginal) => {
24
+ const actual = await importOriginal<typeof import("fs")>();
25
+ return {
26
+ ...actual,
27
+ lstatSync: (...args: Parameters<typeof actual.lstatSync>) => {
28
+ lstatCalls(...args);
29
+ return actual.lstatSync(...args);
30
+ },
31
+ };
32
+ });
33
+
34
+ const { FakeClock, TreeWatcher } = await import("./watcher.js");
35
+
36
+ const hqRoot = path.join(os.tmpdir(), "hqcloud-event-gate-root");
37
+ /** Everything under `repos/` is out of scope, as file AND as directory. */
38
+ const outOfScope = (abs: string) => !abs.split(path.sep).includes("repos");
39
+
40
+ function makeWatcher() {
41
+ return new TreeWatcher({
42
+ hqRoot,
43
+ clock: new FakeClock(),
44
+ debounceMs: 100,
45
+ pathFilter: outOfScope,
46
+ });
47
+ }
48
+
49
+ describe("TreeWatcher.handleEvent — an excluded path costs no syscall", () => {
50
+ it("does NOT stat an out-of-scope rename before dropping it", () => {
51
+ const watcher = makeWatcher();
52
+ lstatCalls.mockClear();
53
+ try {
54
+ watcher.handleEvent(
55
+ path.join(hqRoot, "repos", "hq-cloud", "src", "a.ts"),
56
+ "rename",
57
+ );
58
+ expect(lstatCalls).not.toHaveBeenCalled();
59
+ } finally {
60
+ watcher.dispose();
61
+ }
62
+ });
63
+
64
+ it("still stats an in-scope rename, so kind classification is unchanged", () => {
65
+ const watcher = makeWatcher();
66
+ lstatCalls.mockClear();
67
+ try {
68
+ watcher.handleEvent(
69
+ path.join(hqRoot, "companies", "acme", "note.md"),
70
+ "rename",
71
+ );
72
+ expect(lstatCalls).toHaveBeenCalled();
73
+ } finally {
74
+ watcher.dispose();
75
+ }
76
+ });
77
+
78
+ it("skips its own seed walk when the backend already indexed directories", async () => {
79
+ // The scoped backend indexes every in-scope directory while planning its
80
+ // watch roots. TreeWatcher must NOT then walk the same tree again — on a
81
+ // real HQ root that walk is ~3.6s, and doing it twice per runner start was
82
+ // a straight doubling of startup cost for an identical index.
83
+ const fsp = await import("node:fs/promises");
84
+ const realRoot = await fsp.mkdtemp(
85
+ path.join(os.tmpdir(), "hqcloud-seed-fusion-"),
86
+ );
87
+ try {
88
+ await fsp.mkdir(path.join(realRoot, "companies", "acme", "knowledge"), {
89
+ recursive: true,
90
+ });
91
+
92
+ const watcher = new TreeWatcher({
93
+ hqRoot: realRoot,
94
+ clock: new FakeClock(),
95
+ pathFilter: () => true,
96
+ backendFactory: (opts) => {
97
+ opts.onDirectorySeen?.(path.join(realRoot, "companies"));
98
+ return {
99
+ close: () => {},
100
+ needsKnownKinds: true,
101
+ seededKnownKinds: true,
102
+ watchedPathCount: () => 1,
103
+ };
104
+ },
105
+ });
106
+ watcher.start();
107
+ // Exactly the one directory the backend reported — not the three that a
108
+ // second full walk of this tree would have found.
109
+ expect(watcher.knownDirectoryCount()).toBe(1);
110
+ watcher.dispose();
111
+ } finally {
112
+ await fsp.rm(realRoot, { recursive: true, force: true });
113
+ }
114
+ });
115
+
116
+ it("still walks itself when the backend did NOT index directories", async () => {
117
+ const fsp = await import("node:fs/promises");
118
+ const realRoot = await fsp.mkdtemp(
119
+ path.join(os.tmpdir(), "hqcloud-seed-legacy-"),
120
+ );
121
+ try {
122
+ await fsp.mkdir(path.join(realRoot, "companies", "acme", "knowledge"), {
123
+ recursive: true,
124
+ });
125
+
126
+ const watcher = new TreeWatcher({
127
+ hqRoot: realRoot,
128
+ clock: new FakeClock(),
129
+ pathFilter: () => true,
130
+ backendFactory: () => ({
131
+ close: () => {},
132
+ needsKnownKinds: true,
133
+ watchedPathCount: () => 1,
134
+ }),
135
+ });
136
+ watcher.start();
137
+ expect(watcher.knownDirectoryCount()).toBe(3);
138
+ watcher.dispose();
139
+ } finally {
140
+ await fsp.rm(realRoot, { recursive: true, force: true });
141
+ }
142
+ });
143
+
144
+ it("does not let a duplicate delete downgrade unlinkDir to unlink", () => {
145
+ // One path can reach handleEvent twice when two watches overlap (the
146
+ // scoped backend briefly runs its bootstrap watch alongside the planned
147
+ // ones so in-flight events are not dropped). The first unlinkDir clears
148
+ // the path's known-kind hint, so without this guard the duplicate lands as
149
+ // a plain `unlink` — narrowing the deletion scope the vault applies and
150
+ // discarding the captured descendant snapshots.
151
+ const clock = new FakeClock();
152
+ const w = new TreeWatcher({
153
+ hqRoot,
154
+ clock,
155
+ debounceMs: 100,
156
+ pathFilter: () => true,
157
+ captureLocalDeleteSnapshots: (relativePath, kind) =>
158
+ kind === "unlinkDir"
159
+ ? [
160
+ {
161
+ journalSlug: "acme",
162
+ journalPath: `${relativePath}/child.md`,
163
+ absolutePath: path.join(hqRoot, relativePath, "child.md"),
164
+ remoteEtag: "etag",
165
+ localHash: "hash",
166
+ localKind: "file" as const,
167
+ },
168
+ ]
169
+ : [],
170
+ });
171
+ const batches: Array<Map<string, { kind: string }> | undefined> = [];
172
+ w.onChange((_first, batch) => batches.push(batch?.changes as never));
173
+ try {
174
+ const dir = path.join(hqRoot, "companies", "acme", "moved");
175
+ w.handleEvent(dir, "unlinkDir");
176
+ w.handleEvent(dir, "unlink"); // duplicate delivery of the same delete
177
+ clock.advance(200);
178
+
179
+ const change = batches[0]?.get(dir) as
180
+ | { kind: string; deleteSnapshots?: unknown[] }
181
+ | undefined;
182
+ expect(change?.kind).toBe("unlinkDir");
183
+ expect(change?.deleteSnapshots).toHaveLength(1);
184
+ } finally {
185
+ w.dispose();
186
+ }
187
+ });
188
+
189
+ it("keeps emitting in-scope changes after the gate", () => {
190
+ const watcher = makeWatcher();
191
+ const clock = new FakeClock();
192
+ const w = new TreeWatcher({
193
+ hqRoot,
194
+ clock,
195
+ debounceMs: 100,
196
+ pathFilter: outOfScope,
197
+ });
198
+ watcher.dispose();
199
+ const rels: string[] = [];
200
+ w.onChange((_first, batch) => {
201
+ if (batch) rels.push(...batch.paths.values());
202
+ });
203
+ try {
204
+ w.handleEvent(path.join(hqRoot, "companies", "acme", "note.md"), "change");
205
+ w.handleEvent(path.join(hqRoot, "repos", "x", "code.ts"), "change");
206
+ clock.advance(200);
207
+ expect(rels).toEqual(["companies/acme/note.md"]);
208
+ } finally {
209
+ w.dispose();
210
+ }
211
+ });
212
+ });