@indigoai-us/hq-cloud 6.14.17 → 6.14.18
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/dist/active-company.d.ts +43 -0
- package/dist/active-company.d.ts.map +1 -0
- package/dist/active-company.js +132 -0
- package/dist/active-company.js.map +1 -0
- package/dist/active-company.test.d.ts +2 -0
- package/dist/active-company.test.d.ts.map +1 -0
- package/dist/active-company.test.js +149 -0
- package/dist/active-company.test.js.map +1 -0
- package/dist/bin/sync-runner-planning.d.ts +27 -1
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +33 -4
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-planning.test.d.ts +2 -0
- package/dist/bin/sync-runner-planning.test.d.ts.map +1 -0
- package/dist/bin/sync-runner-planning.test.js +115 -0
- package/dist/bin/sync-runner-planning.test.js.map +1 -0
- package/dist/bin/sync-runner.d.ts +22 -7
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +92 -16
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +357 -5
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/manifest-reconcile.d.ts +118 -5
- package/dist/manifest-reconcile.d.ts.map +1 -1
- package/dist/manifest-reconcile.js +319 -62
- package/dist/manifest-reconcile.js.map +1 -1
- package/dist/manifest-reconcile.test.js +824 -2
- package/dist/manifest-reconcile.test.js.map +1 -1
- package/package.json +1 -1
- package/pnpm-workspace.yaml +1 -1
- package/src/active-company.test.ts +188 -0
- package/src/active-company.ts +168 -0
- package/src/bin/sync-runner-planning.test.ts +131 -0
- package/src/bin/sync-runner-planning.ts +60 -7
- package/src/bin/sync-runner.test.ts +430 -10
- package/src/bin/sync-runner.ts +129 -23
- package/src/manifest-reconcile.test.ts +1019 -3
- package/src/manifest-reconcile.ts +424 -66
- package/test/joiner-manifest-reconcile.integration.test.ts +283 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed `activeCompany` in `.hq/config.json` for a joiner who has exactly one
|
|
3
|
+
* cloud company.
|
|
4
|
+
*
|
|
5
|
+
* A newly invited member arrives with no local company routing at all. The
|
|
6
|
+
* manifest reconciler restores their `companies/manifest.yaml` entry, but
|
|
7
|
+
* nothing tells HQ which company is *current*, so they stay unrouted even
|
|
8
|
+
* though membership resolved fine. When the answer is unambiguous — one company
|
|
9
|
+
* and no existing choice — recording it is strictly better than leaving the
|
|
10
|
+
* user to guess.
|
|
11
|
+
*
|
|
12
|
+
* Everything here is deliberately conservative. This runs unattended on every
|
|
13
|
+
* sync, against a file the user also edits by hand, so the bar for writing is
|
|
14
|
+
* "the answer cannot be wrong": never overwrite an existing choice, never guess
|
|
15
|
+
* between two companies, and never clobber a file we could not parse.
|
|
16
|
+
*/
|
|
17
|
+
export interface SeedActiveCompanyOptions {
|
|
18
|
+
hqRoot: string;
|
|
19
|
+
/** Slugs of companies that reconciled successfully this run. */
|
|
20
|
+
companySlugs: readonly string[];
|
|
21
|
+
reportDiagnostic?: (diagnostic: {
|
|
22
|
+
event: string;
|
|
23
|
+
message: string;
|
|
24
|
+
err: unknown;
|
|
25
|
+
context: Record<string, unknown>;
|
|
26
|
+
}) => void;
|
|
27
|
+
}
|
|
28
|
+
export interface SeedActiveCompanyResult {
|
|
29
|
+
/** True only when `.hq/config.json` was actually written. */
|
|
30
|
+
written: boolean;
|
|
31
|
+
/** The slug that was seeded, when one was. */
|
|
32
|
+
activeCompany?: string;
|
|
33
|
+
/** Why nothing was written. Absent when `written` is true. */
|
|
34
|
+
skippedReason?: "ambiguous" | "no-companies" | "already-set" | "unreadable-config" | "write-failed";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Seed `activeCompany` when — and only when — the answer is unambiguous.
|
|
38
|
+
*
|
|
39
|
+
* Never throws: the caller is a sync run whose exit code must not depend on
|
|
40
|
+
* local routing convenience.
|
|
41
|
+
*/
|
|
42
|
+
export declare function seedActiveCompany(options: SeedActiveCompanyOptions): SeedActiveCompanyResult;
|
|
43
|
+
//# sourceMappingURL=active-company.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"active-company.d.ts","sourceRoot":"","sources":["../src/active-company.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,gBAAgB,CAAC,EAAE,CAAC,UAAU,EAAE;QAC9B,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,GAAG,EAAE,OAAO,CAAC;QACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAClC,KAAK,IAAI,CAAC;CACZ;AAED,MAAM,WAAW,uBAAuB;IACtC,6DAA6D;IAC7D,OAAO,EAAE,OAAO,CAAC;IACjB,8CAA8C;IAC9C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,aAAa,CAAC,EACV,WAAW,GACX,cAAc,GACd,aAAa,GACb,mBAAmB,GACnB,cAAc,CAAC;CACpB;AAwCD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,wBAAwB,GAChC,uBAAuB,CA0EzB"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed `activeCompany` in `.hq/config.json` for a joiner who has exactly one
|
|
3
|
+
* cloud company.
|
|
4
|
+
*
|
|
5
|
+
* A newly invited member arrives with no local company routing at all. The
|
|
6
|
+
* manifest reconciler restores their `companies/manifest.yaml` entry, but
|
|
7
|
+
* nothing tells HQ which company is *current*, so they stay unrouted even
|
|
8
|
+
* though membership resolved fine. When the answer is unambiguous — one company
|
|
9
|
+
* and no existing choice — recording it is strictly better than leaving the
|
|
10
|
+
* user to guess.
|
|
11
|
+
*
|
|
12
|
+
* Everything here is deliberately conservative. This runs unattended on every
|
|
13
|
+
* sync, against a file the user also edits by hand, so the bar for writing is
|
|
14
|
+
* "the answer cannot be wrong": never overwrite an existing choice, never guess
|
|
15
|
+
* between two companies, and never clobber a file we could not parse.
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
function isPlainObject(value) {
|
|
21
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
22
|
+
return false;
|
|
23
|
+
const proto = Object.getPrototypeOf(value);
|
|
24
|
+
return proto === Object.prototype || proto === null;
|
|
25
|
+
}
|
|
26
|
+
function isNonEmptyString(value) {
|
|
27
|
+
return typeof value === "string" && value.length > 0;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Write `config` to `configPath` via temp-file + rename, so a reader never
|
|
31
|
+
* observes a half-written config and a crash cannot truncate the original.
|
|
32
|
+
*/
|
|
33
|
+
function writeConfigAtomically(configPath, config) {
|
|
34
|
+
const dir = path.dirname(configPath);
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
const temporaryPath = path.join(dir, `.config-${process.pid}-${randomUUID()}.json`);
|
|
37
|
+
try {
|
|
38
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
39
|
+
fs.renameSync(temporaryPath, configPath);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
// The rename consumed the temp file on the success path; this only fires
|
|
43
|
+
// when writing or renaming threw, and must not mask the original error.
|
|
44
|
+
try {
|
|
45
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Best effort — a stray temp file is not worth failing a sync over.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Seed `activeCompany` when — and only when — the answer is unambiguous.
|
|
54
|
+
*
|
|
55
|
+
* Never throws: the caller is a sync run whose exit code must not depend on
|
|
56
|
+
* local routing convenience.
|
|
57
|
+
*/
|
|
58
|
+
export function seedActiveCompany(options) {
|
|
59
|
+
const slugs = Array.from(new Set(options.companySlugs.filter(isNonEmptyString)));
|
|
60
|
+
if (slugs.length === 0)
|
|
61
|
+
return { written: false, skippedReason: "no-companies" };
|
|
62
|
+
// Two or more companies is a genuine choice and belongs to the user. Picking
|
|
63
|
+
// one would route them somewhere arbitrary and look like a bug.
|
|
64
|
+
if (slugs.length > 1)
|
|
65
|
+
return { written: false, skippedReason: "ambiguous" };
|
|
66
|
+
const activeCompany = slugs[0];
|
|
67
|
+
const configPath = path.join(options.hqRoot, ".hq", "config.json");
|
|
68
|
+
let config = {};
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = fs.readFileSync(configPath, "utf8");
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (err.code !== "ENOENT") {
|
|
75
|
+
// An unreadable config is user state we cannot reason about. Leave it.
|
|
76
|
+
options.reportDiagnostic?.({
|
|
77
|
+
event: "runner.active_company.config_unreadable",
|
|
78
|
+
message: "could not read .hq/config.json; activeCompany not seeded",
|
|
79
|
+
err,
|
|
80
|
+
context: { configPath },
|
|
81
|
+
});
|
|
82
|
+
return { written: false, skippedReason: "unreadable-config" };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (raw !== undefined) {
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(raw);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
// Malformed JSON is almost always a half-finished hand edit. Overwriting
|
|
92
|
+
// it would destroy whatever the user was in the middle of doing.
|
|
93
|
+
options.reportDiagnostic?.({
|
|
94
|
+
event: "runner.active_company.config_malformed",
|
|
95
|
+
message: ".hq/config.json is not valid JSON; left untouched",
|
|
96
|
+
err,
|
|
97
|
+
context: { configPath },
|
|
98
|
+
});
|
|
99
|
+
return { written: false, skippedReason: "unreadable-config" };
|
|
100
|
+
}
|
|
101
|
+
if (!isPlainObject(parsed)) {
|
|
102
|
+
options.reportDiagnostic?.({
|
|
103
|
+
event: "runner.active_company.config_malformed",
|
|
104
|
+
message: ".hq/config.json is not a JSON object; left untouched",
|
|
105
|
+
err: undefined,
|
|
106
|
+
context: { configPath },
|
|
107
|
+
});
|
|
108
|
+
return { written: false, skippedReason: "unreadable-config" };
|
|
109
|
+
}
|
|
110
|
+
config = parsed;
|
|
111
|
+
}
|
|
112
|
+
// `companySlug` counts as already-set: sync-core resolves it as the active
|
|
113
|
+
// company when `activeCompany` is absent, so writing `activeCompany` here
|
|
114
|
+
// would silently re-route a user who already had an effective answer.
|
|
115
|
+
if (isNonEmptyString(config.activeCompany) || isNonEmptyString(config.companySlug)) {
|
|
116
|
+
return { written: false, skippedReason: "already-set" };
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
writeConfigAtomically(configPath, { ...config, activeCompany });
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
options.reportDiagnostic?.({
|
|
123
|
+
event: "runner.active_company.write_failed",
|
|
124
|
+
message: "failed to seed activeCompany in .hq/config.json",
|
|
125
|
+
err,
|
|
126
|
+
context: { configPath, activeCompany },
|
|
127
|
+
});
|
|
128
|
+
return { written: false, skippedReason: "write-failed" };
|
|
129
|
+
}
|
|
130
|
+
return { written: true, activeCompany };
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=active-company.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"active-company.js","sourceRoot":"","sources":["../src/active-company.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"active-company.test.d.ts","sourceRoot":"","sources":["../src/active-company.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { seedActiveCompany } from "./active-company.js";
|
|
6
|
+
let hqRoot;
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
if (hqRoot) {
|
|
9
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
10
|
+
hqRoot = undefined;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
function makeHqRoot() {
|
|
14
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "hq-active-company-"));
|
|
15
|
+
}
|
|
16
|
+
function configPath(root) {
|
|
17
|
+
return path.join(root, ".hq", "config.json");
|
|
18
|
+
}
|
|
19
|
+
function writeConfig(root, contents) {
|
|
20
|
+
const p = configPath(root);
|
|
21
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
22
|
+
fs.writeFileSync(p, contents, "utf8");
|
|
23
|
+
return p;
|
|
24
|
+
}
|
|
25
|
+
function readConfig(root) {
|
|
26
|
+
return JSON.parse(fs.readFileSync(configPath(root), "utf8"));
|
|
27
|
+
}
|
|
28
|
+
describe("seedActiveCompany", () => {
|
|
29
|
+
it("seeds the single company when activeCompany is unset", () => {
|
|
30
|
+
hqRoot = makeHqRoot();
|
|
31
|
+
writeConfig(hqRoot, JSON.stringify({ someOtherSetting: true }));
|
|
32
|
+
expect(seedActiveCompany({ hqRoot, companySlugs: ["boring-ecom"] })).toEqual({
|
|
33
|
+
written: true,
|
|
34
|
+
activeCompany: "boring-ecom",
|
|
35
|
+
});
|
|
36
|
+
expect(readConfig(hqRoot)).toEqual({
|
|
37
|
+
someOtherSetting: true,
|
|
38
|
+
activeCompany: "boring-ecom",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
it("creates a missing config file", () => {
|
|
42
|
+
hqRoot = makeHqRoot();
|
|
43
|
+
expect(fs.existsSync(configPath(hqRoot))).toBe(false);
|
|
44
|
+
expect(seedActiveCompany({ hqRoot, companySlugs: ["boring-ecom"] })).toEqual({
|
|
45
|
+
written: true,
|
|
46
|
+
activeCompany: "boring-ecom",
|
|
47
|
+
});
|
|
48
|
+
expect(readConfig(hqRoot)).toEqual({ activeCompany: "boring-ecom" });
|
|
49
|
+
});
|
|
50
|
+
it("no-ops on zero companies", () => {
|
|
51
|
+
hqRoot = makeHqRoot();
|
|
52
|
+
const before = writeConfig(hqRoot, JSON.stringify({ keep: 1 }));
|
|
53
|
+
const raw = fs.readFileSync(before, "utf8");
|
|
54
|
+
expect(seedActiveCompany({ hqRoot, companySlugs: [] })).toEqual({
|
|
55
|
+
written: false,
|
|
56
|
+
skippedReason: "no-companies",
|
|
57
|
+
});
|
|
58
|
+
expect(fs.readFileSync(before, "utf8")).toBe(raw);
|
|
59
|
+
});
|
|
60
|
+
// Two companies is a real choice. Guessing would route the user somewhere
|
|
61
|
+
// arbitrary, which is indistinguishable from a bug to the person it happens to.
|
|
62
|
+
it("no-ops on two or more companies rather than guessing", () => {
|
|
63
|
+
hqRoot = makeHqRoot();
|
|
64
|
+
const p = writeConfig(hqRoot, JSON.stringify({ keep: 1 }));
|
|
65
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
66
|
+
expect(seedActiveCompany({ hqRoot, companySlugs: ["boring-ecom", "acme"] })).toEqual({ written: false, skippedReason: "ambiguous" });
|
|
67
|
+
expect(fs.readFileSync(p, "utf8")).toBe(raw);
|
|
68
|
+
});
|
|
69
|
+
it("never overwrites an existing activeCompany", () => {
|
|
70
|
+
hqRoot = makeHqRoot();
|
|
71
|
+
const p = writeConfig(hqRoot, JSON.stringify({ activeCompany: "chosen-by-user" }));
|
|
72
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
73
|
+
expect(seedActiveCompany({ hqRoot, companySlugs: ["boring-ecom"] })).toEqual({
|
|
74
|
+
written: false,
|
|
75
|
+
skippedReason: "already-set",
|
|
76
|
+
});
|
|
77
|
+
expect(fs.readFileSync(p, "utf8")).toBe(raw);
|
|
78
|
+
});
|
|
79
|
+
// Even when the existing value names a company they have since left. Repairing
|
|
80
|
+
// that is a separate, user-visible decision — not something a sync should do
|
|
81
|
+
// silently underneath them.
|
|
82
|
+
it("never overwrites activeCompany even for a company the user has left", () => {
|
|
83
|
+
hqRoot = makeHqRoot();
|
|
84
|
+
writeConfig(hqRoot, JSON.stringify({ activeCompany: "former-employer" }));
|
|
85
|
+
expect(seedActiveCompany({ hqRoot, companySlugs: ["boring-ecom"] })).toEqual({
|
|
86
|
+
written: false,
|
|
87
|
+
skippedReason: "already-set",
|
|
88
|
+
});
|
|
89
|
+
expect(readConfig(hqRoot).activeCompany).toBe("former-employer");
|
|
90
|
+
});
|
|
91
|
+
// sync-core's resolveActiveCompany falls back to `companySlug`, so a config
|
|
92
|
+
// carrying only that already HAS an effective answer. Writing activeCompany
|
|
93
|
+
// would silently re-route the user.
|
|
94
|
+
it("treats a legacy companySlug as already-set", () => {
|
|
95
|
+
hqRoot = makeHqRoot();
|
|
96
|
+
const p = writeConfig(hqRoot, JSON.stringify({ companySlug: "legacy-choice" }));
|
|
97
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
98
|
+
expect(seedActiveCompany({ hqRoot, companySlugs: ["boring-ecom"] })).toEqual({
|
|
99
|
+
written: false,
|
|
100
|
+
skippedReason: "already-set",
|
|
101
|
+
});
|
|
102
|
+
expect(fs.readFileSync(p, "utf8")).toBe(raw);
|
|
103
|
+
});
|
|
104
|
+
it("leaves malformed JSON untouched and reports a diagnostic", () => {
|
|
105
|
+
hqRoot = makeHqRoot();
|
|
106
|
+
const p = writeConfig(hqRoot, '{"activeCompany": "half-edited"');
|
|
107
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
108
|
+
const diagnostics = [];
|
|
109
|
+
expect(seedActiveCompany({
|
|
110
|
+
hqRoot,
|
|
111
|
+
companySlugs: ["boring-ecom"],
|
|
112
|
+
reportDiagnostic: (d) => diagnostics.push(d),
|
|
113
|
+
})).toEqual({ written: false, skippedReason: "unreadable-config" });
|
|
114
|
+
expect(fs.readFileSync(p, "utf8")).toBe(raw);
|
|
115
|
+
expect(diagnostics).toHaveLength(1);
|
|
116
|
+
expect(diagnostics[0].event).toBe("runner.active_company.config_malformed");
|
|
117
|
+
});
|
|
118
|
+
it("leaves a non-object config untouched", () => {
|
|
119
|
+
hqRoot = makeHqRoot();
|
|
120
|
+
const p = writeConfig(hqRoot, "[1, 2, 3]");
|
|
121
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
122
|
+
expect(seedActiveCompany({ hqRoot, companySlugs: ["boring-ecom"] })).toEqual({
|
|
123
|
+
written: false,
|
|
124
|
+
skippedReason: "unreadable-config",
|
|
125
|
+
});
|
|
126
|
+
expect(fs.readFileSync(p, "utf8")).toBe(raw);
|
|
127
|
+
});
|
|
128
|
+
it("preserves unknown keys and leaves no temp files behind", () => {
|
|
129
|
+
hqRoot = makeHqRoot();
|
|
130
|
+
writeConfig(hqRoot, JSON.stringify({ nested: { a: 1 }, list: [1, 2], flag: false }));
|
|
131
|
+
expect(seedActiveCompany({ hqRoot, companySlugs: ["boring-ecom"] }).written).toBe(true);
|
|
132
|
+
expect(readConfig(hqRoot)).toEqual({
|
|
133
|
+
nested: { a: 1 },
|
|
134
|
+
list: [1, 2],
|
|
135
|
+
flag: false,
|
|
136
|
+
activeCompany: "boring-ecom",
|
|
137
|
+
});
|
|
138
|
+
const strays = fs
|
|
139
|
+
.readdirSync(path.join(hqRoot, ".hq"))
|
|
140
|
+
.filter((f) => f.startsWith(".config-"));
|
|
141
|
+
expect(strays).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
it("collapses duplicate slugs — one company named twice is not ambiguous", () => {
|
|
144
|
+
hqRoot = makeHqRoot();
|
|
145
|
+
expect(seedActiveCompany({ hqRoot, companySlugs: ["boring-ecom", "boring-ecom"] })).toEqual({ written: true, activeCompany: "boring-ecom" });
|
|
146
|
+
expect(readConfig(hqRoot).activeCompany).toBe("boring-ecom");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
//# sourceMappingURL=active-company.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"active-company.test.js","sourceRoot":"","sources":["../src/active-company.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,11 +22,35 @@ export interface RunnerTarget {
|
|
|
22
22
|
slug: string;
|
|
23
23
|
name?: string;
|
|
24
24
|
bucketName?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Entity liveness as observed AT PLAN TIME, copied off the `entity.get`
|
|
27
|
+
* result this plan was built from. Carried so a downstream consumer can make
|
|
28
|
+
* a liveness decision without paying a second round trip.
|
|
29
|
+
*
|
|
30
|
+
* These deliberately do NOT gate SYNC. A suspended company still syncs — that
|
|
31
|
+
* is user-visible behavior and not ours to change here. They exist so the
|
|
32
|
+
* MANIFEST decision (see `manifest-reconcile.ts`) can be as strict as it was
|
|
33
|
+
* when it re-fetched the entity itself. Absent fields mean "unknown", and
|
|
34
|
+
* every consumer of them is required to fail closed.
|
|
35
|
+
*/
|
|
36
|
+
entityType?: string;
|
|
37
|
+
entityStatus?: string;
|
|
25
38
|
personalMode?: boolean;
|
|
26
39
|
journalSlug?: string;
|
|
27
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Why a run cannot proceed to a fanout.
|
|
43
|
+
*
|
|
44
|
+
* - `no-memberships` — the caller resolved fine but belongs to no company.
|
|
45
|
+
* Usually a joiner whose invite is still pending acceptance.
|
|
46
|
+
* - `no-person-entity` — the caller is signed in but has no personal entity to
|
|
47
|
+
* sync into, so even `--personal` has no target.
|
|
48
|
+
*/
|
|
49
|
+
export type SetupNeededReason = "no-memberships" | "no-person-entity";
|
|
28
50
|
export type MembershipResolution = {
|
|
29
51
|
status: "setup-needed";
|
|
52
|
+
reason: SetupNeededReason;
|
|
53
|
+
pendingInviteCount?: number;
|
|
30
54
|
} | {
|
|
31
55
|
status: "memberships";
|
|
32
56
|
memberships: Pick<Membership, "companyUid">[];
|
|
@@ -40,9 +64,10 @@ export declare function resolveMembershipsForRun(options: {
|
|
|
40
64
|
stderr: {
|
|
41
65
|
write: (chunk: string) => boolean | void;
|
|
42
66
|
};
|
|
67
|
+
/** Returns how many invites were still pending after the claim attempt. */
|
|
43
68
|
runClaimDance: (client: VaultClientSurface, claims: IdentityClaims, stderr: {
|
|
44
69
|
write: (chunk: string) => boolean | void;
|
|
45
|
-
}) => Promise<
|
|
70
|
+
}) => Promise<number>;
|
|
46
71
|
listMemberships: (client: VaultClientSurface) => Promise<Pick<Membership, "companyUid">[]>;
|
|
47
72
|
}): Promise<MembershipResolution>;
|
|
48
73
|
export declare function buildFanoutPlan(options: {
|
|
@@ -55,6 +80,7 @@ export declare function buildFanoutPlan(options: {
|
|
|
55
80
|
resolveSkipPersonal: (flag: boolean) => boolean;
|
|
56
81
|
}): Promise<{
|
|
57
82
|
status: "setup-needed";
|
|
83
|
+
reason: SetupNeededReason;
|
|
58
84
|
} | {
|
|
59
85
|
status: "plan";
|
|
60
86
|
plan: RunnerTarget[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-runner-planning.d.ts","sourceRoot":"","sources":["../../src/bin/sync-runner-planning.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAMlF,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAExE,UAAU,cAAc;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,MAAM,EAAE,cAAc,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"sync-runner-planning.d.ts","sourceRoot":"","sources":["../../src/bin/sync-runner-planning.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,12 +6,21 @@ export async function resolveMembershipsForRun(options) {
|
|
|
6
6
|
return { status: "memberships", memberships: [] };
|
|
7
7
|
}
|
|
8
8
|
if (options.companies) {
|
|
9
|
+
let pendingInviteCount = 0;
|
|
9
10
|
if (options.claims) {
|
|
10
|
-
await options.runClaimDance(options.client, options.claims, options.stderr);
|
|
11
|
+
pendingInviteCount = await options.runClaimDance(options.client, options.claims, options.stderr);
|
|
11
12
|
}
|
|
12
13
|
const memberships = await options.listMemberships(options.client);
|
|
13
14
|
if (memberships.length === 0) {
|
|
14
|
-
|
|
15
|
+
// Pending invites that did NOT become memberships are the single most
|
|
16
|
+
// useful thing we can tell this user: they are not "solo", they have an
|
|
17
|
+
// invite waiting to be accepted. Reported only on the empty path, where
|
|
18
|
+
// it explains the emptiness.
|
|
19
|
+
return {
|
|
20
|
+
status: "setup-needed",
|
|
21
|
+
reason: "no-memberships",
|
|
22
|
+
...(pendingInviteCount > 0 ? { pendingInviteCount } : {}),
|
|
23
|
+
};
|
|
15
24
|
}
|
|
16
25
|
return { status: "memberships", memberships };
|
|
17
26
|
}
|
|
@@ -25,12 +34,25 @@ export async function buildFanoutPlan(options) {
|
|
|
25
34
|
for (const m of options.memberships) {
|
|
26
35
|
let slug = m.companyUid;
|
|
27
36
|
let name;
|
|
37
|
+
// Carried onto the target so downstream consumers (manifest reconciliation)
|
|
38
|
+
// can use the bucket, and judge liveness, without re-fetching the entity we
|
|
39
|
+
// already resolved here. On the degraded catch path below these stay
|
|
40
|
+
// undefined, which every consumer must read as "unknown" and reject.
|
|
41
|
+
let bucketName;
|
|
42
|
+
let entityType;
|
|
43
|
+
let entityStatus;
|
|
28
44
|
try {
|
|
29
45
|
const info = await options.client.entity.get(m.companyUid);
|
|
30
46
|
if (!info)
|
|
31
47
|
continue;
|
|
32
48
|
slug = info.slug || m.companyUid;
|
|
33
49
|
name = info.name;
|
|
50
|
+
bucketName = info.bucketName;
|
|
51
|
+
// Recorded, never acted on here: a suspended or unexpectedly-typed
|
|
52
|
+
// company still gets a sync leg exactly as before. Only the manifest
|
|
53
|
+
// decision consumes these.
|
|
54
|
+
entityType = info.type;
|
|
55
|
+
entityStatus = info.status;
|
|
34
56
|
}
|
|
35
57
|
catch (err) {
|
|
36
58
|
if (err instanceof VaultAuthError)
|
|
@@ -48,7 +70,14 @@ export async function buildFanoutPlan(options) {
|
|
|
48
70
|
}
|
|
49
71
|
// Best-effort — keep UID as the display identifier.
|
|
50
72
|
}
|
|
51
|
-
plan.push({
|
|
73
|
+
plan.push({
|
|
74
|
+
uid: m.companyUid,
|
|
75
|
+
slug,
|
|
76
|
+
...(name ? { name } : {}),
|
|
77
|
+
...(bucketName ? { bucketName } : {}),
|
|
78
|
+
...(entityType ? { entityType } : {}),
|
|
79
|
+
...(entityStatus ? { entityStatus } : {}),
|
|
80
|
+
});
|
|
52
81
|
}
|
|
53
82
|
if ((options.companies || options.personal) &&
|
|
54
83
|
!options.resolveSkipPersonal(options.skipPersonal)) {
|
|
@@ -81,7 +110,7 @@ export async function buildFanoutPlan(options) {
|
|
|
81
110
|
});
|
|
82
111
|
}
|
|
83
112
|
else if (options.personal) {
|
|
84
|
-
return { status: "setup-needed" };
|
|
113
|
+
return { status: "setup-needed", reason: "no-person-entity" };
|
|
85
114
|
}
|
|
86
115
|
}
|
|
87
116
|
return { status: "plan", plan };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-runner-planning.js","sourceRoot":"","sources":["../../src/bin/sync-runner-planning.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAmB,MAAM,aAAa,CAAC;AAClF,OAAO,EACL,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,2BAA2B,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"sync-runner-planning.js","sourceRoot":"","sources":["../../src/bin/sync-runner-planning.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-runner-planning.test.d.ts","sourceRoot":"","sources":["../../src/bin/sync-runner-planning.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { buildFanoutPlan, resolveMembershipsForRun } from "./sync-runner-planning.js";
|
|
3
|
+
const noopStderr = { write: () => true };
|
|
4
|
+
function clientStub(overrides = {}) {
|
|
5
|
+
return {
|
|
6
|
+
entity: {
|
|
7
|
+
get: () => Promise.resolve(undefined),
|
|
8
|
+
listByType: () => Promise.resolve([]),
|
|
9
|
+
},
|
|
10
|
+
...overrides,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
describe("resolveMembershipsForRun — setup-needed reasons", () => {
|
|
14
|
+
// Each reason must come from its OWN trigger. Collapsing them back into a
|
|
15
|
+
// bare `setup-needed` is what left joiners unable to tell "you have no team"
|
|
16
|
+
// apart from "you have an invite waiting".
|
|
17
|
+
it("reports no-memberships when the membership list came back empty", async () => {
|
|
18
|
+
const result = await resolveMembershipsForRun({
|
|
19
|
+
personal: false,
|
|
20
|
+
companies: true,
|
|
21
|
+
client: clientStub(),
|
|
22
|
+
claims: null,
|
|
23
|
+
stderr: noopStderr,
|
|
24
|
+
runClaimDance: () => Promise.resolve(0),
|
|
25
|
+
listMemberships: () => Promise.resolve([]),
|
|
26
|
+
});
|
|
27
|
+
expect(result).toEqual({ status: "setup-needed", reason: "no-memberships" });
|
|
28
|
+
});
|
|
29
|
+
it("attaches pendingInviteCount when the claim dance left invites unconverted", async () => {
|
|
30
|
+
const result = await resolveMembershipsForRun({
|
|
31
|
+
personal: false,
|
|
32
|
+
companies: true,
|
|
33
|
+
client: clientStub(),
|
|
34
|
+
claims: { sub: "sub-1" },
|
|
35
|
+
stderr: noopStderr,
|
|
36
|
+
runClaimDance: () => Promise.resolve(3),
|
|
37
|
+
listMemberships: () => Promise.resolve([]),
|
|
38
|
+
});
|
|
39
|
+
expect(result).toEqual({
|
|
40
|
+
status: "setup-needed",
|
|
41
|
+
reason: "no-memberships",
|
|
42
|
+
pendingInviteCount: 3,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
it("omits pendingInviteCount rather than reporting zero", async () => {
|
|
46
|
+
const result = await resolveMembershipsForRun({
|
|
47
|
+
personal: false,
|
|
48
|
+
companies: true,
|
|
49
|
+
client: clientStub(),
|
|
50
|
+
claims: { sub: "sub-1" },
|
|
51
|
+
stderr: noopStderr,
|
|
52
|
+
runClaimDance: () => Promise.resolve(0),
|
|
53
|
+
listMemberships: () => Promise.resolve([]),
|
|
54
|
+
});
|
|
55
|
+
expect(result).toEqual({ status: "setup-needed", reason: "no-memberships" });
|
|
56
|
+
expect("pendingInviteCount" in result).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
it("does not report setup-needed at all when memberships exist", async () => {
|
|
59
|
+
const runClaimDance = vi.fn().mockResolvedValue(2);
|
|
60
|
+
const result = await resolveMembershipsForRun({
|
|
61
|
+
personal: false,
|
|
62
|
+
companies: true,
|
|
63
|
+
client: clientStub(),
|
|
64
|
+
claims: { sub: "sub-1" },
|
|
65
|
+
stderr: noopStderr,
|
|
66
|
+
runClaimDance,
|
|
67
|
+
listMemberships: () => Promise.resolve([{ companyUid: "cmp_a" }]),
|
|
68
|
+
});
|
|
69
|
+
// Pending invites are only meaningful as an explanation for emptiness.
|
|
70
|
+
expect(result).toEqual({
|
|
71
|
+
status: "memberships",
|
|
72
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe("buildFanoutPlan — setup-needed reasons", () => {
|
|
77
|
+
it("reports no-person-entity when --personal has no resolvable personal target", async () => {
|
|
78
|
+
const result = await buildFanoutPlan({
|
|
79
|
+
memberships: [],
|
|
80
|
+
companies: false,
|
|
81
|
+
personal: true,
|
|
82
|
+
skipPersonal: false,
|
|
83
|
+
client: clientStub(),
|
|
84
|
+
claims: null,
|
|
85
|
+
resolveSkipPersonal: () => false,
|
|
86
|
+
});
|
|
87
|
+
expect(result).toEqual({ status: "setup-needed", reason: "no-person-entity" });
|
|
88
|
+
});
|
|
89
|
+
// A --companies run does not need a personal target to be useful, so an
|
|
90
|
+
// unresolvable one must not abort the whole run.
|
|
91
|
+
it("does not report no-person-entity for a --companies run", async () => {
|
|
92
|
+
const result = await buildFanoutPlan({
|
|
93
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
94
|
+
companies: true,
|
|
95
|
+
personal: false,
|
|
96
|
+
skipPersonal: false,
|
|
97
|
+
client: clientStub({
|
|
98
|
+
entity: {
|
|
99
|
+
get: () => Promise.resolve({
|
|
100
|
+
uid: "cmp_a",
|
|
101
|
+
slug: "acme",
|
|
102
|
+
type: "company",
|
|
103
|
+
status: "active",
|
|
104
|
+
bucketName: "bucket-a",
|
|
105
|
+
}),
|
|
106
|
+
listByType: () => Promise.resolve([]),
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
claims: null,
|
|
110
|
+
resolveSkipPersonal: () => false,
|
|
111
|
+
});
|
|
112
|
+
expect(result.status).toBe("plan");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
//# sourceMappingURL=sync-runner-planning.test.js.map
|