* test(#4658): add failing-first coverage for native frontend evidence hasStaticFrontendEvidence recognised only JS-ecosystem evidence, so computeUiPlanGate could never block for a SwiftUI/Compose/Flutter/XAML project. Adds evidence-level fixtures for the four suggested markers (import-matched for .swift/.kt/.dart, extension-alone for .xaml), the reporter's non-UI Swift control case, marker-exactness and SKIP_DIRS and I/O-degrade negatives, gate-level block assertions through makeProject's new native frontendEvidence modes, and a pinned-seed fast-check property. All new assertions are RED until src/ui-frontend-evidence.cts grows the native branch. * fix(#4658): give the ui plan gate's evidence check a native branch hasStaticFrontendEvidence recognised only JS-ecosystem evidence (a root package.json UI-framework dep, or a .tsx/.jsx/.vue/.svelte file), so computeUiPlanGate could never block for a SwiftUI, Jetpack Compose, Flutter, or .NET MAUI project — the #3312 gate was structurally unreachable for them. Adds a native BFS over the same bounds and skip rules: .xaml is evidence by extension alone (the .tsx analogue), while .swift/.kt/.dart count only when their content carries the ecosystem's UI import marker (import SwiftUI / import UIKit, androidx.compose, package:flutter) — matched on the import, not the extension, so a non-UI Swift package stays silent exactly as the issue's 37-file control case requires. Marker reads are bounded to a 64 KiB prefix; any I/O failure degrades to false per the module contract. The #3718 vocabulary filter, the JS evidence rules, and the weaker-extension exclusion are untouched. * chore(#4658): regenerate the macos conformance tier list The native-evidence additions to tests/check-ui-plan-gate.test.cjs move the file into the macOS conformance tier per the classifier; the committed generated list is a derived artifact and must match the live tests/ tree (the same sync the fragment-single-edit-propagation install test enforces). * fix(#4658): accept both Dart quote styles and extract the shared bounded walk The isolated reviews' remaining findings: the Dart marker carried only the single-quote anchor, missing legal double-quoted imports (a spec-narrowing deviation); the two evidence walks duplicated the subtle MAX_WALK_ENTRIES cap semantics verbatim, so they are extracted into one walkProjectFiles BFS with a visit callback; tests now use the createTempDir helper, shared fixture literals that cannot drift from NATIVE_UI_CONTENT_MARKERS, a double-quoted Flutter import case, and drop a vacuous assertion and a mid-body re-require alias. * docs(#4658): backfill changeset PR number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/lucky-badgers-forage.md
Normal file
5
.changeset/lucky-badgers-forage.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 4807
|
||||
---
|
||||
**The UI plan gate now recognizes native UI projects** — a SwiftUI, Jetpack Compose, Flutter, or .NET MAUI project never tripped the static frontend-evidence check, so the gate that requires a UI-SPEC before planning a UI phase silently never fired for them. A `.xaml` file, or a `.swift`/`.kt`/`.dart` file importing its ecosystem's UI framework, now counts as evidence; non-UI native packages (a Swift CLI, a plain Kotlin server) stay silent. (#4658)
|
||||
@@ -32,6 +32,7 @@ module.exports = {
|
||||
"tests/changeset-parse.test.cjs",
|
||||
"tests/check-contract-drift.test.cjs",
|
||||
"tests/check-predicate.test.cjs",
|
||||
"tests/check-ui-plan-gate.test.cjs",
|
||||
"tests/check-ui-safety-gate.test.cjs",
|
||||
"tests/check-update-config-dir.test.cjs",
|
||||
"tests/chunked-planning-parallel.test.cjs",
|
||||
|
||||
@@ -569,8 +569,10 @@ function findUiSpecInDir(phaseDir: string): string {
|
||||
* matches the token `dashboard` exactly like the real compound `micro-frontend`
|
||||
* (the boundary rule of #3718 is intentional and untouched). The gate therefore
|
||||
* blocks only when the token match is corroborated by static frontend evidence
|
||||
* in the repo tree (hasStaticFrontendEvidence: package.json UI-framework dep or
|
||||
* a component-framework file). This mirrors the sibling post-wave gate
|
||||
* in the repo tree (hasStaticFrontendEvidence: package.json UI-framework dep, a
|
||||
* component-framework file, or native UI evidence — a `.xaml` file or a
|
||||
* `.swift`/`.kt`/`.dart` file carrying its ecosystem's UI import marker,
|
||||
* #4658). This mirrors the sibling post-wave gate
|
||||
* computeUiSafetyGate, which requires `hasUiFiles` (git diff) before blocking.
|
||||
* matchedToken/matchedLine surface what tripped the sniffer so an operator can
|
||||
* judge the flag in one second instead of reaching for --skip-ui.
|
||||
|
||||
@@ -22,6 +22,19 @@
|
||||
* html, ...) are deliberately NOT static evidence: docs sites and
|
||||
* markdown/bash/config repos routinely carry stray `.html`/`.css`, which
|
||||
* is precisely the false-positive class #3312 reports.
|
||||
* (c) any `*.xaml` file, or a `*.swift` / `*.kt` / `*.dart` file whose
|
||||
* content carries its ecosystem's UI-framework import marker
|
||||
* (`import SwiftUI` / `import UIKit`, `androidx.compose`,
|
||||
* `package:flutter`) (#4658). Native UI projects (SwiftUI, Jetpack
|
||||
* Compose, Flutter, .NET MAUI) carry neither (a) nor (b), which made the
|
||||
* gate structurally unreachable for them. Source files match on the
|
||||
* IMPORT, not the extension alone — the same unambiguity bar that
|
||||
* justifies (b)'s subset and excludes (css, scss, html): a non-UI Swift
|
||||
* package (a CLI, a server) imports Foundation, not SwiftUI, and must
|
||||
* stay silent. `.xaml` is extension-alone for the same reason `.tsx` is —
|
||||
* the extension itself is unambiguous. Marker matching is case-sensitive
|
||||
* (imports are case-sensitive in all four ecosystems); extension
|
||||
* matching is case-insensitive, mirroring `UI_COMPONENT_FILE_RE`.
|
||||
*
|
||||
* All I/O failures degrade to `false` (no evidence) — never throw.
|
||||
*/
|
||||
@@ -32,6 +45,25 @@ import path from 'node:path';
|
||||
/** Component-framework file extensions — the static-evidence subset of UI_FILE_EXTENSIONS_RE. */
|
||||
export const UI_COMPONENT_FILE_RE = /\.(tsx|jsx|vue|svelte)$/i;
|
||||
|
||||
/** Native UI source files whose CONTENT is scanned for an import marker (#4658). */
|
||||
export const NATIVE_UI_SOURCE_RE = /\.(swift|kt|dart)$/i;
|
||||
|
||||
/** Native UI files that are evidence by extension alone — `.xaml`, the `.tsx` analogue. */
|
||||
export const NATIVE_UI_XAML_RE = /\.xaml$/i;
|
||||
|
||||
/**
|
||||
* Per-extension UI-framework import markers for `NATIVE_UI_SOURCE_RE` files
|
||||
* (#4658). Every marker embeds its ecosystem's import keyword, so a bare
|
||||
* framework-name mention in prose or a comment is not evidence; the Dart
|
||||
* marker carries both legal quote styles. Case-sensitive: imports are
|
||||
* case-sensitive in Swift, Kotlin and Dart.
|
||||
*/
|
||||
export const NATIVE_UI_CONTENT_MARKERS: Readonly<Record<string, readonly string[]>> = {
|
||||
'.swift': ['import SwiftUI', 'import UIKit'],
|
||||
'.kt': ['import androidx.compose'],
|
||||
'.dart': ["import 'package:flutter", 'import "package:flutter'],
|
||||
};
|
||||
|
||||
/**
|
||||
* UI-framework package.json dependencies (dependencies OR devDependencies).
|
||||
* Component frameworks/renderers only — deliberately excludes meta tooling
|
||||
@@ -106,9 +138,19 @@ function packageJsonHasUiFramework(projectDir: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function treeHasComponentFile(projectDir: string): boolean {
|
||||
// Iterative BFS — bounded by MAX_WALK_ENTRIES so a pathological tree cannot
|
||||
// stall the gate. Symlinks are never followed (withFileTypes + isDirectory).
|
||||
/**
|
||||
* Shared bounded BFS over the project tree — the single home of the walk
|
||||
* semantics both evidence walks depend on: SKIP_DIRS pruning, the
|
||||
* MAX_WALK_ENTRIES entry cap (cap-hit → `false`: the tree is treated as
|
||||
* scanned and evidence stays undecided), symlinks never followed
|
||||
* (withFileTypes Dirents), unreadable directories skipped. `visit` is called
|
||||
* for every regular file with its name and full path; returning `true` stops
|
||||
* the walk with `true` (evidence found).
|
||||
*/
|
||||
function walkProjectFiles(
|
||||
projectDir: string,
|
||||
visit: (name: string, fullPath: string) => boolean,
|
||||
): boolean {
|
||||
const queue: string[] = [projectDir];
|
||||
let visited = 0;
|
||||
while (queue.length > 0 && visited < MAX_WALK_ENTRIES) {
|
||||
@@ -124,7 +166,7 @@ function treeHasComponentFile(projectDir: string): boolean {
|
||||
if (visited >= MAX_WALK_ENTRIES) return false;
|
||||
if (entry.isDirectory()) {
|
||||
if (!SKIP_DIRS.has(entry.name)) queue.push(path.join(dir, entry.name));
|
||||
} else if (entry.isFile() && UI_COMPONENT_FILE_RE.test(entry.name)) {
|
||||
} else if (entry.isFile() && visit(entry.name, path.join(dir, entry.name))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -132,16 +174,68 @@ function treeHasComponentFile(projectDir: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function treeHasComponentFile(projectDir: string): boolean {
|
||||
return walkProjectFiles(projectDir, (name) => UI_COMPONENT_FILE_RE.test(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read at most the first 64 KiB of `file` and report whether any of `markers`
|
||||
* occurs in it (#4658). Import sections live at the top of a source file, so a
|
||||
* bounded prefix read keeps the gate's plan-time cost profile without reading
|
||||
* generated monsters in full. Any I/O failure degrades to false — never throw.
|
||||
*/
|
||||
function fileHasAnyMarker(file: string, markers: readonly string[]): boolean {
|
||||
let fd: number;
|
||||
try {
|
||||
fd = fs.openSync(file, 'r');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const bytes = Buffer.alloc(64 * 1024);
|
||||
const read = fs.readSync(fd, bytes, 0, bytes.length, 0);
|
||||
const prefix = bytes.toString('utf8', 0, read);
|
||||
return markers.some((m) => prefix.includes(m));
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
try {
|
||||
fs.closeSync(fd);
|
||||
} catch {
|
||||
// already closed — nothing to degrade
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Native-UI walk over the shared bounded BFS (#4658): `.xaml` is evidence by
|
||||
* extension alone; `.swift`/`.kt`/`.dart` are evidence only when the file's
|
||||
* content carries its ecosystem's import marker.
|
||||
*/
|
||||
function treeHasNativeUiFile(projectDir: string): boolean {
|
||||
return walkProjectFiles(projectDir, (name, fullPath) => {
|
||||
if (NATIVE_UI_XAML_RE.test(name)) return true;
|
||||
if (NATIVE_UI_SOURCE_RE.test(name)) {
|
||||
const markers = NATIVE_UI_CONTENT_MARKERS[path.extname(name).toLowerCase()];
|
||||
return markers != null && fileHasAnyMarker(fullPath, markers);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the project tree carry static evidence of a frontend?
|
||||
*
|
||||
* @param projectDir - Absolute path to the project root (the gate's cwd).
|
||||
* @returns true when package.json declares a UI-framework dependency or the
|
||||
* tree contains a component-framework file; false otherwise (including
|
||||
* on any I/O failure — evidence must be affirmative).
|
||||
* @returns true when package.json declares a UI-framework dependency, the tree
|
||||
* contains a component-framework file, or the tree contains native UI
|
||||
* evidence (a `.xaml` file, or a `.swift`/`.kt`/`.dart` file carrying
|
||||
* its ecosystem's UI import marker — #4658); false otherwise
|
||||
* (including on any I/O failure — evidence must be affirmative).
|
||||
*/
|
||||
export function hasStaticFrontendEvidence(projectDir: string): boolean {
|
||||
if (typeof projectDir !== 'string' || projectDir === '') return false;
|
||||
if (packageJsonHasUiFramework(projectDir)) return true;
|
||||
return treeHasComponentFile(projectDir);
|
||||
if (treeHasComponentFile(projectDir)) return true;
|
||||
return treeHasNativeUiFile(projectDir);
|
||||
}
|
||||
|
||||
@@ -23,8 +23,17 @@ const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { cleanup, createTempDir } = require('./helpers.cjs');
|
||||
const fc = require('./helpers/fast-check-setup.cjs');
|
||||
const { computeUiPlanGate } = require('../gsd-core/bin/lib/check-command-router.cjs');
|
||||
const { hasStaticFrontendEvidence } = require('../gsd-core/bin/lib/ui-frontend-evidence.cjs');
|
||||
|
||||
// Shared native fixture literals — kept in one place so the fixtures cannot
|
||||
// drift from NATIVE_UI_CONTENT_MARKERS when a marker is tightened (#4658).
|
||||
const COMPOSE_IMPORT = 'import androidx.compose.material3.Text\n';
|
||||
const FLUTTER_IMPORT = "import 'package:flutter/material.dart';\n";
|
||||
const FLUTTER_IMPORT_DOUBLE_QUOTED = 'import "package:flutter/material.dart";\n';
|
||||
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -54,6 +63,20 @@ function makeProject({ phaseSection = '', hasUiSpec = false, frontendEvidence =
|
||||
JSON.stringify({ name: 'fixture', dependencies: { react: '^18.3.1' } }),
|
||||
'utf8',
|
||||
);
|
||||
} else if (frontendEvidence === 'swiftui') {
|
||||
fs.mkdirSync(path.join(tmpDir, 'Sources'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, 'Sources', 'ContentView.swift'), 'import SwiftUI\nstruct ContentView: View { var body: some View { Text("hi") } }\n', 'utf8');
|
||||
} else if (frontendEvidence === 'compose') {
|
||||
fs.mkdirSync(path.join(tmpDir, 'lib'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, 'lib', 'Main.kt'), COMPOSE_IMPORT, 'utf8');
|
||||
} else if (frontendEvidence === 'flutter') {
|
||||
fs.mkdirSync(path.join(tmpDir, 'lib'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, 'lib', 'main.dart'), `${FLUTTER_IMPORT}void main() {}\n`, 'utf8');
|
||||
} else if (frontendEvidence === 'xaml') {
|
||||
fs.writeFileSync(path.join(tmpDir, 'MainPage.xaml'), '<Page x:Class="App.MainPage" />\n', 'utf8');
|
||||
} else if (frontendEvidence === 'swift-non-ui') {
|
||||
fs.mkdirSync(path.join(tmpDir, 'Sources'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, 'Sources', 'TaskRunner.swift'), 'import Foundation\nstruct TaskRunner { }\n', 'utf8');
|
||||
}
|
||||
|
||||
// Minimal ROADMAP.md with one phase section
|
||||
@@ -460,3 +483,239 @@ describe('computeUiPlanGate — ui.plan-gate check logic (#1026)', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #4658 — native (non-JS) frontend evidence ────────────────────────────────
|
||||
// hasStaticFrontendEvidence recognised only JS-ecosystem evidence (a root
|
||||
// package.json UI-framework dep, or a .tsx/.jsx/.vue/.svelte file), so
|
||||
// computeUiPlanGate could never block for a SwiftUI / Compose / Flutter / XAML
|
||||
// project — the #3312 gate was structurally unreachable for an entire class of
|
||||
// project. The native branch keeps the same unambiguous-component bar as the
|
||||
// .tsx subset: source files count only with their import marker (the reporter's
|
||||
// 37-file Swift CLI with zero UI imports stays silent), while .xaml is
|
||||
// extension-alone for the same reason .tsx is.
|
||||
|
||||
describe('hasStaticFrontendEvidence — native evidence branch (#4658)', () => {
|
||||
// relPath -> content; a `null` value creates a DIRECTORY at that path.
|
||||
function nativeProject(files) {
|
||||
const tmpDir = createTempDir('ui-evidence-4658-');
|
||||
for (const rel of Object.keys(files)) {
|
||||
const target = path.join(tmpDir, rel);
|
||||
if (files[rel] === null) {
|
||||
fs.mkdirSync(target, { recursive: true });
|
||||
} else {
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.writeFileSync(target, files[rel], 'utf8');
|
||||
}
|
||||
}
|
||||
return tmpDir;
|
||||
}
|
||||
|
||||
test('a SwiftUI import counts as static frontend evidence (#4658)', (t) => {
|
||||
const dir = nativeProject({ 'Sources/ContentView.swift': 'import SwiftUI\nstruct ContentView: View {}\n' });
|
||||
t.after(() => cleanup(dir));
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dir), true, 'import SwiftUI is unambiguous native UI evidence');
|
||||
});
|
||||
|
||||
test('a UIKit import counts as static frontend evidence (#4658)', (t) => {
|
||||
const dir = nativeProject({ 'AppDelegate.swift': 'import UIKit\n' });
|
||||
t.after(() => cleanup(dir));
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dir), true);
|
||||
});
|
||||
|
||||
test('a Compose import counts as static frontend evidence (#4658)', (t) => {
|
||||
const dir = nativeProject({ 'lib/Main.kt': COMPOSE_IMPORT });
|
||||
t.after(() => cleanup(dir));
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dir), true);
|
||||
});
|
||||
|
||||
test('a Flutter import counts as static frontend evidence (#4658)', (t) => {
|
||||
const dir = nativeProject({ 'lib/main.dart': `${FLUTTER_IMPORT}void main() {}\n` });
|
||||
t.after(() => cleanup(dir));
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dir), true);
|
||||
});
|
||||
|
||||
test('a double-quoted Flutter import also counts as evidence (#4658)', (t) => {
|
||||
const dir = nativeProject({ 'lib/main.dart': `${FLUTTER_IMPORT_DOUBLE_QUOTED}void main() {}\n` });
|
||||
t.after(() => cleanup(dir));
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dir), true, 'both Dart quote styles are imports');
|
||||
});
|
||||
|
||||
test('a XAML page counts as static frontend evidence (#4658)', (t) => {
|
||||
const dir = nativeProject({ 'MainPage.xaml': '<Page x:Class="App.MainPage" />\n' });
|
||||
t.after(() => cleanup(dir));
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dir), true, '.xaml is extension-alone, like .tsx');
|
||||
});
|
||||
|
||||
test('a non-UI Swift package stays non-evidence (import marker required)', (t) => {
|
||||
// The reporter's control case: 37 Swift files, zero UI imports — a CLI, a
|
||||
// server. Extension-alone matching would misclassify exactly this class.
|
||||
const dir = nativeProject({ 'Sources/TaskRunner.swift': 'import Foundation\nstruct TaskRunner { }\n' });
|
||||
t.after(() => cleanup(dir));
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dir), false);
|
||||
});
|
||||
|
||||
test('plain Kotlin and Dart sources stay non-evidence', (t) => {
|
||||
const dir = nativeProject({
|
||||
'lib/Repo.kt': 'class Repo { fun get(): Int = 1 }\n',
|
||||
'bin/main.dart': 'void main() { print(1); }\n',
|
||||
});
|
||||
t.after(() => cleanup(dir));
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dir), false);
|
||||
});
|
||||
|
||||
test('marker matching is exact — prose mentions and case changes are not imports', (t) => {
|
||||
const mentioned = nativeProject({ 'Sources/Notes.swift': '// SwiftUI is nice, but this is a comment\nimport Foundation\n' });
|
||||
t.after(() => cleanup(mentioned));
|
||||
assert.strictEqual(hasStaticFrontendEvidence(mentioned), false, 'a prose/comment mention is not an import');
|
||||
|
||||
const lowercased = nativeProject({ 'Sources/A.swift': 'import swiftui\n' });
|
||||
t.after(() => cleanup(lowercased));
|
||||
assert.strictEqual(hasStaticFrontendEvidence(lowercased), false, 'imports are case-sensitive in all three marker-scanned languages');
|
||||
|
||||
const kotlinComment = nativeProject({ 'lib/Main.kt': '// TODO: migrate to androidx.compose\nfun main() {}\n' });
|
||||
t.after(() => cleanup(kotlinComment));
|
||||
assert.strictEqual(hasStaticFrontendEvidence(kotlinComment), false, 'a bare framework-name mention is not a Kotlin import');
|
||||
|
||||
const dartComment = nativeProject({ 'lib/main.dart': '// see package:flutter docs\nvoid main() {}\n' });
|
||||
t.after(() => cleanup(dartComment));
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dartComment), false, 'a bare package-path mention is not a Dart import');
|
||||
});
|
||||
|
||||
test('native files are found below the project root', (t) => {
|
||||
const dir = nativeProject({ 'lib/ui/deep/Chart.kt': 'import androidx.compose.foundation.Canvas\n' });
|
||||
t.after(() => cleanup(dir));
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dir), true);
|
||||
});
|
||||
|
||||
test('SKIP_DIRS still excludes vendored native matches', (t) => {
|
||||
const dir = nativeProject({ 'node_modules/somepkg/Chart.kt': 'import androidx.compose.foundation.Canvas\n' });
|
||||
t.after(() => cleanup(dir));
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dir), false, 'vendored trees are not project evidence');
|
||||
});
|
||||
|
||||
test('extension matching is case-insensitive (.XAML)', (t) => {
|
||||
const dir = nativeProject({ 'MainPage.XAML': '<Page />\n' });
|
||||
t.after(() => cleanup(dir));
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dir), true, 'matching UI_COMPONENT_FILE_RE\'s existing /i semantics');
|
||||
});
|
||||
|
||||
test('unreadable directories degrade to false, never throw', (t) => {
|
||||
const dir = nativeProject({ 'Sources/ContentView.swift': 'import SwiftUI\n' });
|
||||
const origReaddir = fs.readdirSync;
|
||||
t.after(() => { cleanup(dir); });
|
||||
t.mock.method(fs, 'readdirSync', (p, opts) => {
|
||||
if (String(p).endsWith('Sources')) throw new Error('EIO: simulated read failure');
|
||||
return origReaddir.call(fs, p, opts);
|
||||
});
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dir), false, 'I/O failure = no evidence, per the module contract');
|
||||
});
|
||||
|
||||
test('property: native evidence is exactly the marker-matched source content', () => {
|
||||
// Deterministic: pinned seed, bounded runs, counterexample printed on
|
||||
// failure (fc's default). Filler is filtered so it cannot carry a marker.
|
||||
const markers = [
|
||||
['.swift', 'import SwiftUI'],
|
||||
['.swift', 'import UIKit'],
|
||||
['.kt', 'import androidx.compose'],
|
||||
['.dart', "import 'package:flutter"],
|
||||
['.dart', 'import "package:flutter'],
|
||||
];
|
||||
const filler = fc.string({ minLength: 0, maxLength: 80 })
|
||||
.filter((s) => !s.includes('import') && !s.includes('androidx') && !s.includes('package:') && !s.includes('\n'));
|
||||
// try/finally lives in this HELPER (no test context), per the exemption.
|
||||
function withFixture(content, fn) {
|
||||
const dir = createTempDir('ui-evidence-prop-4658-');
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, `probe${content.ext}`), content.text, 'utf8');
|
||||
return fn(dir);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
}
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.constantFrom(...markers),
|
||||
filler,
|
||||
([ext, marker], noise) => withFixture({ ext, text: `${noise}\n${marker}\n${noise}\n` }, (dir) => {
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dir), true, `marker ${JSON.stringify(marker)} must be evidence`);
|
||||
}),
|
||||
),
|
||||
{ seed: 4658, numRuns: 100 },
|
||||
);
|
||||
fc.assert(
|
||||
fc.property(filler, (noise) => withFixture({ ext: '.swift', text: `${noise}\n${noise}\n` }, (dir) => {
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dir), false, 'filler-only content is not evidence');
|
||||
})),
|
||||
{ seed: 4659, numRuns: 100 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeUiPlanGate — native evidence fires the gate (#4658)', () => {
|
||||
test('native frontend evidence fires the gate on a UI phase without a UI-SPEC (#4658)', (t) => {
|
||||
const proj = makeProject({
|
||||
phaseSection: 'Build the SwiftUI dashboard list and detail views.',
|
||||
hasUiSpec: false,
|
||||
frontendEvidence: 'swiftui',
|
||||
});
|
||||
t.after(() => cleanup(proj.tmpDir));
|
||||
const r = computeUiPlanGate(proj.tmpDir, '1');
|
||||
assert.strictEqual(r.frontend, true, 'the vocabulary match already succeeded (#3718 untouched)');
|
||||
assert.strictEqual(r.hasFrontendEvidence, true, 'a SwiftUI project IS structural frontend evidence');
|
||||
assert.strictEqual(r.block, true, 'the issue title criterion: the gate must actually FIRE for native projects');
|
||||
});
|
||||
|
||||
test('native evidence respects the UI-SPEC branch (block:false when the spec exists)', (t) => {
|
||||
const proj = makeProject({
|
||||
phaseSection: 'Build the SwiftUI dashboard list and detail views.',
|
||||
hasUiSpec: true,
|
||||
frontendEvidence: 'swiftui',
|
||||
});
|
||||
t.after(() => cleanup(proj.tmpDir));
|
||||
const r = computeUiPlanGate(proj.tmpDir, '1');
|
||||
assert.strictEqual(r.hasFrontendEvidence, true);
|
||||
assert.strictEqual(r.hasUiSpec, true);
|
||||
assert.strictEqual(r.block, false, 'branch (b) is unchanged for native evidence');
|
||||
});
|
||||
|
||||
test('non-UI native source stays block:false (corroboration still required)', (t) => {
|
||||
const proj = makeProject({
|
||||
phaseSection: 'Build the dashboard views for the settings screen.',
|
||||
hasUiSpec: false,
|
||||
frontendEvidence: 'swift-non-ui',
|
||||
});
|
||||
t.after(() => cleanup(proj.tmpDir));
|
||||
const r = computeUiPlanGate(proj.tmpDir, '1');
|
||||
assert.strictEqual(r.frontend, true, 'vocabulary match is unchanged');
|
||||
assert.strictEqual(r.hasFrontendEvidence, false, 'import Foundation is not UI evidence');
|
||||
assert.strictEqual(r.block, false, 'the control case: extension-alone would have blocked this');
|
||||
});
|
||||
});
|
||||
|
||||
// Review-pass additions (#4658): gate-level coverage for a second native
|
||||
// ecosystem, and the unreadable-FILE degrade path of the bounded marker read.
|
||||
describe('computeUiPlanGate — native evidence, additional ecosystems (#4658)', () => {
|
||||
test('compose evidence fires the gate through the same block formula', (t) => {
|
||||
const proj = makeProject({
|
||||
phaseSection: 'Migrate the dashboard list to Jetpack Compose components.',
|
||||
hasUiSpec: false,
|
||||
frontendEvidence: 'compose',
|
||||
});
|
||||
t.after(() => cleanup(proj.tmpDir));
|
||||
const r = computeUiPlanGate(proj.tmpDir, '1');
|
||||
assert.strictEqual(r.frontend, true);
|
||||
assert.strictEqual(r.hasFrontendEvidence, true, 'a Compose project IS structural frontend evidence');
|
||||
assert.strictEqual(r.block, true);
|
||||
});
|
||||
|
||||
test('an unreadable candidate file degrades to no evidence, never throws', (t) => {
|
||||
const dir = createTempDir('ui-evidence-io-4658-');
|
||||
t.after(() => { cleanup(dir); });
|
||||
fs.writeFileSync(path.join(dir, 'Main.kt'), COMPOSE_IMPORT, 'utf8');
|
||||
// t.mock.method auto-restores readSync at test end.
|
||||
t.mock.method(fs, 'readSync', () => {
|
||||
throw new Error('EIO: simulated read failure');
|
||||
});
|
||||
assert.strictEqual(hasStaticFrontendEvidence(dir), false, 'read failure = no evidence, per the module contract');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user