feat(web): add eslint + prettier + vitest toolchain + web CI job (R8)
Added to web/package.json devDeps: eslint (9, flat config) + eslint-plugin-svelte + typescript-eslint + globals; prettier + prettier-plugin-svelte; vitest (jsdom env) + jsdom. New scripts: lint, lint:fix, format, format:check, test, test:watch. Configs: - web/eslint.config.js — flat config, TS + Svelte, browser/node globals, no-explicit-any as warn, unused-vars as error (ignores _-prefixed). - web/.prettierrc.json — single-quote, 100 width, svelte parser override. - web/.prettierignore — dist/node_modules/build/lockfiles. - web/vite.config.ts — vitest test block via reference directive, jsdom env, globals enabled. Sample test: web/src/lib/utils.test.ts (6 tests covering relativeTime, truncateMiddle, debounce — all passing). CI: new web job in .gitea/workflows/ci.yml (npm ci, check [advisory], lint [advisory], format:check [advisory], test [gate], build [gate]). Advisory steps use continue-on-error until the baseline is clean — matching the existing golangci-lint advisory pattern. Known baseline surfaced by the new toolchain (pre-existing, not caused by R8): svelte-check 154 errors (133-file config cascade), eslint 126 errors + 12 warnings (unused vars, @html XSS, unused CSS), prettier 175 unformatted files. Fixing these is a follow-up cleanup. VERSION 0.7.9 -> 0.7.10. Plan R8 marked done; C.1 updated.
This commit is contained in:
@@ -70,3 +70,30 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: docker build (verify image builds; no push)
|
||||
run: docker build -f compose/oikos/Dockerfile -t oikos:ci .
|
||||
|
||||
web:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- run: npm ci
|
||||
- name: svelte-check (advisory — baseline not yet clean)
|
||||
run: npm run check
|
||||
continue-on-error: true
|
||||
- name: eslint (advisory — baseline not yet clean)
|
||||
run: npm run lint
|
||||
continue-on-error: true
|
||||
- name: prettier format check (advisory — baseline not yet clean)
|
||||
run: npm run format:check
|
||||
continue-on-error: true
|
||||
- name: test
|
||||
run: npm run test
|
||||
- name: build
|
||||
run: npm run build
|
||||
|
||||
@@ -176,14 +176,29 @@ CONTRIBUTING §Testing gates: policy + learning ≥ 80%, others ≥ 60%.
|
||||
`npm run build` succeeds with Svelte 5 warnings. 722 KB JS bundle (222 KB
|
||||
gzip), no code splitting.
|
||||
|
||||
### C.1 Tooling gaps — fixed in this pass
|
||||
### C.1 Tooling gaps — fixed in this pass + R8
|
||||
|
||||
- `package.json` had only `dev`/`build`/`preview`. **Added** `check`
|
||||
(`svelte-check`), `typecheck` (`tsc --noEmit`), and `lint` scripts, plus
|
||||
`svelte-check` + `typescript` devDeps. Run `npm install` to pick them up.
|
||||
- Still missing: `eslint` + `eslint-plugin-svelte` + `prettier`, and any test
|
||||
runner (`vitest`). Recommended next step.
|
||||
- No CI config in `web/` — nothing enforces type-check/build on PR.
|
||||
- **R8 added:** `eslint` (flat config) + `eslint-plugin-svelte` +
|
||||
`typescript-eslint` + `globals`; `prettier` + `prettier-plugin-svelte`;
|
||||
`vitest` (jsdom env) with a sample test (`src/lib/utils.test.ts`, 6 tests).
|
||||
New scripts: `lint`, `lint:fix`, `format`, `format:check`, `test`,
|
||||
`test:watch`. Vitest config wired into `vite.config.ts` via
|
||||
`/// <reference types="vitest/config" />`.
|
||||
- **CI:** new `web` job in `.gitea/workflows/ci.yml` runs `npm ci`,
|
||||
`npm run check` (advisory), `npm run lint` (advisory),
|
||||
`npm run format:check` (advisory), `npm run test` (gate),
|
||||
`npm run build` (gate). Advisory steps use `continue-on-error: true`
|
||||
until the lint/check baseline is clean — matching the existing
|
||||
`golangci-lint` advisory pattern.
|
||||
- **Known baseline:** svelte-check reports 154 pre-existing errors (133
|
||||
files, "No Svelte configuration found in vite config" cascade — not
|
||||
caused by R8); eslint reports 126 errors + 12 warnings (unused vars,
|
||||
`@html` XSS, unused CSS); prettier reports 175 unformatted files. These
|
||||
are real findings surfaced by the new toolchain — fixing them is a
|
||||
follow-up cleanup task.
|
||||
|
||||
### C.2 Dead code — the tool-renderer registry (21 files, ~1.5k lines)
|
||||
|
||||
@@ -414,7 +429,7 @@ the gitignore comment); `build` target ensures `bin/` exists; `clean` removes `b
|
||||
| R5 | Rewrite `.agents/domains/knowledge/schema.md` + `.agents/shared/llm-wiki.md` for the DB-native model; delete/deprecate root `inventory.yaml` | M | Low | ✅ done |
|
||||
| R6 | Inject desktop `version` from `VERSION` via ldflags; fix `Makefile` `BINARY` colliding with `oikos/` dir | S | Low | ✅ done |
|
||||
| R7 | Add tests for `learning` (80% gate), `actuator`, `scheduler`, `domain`, `notifier`, `knowledge` | L | Low |
|
||||
| R8 | Add `eslint`+`prettier`+`vitest` to `web/`; wire `svelte-check`+`tsc` into CI; add `web/` CI job | M | Low |
|
||||
| R8 | Add `eslint`+`prettier`+`vitest` to `web/`; wire `svelte-check`+`tsc` into CI; add `web/` CI job | M | Low | ✅ done |
|
||||
| R9 | Define `OikosEvent` discriminated union; eliminate ~15 `any` sites in web | S | Low |
|
||||
| R10 | Replace `<svelte:component>` in `ActivityTimeline.svelte:103`; fix `state_referenced_locally` warnings | S | Low |
|
||||
| R11 | Add the 8 manually-registered `serve*` routes to `openapi.yaml` (or document the carve-out) | S | Low |
|
||||
|
||||
4
web/.prettierignore
Normal file
4
web/.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
build/
|
||||
package-lock.json
|
||||
9
web/.prettierrc.json
Normal file
9
web/.prettierrc.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
39
web/eslint.config.js
Normal file
39
web/eslint.config.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import js from '@eslint/js'
|
||||
import ts from 'typescript-eslint'
|
||||
import svelte from 'eslint-plugin-svelte'
|
||||
import globals from 'globals'
|
||||
|
||||
export default ts.config(
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs['flat/recommended'],
|
||||
{
|
||||
files: ['**/*.{ts,js,svelte}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: ts.parser
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: ['dist/', 'node_modules/', 'build/', '*.config.{ts,js}']
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
3971
web/package-lock.json
generated
3971
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,12 @@
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "svelte-check --tsconfig ./tsconfig.json"
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lucide/svelte": "^1.23.0",
|
||||
@@ -18,13 +23,21 @@
|
||||
"@tsconfig/svelte": "^5.0.0",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"bits-ui": "^2.18.1",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-plugin-svelte": "^2.46.0",
|
||||
"globals": "^15.0.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"prettier": "^3.3.0",
|
||||
"prettier-plugin-svelte": "^3.3.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-sonner": "^1.1.1",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"typescript": "^5.5.0",
|
||||
"vite": "^6.0.0"
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vite": "^6.0.0",
|
||||
"vitest": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
54
web/src/lib/utils.test.ts
Normal file
54
web/src/lib/utils.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { relativeTime, truncateMiddle, debounce } from './utils'
|
||||
|
||||
describe('relativeTime', () => {
|
||||
it('returns "never" for null/undefined/empty', () => {
|
||||
expect(relativeTime(null)).toBe('never')
|
||||
expect(relativeTime(undefined)).toBe('never')
|
||||
expect(relativeTime('')).toBe('never')
|
||||
})
|
||||
|
||||
it('returns "just now" for future timestamps', () => {
|
||||
const future = new Date(Date.now() + 10_000).toISOString()
|
||||
expect(relativeTime(future)).toBe('just now')
|
||||
})
|
||||
|
||||
it('formats seconds/minutes/hours/days', () => {
|
||||
const now = Date.now()
|
||||
expect(relativeTime(new Date(now - 5_000).toISOString())).toBe('5s ago')
|
||||
expect(relativeTime(new Date(now - 120_000).toISOString())).toBe('2m ago')
|
||||
expect(relativeTime(new Date(now - 3_600_000).toISOString())).toBe('1h ago')
|
||||
expect(relativeTime(new Date(now - 86_400_000 * 2).toISOString())).toBe('2d ago')
|
||||
})
|
||||
})
|
||||
|
||||
describe('truncateMiddle', () => {
|
||||
it('returns the string unchanged when at or under maxLen', () => {
|
||||
expect(truncateMiddle('short', 36)).toBe('short')
|
||||
expect(truncateMiddle('exactly36chars_exactly36chars_xxxxx', 36)).toBe(
|
||||
'exactly36chars_exactly36chars_xxxxx'
|
||||
)
|
||||
})
|
||||
|
||||
it('truncates in the middle, preserving both ends', () => {
|
||||
const out = truncateMiddle('investigation:nomos/dragonfly-memlock-overrun', 20)
|
||||
expect(out).toContain('…')
|
||||
// keep = floor((20 - 1) / 2) = 9 chars from each end
|
||||
expect(out.startsWith('investiga')).toBe(true)
|
||||
expect(out.endsWith('-overrun')).toBe(true)
|
||||
expect(out.length).toBeLessThanOrEqual(20)
|
||||
})
|
||||
})
|
||||
|
||||
describe('debounce', () => {
|
||||
it('collapses rapid calls into one trailing invocation', async () => {
|
||||
let calls = 0
|
||||
const fn = debounce(() => calls++, 20)
|
||||
fn()
|
||||
fn()
|
||||
fn()
|
||||
expect(calls).toBe(0)
|
||||
await new Promise((r) => setTimeout(r, 40))
|
||||
expect(calls).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,8 @@
|
||||
/// <reference types="vitest/config" />
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import { readFileSync, existsSync } from 'fs'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// In Docker, VERSION is copied into the build WORKDIR (/build/web/VERSION).
|
||||
// In local dev, the cwd is web/ and VERSION is two dirs up (../../VERSION
|
||||
@@ -50,5 +51,10 @@ export default defineConfig({
|
||||
// here so dev and prod agree on nomos's actual route paths.
|
||||
'/agent': authProxy('http://localhost:8092', (path) => path.replace(/^\/agent/, ''))
|
||||
}
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
include: ['src/**/*.{test,spec}.{ts,js}']
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user