From b859a54b492a39fc3754021300023fd74065184d Mon Sep 17 00:00:00 2001 From: hopeatina Date: Sat, 11 Apr 2026 12:37:42 -0500 Subject: [PATCH] fix(reporting): normalize source client identifiers --- AGENTS.md | 7 ++++ hooks/scripts/post-reporting-event.mjs | 17 ++++++++- tests/post-reporting-event.test.mjs | 41 ++++++++++++++++++++ tests/run-claude-dispatch-job.test.mjs | 52 +++++++++++++++++++++++++- 4 files changed, 115 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7001a72..5864874 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,10 +15,17 @@ Build mode is the default unless explicitly switched to release mode. - Re-check `git status -sb` only before commits, branch switches, or destructive operations. - Batch related tasks (FE + BE + tests) when they share the same outcome. - Use requested tools when possible; if blocked, use a reliable fallback and note it briefly. +- Solve-first behavior: attempt practical fixes and verification before asking for more user input. - Prefer small, progressive commits for long-running work. - Run targeted verification for touched files/surfaces only. - Do not block on full-suite checks during iterative development. +### Problem-Solving-First Policy +- Default posture is persistence: try to solve the issue end-to-end in the current turn. +- When blocked, attempt at least one concrete fallback path before escalating. +- Escalate early only for high-risk actions or true external blockers (permissions, missing credentials, production-risk decisions). +- Do not stop at analysis if implementation is feasible; implement, verify, then report. + ### Build-mode Verification - Backend changes: run targeted tests for changed modules. - Frontend changes: run focused desktop/mobile smoke checks for changed flows. diff --git a/hooks/scripts/post-reporting-event.mjs b/hooks/scripts/post-reporting-event.mjs index b91f30c..ce29c19 100644 --- a/hooks/scripts/post-reporting-event.mjs +++ b/hooks/scripts/post-reporting-event.mjs @@ -24,6 +24,18 @@ export function pickString(...values) { return undefined; } +export function normalizeSourceClient(value, fallback = "claude-code") { + const fallbackClient = pickString(fallback, "claude-code")?.toLowerCase() ?? "claude-code"; + const raw = pickString(value); + if (!raw) return fallbackClient; + + const normalized = raw.toLowerCase(); + if (!/^[a-z][a-z0-9._-]{1,63}$/.test(normalized)) { + return fallbackClient; + } + return normalized; +} + async function postJson(url, payload, headers, fetchImpl = fetch) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 8000); @@ -156,7 +168,10 @@ export async function main({ const initiativeId = pickString(args.initiative, env.ORGX_INITIATIVE_ID); const apiKey = pickString(env.ORGX_API_KEY); - const sourceClient = pickString(args.source_client, env.ORGX_SOURCE_CLIENT, "claude-code"); + const sourceClient = normalizeSourceClient( + pickString(args.source_client, env.ORGX_SOURCE_CLIENT), + "claude-code" + ); const runId = pickString(args.run_id, env.ORGX_RUN_ID); const correlationId = runId ? undefined diff --git a/tests/post-reporting-event.test.mjs b/tests/post-reporting-event.test.mjs index eefc12e..76f8581 100644 --- a/tests/post-reporting-event.test.mjs +++ b/tests/post-reporting-event.test.mjs @@ -4,6 +4,7 @@ import assert from "node:assert/strict"; import { parseArgs, pickString, + normalizeSourceClient, buildRuntimePayload, buildActivityPayload, buildCompletionChangesetPayload, @@ -22,6 +23,13 @@ test("pickString returns first non-empty string", () => { assert.equal(pickString("", " "), undefined); }); +test("normalizeSourceClient falls back for invalid values", () => { + assert.equal(normalizeSourceClient("claude-code"), "claude-code"); + assert.equal(normalizeSourceClient("ORGX.CLI"), "orgx.cli"); + assert.equal(normalizeSourceClient("5"), "claude-code"); + assert.equal(normalizeSourceClient("bad source"), "claude-code"); +}); + test("payload builders shape expected OrgX fields", () => { const runtime = buildRuntimePayload({ initiativeId: "init-1", @@ -118,3 +126,36 @@ test("main posts activity and completion changeset", async () => { assert.equal(calls[1].url, "https://www.useorgx.com/api/client/live/changesets/apply"); assert.equal(calls[0].init.headers.Authorization, "Bearer oxk_test"); }); + +test("main normalizes invalid ORGX_SOURCE_CLIENT env value", async () => { + const calls = []; + const fetchImpl = async (url, init) => { + calls.push({ url, init }); + return { + ok: true, + status: 200, + statusText: "OK", + async json() { + return { ok: true }; + }, + async text() { + return ""; + }, + }; + }; + + const result = await main({ + argv: ["--event=post_tool_use", "--message=hello"], + env: { + ORGX_API_KEY: "oxk_test", + ORGX_INITIATIVE_ID: "init-1", + ORGX_SOURCE_CLIENT: "5", + ORGX_BASE_URL: "https://www.useorgx.com", + }, + fetchImpl, + }); + + assert.equal(result.ok, true); + const payload = JSON.parse(calls[0].init.body); + assert.equal(payload.source_client, "claude-code"); +}); diff --git a/tests/run-claude-dispatch-job.test.mjs b/tests/run-claude-dispatch-job.test.mjs index 75c4929..463a761 100644 --- a/tests/run-claude-dispatch-job.test.mjs +++ b/tests/run-claude-dispatch-job.test.mjs @@ -4,7 +4,11 @@ import { mkdirSync, mkdtempSync, utimesSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { findMostRecentStateFile } from "../scripts/run-claude-dispatch-job.mjs"; +import { + createReporter, + findMostRecentStateFile, + normalizeSourceClient, +} from "../scripts/run-claude-dispatch-job.mjs"; test("findMostRecentStateFile returns null when logs root has no state files", () => { const root = mkdtempSync(join(tmpdir(), "orgx-dispatch-empty-")); @@ -30,3 +34,49 @@ test("findMostRecentStateFile returns newest job-state.json by mtime", () => { assert.equal(findMostRecentStateFile(root), newerState); }); + +test("normalizeSourceClient falls back for invalid identifiers", () => { + assert.equal(normalizeSourceClient("claude-code"), "claude-code"); + assert.equal(normalizeSourceClient("ORGX.Dispatch"), "orgx.dispatch"); + assert.equal(normalizeSourceClient("5"), "claude-code"); + assert.equal(normalizeSourceClient("bad source"), "claude-code"); +}); + +test("createReporter keeps source_client after run_id is established", async () => { + const activityPayloads = []; + const client = { + async emitActivity(payload) { + activityPayloads.push(payload); + if (activityPayloads.length === 1) { + return { ok: true, run_id: "run-123" }; + } + return { ok: true }; + }, + async applyChangeset() { + return { ok: true }; + }, + async updateEntity() { + return { ok: true }; + }, + }; + + const reporter = createReporter({ + client, + initiativeId: "init-1", + sourceClient: "claude-code", + correlationId: "corr-1", + planPath: null, + planHash: "abc123", + jobId: "job-1", + dryRun: false, + }); + + await reporter.emit({ message: "first" }); + await reporter.emit({ message: "second" }); + + assert.equal(activityPayloads.length, 2); + assert.equal(activityPayloads[0].source_client, "claude-code"); + assert.equal(activityPayloads[0].correlation_id, "corr-1"); + assert.equal(activityPayloads[1].source_client, "claude-code"); + assert.equal(activityPayloads[1].run_id, "run-123"); +});