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
2 changes: 1 addition & 1 deletion docs/runtime-intelligence.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ The Node Runtime Intelligence snapshot can include one backend-produced `validat

The four finding codes in this cut are:

- `runtime.operation-undocumented` — the running backend positively exposes an operation that OpenAPI does not document. Severity: `warning`. This is runtime evidence only and is not clickable as a spec operation.
- `runtime.operation-undocumented` — the running backend positively exposes an operation that OpenAPI does not document. Severity: `error`. The finding names the method and path. This is runtime evidence only and is not clickable as a spec operation. Because it is an error, the derived status is `fail`, and `flexdoc validate` exits `1` under the default policy.
- `runtime.operation-unobserved` — OpenAPI documents an operation that runtime discovery did not observe. Severity: `error` when discovery is complete, `info` when discovery is partial.
- `runtime.method-mismatch` — the same wire-equivalent path exists in both sources but under different HTTP methods. Severity: `error` when discovery is complete, `warning` when partial. The finding carries an expected documented method so the renderer can navigate to the spec operation.
- `runtime.duplicate-operation` — the host exposes multiple registrations for one wire-equivalent method/path. Severity: `warning`. In 3.1 this is an explicitly supported/verified claim for Express (including Nest-on-Express) and Hono. Fastify participates in the other validation checks, but exact duplicate-registration reporting is not claimed for Fastify in this cut.
Expand Down
8 changes: 5 additions & 3 deletions packages/backend/src/contract-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('runtime contract validation', () => {
}));
});

it('reports undocumented runtime operations as warnings', () => {
it('fails the default validation status when a runtime operation is undocumented', () => {
const result = validateRuntimeContract({
documentedRoutes: [{ method: 'GET', path: '/pets' }],
runtimeRoutes: [
Expand All @@ -66,11 +66,13 @@ describe('runtime contract validation', () => {
discoveryComplete: true,
});

expect(result.status).toBe('warn');
expect(result.status).toBe('fail');
expect(result.summary).toEqual({ total: 1, errors: 1, warnings: 0, info: 0 });
expect(result.findings).toEqual([expect.objectContaining({
code: 'runtime.operation-undocumented',
severity: 'warning',
severity: 'error',
location: { kind: 'operation', method: 'POST', path: '/internal/reindex' },
message: 'Runtime implements POST /internal/reindex, but OpenAPI does not document that operation.',
})]);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/contract-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export function validateRuntimeContract(options: ValidateRuntimeContractOptions)
findings.push({
id: findingId('runtime.operation-undocumented', route.path, route.method),
code: 'runtime.operation-undocumented',
severity: 'warning',
severity: 'error',
location: { kind: 'operation', method: route.method.toUpperCase(), path: route.path },
message: `Runtime implements ${route.method.toUpperCase()} ${route.path}, but OpenAPI does not document that operation.`,
expected: 'Operation is represented in OpenAPI',
Expand Down
3 changes: 2 additions & 1 deletion packages/backend/src/runtime-intelligence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ describe('runtime intelligence', () => {
expect(snapshot.summary).toEqual({ documented: 2, runtime: 2, matched: 1, runtimeOnly: 1, documentedOnly: 1 });
expect(snapshot.runtimeOnly).toEqual([{ method: 'POST', path: '/internal/reindex' }]);
expect(snapshot.documentedOnly).toEqual([{ method: 'GET', path: '/pets/{petId}' }]);
expect(snapshot.validation.summary).toEqual({ total: 2, errors: 1, warnings: 1, info: 0 });
expect(snapshot.validation.status).toBe('fail');
expect(snapshot.validation.summary).toEqual({ total: 2, errors: 2, warnings: 0, info: 0 });
expect(snapshot.serverOrigin).toBe('https://api.example.com');
expect(snapshot.server).toEqual({ localPort: 8443 });
expect(snapshot.environment).toEqual({ name: 'production' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ const snapshot = {
{
id: 'runtime.operation-undocumented:POST:/internal/reindex',
code: 'runtime.operation-undocumented' as const,
severity: 'warning' as const,
severity: 'error' as const,
location: { kind: 'operation' as const, method: 'POST', path: '/internal/reindex' },
message: 'Runtime implements POST /internal/reindex, but OpenAPI does not document that operation.',
expected: 'Operation is represented in OpenAPI',
observed: 'Operation exists only in the running backend',
},
],
summary: { total: 2, errors: 1, warnings: 1, info: 0 },
summary: { total: 2, errors: 2, warnings: 0, info: 0 },
},
};

Expand All @@ -48,7 +48,7 @@ describe('RuntimeIntelligencePanel', () => {
it('shows runtime metadata and structured 3.1 contract findings', () => {
render(<RuntimeIntelligencePanel open theme='light' loading={false} onClose={() => undefined} snapshot={snapshot} />);
expect(screen.getByText('FAIL · 2 findings')).toBeInTheDocument();
expect(screen.getByText('1 errors · 1 warnings · 0 info')).toBeInTheDocument();
expect(screen.getByText('2 errors · 0 warnings · 0 info')).toBeInTheDocument();
expect(screen.getByText('runtime.operation-unobserved')).toBeInTheDocument();
expect(screen.getByText('runtime.operation-undocumented')).toBeInTheDocument();
expect(screen.getByText('Expected: Operation is exposed by the running backend')).toBeInTheDocument();
Expand Down
2 changes: 1 addition & 1 deletion tools/flexdoc-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ npx @prauga/flexdoc-cli validate "$FLEXDOC_RUNTIME_URL" \

`--header <name:value>` is repeatable. Do not combine an explicit `Authorization` header with `--bearer` or `--basic`, and do not use bearer and basic together. CLI arguments can be visible to local process inspection, so supply credentials from your CI secret facility rather than committing them to scripts or repository configuration.

By default, `validate` exits `1` only when the backend validation status is `fail` (or when the endpoint/payload cannot be consumed). `warn` and `partial` remain successful so the backend's severity model is preserved without surprising existing CI.
By default, `validate` exits `1` only when the backend validation status is `fail` (or when the endpoint/payload cannot be consumed). An implemented-but-undocumented operation is an error, so it fails that default policy and the log names the method and path. Other warnings, and `partial` discovery, remain successful unless `--fail-on` asks for them.

To make CI stricter, add a fail policy:

Expand Down
24 changes: 24 additions & 0 deletions tools/flexdoc-cli/test/validation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,30 @@ test('formats actionable human output for CI logs', () => {
assert.match(output, /Observed: 2 runtime registrations/);
});

test('default validate policy fails an undocumented runtime operation and names it', async () => {
const output = [];
const undocumented = {
status: 'fail',
complete: true,
findings: [{
id: 'runtime.operation-undocumented:POST:/internal/reindex',
code: 'runtime.operation-undocumented',
severity: 'error',
location: { kind: 'operation', method: 'POST', path: '/internal/reindex' },
message: 'Runtime implements POST /internal/reindex, but OpenAPI does not document that operation.',
expected: 'Operation is represented in OpenAPI',
observed: 'Operation exists only in the running backend',
}],
summary: { total: 1, errors: 1, warnings: 0, info: 0 },
};
const exitCode = await runValidationCli(['https://api.example.test/docs/__flexdoc/runtime'], {
fetchImpl: async () => ({ ok: true, status: 200, json: async () => ({ validation: undocumented }) }),
log: (value) => output.push(value),
});
assert.equal(exitCode, 1);
assert.match(output.join('\n'), /runtime\.operation-undocumented · POST \/internal\/reindex/);
});

test('default validate policy exits 1 only for backend fail status', async () => {
const output = [];
const failureCode = await runValidationCli(['https://api.example.test/docs/__flexdoc/runtime', '--json'], {
Expand Down
Loading