Skip to content

Commit 307e179

Browse files
Merge branch 'main' into fix/hotkey-coldkey-secret-scan-false-positive
2 parents 4f13691 + b3bd0ba commit 307e179

47 files changed

Lines changed: 2167 additions & 131 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/loopover-ui/src/lib/config-generator-yaml.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,71 @@ describe("formStateToYaml", () => {
9696
[HEADER, "gate:", " aiReview:", ` model: ${JSON.stringify(model)}`].join("\n"),
9797
);
9898
});
99+
100+
// Regression (#8022): rees.analyzers overrides never reached the preview at all, since this module
101+
// declared its own narrower GeneratorFormState duplicate with no `rees` field.
102+
describe("review.enrichment (REES analyzer overrides, #8022)", () => {
103+
it("serializes rees.analyzers overrides as a review.enrichment map", () => {
104+
const state: GeneratorFormState = {
105+
rees: { analyzers: { "deep-nesting": true, "error-swallow": false } },
106+
};
107+
expect(formStateToYaml(state)).toBe(
108+
[
109+
HEADER,
110+
"review:",
111+
" enrichment:",
112+
" deep-nesting: true",
113+
" error-swallow: false",
114+
].join("\n"),
115+
);
116+
});
117+
118+
it("sorts analyzer entries alphabetically regardless of insertion order", () => {
119+
const state: GeneratorFormState = {
120+
rees: { analyzers: { "zebra-analyzer": true, "alpha-analyzer": true } },
121+
};
122+
expect(formStateToYaml(state)).toBe(
123+
[
124+
HEADER,
125+
"review:",
126+
" enrichment:",
127+
" alpha-analyzer: true",
128+
" zebra-analyzer: true",
129+
].join("\n"),
130+
);
131+
});
132+
133+
it("omits the review.enrichment block when rees.analyzers is empty", () => {
134+
expect(formStateToYaml({ rees: { analyzers: {} } })).toBe(HEADER);
135+
});
136+
137+
it("omits the review.enrichment block when rees is entirely unset", () => {
138+
expect(formStateToYaml({ rees: undefined })).toBe(HEADER);
139+
});
140+
141+
it("keeps a false override in the output rather than treating it as unset", () => {
142+
const state: GeneratorFormState = { rees: { analyzers: { "secret-scan": false } } };
143+
expect(formStateToYaml(state)).toBe(
144+
[HEADER, "review:", " enrichment:", " secret-scan: false"].join("\n"),
145+
);
146+
});
147+
148+
it("renders both gate.aiReview and review.enrichment when both are configured", () => {
149+
const state: GeneratorFormState = {
150+
gate: { aiReview: { provider: "anthropic" } },
151+
rees: { analyzers: { "deep-nesting": true } },
152+
};
153+
expect(formStateToYaml(state)).toBe(
154+
[
155+
HEADER,
156+
"gate:",
157+
" aiReview:",
158+
" provider: anthropic",
159+
"review:",
160+
" enrichment:",
161+
" deep-nesting: true",
162+
].join("\n"),
163+
);
164+
});
165+
});
99166
});

apps/loopover-ui/src/lib/config-generator-yaml.ts

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,23 @@
22
// text builder — the reviewable-output half of the config generator; field-group panels (#2208 and
33
// siblings) own collecting GeneratorFormState, this module only turns it into text. Key names and
44
// nesting match the real manifest schema (packages/loopover-engine/src/focus-manifest.ts's
5-
// gateConfigToJson `gate.aiReview` shape, the same shape documented in this repo's own root
6-
// .loopover.yml under the commented-out `aiReview:` example) — no parallel schema. Every field is
7-
// optional so a fresh/partial form never produces invalid output; unset keys are omitted entirely.
8-
// The generated header names the `.loopover.yml` filename.
9-
10-
export type AiCombineStrategy = "single" | "consensus" | "synthesis";
11-
export type AiProvider = "anthropic" | "openai";
12-
13-
export type GeneratorGateAiReviewState = {
14-
combine?: AiCombineStrategy | null;
15-
provider?: AiProvider | null;
16-
model?: string | null;
17-
};
18-
19-
export type GeneratorFormState = {
20-
gate?: {
21-
aiReview?: GeneratorGateAiReviewState;
22-
};
23-
};
5+
// gateConfigToJson `gate.aiReview` shape and `review.enrichment` shape, both documented in this repo's
6+
// own root .loopover.yml.example) — no parallel schema. Every field is optional so a fresh/partial form
7+
// never produces invalid output; unset keys are omitted entirely. The generated header names the
8+
// `.loopover.yml` filename.
9+
//
10+
// GeneratorFormState is re-exported from config-generator-form-state.ts (#8022) rather than declared a
11+
// second time here -- ConfigGeneratorPanel collects the one richer state object field-group panels
12+
// (including ReesAnalyzerFieldGroup) write to, and this module needs the same shape, not a structurally-
13+
// compatible-but-narrower duplicate that silently can't see fields like `rees`.
14+
import type { GeneratorFormState } from "./config-generator-form-state";
15+
16+
export type {
17+
AiCombineStrategy,
18+
AiProvider,
19+
GeneratorFormState,
20+
GeneratorGateAiReviewState,
21+
} from "./config-generator-form-state";
2422

2523
const YAML_HEADER = "# .loopover.yml — generated by the config generator";
2624

@@ -39,18 +37,39 @@ function yamlLine(indent: number, key: string, value: string): string {
3937
return `${" ".repeat(indent)}${key}: ${yamlScalar(value)}`;
4038
}
4139

42-
/** Serializes the `gate.aiReview` slice of GeneratorFormState into `.loopover.yml` text, omitting
43-
* every unset field/block so the preview is always valid YAML (including the empty-state case). */
40+
// Analyzer names come from the app's own REES_ANALYZERS catalog (validated GeneratorReesState keys), never
41+
// free text, so a boolean value needs no scalar-quoting -- `true`/`false` are unambiguous YAML literals.
42+
function yamlBooleanLine(indent: number, key: string, value: boolean): string {
43+
return `${" ".repeat(indent)}${key}: ${value}`;
44+
}
45+
46+
/** Serializes the `gate.aiReview` and `review.enrichment` slices of GeneratorFormState into `.loopover.yml`
47+
* text, omitting every unset field/block so the preview is always valid YAML (including the empty-state
48+
* case). `review.enrichment` mirrors `state.rees.analyzers` directly (an overrides-only map, matching the
49+
* real manifest field's own shape) rather than going through `reesAnalyzersManifestPatch`'s exact-list
50+
* output, which serializes the REES_ANALYZERS env var, a different representation of the same overrides
51+
* for a different consumer. */
4452
export function formStateToYaml(state: GeneratorFormState): string {
4553
const aiReview = state.gate?.aiReview;
4654
const aiReviewLines: string[] = [];
4755
if (aiReview?.combine) aiReviewLines.push(yamlLine(2, "combine", aiReview.combine));
4856
if (aiReview?.provider) aiReviewLines.push(yamlLine(2, "provider", aiReview.provider));
4957
if (aiReview?.model) aiReviewLines.push(yamlLine(2, "model", aiReview.model));
5058

59+
const enrichmentEntries = Object.entries(state.rees?.analyzers ?? {}).sort(([a], [b]) =>
60+
a.localeCompare(b),
61+
);
62+
5163
const lines = [YAML_HEADER];
5264
if (aiReviewLines.length > 0) {
5365
lines.push("gate:", " aiReview:", ...aiReviewLines);
5466
}
67+
if (enrichmentEntries.length > 0) {
68+
lines.push(
69+
"review:",
70+
" enrichment:",
71+
...enrichmentEntries.map(([name, enabled]) => yamlBooleanLine(2, name, enabled)),
72+
);
73+
}
5574
return lines.join("\n");
5675
}

control-plane/src/tenant-provisioning-driver.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,24 @@ export type FakeDriverCall = {
9292

9393
/** A fake `TenantProvisioningDriver` plus the recorded state a test inspects. */
9494
export type FakeTenantProvisioningDriver = TenantProvisioningDriver & {
95-
/** Tenant names whose container currently "exists" (an in-memory stand-in for real infrastructure). */
95+
/** Product-scoped keys (`${product}:${tenant.name}`, same as container-driver.ts's `instanceNameFor`)
96+
* whose container currently "exists" (an in-memory stand-in for real infrastructure). */
9697
readonly containers: ReadonlySet<string>;
97-
/** Tenant names whose database currently "exists". */
98+
/** Product-scoped keys whose database currently "exists". */
9899
readonly databases: ReadonlySet<string>;
99-
/** Tenant names whose secrets are currently injected. */
100+
/** Product-scoped keys whose secrets are currently injected. */
100101
readonly injectedSecrets: ReadonlySet<string>;
101102
/** Every driver step this fake has run, in call order. */
102103
readonly calls: readonly FakeDriverCall[];
103104
};
104105

106+
/** Same composite key as container-driver.ts's `instanceNameFor` (#8025) — ORB and AMS tenants that share a
107+
* name must not collide in the fake's in-memory maps (production composes this fake for any step without a
108+
* real backend yet). */
109+
function instanceKeyFor(request: TenantProvisioningRequest): string {
110+
return `${request.product}:${request.tenant.name}`;
111+
}
112+
105113
/** Minimal in-memory fake for orchestration/contract tests — three in-memory maps stand in for real infra
106114
* ("a container exists" / "a DB exists" / "secrets injected"), toggled by the create/destroy steps, plus an
107115
* ordered call log. NO Cloudflare, Postgres, or secret-broker IO of any kind. Mirrors
@@ -135,11 +143,11 @@ export function createFakeTenantProvisioningDriver(): FakeTenantProvisioningDriv
135143
},
136144
async createContainer(request) {
137145
record("createContainer", request);
138-
containers.add(request.tenant.name);
146+
containers.add(instanceKeyFor(request));
139147
},
140148
async provisionDatabase(request) {
141149
record("provisionDatabase", request);
142-
databases.add(request.tenant.name);
150+
databases.add(instanceKeyFor(request));
143151
// Deterministic per-tenant fake connection details -- no real IO, no state beyond the existing
144152
// `databases` set, just enough shape for callers/tests exercising the widened (#7653) return contract.
145153
const host = `fake-${request.tenant.name}.control-plane.invalid`;
@@ -151,30 +159,33 @@ export function createFakeTenantProvisioningDriver(): FakeTenantProvisioningDriv
151159
},
152160
async injectSecrets(request) {
153161
record("injectSecrets", request);
154-
injectedSecrets.add(request.tenant.name);
162+
injectedSecrets.add(instanceKeyFor(request));
155163
},
156164
async destroyContainer(request) {
157165
record("destroyContainer", request);
158166
// Idempotent teardown: the else-branch (nothing to remove) is the "destroy-of-a-nonexistent-tenant"
159167
// lifecycle path — a no-op, never a throw.
160-
if (containers.has(request.tenant.name)) {
161-
containers.delete(request.tenant.name);
168+
const key = instanceKeyFor(request);
169+
if (containers.has(key)) {
170+
containers.delete(key);
162171
}
163172
},
164173
async dropDatabase(request) {
165174
record("dropDatabase", request);
166-
if (databases.has(request.tenant.name)) {
167-
databases.delete(request.tenant.name);
175+
const key = instanceKeyFor(request);
176+
if (databases.has(key)) {
177+
databases.delete(key);
168178
}
169179
},
170180
async revokeSecrets(request) {
171181
record("revokeSecrets", request);
172-
if (injectedSecrets.has(request.tenant.name)) {
173-
injectedSecrets.delete(request.tenant.name);
182+
const key = instanceKeyFor(request);
183+
if (injectedSecrets.has(key)) {
184+
injectedSecrets.delete(key);
174185
}
175186
},
176187
async containerExists(request) {
177-
return containers.has(request.tenant.name);
188+
return containers.has(instanceKeyFor(request));
178189
},
179190
};
180191
}

control-plane/test/driver-factory.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,21 +69,21 @@ test("withRealDatabaseDriver: overrides provisionDatabase/dropDatabase, forwards
6969
assert.deepEqual(calls, ["real-provision"]);
7070
// The fake's own provisionDatabase never ran -- its `databases` set stays empty even though the composed
7171
// driver's provisionDatabase resolved successfully.
72-
assert.equal(base.databases.has("acme"), false);
72+
assert.equal(base.databases.has("orb:acme"), false);
7373

7474
await composed.dropDatabase(REQUEST);
7575
assert.deepEqual(calls, ["real-provision", "real-drop"]);
7676

7777
// Every non-database step still runs against `base` exactly as before composition.
7878
await composed.createContainer(REQUEST);
79-
assert.ok(base.containers.has("acme"));
79+
assert.ok(base.containers.has("orb:acme"));
8080
assert.equal(await composed.containerExists(REQUEST), true);
8181
await composed.injectSecrets(REQUEST);
82-
assert.ok(base.injectedSecrets.has("acme"));
82+
assert.ok(base.injectedSecrets.has("orb:acme"));
8383
await composed.destroyContainer(REQUEST);
84-
assert.equal(base.containers.has("acme"), false);
84+
assert.equal(base.containers.has("orb:acme"), false);
8585
await composed.revokeSecrets(REQUEST);
86-
assert.equal(base.injectedSecrets.has("acme"), false);
86+
assert.equal(base.injectedSecrets.has("orb:acme"), false);
8787
});
8888

8989
test("withRealContainerDriver: overrides createContainer/destroyContainer/containerExists, forwards every other step to base", async () => {
@@ -110,13 +110,13 @@ test("withRealContainerDriver: overrides createContainer/destroyContainer/contai
110110
assert.deepEqual(calls, ["real-create", "real-exists", "real-destroy"]);
111111
// The fake's own createContainer never ran -- its `containers` set stays empty even though the composed
112112
// driver's own lifecycle calls all resolved successfully.
113-
assert.equal(base.containers.has("acme"), false);
113+
assert.equal(base.containers.has("orb:acme"), false);
114114

115115
// Every non-container step still runs against `base` exactly as before composition.
116116
const details = await composed.provisionDatabase(REQUEST);
117117
assert.equal(details.host, "fake-acme.control-plane.invalid");
118118
await composed.injectSecrets(REQUEST);
119-
assert.ok(base.injectedSecrets.has("acme"));
119+
assert.ok(base.injectedSecrets.has("orb:acme"));
120120
});
121121

122122
test("createTenantProvisioningDriver: falls back to the fake container behavior when containerBindings is omitted or empty", async () => {

control-plane/test/provisioning.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ test("provisionTenant runs the three #7180 steps in order and reports the tenant
3737
);
3838
// Container "exists"/reachable via the fake after provision.
3939
assert.equal(await driver.containerExists({ tenant, product: "orb" }), true);
40-
assert.ok(driver.databases.has("acme"));
41-
assert.ok(driver.injectedSecrets.has("acme"));
40+
assert.ok(driver.databases.has("orb:acme"));
41+
assert.ok(driver.injectedSecrets.has("orb:acme"));
4242
});
4343

4444
test("full lifecycle: provision → container exists → deprovision → container gone", async () => {
@@ -52,8 +52,8 @@ test("full lifecycle: provision → container exists → deprovision → contain
5252

5353
assert.deepEqual(result, { tenant, product: "ams", state: "torn down" });
5454
assert.equal(await driver.containerExists({ tenant, product: "ams" }), false);
55-
assert.equal(driver.databases.has("acme"), false);
56-
assert.equal(driver.injectedSecrets.has("acme"), false);
55+
assert.equal(driver.databases.has("ams:acme"), false);
56+
assert.equal(driver.injectedSecrets.has("ams:acme"), false);
5757
});
5858

5959
test("deprovisionTenant tears the steps down in reverse order", async () => {
@@ -83,7 +83,7 @@ test("deprovisionTenant on a never-provisioned tenant is a safe no-op that still
8383

8484
assert.deepEqual(result, { tenant, product: "ams", state: "torn down" });
8585
assert.equal(await driver.containerExists({ tenant, product: "ams" }), false);
86-
assert.equal(driver.containers.has("ghost"), false);
86+
assert.equal(driver.containers.has("ams:ghost"), false);
8787
});
8888

8989
test("the call shape is identical for an ORB tenant and an AMS tenant (product-agnostic)", async () => {

0 commit comments

Comments
 (0)