This is an automated email from the ASF dual-hosted git repository.

voidmatcha pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new 6a5ed44594 [ZEPPELIN-6724] Add a convention for frontend type tests
6a5ed44594 is described below

commit 6a5ed44594e88b22140e9159e6b96589fccf4e06
Author: ChanHo Lee <[email protected]>
AuthorDate: Sat Oct 3 11:05:08 2026 +0900

    [ZEPPELIN-6724] Add a convention for frontend type tests
    
    ### What is this PR for?
    Type tests in the frontend currently come in two layouts: assertions inside 
an SDK spec (ZEPPELIN-6664) and a separate `type-tests/` directory 
(ZEPPELIN-6663). This PR settles on the first and documents it in 
`zeppelin-web-angular/AGENTS.md`, so the next type test has one place to go.
    
    **The convention**
    - Type assertions (`expectTypeOf`, `assertType`, `<at>ts-expect-error`) go 
in ordinary `.spec.ts` files, beside the interface they pin.
    - They count only where `tsc` checks the spec: today, the zeppelin-sdk and 
notebook-core specs. Lint now accepts a type-only test there and still rejects 
one elsewhere.
    - To show a value is accepted, pass it to `assertType<T>(...)`. Prefer an 
exact matcher to `<at>ts-expect-error`, which any error on the next line 
satisfies.
    
    **Why ordinary specs**
    - Type assertions are erased before Vitest runs a spec, so only `tsc` 
checks them. The two spec programs above already have a `tsc` pass in Maven 
(`typecheck:sdk-contracts`, `typecheck:notebook-core`), so a type test there 
needs no extra tsconfig, script or runner.
    - The usual alternative is separate `*.test-d.ts` files checked by `vitest 
--typecheck`. It reports failures per test, but it is still experimental and 
reports a file outside its tsconfig as passed 
([vitest#7988](https://github.com/vitest-dev/vitest/issues/7988)), so a type 
test could go unchecked without anyone noticing. Per-test output is not worth 
that risk or a second checker.
    
    **Commits** (each passes typecheck, lint and tests on its own)
    1. `eslint.config.js`: accept type assertions in the two typechecked spec 
programs.
    2. Move the `CompletionItem` type test into `completion-item.spec.ts`, 
using exact matchers instead of `<at>ts-expect-error`.
    3. Drop the now-empty `type-tests/` include from the SDK spec tsconfig.
    4. List `typecheck:sdk-contracts` in AGENTS.md. Maven already ran it.
    5. Document the convention.
    6. Replace runtime `expect`s on typed literals in the SDK specs with 
`assertType`. `tsc` already checked those literals; `assertType` keeps that 
check, excess properties included, without a variable that needs a use.
    
    ### What type of PR is it?
    Improvement
    
    ### Todos
    * [x] Accept type-only tests in typechecked spec programs
    * [x] Move the `CompletionItem` type test into an SDK spec
    * [x] Document the convention in `zeppelin-web-angular/AGENTS.md`
    
    ### What is the Jira issue?
    https://issues.apache.org/jira/browse/ZEPPELIN-6724
    
    ### How should this be tested?
    ```bash
    cd zeppelin-web-angular
    npm run typecheck:sdk-contracts
    npm run typecheck:notebook-core
    npm run test:shell
    npx eslint projects/zeppelin-sdk projects/zeppelin-notebook-core 
test/notebook-core
    ```
    - To see the lint boundary, put a test containing only `expectTypeOf` under 
`src/`: `vitest/expect-expect` reports it. The same test under 
`projects/zeppelin-sdk/src` passes.
    - To see the type test fail, make `CompletionItem.name` optional: 
`typecheck:sdk-contracts` reports `completion-item.spec.ts`.
    
    ### Screenshots (if appropriate)
    N/A
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? Yes, `zeppelin-web-angular/AGENTS.md` is 
updated in this PR
    
    
    Closes #5520 from tbonelee/ZEPPELIN-6724.
    
    Signed-off-by: YONGJAE LEE <[email protected]>
---
 zeppelin-web-angular/AGENTS.md                     | 25 ++++++++++++++++++++--
 zeppelin-web-angular/eslint.config.js              |  5 +++++
 .../src/interfaces/completion-item.spec.ts         | 24 +++++++++++++++++++++
 .../message-data-type-map.interface.spec.ts        |  8 +++----
 .../src/interfaces/notebook-wire-fields.spec.ts    | 13 +++++------
 .../projects/zeppelin-sdk/tsconfig.spec.json       |  2 +-
 .../type-tests/completion-item-meta.ts             | 24 ---------------------
 7 files changed, 61 insertions(+), 40 deletions(-)

diff --git a/zeppelin-web-angular/AGENTS.md b/zeppelin-web-angular/AGENTS.md
index 29aab36887..27a29f4785 100644
--- a/zeppelin-web-angular/AGENTS.md
+++ b/zeppelin-web-angular/AGENTS.md
@@ -39,8 +39,9 @@ The repository root `AGENTS.md` asks every change to include 
unit tests. This fi
 | `npm run test:shell -- foo.spec.ts` | Run one file |
 | `npm run test:notebook-core` | Run the dedicated notebook-core Node suite |
 | `npm run typecheck:notebook-core` | Check core source and specs, rebuild the 
package, and check the React type-only contract against built declarations and 
the same core source |
+| `npm run typecheck:sdk-contracts` | Check the `zeppelin-sdk` specs, 
including their [type assertions](#type-assertions) |
 
-`test:shell`, `test:notebook-core`, and `typecheck:notebook-core` are bound to 
the Maven `test` phase (`pom.xml`), so a spec added here starts running in CI 
the day it merges. It does not run where you would expect. `frontend.yml` 
builds this module with `-DskipTests`, which frontend-maven-plugin honours by 
skipping `test`-phase executions, so the run that counts is `mvnw verify 
-Pweb-e2e` inside the `run-playwright-e2e-tests` job. A failing spec surfaces 
there, under an e2e job name. Gi [...]
+`test:shell`, `test:notebook-core`, `typecheck:notebook-core`, and 
`typecheck:sdk-contracts` are bound to the Maven `test` phase (`pom.xml`), so a 
spec added here starts running in CI the day it merges. It does not run where 
you would expect. `frontend.yml` builds this module with `-DskipTests`, which 
frontend-maven-plugin honours by skipping `test`-phase executions, so the run 
that counts is `mvnw verify -Pweb-e2e` inside the `run-playwright-e2e-tests` 
job. A failing spec surfaces there [...]
 
 ## Where a test belongs
 
@@ -89,6 +90,26 @@ A spec with no assertion, or one whose assertion sits inside 
an `if`, passes by
 
 The e2e suite gets the same protection from `eslint-plugin-playwright`.
 
+## Type assertions
+
+`expectTypeOf`, `assertType` and `@ts-expect-error` are erased before a spec 
runs, so Vitest passes them whatever they say. Only a `tsc` pass over the spec 
checks them, and only two spec programs have one in Maven:
+
+| Specs | Checked by |
+| --- | --- |
+| `projects/zeppelin-sdk` | `typecheck:sdk-contracts` |
+| `projects/zeppelin-notebook-core`, `test/notebook-core` | 
`typecheck:notebook-core` |
+| `src/`, the rest of `test/`, `projects/zeppelin-visualization` | nothing yet 
|
+| `projects/zeppelin-react` | nothing yet 
([ZEPPELIN-6566](https://issues.apache.org/jira/browse/ZEPPELIN-6566)) |
+
+Lint encodes the table: `vitest/expect-expect` accepts a type assertion as a 
test's only assertion in the first two rows and rejects it elsewhere. A type 
assertion in an unchecked spec, even beside an `expect`, cannot fail. When a 
program gains a `tsc` pass in Maven, add its glob to the `settings: { vitest: { 
typecheck: true } }` block in `eslint.config.js`, or set the same in 
`projects/zeppelin-react/eslint.config.js`.
+
+- Put type assertions in an ordinary `.spec.ts`, never a separate type-test 
file or directory. One SDK interface file declares many unrelated types, so a 
type contract spec sits beside that file and is named for the contract it pins: 
`notebook-wire-fields.spec.ts`, `completion-item.spec.ts`.
+- To show a value is accepted, pass it to `assertType<T>(...)`. A typed 
variable then needs a use, and a runtime `expect` on a literal only restates 
the literal.
+- Prefer an exact matcher to `@ts-expect-error`. 
`expectTypeOf<CompletionItem>().toHaveProperty('name').toEqualTypeOf<string>()` 
fails when `name` becomes optional; `@ts-expect-error` is satisfied by any 
error on the next line, a typo included. Keep the directive for what no matcher 
can say, such as assigning to a `readonly` member 
(`host-remote-contract.spec.ts`), with one statement under it and the expected 
failure after it.
+- A type assertion pins what the SDK declares, not what the server sends. 
Payload shapes are evidenced by the server code that builds them, and later by 
the captured-traffic contract specs described above.
+
+`vitest --typecheck` with `*.test-d.ts` files is not used: it is still 
experimental, and it reports a file its tsconfig does not include as passed 
([vitest#7988](https://github.com/vitest-dev/vitest/issues/7988)). 
[TSTyche](https://tstyche.org) checks the message after `@ts-expect-error` and 
has no such gap, but it is a second runner for a handful of files. Revisit it 
if must-not-compile contracts multiply or a swallowed error is found.
+
 ## monaco-editor and path aliases in specs
 
 `vitest.shell.config.mts` mirrors the `paths` block in `tsconfig.base.json`, 
which Vite does not read; add an alias there when you add a path. Eleven files 
under `src/` import `monaco-editor`, two behind the `@zeppelin/services` 
barrel, so a spec that reaches the editor or notebook area loads it: a few 
seconds on first import and a `marked.umd.js.map` sourcemap warning, both 
monaco's, not ours. Mock it with `vi.mock('monaco-editor', ...)` when the spec 
only needs to assert the editor was [...]
@@ -144,4 +165,4 @@ This is a different measurement from 
`e2e/reporter.coverage.ts`, which counts an
 3. Import from `vitest` (`describe`, `expect`, `it`), not from Jasmine or Jest.
    Check the target has callers before you invest in it. 
`get-keyword-positions.spec.ts` is a worked example of a function that turned 
out to have none.
 4. Construct the class directly unless the behavior depends on Angular wiring; 
use `TestBed` when it does.
-5. Run `npm run test:shell` for shell/SDK/visualization changes. For 
notebook-core changes, run `npm run test:notebook-core` and `npm run 
typecheck:notebook-core`. Confirm the relevant checks pass before opening a PR.
+5. Run `npm run test:shell` for shell/SDK/visualization changes, plus `npm run 
typecheck:sdk-contracts` for SDK changes. For notebook-core changes, run `npm 
run test:notebook-core` and `npm run typecheck:notebook-core`. Confirm the 
relevant checks pass before opening a PR.
diff --git a/zeppelin-web-angular/eslint.config.js 
b/zeppelin-web-angular/eslint.config.js
index 239a119789..7b7c9d5bde 100644
--- a/zeppelin-web-angular/eslint.config.js
+++ b/zeppelin-web-angular/eslint.config.js
@@ -220,6 +220,11 @@ module.exports = tseslint.config(
       'vitest/no-focused-tests': 'error'
     }
   },
+  {
+    // Only specs a Maven tsc pass compiles; anywhere else a type-only test 
cannot fail.
+    files: ['projects/zeppelin-{notebook-core,sdk}/**/*.spec.ts', 
'test/notebook-core/**/*.spec.ts'],
+    settings: { vitest: { typecheck: true } }
+  },
   {
     // The shell test setup intentionally loads Zone.js for its side effects.
     files: ['test/test-setup.ts'],
diff --git 
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/completion-item.spec.ts
 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/completion-item.spec.ts
new file mode 100644
index 0000000000..b8bcecd848
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/completion-item.spec.ts
@@ -0,0 +1,24 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expectTypeOf, it } from 'vitest';
+
+import { CompletionItem } from './message-paragraph.interface';
+
+it('accepts the Spark and Flink completion payload, which has no meta key', () 
=> {
+  expectTypeOf({ name: 'println', value: 'println' 
}).toExtend<CompletionItem>();
+});
+
+it('keeps name and value required', () => {
+  
expectTypeOf<CompletionItem>().toHaveProperty('name').toEqualTypeOf<string>();
+  
expectTypeOf<CompletionItem>().toHaveProperty('value').toEqualTypeOf<string>();
+});
diff --git 
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.spec.ts
 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.spec.ts
index ec7797ecba..f4013f2c8c 100644
--- 
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.spec.ts
+++ 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.spec.ts
@@ -10,7 +10,7 @@
  * limitations under the License.
  */
 
-import { expect, expectTypeOf, it } from 'vitest';
+import { assertType, expectTypeOf, it } from 'vitest';
 
 import { MessageReceiveDataTypeMap } from './message-data-type-map.interface';
 import { OP } from './message-operator.interface';
@@ -23,12 +23,10 @@ it('declares the asymmetric paragraph output payloads sent 
by the server', () =>
     index: 0,
     data: 'chunk'
   };
-  const update: MessageReceiveDataTypeMap[OP.PARAGRAPH_UPDATE_OUTPUT] = {
+  assertType<MessageReceiveDataTypeMap[OP.PARAGRAPH_UPDATE_OUTPUT]>({
     ...append,
     type: DatasetType.TEXT
-  };
-  expect(append).not.toHaveProperty('type');
-  expect(update.type).toBe(DatasetType.TEXT);
+  });
   
expectTypeOf<MessageReceiveDataTypeMap[OP.PARAGRAPH_APPEND_OUTPUT]>().toEqualTypeOf<ParagraphAppendOutput>();
   
expectTypeOf<MessageReceiveDataTypeMap[OP.PARAGRAPH_APPEND_OUTPUT]>().not.toHaveProperty('type');
   
expectTypeOf<MessageReceiveDataTypeMap[OP.PARAGRAPH_UPDATE_OUTPUT]>().toEqualTypeOf<ParagraphUpdateOutput>();
diff --git 
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/notebook-wire-fields.spec.ts
 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/notebook-wire-fields.spec.ts
index 471d556498..142f90c087 100644
--- 
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/notebook-wire-fields.spec.ts
+++ 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/notebook-wire-fields.spec.ts
@@ -10,7 +10,7 @@
  * limitations under the License.
  */
 
-import { expect, expectTypeOf, it } from 'vitest';
+import { assertType, expectTypeOf, it } from 'vitest';
 
 import { EditorSettingReceived, ImportNote, Note } from 
'./message-notebook.interface';
 import { AngularObjectRemove, ImportParagraphItem, ParagraphItem } from 
'./message-paragraph.interface';
@@ -45,7 +45,7 @@ it('separates received wire fields from backward-compatible 
import input', () =>
     lineNumbers: false,
     fontSize: 9
   } satisfies ImportParagraphItem;
-  const importWithoutVersion: ImportNote = {
+  assertType<ImportNote>({
     note: {
       paragraphs: [legacyImportParagraph],
       name: 'Imported note',
@@ -63,20 +63,18 @@ it('separates received wire fields from backward-compatible 
import input', () =>
       },
       info: {}
     }
-  };
+  });
 
   
expectTypeOf<ImportNote['note']>().toHaveProperty('version').toEqualTypeOf<string
 | undefined>();
   expectTypeOf<ImportNote['note']['paragraphs'][number]>()
     .toHaveProperty('progress')
     .toEqualTypeOf<number | undefined>();
-  expect(importWithoutVersion.note).not.toHaveProperty('version');
-  
expect(importWithoutVersion.note.paragraphs[0]).not.toHaveProperty('progress');
 });
 
 it('accepts the personalized GET_NOTE response without a version', () => {
   // NotebookService.getNote returns Note.getUserNote for personalized 
notebooks.
   // That copy is constructed with Note(), so its nullable version is omitted 
by Message serialization.
-  const personalizedNote: Note = {
+  assertType<Note>({
     note: {
       paragraphs: [],
       name: 'Personalized note',
@@ -94,8 +92,7 @@ it('accepts the personalized GET_NOTE response without a 
version', () => {
       },
       info: {}
     }
-  };
+  });
 
   expectTypeOf<NonNullable<Note['note']>['version']>().toEqualTypeOf<string | 
undefined>();
-  expect(personalizedNote.note).not.toHaveProperty('version');
 });
diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/tsconfig.spec.json 
b/zeppelin-web-angular/projects/zeppelin-sdk/tsconfig.spec.json
index 3011c5aeac..436d2bfdf2 100644
--- a/zeppelin-web-angular/projects/zeppelin-sdk/tsconfig.spec.json
+++ b/zeppelin-web-angular/projects/zeppelin-sdk/tsconfig.spec.json
@@ -4,5 +4,5 @@
     "noEmit": true,
     "types": ["node"]
   },
-  "include": ["src/**/*.spec.ts", "type-tests/**/*.ts"]
+  "include": ["src/**/*.spec.ts"]
 }
diff --git 
a/zeppelin-web-angular/projects/zeppelin-sdk/type-tests/completion-item-meta.ts 
b/zeppelin-web-angular/projects/zeppelin-sdk/type-tests/completion-item-meta.ts
deleted file mode 100644
index d61cce076e..0000000000
--- 
a/zeppelin-web-angular/projects/zeppelin-sdk/type-tests/completion-item-meta.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *     http://www.apache.org/licenses/LICENSE-2.0
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { CompletionItem } from '../src/interfaces/message-paragraph.interface';
-
-// Spark and Flink interpreters build InterpreterCompletion(name, value, 
null), and
-// NotebookServer's Gson omits null fields, so their completion payloads have 
no `meta` key.
-export const sparkFlinkCompletion: CompletionItem = { name: 'println', value: 
'println' };
-
-// `name` and `value` are always supplied by production construction sites and 
must stay required.
-// @ts-expect-error `name` is required
-export const missingName: CompletionItem = { value: 'println' };
-
-// @ts-expect-error `value` is required
-export const missingValue: CompletionItem = { name: 'println' };

Reply via email to