Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .changeset/name-shaped-log-splice-sweep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
'@objectstack/service-automation': patch
---

fix(service-automation): the five NAME-shaped log splices stop interpolating foreign identifiers into log messages (#6654)

The tail #6499 reported but did not fix and #6587 deliberately excluded: five
`service-automation` log records still spliced **names/identifiers** that
originate outside the engine's control and are not schema-constrained to reject
newlines. `ObjectLogger.write()` adds one `<ts> <LEVEL>` head per call, so a
newline in any of them turns ONE record into several physical lines of which
only the first is greppable, and `serve`'s boot-quiet window drops the headless
continuations outright on the stdout (warn) path — the same downstream damage as
the closed thrown-text class, reached through a different door.

All five now log a single-line message carrying only controlled facts, with the
foreign identifier(s) in the logger's structured slot:

- the **re-entrancy guard** — the caller's record id → `recordId`;
- the **refused resume** — the caller's resume-signal keys → `rejected`;
- the **screen-input refusal** — the user-submitted keys, which reach the
message via `validateScreenInputs`' `Unknown screen field "…"` findings →
`issues` (the message now states the issue COUNT);
- **`warnUnknownNodeTypes`** — the flow's unknown node type names and the
registered vocabulary → `unknownTypes` / `knownTypes`;
- the **unclaimed branch label** (#4414) — the computed, potentially
record-derived branch label and the out-edge labels → `branchLabel` /
`outEdges`.

**No level changes**: every one of the five is #4632-FUNCTIONAL and stays
`warn`. Behaviour is unchanged at all five sites, and the caller-facing refusal
ENVELOPES (`INVALID_SIGNAL`, `INVALID_SCREEN_INPUT`) are untouched — they still
name the offending variables and fields, because an envelope is not a log
record.

Operator-visible: each message keeps its lead phrase so existing greps still
match — `re-entered for the same record`, `signal writes engine-internal`,
`violates its declared field contract`, `no registered executor or descriptor`
(load-bearing: tests and log filters count per-flow findings by it), and
`no out-edge carries that label`. Anything keyed on the spliced identifier
inside those messages must read the structured field instead.
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ import { registerLogicNodes } from './logic-nodes.js';
* The consequence shipped in `examples/app-crm` — see the first block below.
*/

const warnings: string[] = [];
/** One captured `warn` call — message AND structured meta (#6654 moved the
* computed branch label and the out-edge labels into the meta slot). */
type CapturedWarn = { msg: string; meta?: Record<string, any> };

const warnings: CapturedWarn[] = [];

function createTestLogger(): any {
return {
info: () => {},
warn: (msg: string) => { warnings.push(String(msg)); },
warn: (msg: string, meta?: Record<string, any>) => { warnings.push({ msg: String(msg), meta }); },
error: () => {},
debug: () => {},
child: () => createTestLogger(),
Expand Down Expand Up @@ -196,10 +200,19 @@ describe('decision branch routing (#4414)', () => {
}));
await run({ status: 'converted' });

// #6654 — the computed branch label is potentially record-derived and
// the edge labels are flow-author metadata, so both moved out of the
// message into the structured slot. The #4414 fact under test is
// unchanged: the unclaimed selection is REPORTED, naming the computed
// branch and every out-edge label.
expect(warnings.some((w) =>
w.includes("selected branch 'Yes — already converted'")
&& w.includes("'Yes'") && w.includes("'No'")
&& w.includes('#4414'),
w.msg.includes('no out-edge carries that label')
&& w.msg.includes('#4414')
&& w.meta?.branchLabel === 'Yes — already converted'
&& (w.meta?.outEdges as Array<{ label: string | null }>)
.map((e) => e.label).includes('Yes')
&& (w.meta?.outEdges as Array<{ label: string | null }>)
.map((e) => e.label).includes('No'),
)).toBe(true);
// Behaviour is unchanged (a run mid-flight must not die on it) — but it
// is no longer invisible.
Expand Down
Loading
Loading