@indigoai-us/hq-cloud 6.14.36 → 6.14.39

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.
Files changed (43) hide show
  1. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  2. package/dist/bin/sync-runner-company.js +20 -3
  3. package/dist/bin/sync-runner-company.js.map +1 -1
  4. package/dist/bin/sync-runner.d.ts +5 -0
  5. package/dist/bin/sync-runner.d.ts.map +1 -1
  6. package/dist/bin/sync-runner.js.map +1 -1
  7. package/dist/bin/sync-runner.test.js +46 -0
  8. package/dist/bin/sync-runner.test.js.map +1 -1
  9. package/dist/cli/reindex.d.ts.map +1 -1
  10. package/dist/cli/reindex.js +1 -9
  11. package/dist/cli/reindex.js.map +1 -1
  12. package/dist/cli/share.d.ts +178 -1
  13. package/dist/cli/share.d.ts.map +1 -1
  14. package/dist/cli/share.js +555 -32
  15. package/dist/cli/share.js.map +1 -1
  16. package/dist/cli/share.test.js +780 -2
  17. package/dist/cli/share.test.js.map +1 -1
  18. package/dist/cli/sync.d.ts +27 -0
  19. package/dist/cli/sync.d.ts.map +1 -1
  20. package/dist/cli/sync.js +113 -13
  21. package/dist/cli/sync.js.map +1 -1
  22. package/dist/cli/sync.test.js +188 -0
  23. package/dist/cli/sync.test.js.map +1 -1
  24. package/dist/lib/readlink-safe.d.ts +11 -0
  25. package/dist/lib/readlink-safe.d.ts.map +1 -0
  26. package/dist/lib/readlink-safe.js +27 -0
  27. package/dist/lib/readlink-safe.js.map +1 -0
  28. package/dist/lib/readlink-safe.test.d.ts +2 -0
  29. package/dist/lib/readlink-safe.test.d.ts.map +1 -0
  30. package/dist/lib/readlink-safe.test.js +34 -0
  31. package/dist/lib/readlink-safe.test.js.map +1 -0
  32. package/package.json +1 -1
  33. package/src/bin/sync-runner-company.ts +19 -3
  34. package/src/bin/sync-runner.test.ts +54 -0
  35. package/src/bin/sync-runner.ts +4 -0
  36. package/src/cli/reindex.ts +1 -9
  37. package/src/cli/share.test.ts +974 -2
  38. package/src/cli/share.ts +675 -32
  39. package/src/cli/sync.test.ts +209 -0
  40. package/src/cli/sync.ts +151 -13
  41. package/src/lib/readlink-safe.test.ts +43 -0
  42. package/src/lib/readlink-safe.ts +29 -0
  43. package/test/e2e/sync/windows-unreadable-link-leg.test.ts +191 -0
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Artifact-level regression for the Windows EINVAL reparse-point failure.
3
+ *
4
+ * The Linux test runner cannot create the production reparse-point shape, so
5
+ * the fs seam makes a real symlink's readlink raise EINVAL while the actual
6
+ * runner → company fanout → share walk executes over a temp HQ tree.
7
+ */
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import {
12
+ runRunner,
13
+ type RunnerDeps,
14
+ type RunnerEvent,
15
+ type VaultClientSurface,
16
+ } from "../../../src/bin/sync-runner.js";
17
+ import { share } from "../../../src/cli/share.js";
18
+ import type { EntityContext, EntityInfo } from "../../../src/types.js";
19
+ import { uploadFile, uploadSymlink } from "../../../src/s3.js";
20
+
21
+ vi.mock("fs", async (importOriginal) => {
22
+ const actual = await importOriginal<typeof import("fs")>();
23
+ return { ...actual };
24
+ });
25
+
26
+ vi.mock("../../../src/s3.js", async (importOriginal) => {
27
+ const actual = await importOriginal<typeof import("../../../src/s3.js")>();
28
+ return {
29
+ ...actual,
30
+ uploadFile: vi.fn().mockResolvedValue({ etag: '"upload-etag"' }),
31
+ uploadSymlink: vi.fn().mockResolvedValue({ etag: '"symlink-etag"' }),
32
+ headRemoteFile: vi.fn().mockResolvedValue(null),
33
+ primeObjectTransport: vi.fn().mockResolvedValue(undefined),
34
+ primeUploads: vi.fn().mockResolvedValue(undefined),
35
+ };
36
+ });
37
+
38
+ function errnoError(code: string): NodeJS.ErrnoException {
39
+ const err = new Error(`${code}: invalid argument, readlink`) as NodeJS.ErrnoException;
40
+ err.code = code;
41
+ return err;
42
+ }
43
+
44
+ function makeWriter(): {
45
+ write: (chunk: string) => boolean;
46
+ events: () => RunnerEvent[];
47
+ } {
48
+ let output = "";
49
+ return {
50
+ write: (chunk) => {
51
+ output += chunk;
52
+ return true;
53
+ },
54
+ events: () =>
55
+ output
56
+ .split("\n")
57
+ .filter(Boolean)
58
+ .map((line) => JSON.parse(line) as RunnerEvent),
59
+ };
60
+ }
61
+
62
+ let hqRoot: string;
63
+ let stateDir: string;
64
+ let unreadableLink: string;
65
+
66
+ beforeEach(() => {
67
+ // Keep fixture roots inside the checkout so operator runs never create a
68
+ // scratch HQ tree outside the task worktree.
69
+ hqRoot = fs.mkdtempSync(path.join(process.cwd(), ".hqcloud-einval-e2e-"));
70
+ stateDir = fs.mkdtempSync(path.join(process.cwd(), ".hqcloud-einval-state-"));
71
+ process.env.HQ_STATE_DIR = stateDir;
72
+ const companyRoot = path.join(hqRoot, "companies", "acme");
73
+ const policiesDir = path.join(companyRoot, "policies");
74
+ const targetDir = path.join(hqRoot, "outside", "target");
75
+ fs.mkdirSync(policiesDir, { recursive: true });
76
+ fs.mkdirSync(targetDir, { recursive: true });
77
+ fs.writeFileSync(path.join(policiesDir, "safe.md"), "safe bytes");
78
+ fs.writeFileSync(path.join(targetDir, "must-not-upload.md"), "target bytes");
79
+ unreadableLink = path.join(policiesDir, "unreadable-link");
80
+ fs.symlinkSync(targetDir, unreadableLink, "dir");
81
+ });
82
+
83
+ afterEach(() => {
84
+ vi.restoreAllMocks();
85
+ fs.rmSync(hqRoot, { recursive: true, force: true });
86
+ fs.rmSync(stateDir, { recursive: true, force: true });
87
+ delete process.env.HQ_STATE_DIR;
88
+ });
89
+
90
+ describe("sync runner unreadable Windows link", () => {
91
+ it("keeps the leg complete and emits the offending relative path", async () => {
92
+ const stdout = makeWriter();
93
+ const stderr = makeWriter();
94
+ const entityContext: EntityContext = {
95
+ uid: "cmp_acme",
96
+ slug: "acme",
97
+ bucketName: "hq-vault-acme-test",
98
+ region: "us-east-1",
99
+ credentials: {
100
+ accessKeyId: "test-key",
101
+ secretAccessKey: "test-secret",
102
+ sessionToken: "test-session",
103
+ },
104
+ expiresAt: new Date(Date.now() + 15 * 60 * 1000).toISOString(),
105
+ };
106
+ const client = {
107
+ listMyMemberships: async () => [{ companyUid: "cmp_acme" }],
108
+ listMyPendingInvitesByEmail: async () => [],
109
+ claimPendingInvitesByEmail: async () => undefined,
110
+ ensureMyPersonEntity: async () => ({}) as EntityInfo,
111
+ entity: {
112
+ get: async (uid: string) => ({
113
+ uid,
114
+ type: "company",
115
+ slug: "acme",
116
+ bucketName: "hq-vault-acme-test",
117
+ status: "active",
118
+ }) as EntityInfo,
119
+ listByType: async () => [],
120
+ },
121
+ } as unknown as VaultClientSurface;
122
+ const realReadlinkSync = fs.readlinkSync;
123
+ const readlinkSpy = vi
124
+ .spyOn(fs, "readlinkSync")
125
+ .mockImplementation(((candidate: fs.PathLike) => {
126
+ if (candidate === unreadableLink) throw errnoError("EINVAL");
127
+ return realReadlinkSync(candidate);
128
+ }) as typeof fs.readlinkSync);
129
+ const deps: RunnerDeps = {
130
+ stdout,
131
+ stderr,
132
+ getAccessToken: async () => "test-access-token",
133
+ getIdTokenClaims: () => null,
134
+ createVaultClient: () => client,
135
+ share: async (options) => {
136
+ const { vaultConfig: _vaultConfig, ...shareOptions } = options;
137
+ return share({ ...shareOptions, entityContext });
138
+ },
139
+ reindex: () => ({ status: 0 }) as ReturnType<NonNullable<RunnerDeps["reindex"]>>,
140
+ qmdReindex: () => ({
141
+ qmdAvailable: true,
142
+ collectionsAdded: [],
143
+ pathDriftDetected: [],
144
+ collectionsRepaired: [],
145
+ updated: false,
146
+ embedded: false,
147
+ pendingDirty: false,
148
+ lockBusy: false,
149
+ timedOut: false,
150
+ corruptionQuarantined: false,
151
+ corruptionQuarantineFailed: false,
152
+ indexDir: null,
153
+ }) as ReturnType<NonNullable<RunnerDeps["qmdReindex"]>>,
154
+ reconcileManifest: async () => ({
155
+ written: false,
156
+ added: [],
157
+ updated: [],
158
+ skipped: [],
159
+ }) as Awaited<ReturnType<NonNullable<RunnerDeps["reconcileManifest"]>>>,
160
+ collectTelemetry: async () => undefined,
161
+ };
162
+
163
+ try {
164
+ const code = await runRunner(
165
+ ["--companies", "--direction", "push", "--hq-root", hqRoot],
166
+ deps,
167
+ );
168
+
169
+ expect(code).toBe(0);
170
+ expect(stdout.events()).toContainEqual({
171
+ type: "not-shipped",
172
+ company: "acme",
173
+ reason: "unreadable-link",
174
+ count: 1,
175
+ samplePaths: ["policies/unreadable-link"],
176
+ });
177
+ expect(stdout.events()).toContainEqual(
178
+ expect.objectContaining({
179
+ type: "all-complete",
180
+ errors: [],
181
+ partial: false,
182
+ }),
183
+ );
184
+ expect(stderr.events()).toEqual([]);
185
+ expect(uploadFile).toHaveBeenCalledTimes(1);
186
+ expect(uploadSymlink).not.toHaveBeenCalled();
187
+ } finally {
188
+ readlinkSpy.mockRestore();
189
+ }
190
+ });
191
+ });