Files
fusion/scripts/__tests__/check-plugin-interop-drift.test.mjs
gsxdsm ccf562f178 gate: compare mirrored INTERFACES too, and delete the dead prop that found (#3034)
> **Re-landing the second half of #3031.** That PR merged into #3029's
branch and only its first commit reached `main` — the arity rule
shipped, the interface rule and its finding did not. Verified on `main`:
the gate reports *"7 mirrored function(s)"* with no interface count, and
the dead prop below is still there.

## What

The arity rule covers exported functions. The same files also mirror
**interfaces**, which is the larger surface — six copies of
`PluginDashboardViewContext` alone.

**One direction only.** A mirror may declare *fewer* properties, and all
six do (6, 8, 7, 7, 3, 6 against the real nine) because a plugin mirrors
the fields it uses. Demanding equality would fail every plugin for not
using everything — which is how a check gets ignored and then deleted. A
property the real type **doesn't have** is the drift that matters: a
rename nobody propagated, where the plugin keeps compiling and reads a
field the host never sends.

## Its first interface run found a live one

```
dashboard-interop.d.ts:67  TaskCardProps.workflowStepNameLookup is not a property of the real TaskCardProps
```

Git history says it **was** one when FN-2466 and FN-7039 added this
threading. The dashboard removed it later; nothing propagated that to
the plugin's hand-written declaration. So the plugin built a lookup map
from `context.workflowSteps` on every render, threaded it through two
components, and handed it to a `TaskCard` with no such prop.

Deleted rather than exempted — a new gate shouldn't ship with a waiver
for its own first finding. Behaviour-preserving: the value never reached
anything.

## Measured on `main`

| check | result |
|---|---|
| population | **7 functions + 10 interfaces across 6 plugins**, all
matching after the deletion |
| control probe | phantom property **caught**; clean tree exits 0 |
| anti-vacuity | now also requires a non-zero *interface* comparison |
| gate's own suite | **5 → 8** |
| dependency-graph suite | 179 green; `tsc` clean |
| other five gates · census | green |

## Running total for this check

Three real drifts, none of which any other instrument reported:

1. `isTaskStuck` stuck at three parameters through the whole lane
conversion (#3003)
2. `taskStuckTimeoutMs?: number` vs the required `number | undefined` —
in **two independent authors'** declarations
3. `workflowStepNameLookup` outliving its removal from `TaskCard`

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 01:43:58 -07:00

69 lines
3.1 KiB
JavaScript

/*
FNXC:PluginInteropDrift 2026-07-31-07:35:
THE NON-FUNCTION EXPORT RULE IS WHAT KEEPS THIS CHECK CREDIBLE.
Its first run reported `TaskCard` as a function the dashboard no longer exports. It exports it as
`export const TaskCard = memo(TaskCardComponent, ...)` — present, but with an arity that belongs to a
wrapped component rather than to the export. A check whose debut finding is a false positive does not
get a second reading, so the distinction between ABSENT and NOT-COMPARABLE is pinned here.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { exportedFunctions } from "../check-plugin-interop-drift.mjs";
const parse = (src) => exportedFunctions(src, "t.tsx");
test("an exported function declaration reports its arity", () => {
const found = parse("export function f(a, b, c) { return a; }");
assert.deepEqual(found.get("f"), { total: 3, required: 3 });
});
test("optional and defaulted parameters are not required", () => {
const found = parse("export function f(a, b?, c = 1, ...rest) { return a; }");
assert.deepEqual(found.get("f"), { total: 4, required: 1 });
});
test("an exported arrow function is comparable", () => {
const found = parse("export const f = (a, b) => a + b;");
assert.deepEqual(found.get("f"), { total: 2, required: 2 });
});
test("a memo()-wrapped export is PRESENT but not comparable", () => {
/* The false positive the first run produced: reported as a rename. */
const found = parse("export const TaskCard = memo(TaskCardComponent, areEqual);");
assert.equal(found.has("TaskCard"), true);
assert.equal(found.get("TaskCard"), null);
});
test("a non-exported function is invisible", () => {
assert.equal(parse("function hidden(a) { return a; }").has("hidden"), false);
});
/*
FNXC:PluginInteropDrift 2026-07-31-08:20:
INTERFACES ARE ONE-DIRECTIONAL: fewer properties is correct, unknown ones are the drift.
All six mirrors declare subsets (6, 8, 7, 7, 3, 6 against the real nine) because a plugin mirrors
only the fields it uses. Demanding equality would fail every plugin for not using everything, which
is how a check gets deleted. A property the real type lacks is a rename nobody propagated — the
plugin keeps compiling and reads a field the host never sends.
*/
import { declaredInterfacesForTest } from "../check-plugin-interop-drift.mjs";
test("an interface's property names are collected", () => {
const found = declaredInterfacesForTest("export interface P { a: string; b?: number }", "t.tsx");
assert.deepEqual([...(found.get("P") ?? new Map()).keys()], ["a", "b"]);
});
test("a mirror declaring FEWER properties is not drift", () => {
const real = declaredInterfacesForTest("export interface P { a: string; b?: number; c?: boolean }", "t.tsx").get("P");
const mirrored = ["a"];
assert.equal(mirrored.every((p) => real.has(p)), true);
});
test("a mirror declaring an UNKNOWN property is drift", () => {
/* The live case: `TaskCardProps.workflowStepNameLookup` outlived its removal from TaskCard. */
const real = declaredInterfacesForTest("export interface P { a: string }", "t.tsx").get("P");
assert.equal(real.has("workflowStepNameLookup"), false);
});