Files
msd-core/tests/api-coverage.test.cjs
Behruz Nassre Esfahani d04e287fa9 fix(#2365): stop api-coverage detector false-positiving non-API phases (#2397)
* fix(#2365): stop the api-coverage detector false-positiving non-API phases

detectApiIntegration fired on any integration verb co-occurring anywhere on a
line with any API noun, treated / as a word boundary (so a first-party Next.js
src/app/api/... route path matched the noun "api"), and read any capitalized
word before API/SDK/REST/GraphQL as a service name behind a fixed stopword
denylist (so threat-model prose like "Resolver-only API" fired). Because the
verify:pre seal gate is BLOCKING, a phase touching no external API could not
reach UAT without fabricating a coverage matrix.

The compound rule now requires the verb and noun to share one clause (sentence
punctuation and table-cell walls end a clause) within a bounded word gap.
Non-prose spans are excluded before matching: fenced code (already), inline
code spans (new stripInlineCode in the markdown-sectionizer seam), and
path-shaped tokens. The <Service> API surface rule requires proper-noun
position — a clause-initial capitalized word is ordinary English and needs
dependency evidence (URL / package reference) on the same line — and rejects
compound modifiers ("Resolver-only", lowercase after the hyphen).

A phase that integrates no external API now has a first-class, reasoned way to
say so: a COVERAGE.md containing "No external API integration: <reason>"
satisfies the gate (declaration + rows is contradictory and blocks). The
true-positive path is pinned by regression tests: every default-vocabulary
positive still fires, including the widest word-gap pairing and the
surface-rule-only shape.

Fixes #2365

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(#2365): tighten api-coverage detector per Codex review (round 2)

Applies the Codex review findings on the initial #2365 fix:

- S-1: a COVERAGE.md "no external API integration" declaration is the human
  override for a fallible detector, so it must PASS even when detection still
  fires — but the contradiction is now SURFACED in the gate output (overridden
  signal count + terms) instead of passing silently.
- S-2: verb/noun pairing is now a term-group nearest-pair merge walk over
  precomputed word ordinals (computeWordStarts / minWordGap), not a match×match
  cross product — a hostile line repeating one pair thousands of times stays
  linear instead of going quadratic.
- FN-4: package-shaped inline-code spans (`stripe-sdk`, `@stripe/stripe-js`)
  are kept as noun/dependency evidence rather than being fully masked, so a
  genuine dependency reference inside code ticks still corroborates.
- C-1: the <Service> API surface rule now scans every candidate in every
  clause; a rejected first candidate no longer shadows a later genuine service.
- Cross-clause binding: a verb may bind a noun in the immediately following
  clause only when its own clause names a service object, within a tight gap —
  admits "Integrate Stripe, exposing its endpoints …" without re-admitting the
  unrelated-clauses false-positive class.
- Internal-descriptor negative evidence ("internal Payments API",
  "the internal endpoint") never pairs; URL/scheme matching generalized beyond
  http(s).

All 5 acceptance criteria still hold: the three reported false positives are
clean and "integrate the Stripe API" still fires. Built .cjs committed
alongside the .cts. tsc + eslint (incl. no-adhoc-markdown-parsing) +
lint:regression-names clean; affected suites 256/256 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#2365): retune api-coverage detector fail-closed per Codex review (round 3)

Codex's second-round review found the round-2 tightening had over-corrected into
FAIL-OPEN false negatives — realistic external-API prose that the BLOCKING seal
gate silently let through (the catastrophic class, since a missed API surface is
worse than a dismissable false positive). Retuned the detector to be explicitly
fail-closed: lean toward detecting, and let the one-line COVERAGE.md "no external
API integration" declaration dismiss the residual false positives.

Fail-open false negatives fixed (all now detect):
- F1 clause-initial `<Service> API` with a plain follower ("Stripe API for
  payment processing") — dropped the follower-allowlist / corroboration gate on
  clause-initial surfaces; a service that is not a stopword, descriptor, or
  compound modifier is a real name from any clause position.
- F2 scheme-less external host ("api.stripe.com/v1") — a dotted host with an
  alphabetic final label now contributes its API nouns; a first-party route
  path (no dotted host) still does not.
- F3 vendor's first-party SDK ("Integrate Shopify's first-party SDK") — the
  compound path no longer filters nouns on "internal"/"first-party" (Codex: the
  qualifier can describe the vendor's own API, not the consuming project's).
- F4 long single integration clause — removed the word-gap cap entirely: it
  could not separate a 21-word genuine clause from an 18-word internal one, so
  the clause boundary is now the whole relationship test.
- F5 lowercase cross-clause service — cross-clause binding no longer requires a
  capitalized "service object".

New false positives fixed (all now clean):
- F6 a URL token that swallowed a trailing clause comma, merging two clauses —
  trailing clause punctuation is kept literal so the split survives.
- F7 a capitalized internal component authorizing cross-clause binding — the new
  gate requires a dependent elaboration, not a new coordinate clause opened by a
  conjunction ("…, then document…").
- F8 a protocol name read as a service ("REST API", "GraphQL API") — protocol
  and locality descriptors are rejected in the `<Service>` position.

- Finding 9: the inline-code-span scanner was O(n^2) on pathological backtick
  runs; rewritten to linear via a per-length run cursor (2 MB: 4.15 s -> ~6 ms),
  semantics preserved (148 sectionizer tests unchanged).

Net simplification: the fail-closed model removed the round-2 minWordGap /
groupByTerm / follower / corroboration machinery (350 insertions vs 445
deletions across the touched files). Under fail-closed, three round-2 negative
tests now correctly detect (integration verb + "internal"-qualified noun, and
the distant-same-clause case); none were trek-e acceptance FPs.

Verified: 1491/1491 unit tests pass; tsc + eslint (incl. no-adhoc-markdown-
parsing) + lint:regression-names clean; all 8 review findings reproduced as
regression tests, both directions. Built .cjs committed alongside the .cts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#2365): resolve round-3 Codex review findings (fail-closed, round 4)

Codex's round-3 adversarial review found the fail-closed retune had introduced
new holes in both directions. Resolved:

Fail-open false negatives (now detect):
- External host addressing a PATH ("graph.microsoft.com/v1.0/me") is itself an
  integration surface and contributes an endpoint noun even when the host names
  no vocabulary word. A bare domain link with no path ("https://example.com")
  stays a non-signal, so "Integrate … from example.com, document …" is still
  clean.
- Locality qualification ("internal", "private") no longer leaks across a
  sentence or clause boundary: only plain spaces may separate the descriptor
  from the service, so "The cache is private. Stripe API …" now detects.
- Cross-clause binding: the fragile head-word cap (which could not tell a
  genuine "Connect … to Stripe payments, exposing its endpoints" from an
  unrelated "Integrate … from URL, document …" — both 4 words after the verb)
  is replaced by a participial-continuation rule: a verb binds a noun in the
  next clause only when that clause begins with an "-ing" elaboration. This
  fixes the 4-word-head false negative AND the false positive below at once.

False positives (now clean):
- Cross-clause no longer binds a finite continuation regardless of separator:
  "Wire the settings form. Document endpoint props." / "…; document …" /
  "…, document …" are separate actions, not elaborations.

Perf (quadratic → linear):
- The trailing-punctuation peel is a backward char scan instead of an
  unanchored `[…]+$` regex (16k chars: 156 ms → ~1 ms).
- SERVICE_SURFACE_API_RE bounds the service-name length {1,40} so a hostile
  "A-A-…-x" run cannot drive O(n^2) backtracking (16k: 385 ms → ~3 ms).

Consumer fail-open (blocking gate):
- readPhaseScope now distinguishes "no plans" from a plan that EXISTS but is
  unreadable. On a read error the gate BLOCKS ("could not read the phase
  scope …") instead of silently certifying no-integration from partial scope —
  an unreadable plan could be the one describing the integration.

Documented fail-closed tradeoffs, now pinned with tests so they are not
"fixed" back into a fail-open: a clause-initial capitalized common word before
"API" ("Payment API", "Search API") reads as a service name; a long clause
pairs a verb with a distant noun; and a CommonMark inline code span that wraps
a newline is matched within-line only. Codex judged these acceptable because
the COVERAGE.md declaration is a cheap override.

One documented limitation remains out of scope: "Integrate Stripe, and
authenticate requests with its API" (a coordinate finite clause whose noun
refers back by pronoun) needs coreference resolution, beyond a lexical detector.

Verified: 379/379 affected + command-router tests pass (+14 new regression
tests covering every round-3 finding, both directions); tsc + eslint
(no-adhoc-markdown-parsing) + lint:regression-names clean. Built .cjs committed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#2365): simplify to robust core — remove whack-a-mole heuristics (round 5)

Round-4 review confirmed the detector's two most complex features generate
findings in both directions no matter how they are tuned, because they need a
vendor dictionary + coreference the issue rules out in principle. Per the
operator's "ship the robust core" decision, both are removed and their gaps are
documented rather than chased further:

- Cross-clause binding DELETED (allowsCrossClause / participle rule). It caused
  a fail-open on finite continuations ("Integrate Stripe; use its OAuth
  endpoints" — missed) and a false positive on "-ing"-SPELLED nouns ("…, billing
  endpoint terminology…" — wrongly fired). Detection is now same-clause only.
- URL-path-as-evidence REVERTED. Treating every path-bearing URL as an endpoint
  fired on ordinary asset/link URLs ("…/theme.css", "…?next=/x", a docs/repo
  link) and recreated routine UI-phase false positives. An external URL is
  evidence only when it NAMES an API vocabulary word ("api.stripe.com/v1").

Two fail-open cases are now DOCUMENTED limitations, pinned by tests so a future
maintainer does not re-add the heuristics that caused the false positives above:
a service named only in a clause separate from its API noun, and a bare external
host that names no vocabulary word. Both are cheaply covered by the COVERAGE.md
declaration and rare in real phase prose ("integrate the X API").

Also fixed from the round-4 review:
- Qualification now survives markdown emphasis ("The **internal** Payments API"
  stays clean) while still not crossing a sentence/clause boundary.
- readPhaseScope fail-closes on a REAL read failure (EACCES/EIO) enumerating the
  phase directory or reading the roadmap fallback — not only per-plan-file
  failures; a missing directory/section remains a legitimate no-op. The
  declaration-override path surfaces scope_read_error so an incomplete-scope
  override stays visible.
- SERVICE_SURFACE_API_RE length-bound comment no longer overclaims.

Net: the detector is same-clause verb+noun + `<Service> API` surface, with
path/code/inline masking and a fail-closed posture. All five acceptance criteria
hold. 1573/1573 unit tests pass; tsc + eslint (no-adhoc-markdown-parsing) +
lint:regression-names clean. Built .cjs committed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#2365): close roadmap-fallback fail-open + stale JSDoc (round-5 review)

The round-5 sanity review confirmed the detector simplification is sound (all
acceptance positives fire, all required negatives clean) and flagged one real
blocker plus a nit:

- Blocker: readPhaseScope's roadmap fallback could still silently pass an
  UNREADABLE roadmap. getRoadmapPhaseWithFallback gated on fs.existsSync(), which
  returns false on EACCES/EIO too — so an unreadable ROADMAP.md read as "absent",
  no exception reached isRealReadFailure, and the blocking gate certified empty
  scope. Fixed at the source: read the roadmap directly and honor the function's
  OWN documented contract — null only on ENOENT (genuinely absent), otherwise
  throw. Both existing callers already wrap it in try/catch expecting that throw,
  and readPhaseScope now fail-closes (blocks) via its roadmap catch. Verified by
  a new e2e test (unreadable roadmap fallback → block).

- Nit: the detectApiIntegration JSDoc still described the removed cross-clause
  participial binding and "every external hostname counts" — corrected to the
  actual same-clause-only behavior and the names-a-vocab-word URL rule.

Verified: full unit suite green; tsc + eslint + lint:regression-names clean.
Built .cjs committed (roadmap.cjs is gitignored/rebuilt, per repo convention).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#2365): backfill changeset PR number (#2397)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#2365): sync generated capability-registry + recapture install goldens

CI surfaced two generated-artifact staleness issues (all failing test shards +
lint-tests traced to these, not to a logic defect):

- gsd-core/bin/lib/capability-registry.cjs was stale: the initial fix edited the
  ai-integration `api-coverage-plan-pre.md` fragment (added the "No external API
  integration" declaration section) but did not regenerate the registry, which
  embeds an inline copy of that fragment. Regenerated via
  `gen-capability-registry.cjs --write` — the diff is exactly the fragment text
  sync. Fixes `lint:generated-sync` and the "committed registry is in sync" +
  "registry integration" tests.

- The 18 golden-install-parity fixtures were stale by exactly one hash line each
  — `gsd-core/references/api-coverage.md`, which this PR edits and which is a
  hashed installed artifact. Recaptured with `UPDATE_GOLDEN=1`; the diff is that
  single hash per runtime and nothing else. Fixes the `golden parity — *` tests.

No source or behavior change — generated artifacts only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(#2365): flip representative-corpus manifest to assert the fixed behavior

The #2371 representative corpus (merged into next after this branch was cut) is a
known-bug tripwire: it asserts each fixture's currentBuggyOutput so the test
fails loudly the moment #2365 is fixed, at which point — per its own contract in
representative-corpus.test.cjs — the fixer removes currentBuggyOutput so the
assertion checks expectedDetected instead.

This is that moment. Removed currentBuggyOutput from the three detector fixtures
(nextjs-route-path, unrelated-verb-noun, threat-model-prose); the corpus now
asserts detected:false, which the fail-closed same-clause detector satisfies.
Notes updated to describe the fix rather than the bug. The #2366 matrix corpus
is left untouched — that tripwire belongs to its own PR (#2374).

Corpus test: 7/7 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(#2365): skip chmod-000 fail-closed e2e tests on Windows

The three fail-closed gate tests induce an unreadable plan / directory / roadmap
with chmod 000, but Windows does not enforce POSIX mode bits — readFileSync
still succeeds, so the gate never reaches the read-error path and the assertion
fails on the windows-latest CI leg. The fail-closed LOGIC is platform-
independent (readError → block) and is fully exercised on the macOS/Linux legs;
only the method of inducing EACCES is POSIX-specific. Guard the three tests to
skip on win32 as well as root, mirroring golden-install-parity's win32 skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(#2365): address trek-e review — glossary, clock-seam, IO injection, bounds

Review response to PR #2397 (trek-e, CHANGES_REQUESTED). Fix logic unchanged;
this closes the test/process-hygiene findings.

Major:
- CONTEXT.md "Markdown Sectionizer" glossary now lists the two exports this fix
  relies on, `stripInlineCode` and `scanInlineCodeSpans` (glossary is a PR gate).
- Replaced the banned wall-clock assertion in the "hostile repeated-term line"
  test (Clock Seams rule — no elapsed-time asserts) with a deterministic
  signal-count assertion, which also directly verifies the term-dedup that keeps
  pairing linear (one signal for a 10k-pair line, not thousands).
- Rewrote the three fail-closed read-failure tests: instead of chmod 0o000
  (a no-op under root / on Windows, the pattern the repo's IO-failure convention
  avoids) they now exercise the newly-exported `readPhaseScope` in-process and
  inject the failure by monkeypatching fs.readFileSync/readdirSync to throw,
  restoring in finally. Deterministic and platform-independent (no skip needed),
  and they add the ENOENT-is-absence case that the chmod tests couldn't express.

Minor:
- Added limit / limit+1 boundary tests for SERVICE_SURFACE_API_RE's {1,40}
  service-name bound, QUALIFIER_LOOKBACK's 24-char window, and REASON_MAX_LEN
  (200) on the declaration reason.
- Added a fast-check property that fuzzes the tokenizer / clause splitter /
  masking (scanLineTokens, splitClauses, collectTermMatches) with adversarial
  tokens (slashes, backticks, URLs, clause punctuation) and asserts the detector
  is total (never throws), shape-stable, holds detected <=> signals, and is
  deterministic.

readPhaseScope is exported for the in-process tests. Verified: 125 detector +
19 gate tests pass; tsc + eslint + generated-sync (glossary/registry) +
lint-regression-test-names + lint-test-file-count clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 14:59:25 -04:00

861 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Tests for the API-coverage detector + matrix validator (#1562).
*
* Two pure functions under test, both returning typed IR (asserted, never
* text-matched on prose):
* - detectApiIntegration(text) -> { detected, signals, terms }
* - validateCoverageMatrix(text) -> { valid, errors, counts }
*
* Acceptance-criterion mapping:
* #2 (opt-out needs reason; un-enumerated blocks) → validateCoverageMatrix suite
* #4 (non-API phases unaffected, low false-positive) → false-positive suite
* Matrix parse/render bijectivity → fast-check property
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const path = require('node:path');
const fc = require('fast-check');
const MODULE_PATH = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'api-coverage.cjs');
// Shared row-shape generators for the coverage-matrix property tests below
// (the parse/render bijection and the #2371 document-shaped property both
// build matrices from the same canonical row shape — a single declaration
// here means the two properties can't silently desync).
const capabilityGen = fc.stringMatching(/^[a-z][a-z0-9-]{0,14}$/);
const rowGen = fc.record({
capability: capabilityGen,
decision: fc.constantFrom('INTEGRATE', 'OPT-OUT'),
// Reasons are short prose (e.g. "not needed yet"). The matrix is a
// markdown table, so cell text is format-safe: no pipes / newlines.
reason: fc.stringMatching(/^[a-z0-9 ,.\-!?]{0,20}$/),
});
// OPT-OUT rows must carry a non-empty reason for the round-trip to validate.
const validRowGen = rowGen.map((r) =>
r.decision === 'OPT-OUT' && r.reason.trim() === ''
? { ...r, reason: 'because' }
: { ...r, reason: r.reason.trim() }
);
describe('detectApiIntegration — pure detector (#1562)', () => {
let mod;
try {
mod = require(MODULE_PATH);
} catch (err) {
throw new Error(
`Could not require ${MODULE_PATH}. Run "npm run build:lib" first. Underlying: ${err.message}`
);
}
const { detectApiIntegration, DEFAULT_API_COVERAGE_TERMS } = mod;
test('result shape — always carries detected, signals[], terms', () => {
const r = detectApiIntegration('refactor the login function');
assert.strictEqual(r.detected, false);
assert(Array.isArray(r.signals));
assert.strictEqual(r.signals.length, 0);
assert.ok(r.terms && Array.isArray(r.terms.verbs));
assert.ok(Array.isArray(r.terms.nouns));
});
test('non-string input degrades to {detected:false} without throwing', () => {
assert.strictEqual(detectApiIntegration(undefined).detected, false);
assert.strictEqual(detectApiIntegration(null).detected, false);
assert.strictEqual(detectApiIntegration(42).detected, false);
assert.strictEqual(detectApiIntegration({}).detected, false);
});
test('empty / whitespace-only input does not fire', () => {
assert.strictEqual(detectApiIntegration('').detected, false);
assert.strictEqual(detectApiIntegration(' \n\t ').detected, false);
});
test('terms echo is the effective set actually used', () => {
const r = detectApiIntegration('nothing relevant here');
assert.deepStrictEqual(r.terms.verbs, [...DEFAULT_API_COVERAGE_TERMS.verbs]);
assert.deepStrictEqual(r.terms.nouns, [...DEFAULT_API_COVERAGE_TERMS.nouns]);
});
test('terms override — explicit verbs+nouns replace defaults', () => {
const r = detectApiIntegration('xyzzy the frobninator', { verbs: ['xyzzy'], nouns: ['frobninator'] });
assert.deepStrictEqual(r.terms.verbs, ['xyzzy']);
assert.deepStrictEqual(r.terms.nouns, ['frobninator']);
assert.strictEqual(r.detected, true);
});
// ── Positive: compound verb+noun (acceptance #1 trigger) ─────────────────
for (const scope of [
'Integrate the Stripe API for payment processing',
'Wrap the GitHub GraphQL API for issue triage',
'Connect to the SendGrid REST endpoint for transactional email',
'Consume the billing service over gRPC',
'Wire up the Slack webhook for deploy notifications',
'Onboard the Twilio SDK for SMS',
'integrate oauth for login',
]) {
test(`POSITIVE fires on: "${scope}"`, () => {
const r = detectApiIntegration(scope);
assert.strictEqual(r.detected, true, `expected detection for: ${scope}`);
assert.ok(r.signals.length > 0);
assert.ok(r.signals.every((s) => s.snippet.length > 0));
});
}
// ── Positive: explicit <Service> API|SDK surface (no verb needed) ────────
for (const scope of ['Add a Spotify API client', 'Ship the Notion SDK helper']) {
test(`POSITIVE (surface) fires on: "${scope}"`, () => {
const r = detectApiIntegration(scope);
assert.strictEqual(r.detected, true);
assert.ok(r.signals.some((s) => s.verb === '(surface)'));
});
}
// ── Negative: false-positive guards (acceptance #4 — the crux) ───────────
// Each of these is a phase that is NOT an external-API integration. The
// detector must stay silent. The "public API of UserController" case is the
// canonical FP trap: "api" is present but there is no integration verb.
for (const [label, scope] of [
['internal API mention', 'The public API of the UserController should accept pagination params'],
['refactor', 'Refactor the authentication module to use bcrypt'],
['feature toggle', 'Add a dark mode toggle to the settings page'],
['bug fix', 'Fix the off-by-one error in the pagination helper'],
['docs', 'Update the README to document the config options'],
['internal client code', 'Add a client-side helper to debounce input'],
['bare noun no verb', 'We expose a REST-ish JSON shape already'],
['bare verb no noun', 'We will integrate the new design system tokens'],
]) {
test(`NEGATIVE does not fire (${label}): "${scope}"`, () => {
const r = detectApiIntegration(scope);
assert.strictEqual(r.detected, false, `unexpected detection for [${label}]: ${scope}`);
});
}
test('fenced code blocks are stripped — trigger inside a code fence does not fire', () => {
const scope = [
'Refactor the helpers.',
'',
'```bash',
'# integrate the Stripe API (example command in docs)',
'```',
'',
'No integration in this phase.',
].join('\n');
assert.strictEqual(detectApiIntegration(scope).detected, false);
});
// ── H1 fix (#1562 code review): capitalized sentence starters must not fire
// the <Service> API/SDK surface rule. These are common English, not services.
for (const [label, scope] of [
['The API', 'The API documentation needs updating.'],
['An SDK', 'An SDK is already present in the repo.'],
['Our REST', 'Our REST endpoints return JSON.'],
['This GraphQL', 'This GraphQL schema is internal.'],
['New API', 'New API surface was added by the refactor.'],
]) {
test(`NEGATIVE (stopword) does not fire (${label}): "${scope}"`, () => {
assert.strictEqual(detectApiIntegration(scope).detected, false);
});
}
test('compound signal fires when verb and noun are on the same line only', () => {
const sameLine = 'Phase A integrates things.\nLater we mention an api.';
const splitLine = 'Phase A integrates things.\nLater we mention an api here too.';
assert.strictEqual(detectApiIntegration(sameLine).detected, false);
assert.strictEqual(detectApiIntegration(splitLine).detected, false);
assert.strictEqual(detectApiIntegration('integrates the api').detected, true);
});
});
// ──────────────────────────────────────────────────────────────────────────────
// #2365 — detector false positives (first-party paths, unrelated same-line
// clauses, descriptive "API" prose) + the no-integration declaration.
// ──────────────────────────────────────────────────────────────────────────────
describe('#2365 detector false positives + no-integration declaration', () => {
let mod;
try {
mod = require(MODULE_PATH);
} catch (err) {
throw new Error(`Could not require ${MODULE_PATH}. Run "npm run build:lib". Underlying: ${err.message}`);
}
const { detectApiIntegration, parseCoverageMatrix, validateCoverageMatrix } = mod;
// ── acceptance #1: first-party framework route paths are not integration prose
for (const [label, scope] of [
['Next.js route file in prose', 'Run integration tests for src/app/api/profile/route.test.ts'],
['route handler path with verb', 'Wire the src/app/api/profile/route.ts handler into the settings page'],
['inline-code span', 'Verify the `api` helper wiring end to end'],
]) {
test(`NEGATIVE path/inline-code (${label}): "${scope}"`, () => {
const r = detectApiIntegration(scope);
assert.strictEqual(r.detected, false, `unexpected detection for [${label}]: ${scope}`);
});
}
// ── acceptance #2: verb + noun in unrelated clauses of one line
test('NEGATIVE unrelated clauses: verb and noun in different clauses do not compound', () => {
const r = detectApiIntegration(
'Render the page and prove label endpoint, filename, and CSV/XLSX wiring.'
);
assert.strictEqual(r.detected, false);
});
// ── acceptance #3: descriptive/local "API" prose (threat-model shape).
// NOTE: the detector is FAIL-CLOSED — the classes below stay clean because
// they are unambiguously NOT external integration (no integration verb + a
// named service, or a first-party-qualified surface). Prose that pairs an
// integration VERB with an API noun ("wire … the internal endpoint") is a
// fail-closed POSITIVE now (see the "#2365 review — fail-open fixes" group);
// a one-line COVERAGE.md declaration dismisses it if it is a false alarm.
for (const [label, scope] of [
['threat-model table cell', '| Tampering | Resolver-only API rejects arbitrary caller URLs. |'],
['compound-modifier mid-sentence', 'The Resolver-only API rejects arbitrary caller URLs.'],
['clause-initial capitalized prose', 'Internal API surface stays unchanged in this phase.'],
['localhost URL', 'Run integration tests against https://localhost:3000/api/profile'],
['bare external domain, no path', 'Integrate the design tokens from https://example.com into the theme'],
['internal-qualified service (no verb)', 'The internal Payments API remains unchanged.'],
['descriptor service + unrelated URL', 'Internal API surface stays unchanged; see https://example.com/style-guide.'],
['Windows path', 'Wire tests for src\\app\\api\\profile\\route.ts.'],
['loopback shorthand URL', 'Connect tests to http://127.1:3000/api/profile.'],
['protocol-only surface', 'Document the REST API behavior for maintainers.'],
['protocol-only surface (GraphQL)', 'Review the GraphQL API schema naming conventions.'],
['cross-clause coordinate action', 'Wire the header, then update the endpoint docs'],
]) {
test(`NEGATIVE descriptive API prose (${label}): "${scope}"`, () => {
const r = detectApiIntegration(scope);
assert.strictEqual(r.detected, false, `unexpected detection for [${label}]: ${scope}`);
});
}
// ── acceptance #4: true positives preserved (the fail-open guard — a fix that
// silences these is strictly worse than the false positives it removes).
for (const [label, scope] of [
['canonical compound', 'integrate the Stripe API'],
['compound with trailing prose', 'Integrate the Stripe API for payment processing'],
['surface rule, no verb', 'Add a Spotify API client'],
['widest default-suite word gap', 'Consume the billing service over gRPC'],
['clause-initial service + URL corroboration', 'Stripe API — docs at https://stripe.com/docs/api'],
['webhook compound', 'Wire up the Slack webhook for deploy notifications'],
['verb + API-naming URL', 'Connect the app to https://api.stripe.com/v1 for charges'],
['slashed noun shorthand', 'Integrate the Stripe API/SDK for payments'],
['long single-clause gap', "Connect our checkout to Stripe's hosted payment processing service through its v1 endpoints."],
['non-http URI scheme', 'Connect the realtime client to wss://api.openai.com/v1/realtime.'],
['versioned noun shorthand', 'Integrate Stripe API/v2 for legacy payments.'],
['clause-initial service + object follower', 'Stripe API client for payments.'],
['inline-code package corroboration', 'Stripe SDK client via `@stripe/stripe-js` for payment intents.'],
['inline-code package as only noun', 'Integrate `stripe-sdk` for payment intents.'],
['later surface after rejected first candidate', 'Internal API facade around Stripe SDK payment flows.'],
['later surface after rejected modifier', 'Resolver-only API facade delegates to Stripe SDK for payments.'],
]) {
test(`POSITIVE still fires (${label}): "${scope}"`, () => {
const r = detectApiIntegration(scope);
assert.strictEqual(r.detected, true, `true-positive regression [${label}]: ${scope}`);
});
}
// ── #2365 review — fail-open fixes. Codex's second-round review found the
// round-2 tightening had over-corrected into FAIL-OPEN false negatives:
// realistic external-API prose that a BLOCKING gate silently let through.
// Under the fail-closed decision these MUST detect. This is the guard the
// handoff flagged in bold — a fix that lets these slip is strictly worse
// than the false positives it removes.
for (const [label, scope] of [
['clause-initial service, plain follower (F1)', 'Stripe API for payment processing.'],
['external host that names an API vocab word (F2)', 'Connect the client to api.stripe.com/v1 for charges.'],
["vendor's first-party SDK (F3)", "Integrate Shopify's first-party SDK for checkout."],
['long single integration clause (F4)', "Integrate Stripe's hosted payment processing service into checkout using the vendor-recommended asynchronous flow for recurring subscriptions and one-time card payments through its API."],
// Fail-closed reversal of the round-2 "internal" negatives: an integration
// verb bound to an API noun detects even when the noun is "internal"-qualified
// (Codex: "internal" can name the vendor's own API). Dismissed by declaration.
['integration verb + internal noun', 'Wire the settings form to the internal endpoint.'],
['coordinated integration verb + internal noun', 'Wire the form and document the internal API.'],
['distant same-clause verb+noun', 'Wiring the settings drawer means the profile page the sidebar and the account menu all reach the same internal endpoint'],
// Qualification must NOT leak across a sentence/clause boundary.
['qualifier does not leak across a sentence', 'The cache is private. Stripe API client for payments.'],
['qualifier does not leak across a semicolon', 'Keep the cache private; Stripe API client for payments.'],
]) {
test(`POSITIVE fail-open guard (${label}): "${scope}"`, () => {
const r = detectApiIntegration(scope);
assert.strictEqual(r.detected, true, `fail-open regression [${label}]: ${scope}`);
});
}
// ── #2365 review — false-positive fixes. The reviews found false positives
// from over-broad heuristics; these MUST stay clean.
for (const [label, scope] of [
['bare external domain, no path (F6)', 'Integrate the design tokens from https://example.com, document the endpoint terminology.'],
['internal UI component, separate action (F7)', 'Wire the SettingsForm, then document the endpoint props.'],
['protocol name as service (F8)', 'Document the REST API behavior for maintainers.'],
['finite continuation after a period', 'Wire the settings form. Document endpoint props.'],
['finite continuation after a semicolon', 'Wire the settings form; document endpoint props.'],
['finite continuation after a comma', 'Wire the form, document endpoint props.'],
// Round-4 review: an external asset/link URL is NOT an API endpoint.
['external stylesheet asset URL', 'Wire stylesheet from https://cdn.example.com/assets/theme.css into the page.'],
['external URL with a query string', 'Wire the login link to https://example.com?next=/dashboard.'],
['external docs/repo link, not an API', 'Wire the docs link to https://github.com/org/repo into the footer'],
// Round-4 review: an "-ing"-SPELLED noun ("billing") is not a participle.
['-ing-spelled noun in an unrelated clause', 'Wire the new settings form component, billing endpoint terminology remains unchanged.'],
// Round-4 review: qualification survives markdown emphasis.
['descriptor qualifies through markdown emphasis', 'The **internal** Payments API remains unchanged.'],
]) {
test(`NEGATIVE fail-closed FP guard (${label}): "${scope}"`, () => {
const r = detectApiIntegration(scope);
assert.strictEqual(r.detected, false, `new false positive [${label}]: ${scope}`);
});
}
// ── #2365 — DOCUMENTED fail-open LIMITATIONS. Detection is same-clause only
// (no cross-clause binding) and an external host is evidence only when it
// NAMES an API vocabulary word. Catching the cases below robustly needs a
// vendor dictionary + coreference, which the issue rules out in principle;
// every lexical rule tried across four review rounds traded a false
// negative for a false positive. These are cheaply covered by the
// COVERAGE.md declaration and rare in real phase prose. The tests pin the
// behavior as INTENTIONAL — a future maintainer re-adding a cross-clause or
// every-URL heuristic would reintroduce the false positives above.
for (const [label, scope] of [
['service named only in a following participial clause', 'Integrate Stripe, exposing its endpoints for payment capture.'],
['service named only in a following finite clause', 'Integrate Stripe; use its OAuth endpoints for checkout.'],
['bare external host naming no vocab word', 'Connect the client to graph.microsoft.com:443/v1.0/me.'],
]) {
test(`DOCUMENTED fail-open limitation stays clean (${label}): "${scope}"`, () => {
const r = detectApiIntegration(scope);
assert.strictEqual(r.detected, false, `limitation changed [${label}]: ${scope}`);
});
}
// ── #2365 review — DOCUMENTED fail-closed tradeoffs. A clause-initial
// capitalized common word before "API" ("Payment API", "Search API") is
// treated as a service name, and a long clause pairs a verb with a distant
// noun. Codex judged these acceptable because the COVERAGE.md declaration
// is a cheap override; these tests exist so the behavior is INTENTIONAL and
// a future maintainer does not "fix" it back into a fail-open cap.
for (const [label, scope] of [
['capitalized common word as service', 'Payment API remains unchanged in this refactor.'],
['capitalized common word as service (Search)', 'Search API types are generated locally.'],
['long clause pairs verb with distant noun', 'Wire the settings form to validation state so the designer can review field behavior and document every public API and endpoint symbol without changing runtime dependencies.'],
]) {
test(`POSITIVE documented fail-closed tradeoff (${label}): "${scope}"`, () => {
const r = detectApiIntegration(scope);
assert.strictEqual(r.detected, true, `expected documented fail-closed detection [${label}]: ${scope}`);
});
}
// ── #2365 review finding 7: inline code spans are matched WITHIN a line by
// design (phase scope prose is line-oriented). A CommonMark code span that
// wraps a newline is NOT recognized, so its contents are treated as prose —
// a documented, narrow limitation (fail-closed: a stray detection is
// dismissed by the declaration). This test pins the current behavior.
test('multi-line inline code span is not treated as code (documented limitation)', () => {
const r = detectApiIntegration('Documentation example: `integrate\nStripe API` only.');
assert.strictEqual(r.detected, true);
});
// ── acceptance #5: a legitimate, non-fabricated "no external API" declaration
test('declaration-only COVERAGE.md is VALID with zero rows (none_declared)', () => {
const md = '# API Coverage\n\nNo external API integration: UI-only phase, no third-party surface.\n';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, true, `expected valid, errors: ${v.errors.join('; ')}`);
assert.strictEqual(v.none_declared, true);
assert.deepStrictEqual(v.counts, { surface: 0, integrate: 0, optout: 0 });
});
test('declaration accepts the bold/em-dash form', () => {
const md = '**No external API integration** — resolver work is local-only.\n';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, true);
assert.strictEqual(v.none_declared, true);
});
test('declaration WITHOUT a reason is not recognized (reasoned opt-out, like OPT-OUT rows)', () => {
const v = validateCoverageMatrix('No external API integration\n');
assert.strictEqual(v.valid, false);
assert.notStrictEqual(v.none_declared, true);
});
test('declaration PLUS coverage rows is contradictory → invalid', () => {
const md = [
'No external API integration: nothing external here.',
'',
'| capability | decision | reason |',
'|---|---|---|',
'| search | INTEGRATE | |',
].join('\n');
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /declar/i.test(e)), `errors: ${v.errors.join('; ')}`);
});
test('declaration inside a fenced code block is NOT recognized', () => {
const md = '```markdown\nNo external API integration: example only.\n```\n';
const p = parseCoverageMatrix(md);
assert.notStrictEqual(p.declaration && p.declaration.none, true);
const v = validateCoverageMatrix(md);
assert.notStrictEqual(v.none_declared, true);
});
test('declaration inside an HTML comment is NOT recognized', () => {
const md = '<!--\nNo external API integration: quoted example only.\n-->\n';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
assert.notStrictEqual(v.none_declared, true);
});
test('blockquoted declaration is NOT recognized (quoted text is not a decision)', () => {
const v = validateCoverageMatrix('> No external API integration: copied from the old PLAN.md.\n');
assert.strictEqual(v.valid, false);
assert.notStrictEqual(v.none_declared, true);
});
// ── A hostile line repeating one verb+noun pair thousands of times must
// collapse to a SINGLE signal — pairing is by distinct term, not a
// match×match cross product. Asserting the signal count is a deterministic
// proxy for that linearity (no wall-clock timing — Clock Seams rule).
test('hostile repeated-term line dedups to one signal', () => {
const s = 'integrate api '.repeat(10000); // 140 KB single line, 10k pairs
const r = detectApiIntegration(s);
assert.strictEqual(r.detected, true);
assert.strictEqual(
r.signals.length,
1,
`repeated verb+noun pair must dedup to one signal, got ${r.signals.length}`
);
});
});
// ──────────────────────────────────────────────────────────────────────────────
// #2365 review — hardening-constant boundaries + parser fuzz property
// ──────────────────────────────────────────────────────────────────────────────
describe('#2365 hardening-constant boundaries + parser property', () => {
const { detectApiIntegration, validateCoverageMatrix } = require(MODULE_PATH);
// SERVICE_SURFACE_API_RE bounds the service token to `[A-Z][A-Za-z0-9_-]{1,40}`
// (2..41 chars) so a hostile hyphen run cannot drive O(n^2) backtracking.
test('surface service name at the 41-char limit still fires', () => {
const svc = 'S' + 'a'.repeat(40); // exactly 41 chars
assert.strictEqual(detectApiIntegration(`${svc} API`).detected, true);
});
test('surface service name at 42 chars is past the length bound (surface path)', () => {
const svc = 'S' + 'a'.repeat(41); // 42 chars, no integration verb → surface-only
assert.strictEqual(detectApiIntegration(`${svc} API`).detected, false);
});
// QUALIFIER_LOOKBACK (24): a first-party descriptor only suppresses the surface
// within the bounded lookback window. Pin the EXACT constant: 8-char "internal"
// + 16 spaces places its start at offset-24 (the window edge) → still qualifies;
// + 17 spaces pushes its start one char outside → truncated → no longer
// qualifies. Both would pass for any lookback in ~9..37, so use the exact pair.
test('internal qualifier exactly at the 24-char window edge still suppresses the surface', () => {
const atEdge = 'internal' + ' '.repeat(16) + 'Payments API'; // start at offset-24
assert.strictEqual(detectApiIntegration(atEdge).detected, false);
});
test('internal qualifier one char past the 24-char window no longer qualifies (fires)', () => {
const pastEdge = 'internal' + ' '.repeat(17) + 'Payments API'; // start at offset-25
assert.strictEqual(detectApiIntegration(pastEdge).detected, true);
});
// REASON_MAX_LEN (200): the no-integration declaration reason is length-bounded.
test('declaration reason at 200 chars is valid; 201 is rejected', () => {
const at = 'No external API integration: ' + 'x'.repeat(200) + '\n';
const over = 'No external API integration: ' + 'x'.repeat(201) + '\n';
assert.strictEqual(validateCoverageMatrix(at).valid, true);
const v = validateCoverageMatrix(over);
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /exceeds 200 chars/.test(e)), `errors: ${v.errors.join('; ')}`);
});
// Fuzz the tokenizer / clause splitter / masking (scanLineTokens, splitClauses,
// collectTermMatches) with adversarial tokens — slashes, backticks, URLs,
// clause punctuation. The detector must never throw, keep its typed shape, hold
// `detected ⇔ signals.length > 0`, and be deterministic on any input.
test('property: parser is total, shape-stable, and deterministic on arbitrary prose', () => {
const token = fc.oneof(
fc.constantFrom(
'integrate', 'connect', 'wire', 'the', 'Stripe', 'API', 'SDK', 'endpoint',
'api', 'internal', 'Resolver-only', '/', '//', '`', 'https://api.x.com/v1',
'src/app/api/x.ts', 'graph.microsoft.com/v1'
),
fc.stringMatching(/^[A-Za-z0-9/.:`_-]{0,12}$/)
);
fc.assert(
fc.property(
fc.array(token, { maxLength: 40 }),
fc.constantFrom(' ', ', ', '. ', '; ', ' | ', '\n'),
(words, sep) => {
const line = words.join(sep);
const r = detectApiIntegration(line);
assert.ok(typeof r.detected === 'boolean' && Array.isArray(r.signals), 'typed shape');
assert.strictEqual(r.detected, r.signals.length > 0, 'detected ⇔ signals present');
assert.deepStrictEqual(detectApiIntegration(line), r, 'deterministic');
return true;
}
),
{ numRuns: 300 }
);
});
});
// ──────────────────────────────────────────────────────────────────────────────
// Matrix parse / validate / render
// ──────────────────────────────────────────────────────────────────────────────
describe('coverage matrix — parse / validate (#1562 acceptance #2)', () => {
let mod;
try {
mod = require(MODULE_PATH);
} catch (err) {
throw new Error(`Could not require ${MODULE_PATH}. Run "npm run build:lib". Underlying: ${err.message}`);
}
const { parseCoverageMatrix, validateCoverageMatrix, renderCoverageMatrix } = mod;
test('parse markdown table — header + 2 rows', () => {
const md = [
'| capability | decision | reason |',
'|---|---|---|',
'| search | INTEGRATE | |',
'| playlists | OPT-OUT | not needed yet |',
].join('\n');
const p = parseCoverageMatrix(md);
assert.strictEqual(p.format, 'table');
assert.strictEqual(p.rows.length, 2);
assert.strictEqual(p.rows[0].capability, 'search');
assert.strictEqual(p.rows[0].decision, 'INTEGRATE');
assert.strictEqual(p.rows[1].decision, 'OPT-OUT');
assert.strictEqual(p.rows[1].reason, 'not needed yet');
assert.strictEqual(p.errors.length, 0);
});
test('parse fenced ```coverage JSON block', () => {
const md = [
'Some prose.',
'',
'```coverage',
'[{"capability":"search","decision":"INTEGRATE","reason":""},',
' {"capability":"skip","decision":"OPT-OUT","reason":"out of scope"}]',
'```',
].join('\n');
const p = parseCoverageMatrix(md);
assert.strictEqual(p.format, 'json');
assert.strictEqual(p.rows.length, 2);
assert.strictEqual(p.errors.length, 0);
});
test('parse empty / non-matrix input → format none, no rows, no errors', () => {
const p = parseCoverageMatrix('# Notes\n\nNothing here.');
assert.strictEqual(p.format, 'none');
assert.strictEqual(p.rows.length, 0);
assert.strictEqual(p.errors.length, 0);
});
test('parse non-string → empty result, no throw', () => {
const p = parseCoverageMatrix(undefined);
assert.strictEqual(p.rows.length, 0);
});
test('parse rejects malformed fenced JSON with an error', () => {
const md = '```coverage\n{not json}\n```';
const p = parseCoverageMatrix(md);
assert.strictEqual(p.format, 'json');
assert.ok(p.errors.length > 0);
assert.strictEqual(p.rows.length, 0);
});
// ── validate: boundaries 0 / 1 / 2 rows (limit-1, limit, limit+1) ────────
test('validate — empty matrix is invalid (acceptance #1: surface must be enumerated)', () => {
const v = validateCoverageMatrix('| capability | decision | reason |\n|---|---|---|');
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /empty/i.test(e)));
assert.strictEqual(v.counts.surface, 0);
});
test('validate — single INTEGRATE row is valid (limit)', () => {
const md = '| capability | decision | reason |\n|---|---|---|\n| search | INTEGRATE | |';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, true);
assert.strictEqual(v.counts.surface, 1);
assert.strictEqual(v.counts.integrate, 1);
assert.strictEqual(v.counts.optout, 0);
});
test('validate — two rows valid (limit+1)', () => {
const md = [
'| capability | decision | reason |',
'|---|---|---|',
'| search | INTEGRATE | |',
'| playlists | OPT-OUT | not needed |',
].join('\n');
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, true);
assert.strictEqual(v.counts.surface, 2);
assert.strictEqual(v.counts.optout, 1);
});
// ── acceptance #2: every OPT-OUT must carry a reason ─────────────────────
test('validate — OPT-OUT without reason is INVALID (acceptance #2)', () => {
const md = '| capability | decision | reason |\n|---|---|---|\n| skip | OPT-OUT | |';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /missing reason/i.test(e)));
});
test('validate — OPT-OUT with one-char reason is valid (boundary)', () => {
const md = '| capability | decision | reason |\n|---|---|---|\n| skip | OPT-OUT | x |';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, true);
});
test('validate — duplicate capability is invalid (matrix is a set of decisions)', () => {
const md = [
'| capability | decision | reason |',
'|---|---|---|',
'| search | INTEGRATE | |',
'| Search | OPT-OUT | dup |',
].join('\n');
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /duplicate/i.test(e)));
});
test('validate — empty capability name is invalid', () => {
const md = '| capability | decision | reason |\n|---|---|---|\n| | INTEGRATE | |';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /empty capability/i.test(e)));
});
// ── review-fix coverage: parser robustness (#1562 code review M1/M2/L1/L2) ──
test('validate — invalid decision cell in a table row is an error, not silently dropped (M1)', () => {
const md = '| capability | decision | reason |\n|---|---|---|\n| x | INTEGRAT | |';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /not in \{INTEGRATE, OPT-OUT\}/i.test(e)));
});
test('validate — a pipe in a cell adds extra columns → invalid (M2)', () => {
const md = '| capability | decision | reason |\n|---|---|---|\n| x | OPT-OUT | a|b |';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /columns|pipe/i.test(e)));
});
test('validate — a pipe in a JSON-fence reason → invalid (M2)', () => {
const md = '```coverage\n[{"capability":"x","decision":"OPT-OUT","reason":"a|b"}]\n```';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
});
test('validate — capability over the length cap → invalid (S3 bound)', () => {
const longCap = 'x'.repeat(81);
const md = `| capability | decision | reason |\n|---|---|---|\n| ${longCap} | INTEGRATE | |`;
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /exceeds/i.test(e)));
});
test('parse — case-insensitive ```Coverage fence is accepted (L1)', () => {
const md = '```Coverage\n[{"capability":"a","decision":"INTEGRATE","reason":""}]\n```';
const p = parseCoverageMatrix(md);
assert.strictEqual(p.format, 'json');
assert.strictEqual(p.rows.length, 1);
});
test('parse — a single-dash cell is not mistaken for a separator (L2)', () => {
const md = '| capability | decision | reason |\n|---|---|---|\n| - | INTEGRATE | |';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.counts.surface, 1);
assert.ok(v.valid, 'a capability named "-" is legal');
});
// ── render round-trip (manual) ───────────────────────────────────────────
test('render → parse round-trips a valid matrix', () => {
const rows = [
{ capability: 'search', decision: 'INTEGRATE', reason: '' },
{ capability: 'playlists', decision: 'OPT-OUT', reason: 'not needed yet' },
];
const rendered = renderCoverageMatrix(rows);
const v = validateCoverageMatrix(rendered);
assert.strictEqual(v.valid, true);
assert.strictEqual(v.counts.surface, 2);
});
});
// ──────────────────────────────────────────────────────────────────────────────
// Property test: parse/render bijectivity (RULESET.TESTS property-based)
// ──────────────────────────────────────────────────────────────────────────────
describe('coverage matrix — parse/render bijection (fast-check)', () => {
let mod;
try {
mod = require(MODULE_PATH);
} catch (err) {
throw new Error(`Could not require ${MODULE_PATH}. Run "npm run build:lib". Underlying: ${err.message}`);
}
const { renderCoverageMatrix, validateCoverageMatrix } = mod;
test('any valid row set renders and re-validates to the same counts', () => {
const matrixGen = fc.uniqueArray(validRowGen, {
minLength: 1,
maxLength: 8,
selector: (r) => r.capability.toLowerCase(),
});
fc.assert(
fc.property(matrixGen, (rows) => {
const rendered = renderCoverageMatrix(rows);
const v = validateCoverageMatrix(rendered);
// Injected reason guarantees validity; any invalid result is a parser bug.
assert.strictEqual(v.valid, true);
assert.strictEqual(v.counts.surface, rows.length);
return true;
}),
{ numRuns: 100 }
);
});
});
// ──────────────────────────────────────────────────────────────────────────────
// Document-shaped property (#2371): the bijection test above generates ROWS and
// renders them through the writer, so the document shape is a constant — it
// cannot generate a second table, a decoy table, or surrounding prose, and so
// cannot fail against #2366's bugs. This property generates the DOCUMENT
// space instead: a canonical matrix interleaved with content a real
// COVERAGE.md may legitimately contain that is NOT the matrix. See
// tests/fixtures/representative/README.md and CONTRIBUTING.md's "Fixture
// provenance" section for the full rationale.
// ──────────────────────────────────────────────────────────────────────────────
describe('coverage matrix — document-shaped fast-check (extract-exactly-canonical, #2371)', () => {
let mod;
try {
mod = require(MODULE_PATH);
} catch (err) {
throw new Error(`Could not require ${MODULE_PATH}. Run "npm run build:lib" first. Underlying: ${err.message}`);
}
const { parseCoverageMatrix, renderCoverageMatrix } = mod;
// Uses the shared capabilityGen/rowGen/validRowGen declared at module scope
// above (same generators the bijection test uses), so the two properties
// exercise the same canonical-row space and can't silently desync.
const canonicalMatrixGen = fc.uniqueArray(validRowGen, {
minLength: 1,
maxLength: 5,
selector: (r) => r.capability.toLowerCase(),
});
// Decoy blocks: content a document may legitimately contain that is NOT the
// canonical matrix. Kept to three explicit, independently-readable shapes
// rather than a generic "random markdown" generator — a combinatorial but
// opaque generator is exactly the kind of cleverness that's unrunnable to
// debug when it fails (Kernighan's Law).
const proseDecoyGen = fc.constantFrom(
'## Notes\n\nSee the ADR for background.',
'This phase also touches the auth helper.',
'## Risks\n\n- Rollout risk is low.',
);
const summaryTableDecoyGen = fc
.record({
label: fc.stringMatching(/^[a-z][a-z0-9 ]{0,10}$/),
integrateCount: fc.nat({ max: 50 }),
optoutCount: fc.nat({ max: 50 }),
})
.map(
({ label, integrateCount, optoutCount }) =>
`## Coverage summary\n\n| tier | INTEGRATE | OPT-OUT |\n|---|---|---|\n` +
`| ${label} | ${integrateCount} | ${optoutCount} |`
);
const secondSectionMatrixGen = fc
.uniqueArray(validRowGen, { minLength: 1, maxLength: 3, selector: (r) => r.capability.toLowerCase() })
.map((rows) => `## Transferred to a later phase\n\n${renderCoverageMatrix(rows)}`);
const decoyGen = fc.oneof(proseDecoyGen, summaryTableDecoyGen, secondSectionMatrixGen);
const documentGen = fc.record({
canonicalRows: canonicalMatrixGen,
decoysBefore: fc.array(decoyGen, { maxLength: 2 }),
decoysAfter: fc.array(decoyGen, { maxLength: 2 }),
});
// #2371's own test-runner (gsd-test / gsd-test-runner v1.6.2) has no concept
// of node:test's `todo` option: its JSONL result parser
// (internal/pipeline/parse.go's parseJSONL, gsd-test-runner repo) only
// recognizes `kind: "pass" | "fail"` and hard-errors on anything else, so a
// `{ todo: true }` test whose body throws is still counted as a failure in
// the tool's own verdict — verified directly against that source, not
// assumed. So this property uses fc's non-throwing `fc.check` (returns
// `RunDetails` instead of throwing — see fast-check's runners docs) and
// asserts on `.failed` directly: today the invariant genuinely does NOT
// hold (that is #2366), so `report.failed === true` is an honest,
// non-vacuous, currently-PASSING characterization of today's known-broken
// reality — not a fake pass. The moment #2366 makes the invariant hold for
// real, `report.failed` becomes `false` and THIS assertion fails loudly,
// forcing whoever's fix landed to notice and flip it. The fix itself stays
// owned by #2366.
test(
'given a document containing exactly one canonical matrix plus arbitrary other content, ' +
'the parser extracts exactly that matrix\'s rows and ignores everything else ' +
'(currently violated — #2366)',
() => {
const report = fc.check(
fc.property(documentGen, ({ canonicalRows, decoysBefore, decoysAfter }) => {
const canonicalBlock = renderCoverageMatrix(canonicalRows);
const doc = [...decoysBefore, canonicalBlock, ...decoysAfter].join('\n\n');
const result = parseCoverageMatrix(doc);
const expectedByCap = new Map(canonicalRows.map((r) => [r.capability.toLowerCase(), r]));
const actualByCap = new Map(result.rows.map((r) => [r.capability.toLowerCase(), r]));
if (actualByCap.size !== expectedByCap.size) return false;
for (const [cap, expected] of expectedByCap) {
const actual = actualByCap.get(cap);
if (!actual || actual.decision !== expected.decision) return false;
}
return result.errors.length === 0;
}),
{ numRuns: 100 }
);
assert.strictEqual(
report.failed,
true,
'This property is expected to be VIOLATED today (#2366 — a decoy summary table or a ' +
'second canonical-schema section corrupts the result or spuriously errors). If this ' +
'assertion fails, the property now HOLDS — #2366 appears fixed; replace this ' +
'characterization with a real fc.assert of the invariant.'
);
}
);
});
// ──────────────────────────────────────────────────────────────────────────────
// CLI entry point (STDIN → exit codes mirror grep, like assumption-delta)
// ──────────────────────────────────────────────────────────────────────────────
describe('api-coverage CLI — STDIN + exit codes', () => {
const CLI = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'api-coverage.cjs');
function runCli(stdin) {
const r = spawnSync(process.execPath, [CLI, '--json'], {
input: stdin,
encoding: 'utf-8',
timeout: 15000,
});
return { exitCode: r.status, stdout: r.stdout, stderr: r.stderr };
}
test('exit 0 + JSON IR when integration detected', () => {
const r = runCli('Integrate the Stripe API for payments');
assert.strictEqual(r.exitCode, 0);
const body = JSON.parse(r.stdout);
assert.strictEqual(body.detected, true);
assert.ok(Array.isArray(body.signals));
});
test('exit 1 when no integration', () => {
const r = runCli('Refactor the login helper');
assert.strictEqual(r.exitCode, 1);
});
});