Skip to content

Commit dccc2e3

Browse files
committed
test: pin the runtime gate's restored-credential positions at both doors
Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
1 parent a22fa55 commit dccc2e3

2 files changed

Lines changed: 264 additions & 39 deletions

File tree

‎packages/lint/src/validate-flow-trigger-readiness.test.ts‎

Lines changed: 69 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import { runRuntimeAuthoringRules } from './runtime-gate.js';
1919
/**
2020
* [#20553] The flow-trigger family as the CLI table runs it: two registry
2121
* entries over one file — `validateFlowTriggerReadiness` and, split out because
22-
* it is CLI-only until #20611, `validateFlowApiTriggerSecret`. Cases that are
22+
* at the runtime gate it reads the host's restored-credential positions
23+
* (#20611), `validateFlowApiTriggerSecret`. Cases that are
2324
* about the WHOLE family's verdict on a stack (the severity map, the clean-stack
2425
* floor, the api-secret cases' exhaustive assertions) judge through this.
2526
*/
@@ -1160,34 +1161,31 @@ describe('validateFlowTriggerReadiness', () => {
11601161
expect(FLOW_API_TRIGGER_SECRET_MISSING).toBe('flow-api-trigger-secret-missing');
11611162
});
11621163

1163-
// [#20611] The id is CLI-only until the runtime publish gate judges the
1164-
// carried-forward body: a `/meta` save is gated BEFORE `saveMetaItem`
1165-
// restores the `config.secret` the flow read path withholds (#20552), so a
1166-
// signed flow's GET → edit → PUT would reach the gate secretless. Each side
1167-
// of the wall is pinned, and the gate pin carries its own positive control.
1168-
describe('one rule id on ONE side of the runtime wall — CLI-only until #20611', () => {
1164+
// [#20611] The id crosses the runtime wall. A `/meta` save is gated BEFORE
1165+
// `saveMetaItem` restores the `config.secret` the flow read path withholds
1166+
// (#20552), so the gate is handed the positions that restore fills, and a
1167+
// withheld-and-stored secret reads as present. Both sides of the wall are
1168+
// pinned, and each gate verdict is paired with the input that flips it.
1169+
describe('#20611 — on both sides of the runtime wall; the gate reads restored positions as present', () => {
11691170
const secretless = apiFlow({ hookId: 'intake' });
11701171
const stack = { objects: [candidateObject], flows: [secretless] };
11711172

1172-
it('the split is whole: validateFlowTriggerReadiness alone no longer emits the id', () => {
1173+
it('the split is whole: validateFlowTriggerReadiness alone does not emit the id', () => {
11731174
expect(validateFlowTriggerReadiness(stack).map((f) => f.rule)).not.toContain(
11741175
FLOW_API_TRIGGER_SECRET_MISSING,
11751176
);
11761177
expect(validateFlowApiTriggerSecret(stack).map((f) => f.rule)).toEqual([FLOW_API_TRIGGER_SECRET_MISSING]);
11771178
});
11781179

1179-
it('its registry entry gates all three commands on the cli surface only, with a reason', () => {
1180+
it('its registry entry gates all three commands AND the runtime publish gate for `flow` writes', () => {
11801181
const entry = AUTHORING_RULES.find((r) => r.name === 'validateFlowApiTriggerSecret');
11811182
expect(entry).toBeDefined();
11821183
expect(entry!.tier).toBe('gating');
11831184
expect([...entry!.commands].sort()).toEqual([...AUTHORING_COMMANDS].sort());
1184-
expect(entry!.surfaces).toEqual(['cli']);
1185-
expect(entry!.runtimeTypes).toBeUndefined();
1186-
expect((entry!.surfaceReason ?? '').trim().length).toBeGreaterThan(40);
1187-
// …while the family's shared entry still crosses the wall.
1188-
expect(AUTHORING_RULES.find((r) => r.name === 'validateFlowTriggerReadiness')!.surfaces).toContain(
1189-
'runtime-publish',
1190-
);
1185+
expect([...entry!.surfaces].sort()).toEqual(['cli', 'runtime-publish']);
1186+
expect(entry!.runtimeTypes).toEqual(['flow']);
1187+
// The reason that held it CLI-only is gone with the wall.
1188+
expect(entry!.surfaceReason).toBeUndefined();
11911189
});
11921190

11931191
it.each([...AUTHORING_COMMANDS])('os %s still refuses a secretless api flow through the table', (command) => {
@@ -1197,26 +1195,64 @@ describe('validateFlowTriggerReadiness', () => {
11971195
expect(hits.map((f) => [f.severity, f.path])).toEqual([['error', 'flows[0].nodes[0].config.secret']]);
11981196
});
11991197

1200-
it('the runtime publish gate does not emit it — and still runs the rest of the family', () => {
1198+
it('the runtime publish gate refuses a secretless api flow — absent and not stored is missing', () => {
12011199
const gated = runRuntimeAuthoringRules({ type: 'flow', item: secretless });
1202-
expect(gated.rulesRun).toContain('validateFlowTriggerReadiness');
1203-
expect(gated.rulesRun).not.toContain('validateFlowApiTriggerSecret');
1204-
expect([...gated.errors, ...gated.advisories].map((f) => f.rule)).not.toContain(
1205-
FLOW_API_TRIGGER_SECRET_MISSING,
1206-
);
1207-
// Positive control: the same door still refuses a flow the family
1208-
// proves dead, so the absence above is the wall, not a gate that ran
1209-
// nothing.
1210-
const dead = runRuntimeAuthoringRules({
1200+
expect(gated.rulesRun).toContain('validateFlowApiTriggerSecret');
1201+
expect(gated.errors.map((f) => [f.rule, f.severity, f.path])).toEqual([
1202+
[FLOW_API_TRIGGER_SECRET_MISSING, 'error', 'flows[0].nodes[0].config.secret'],
1203+
]);
1204+
});
1205+
1206+
it('…and passes the same body when the host restores the secret at that position — withheld and stored is present', () => {
1207+
// The redactor registry's item-relative spelling, as the host states it.
1208+
const restored = runRuntimeAuthoringRules({
12111209
type: 'flow',
1212-
item: {
1213-
name: 'declared_dead',
1214-
type: 'record_change',
1215-
status: 'active',
1216-
nodes: [{ id: 'start', type: 'start', config: { objectName: 'app_candidate', triggerType: 'onCreate' } }],
1217-
},
1210+
item: secretless,
1211+
restoredCredentialPaths: ['nodes.0.config.secret'],
1212+
});
1213+
// The rule RAN — the pass is its verdict, not a gate that ran nothing.
1214+
expect(restored.rulesRun).toContain('validateFlowApiTriggerSecret');
1215+
expect(restored.errors).toEqual([]);
1216+
});
1217+
1218+
it('a restored position excuses that position only — spelled against where the start node really sits', () => {
1219+
// The start node second: the host names `nodes.1…`, and the gate spells
1220+
// it `flows[0].nodes[1].config.secret`, the rule's own finding path.
1221+
const startSecond = apiFlow({ hookId: 'intake' }, {
1222+
nodes: [
1223+
{ id: 'end', type: 'end' },
1224+
{ id: 'start', type: 'start', config: { hookId: 'intake' } },
1225+
],
12181226
});
1219-
expect(dead.errors.map((f) => f.rule)).toContain(FLOW_TRIGGER_UNROUTABLE);
1227+
const at = (paths: string[]) =>
1228+
runRuntimeAuthoringRules({ type: 'flow', item: startSecond, restoredCredentialPaths: paths }).errors.map(
1229+
(f) => [f.rule, f.path],
1230+
);
1231+
expect(at(['nodes.1.config.secret'])).toEqual([]);
1232+
// A position that is not the start node's secret excuses nothing.
1233+
for (const other of [['nodes.0.config.secret'], ['nodes.1.config.signingSecret'], ['nodes.1.config']]) {
1234+
expect(at(other), other.join()).toEqual([[FLOW_API_TRIGGER_SECRET_MISSING, 'flows[0].nodes[1].config.secret']]);
1235+
}
1236+
});
1237+
1238+
it('the rule reads the set in its own finding-path spelling, and the CLI table never forwards it', () => {
1239+
const findingPath = 'flows[0].nodes[0].config.secret';
1240+
expect(validateFlowApiTriggerSecret(stack, { restoredCredentialPaths: new Set([findingPath]) })).toEqual([]);
1241+
expect(
1242+
validateFlowApiTriggerSecret(stack, { restoredCredentialPaths: new Set(['nodes.0.config.secret']) }).map(
1243+
(f) => f.rule,
1244+
),
1245+
).toEqual([FLOW_API_TRIGGER_SECRET_MISSING]);
1246+
// A CLI stack carries the author's own secret; the table does not hand
1247+
// a caller-stated set to any rule, so the refusal stands.
1248+
for (const command of AUTHORING_COMMANDS) {
1249+
const hits = runAuthoringRules(command, {
1250+
normalized: stack,
1251+
parsed: stack,
1252+
restoredCredentialPaths: new Set([findingPath]),
1253+
}).filter((f) => f.rule === FLOW_API_TRIGGER_SECRET_MISSING);
1254+
expect(hits.map((f) => f.path), command).toEqual([findingPath]);
1255+
}
12201256
});
12211257
});
12221258
});

0 commit comments

Comments
 (0)