Skip to content

Commit 842e787

Browse files
Merge pull request #1438 from corbitsdev/cl-10672-config-sprawl-cut-dead-options-merge-duplicate
refactor(config): fold duplicate flag spellings into single parse paths
2 parents 8beff11 + 7ad873e commit 842e787

5 files changed

Lines changed: 100 additions & 53 deletions

File tree

‎docs/PERFTRACE.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ Precedence:
6666
`resourceAttributes["service.name"]` > `corbits-code`. After merge,
6767
`resourceAttributes["service.name"]` is always set to the resolved name so
6868
the two never diverge.
69-
- **resourceAttributes:** settings merged with env; env wins on key conflict
69+
- **resourceAttributes:** when `OTEL_RESOURCE_ATTRIBUTES` is set, it fully
70+
replaces settings attributes (including set-but-empty)
7071
- **enabling export:** setting an endpoint (settings or env) enables export;
7172
`otel.enabled` need not be set. Service name or resource attributes alone do
7273
not enable export.

‎src/config.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,8 @@ describe("loadConfig", () => {
572572
{ argv: ["resume", "--pick"] },
573573
{ argv: ["resume"] },
574574
{ argv: ["continue", "--list"] },
575+
{ argv: ["resume", "--list"] },
576+
{ argv: ["resume", "--pick", "--list"] },
575577
] as const)("$argv opens the session picker", async ({ argv }) => {
576578
const cwd = await emptyCwd();
577579
const config = await loadFor(cwd, argv, { home: await tempHome() });
@@ -581,6 +583,15 @@ describe("loadConfig", () => {
581583
expect(config.skipInitialTask).toBe(true);
582584
});
583585

586+
test("CL-10672 keeps --pick/--list out of flow as unrecognized flags", async () => {
587+
await expect(
588+
loadConfig(["--pick"], { globalSettingsPath: NO_SETTINGS }),
589+
).rejects.toThrow("unrecognized flag: --pick");
590+
await expect(
591+
loadConfig(["--list"], { globalSettingsPath: NO_SETTINGS }),
592+
).rejects.toThrow("unrecognized flag: --list");
593+
});
594+
584595
test("resume <id> reopens a known session and skips the initial task", async () => {
585596
const cwd = await emptyCwd();
586597
const home = await tempHome();

‎src/config/index.ts‎

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -834,16 +834,12 @@ export async function loadConfig(
834834
command = "exec";
835835
args.shift();
836836
} else if (leading === "resume" || leading === "continue") {
837-
command = "tui";
838837
args.shift();
839838
// Bare resume opens the list; a session id is the only direct-resume path.
840839
// Invalid non-flag positionals error instead of falling through (a
841840
// free-form token would otherwise become task while skipInitialTask is set).
842841
const next = args.slice()[0];
843-
if (next === "--pick" || next === "--list") {
844-
resumeMode = "pick";
845-
args.shift();
846-
} else if (next !== undefined && !isFlagToken(next)) {
842+
if (next !== undefined && !isFlagToken(next)) {
847843
if (!isSessionId(next)) {
848844
throw new Error(
849845
`'${next}' is not a session id. Use a UUID session id or \`corbits resume\` to choose.`,
@@ -944,41 +940,37 @@ export async function loadConfig(
944940
continue;
945941
}
946942
if (arg === "-p") {
947-
command = "exec";
948943
continue;
949944
}
950-
if (arg === "--resume") {
951-
if (resumeMode === "id") {
952-
throw new Error("cannot combine a session id with --resume");
953-
}
954-
// Optional session id on the interactive path: `corbits --resume <uuid>`
955-
// reopens that session (alias for `corbits resume <uuid>`); bare
956-
// `--resume` opens the picker. Exec / `-p` require the id — headless has
957-
// no picker. A non-id token errors instead of leaking into task text.
958-
const next = args[i + 1];
959-
if (next !== undefined && !isFlagToken(next)) {
960-
if (!isSessionId(next)) {
945+
if (arg === "--resume" || arg === "--pick" || arg === "--list") {
946+
if (arg === "--resume") {
947+
if (resumeMode === "id") {
948+
throw new Error("cannot combine a session id with --resume");
949+
}
950+
const next = args[i + 1];
951+
if (next !== undefined && !isFlagToken(next)) {
952+
if (!isSessionId(next)) {
953+
throw new Error(
954+
`'${next}' is not a session id. Use a UUID session id or \`corbits resume\` to choose.`,
955+
);
956+
}
957+
resumeMode = "id";
958+
resumeSessionId = next;
959+
i++;
960+
} else if (command === "exec") {
961961
throw new Error(
962-
`'${next}' is not a session id. Use a UUID session id or \`corbits resume\` to choose.`,
962+
`--resume requires a session id in exec mode. Use \`${COMMAND_NAME} resume\` to choose a session.`,
963963
);
964+
} else {
965+
resumeMode = "pick";
964966
}
965-
resumeMode = "id";
966-
resumeSessionId = next;
967-
i++;
968-
} else if (command === "exec") {
969-
throw new Error(
970-
`--resume requires a session id in exec mode. Use \`${COMMAND_NAME} resume\` to choose a session.`,
971-
);
972-
} else {
973-
resumeMode = "pick";
974-
}
975-
continue;
976-
}
977-
if ((arg === "--pick" || arg === "--list") && resumeMode !== undefined) {
978-
if (resumeMode === "id") {
967+
} else if (resumeMode === "id") {
979968
throw new Error("cannot combine a session id with --pick/--list");
969+
} else if (resumeMode !== undefined) {
970+
resumeMode = "pick";
971+
} else {
972+
throw new Error(`unrecognized flag: ${arg}`);
980973
}
981-
resumeMode = "pick";
982974
continue;
983975
}
984976
if (arg.startsWith("--")) {

‎src/perf/otel-config.test.ts‎

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ describe("resolveOtelExportConfig", () => {
197197
}
198198
});
199199

200-
test("resource attributes merge with env winning on conflict", () => {
200+
test("env resource attributes replace settings wholesale (CL-10672)", () => {
201201
const result = resolveOtelExportConfig(
202202
baseSettings({
203203
endpoint: "https://c.example",
@@ -213,7 +213,48 @@ describe("resolveOtelExportConfig", () => {
213213
expect(result.config.resourceAttributes["deployment.environment"]).toBe(
214214
"prod",
215215
);
216-
expect(result.config.resourceAttributes.team).toBe("corbits");
216+
expect(result.config.resourceAttributes.team).toBeUndefined();
217+
expect(result.config.resourceAttributes["service.name"]).toBe(
218+
DEFAULT_OTEL_SERVICE_NAME,
219+
);
220+
}
221+
});
222+
223+
test("settings resource attributes can carry a service.name the env never overrides", () => {
224+
const result = resolveOtelExportConfig(
225+
baseSettings({
226+
endpoint: "https://c.example",
227+
resourceAttributes: {
228+
"deployment.environment": "settings-only",
229+
team: "corbits",
230+
},
231+
}),
232+
{ [OTEL_ENV.resourceAttributes]: "service.name=from-env" },
233+
);
234+
expect(result.ok).toBe(true);
235+
if (result.ok && result.config.enabled) {
236+
expect(result.config.resourceAttributes.team).toBeUndefined();
237+
expect(result.config.serviceName).toBe("from-env");
238+
}
239+
});
240+
241+
test("set-but-empty env resource attributes replace with nothing (regression: no settings fallback)", () => {
242+
const result = resolveOtelExportConfig(
243+
baseSettings({
244+
endpoint: "https://c.example",
245+
resourceAttributes: {
246+
team: "corbits",
247+
"deployment.environment": "dev",
248+
},
249+
}),
250+
{ [OTEL_ENV.resourceAttributes]: "" },
251+
);
252+
expect(result.ok).toBe(true);
253+
if (result.ok && result.config.enabled) {
254+
expect(result.config.resourceAttributes.team).toBeUndefined();
255+
expect(
256+
result.config.resourceAttributes["deployment.environment"],
257+
).toBeUndefined();
217258
expect(result.config.resourceAttributes["service.name"]).toBe(
218259
DEFAULT_OTEL_SERVICE_NAME,
219260
);

‎src/perf/otel-config.ts‎

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export interface OtelSettings {
3636
/** Resource service.name. Env OTEL_SERVICE_NAME overrides. */
3737
serviceName?: string;
3838
/**
39-
* Extra resource attributes (merged; env keys win). Non-secret labels only.
39+
* Extra resource attributes. Non-secret labels only. Replaced wholesale by
40+
* OTEL_RESOURCE_ATTRIBUTES when the env var is set; never merged per-key.
4041
*/
4142
resourceAttributes?: Record<string, string>;
4243
}
@@ -212,8 +213,9 @@ function validateStringMap(
212213
* - endpoint: env > settings
213214
* - headers: env list fully replaces settings when set; else settings
214215
* - serviceName: env > settings.serviceName > attrs["service.name"] > default
215-
* - resourceAttributes: settings then env (env wins on conflict); after merge,
216-
* attrs["service.name"] is synced to the resolved serviceName
216+
* - resourceAttributes: env list fully replaces settings when OTEL_RESOURCE_ATTRIBUTES
217+
* is set; else settings. After resolution, attrs["service.name"] is synced
218+
* to the resolved serviceName
217219
*
218220
* No endpoint → disabled (ok); invalid or malformed → not ok.
219221
*/
@@ -294,19 +296,6 @@ export function resolveOtelExportConfig(
294296
}
295297

296298
let resourceAttributes: Record<string, string> = {};
297-
const settingsAttrs = validateStringMap(
298-
otel?.resourceAttributes,
299-
"settings.otel.resourceAttributes",
300-
);
301-
if (!settingsAttrs.ok) {
302-
return {
303-
ok: false,
304-
code: OTEL_CONFIG_INVALID,
305-
message: settingsAttrs.message,
306-
};
307-
}
308-
resourceAttributes = { ...settingsAttrs.value };
309-
310299
if (envResourceAttrsRaw !== undefined) {
311300
const parsed = parseOtelKeyValueList(
312301
envResourceAttrsRaw,
@@ -315,7 +304,20 @@ export function resolveOtelExportConfig(
315304
if (!parsed.ok) {
316305
return { ok: false, code: OTEL_CONFIG_INVALID, message: parsed.message };
317306
}
318-
resourceAttributes = { ...resourceAttributes, ...parsed.value };
307+
resourceAttributes = parsed.value;
308+
} else {
309+
const settingsAttrs = validateStringMap(
310+
otel?.resourceAttributes,
311+
"settings.otel.resourceAttributes",
312+
);
313+
if (!settingsAttrs.ok) {
314+
return {
315+
ok: false,
316+
code: OTEL_CONFIG_INVALID,
317+
message: settingsAttrs.message,
318+
};
319+
}
320+
resourceAttributes = settingsAttrs.value;
319321
}
320322

321323
// Sync attrs["service.name"] to the resolved name so the two never diverge.

0 commit comments

Comments
 (0)