test(apps): strengthen application and build verification

This commit is contained in:
digua committed 2026-08-01 16:41:30 +08:00
1 parent bdfe4bbeb5
commit 2bb5eb3715
18 files changed
+465 -559

No files matched your search

+1 -1
View File
@@ -35,7 +35,7 @@
],
"scripts": {
"cli": "node scripts/ensure-native.mjs && tsx src/cli.ts",
"build": "tsup",
"build": "tsup && node ../../scripts/check-build-artifacts.mjs cli",
"build:full": "pnpm -w run build:cli-web && pnpm run build && pnpm run prepare:cli-web",
"prepare:cli-web": "rm -rf dist-cli-web && cp -r ../../dist-cli-web dist-cli-web",
"ensure-native": "node scripts/ensure-native.mjs",
+18 -72
View File
@@ -45,26 +45,6 @@ describe('authHook — authentication matrix', () => {
assert.equal(reply.statusCode, 0, '/api/* should pass without token configured')
})
// ── /api/* always requires auth ──
it('rejects /api/* without Bearer header', async () => {
const reply = fakeReply()
await authHook(fakeRequest('/api/v1/status'), reply as never)
assert.equal(reply.statusCode, 401)
})
it('rejects /api/* with wrong token', async () => {
const reply = fakeReply()
await authHook(fakeRequest('/api/v1/status', 'Bearer wrong_token'), reply as never)
assert.equal(reply.statusCode, 401)
})
it('allows /api/* with correct token', async () => {
const reply = fakeReply()
await authHook(fakeRequest('/api/v1/status', `Bearer ${VALID_TOKEN}`), reply as never)
assert.equal(reply.statusCode, 0, 'should not send any error')
})
// ── /_web/* default (requireAuth=false): bypass ──
it('allows /_web/* without auth when requireAuth=false', async () => {
@@ -73,59 +53,25 @@ describe('authHook — authentication matrix', () => {
assert.equal(reply.statusCode, 0)
})
// ── /_web/* with requireAuth=true: requires token ──
const cases = [
['rejects /api/* without Bearer header', '/api/v1/status', undefined, false, 401],
['rejects /api/* with wrong token', '/api/v1/status', 'Bearer wrong_token', false, 401],
['allows /api/* with correct token', '/api/v1/status', `Bearer ${VALID_TOKEN}`, false, 0],
['rejects /_web/* without Bearer when requireAuth=true', '/_web/sessions', undefined, true, 401],
['rejects /_web/* with wrong token when requireAuth=true', '/_web/sessions', 'Bearer bad', true, 401],
['allows /_web/* with correct token when requireAuth=true', '/_web/sessions', `Bearer ${VALID_TOKEN}`, true, 0],
['allows static file paths without auth', '/index.html', undefined, false, 0],
['allows static file paths without auth even with requireAuth=true', '/assets/main.js', undefined, true, 0],
] as const
it('rejects /_web/* without Bearer when requireAuth=true', async () => {
setRequireAuth(true)
const reply = fakeReply()
await authHook(fakeRequest('/_web/sessions'), reply as never)
assert.equal(reply.statusCode, 401)
})
it('rejects /_web/* with wrong token when requireAuth=true', async () => {
setRequireAuth(true)
const reply = fakeReply()
await authHook(fakeRequest('/_web/sessions', 'Bearer bad'), reply as never)
assert.equal(reply.statusCode, 401)
})
it('allows /_web/* with correct token when requireAuth=true', async () => {
setRequireAuth(true)
const reply = fakeReply()
await authHook(fakeRequest('/_web/sessions', `Bearer ${VALID_TOKEN}`), reply as never)
assert.equal(reply.statusCode, 0)
})
// ── Static files / SPA: always public ──
it('allows static file paths without auth', async () => {
const reply = fakeReply()
await authHook(fakeRequest('/index.html'), reply as never)
assert.equal(reply.statusCode, 0)
})
it('allows static file paths without auth even with requireAuth=true', async () => {
setRequireAuth(true)
const reply = fakeReply()
await authHook(fakeRequest('/assets/main.js'), reply as never)
assert.equal(reply.statusCode, 0)
})
// ── Combined: webRoot + requireAuth (the P0 scenario) ──
it('P0 regression: /_web/* is protected when both webRoot and requireAuth are active', async () => {
setRequireAuth(true)
const reply = fakeReply()
await authHook(fakeRequest('/_web/ai/llm/providers'), reply as never)
assert.equal(reply.statusCode, 401, '/_web/* must NOT bypass auth when requireAuth=true')
})
it('P0 regression: /api/* remains protected regardless of requireAuth flag', async () => {
setRequireAuth(false)
const reply = fakeReply()
await authHook(fakeRequest('/api/v1/status'), reply as never)
assert.equal(reply.statusCode, 401, '/api/* must always require auth')
})
for (const [name, url, authorization, requireAuth, expectedStatus] of cases) {
it(name, async () => {
setRequireAuth(requireAuth)
const reply = fakeReply()
await authHook(fakeRequest(url, authorization), reply as never)
assert.equal(reply.statusCode, expectedStatus)
})
}
// ── setRequireAuth reset ──
+39 -3
View File
@@ -1,13 +1,49 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { executeParameterizedSql } from '@openchatlab/core'
import { openBetterSqliteDatabase } from '@openchatlab/node-runtime'
import { responseStatsMessagesSql } from './commands-stats'
describe('responseStatsMessagesSql', () => {
it('orders same-second messages by id for deterministic response gaps', () => {
const sql = responseStatsMessagesSql()
const db = openBetterSqliteDatabase(':memory:')
try {
db.exec(`
CREATE TABLE member (
id INTEGER PRIMARY KEY,
account_name TEXT,
group_nickname TEXT
);
CREATE TABLE message (
id INTEGER PRIMARY KEY,
sender_id INTEGER NOT NULL,
ts INTEGER NOT NULL,
type INTEGER NOT NULL
);
INSERT INTO member (id, account_name) VALUES (1, 'Alice'), (2, 'Bob');
INSERT INTO message (id, sender_id, ts, type) VALUES
(30, 1, 100, 0),
(10, 2, 100, 0),
(20, 1, 100, 0),
(40, 2, 110, 0);
`)
assert.match(sql, /SELECT\s+msg\.id,/i)
assert.match(sql, /ORDER BY msg\.ts ASC, msg\.id ASC/i)
const rows = executeParameterizedSql<{ id: number; sender_id: number }>(db, responseStatsMessagesSql(), {
startTs: 0,
})
assert.deepEqual(
rows.map(({ id, sender_id }) => [id, sender_id]),
[
[10, 2],
[20, 1],
[30, 1],
[40, 2],
]
)
} finally {
db.close()
}
})
})
+26 -35
View File
@@ -3,21 +3,21 @@ import assert from 'node:assert/strict'
import { API_VERSION, QueryError, successEnvelope, errorEnvelope, exitCodeForError } from './envelope'
describe('successEnvelope', () => {
it('builds ok envelope with data, meta and apiVersion', () => {
const envelope = successEnvelope('messages.search', { text: 'hello' }, { totalHits: 3 })
const cases = [
['builds ok envelope with data, meta and apiVersion', 'messages.search', { text: 'hello' }, { totalHits: 3 }],
['does not emit an error key on success', 'sessions.list', { items: [] }, undefined],
] as const
assert.equal(envelope.ok, true)
assert.equal(envelope.command, 'messages.search')
assert.deepEqual(envelope.data, { text: 'hello' })
assert.equal(envelope.meta.totalHits, 3)
assert.equal(envelope.meta.apiVersion, API_VERSION)
})
it('does not emit an error key on success', () => {
const envelope = successEnvelope('sessions.list', { items: [] })
assert.equal('error' in envelope, false)
assert.deepEqual(envelope.meta, { apiVersion: API_VERSION })
})
for (const [name, command, data, meta] of cases) {
it(name, () => {
const envelope = successEnvelope(command, data, meta)
assert.equal(envelope.ok, true)
assert.equal(envelope.command, command)
assert.deepEqual(envelope.data, data)
assert.equal('error' in envelope, false)
assert.deepEqual(envelope.meta, { ...meta, apiVersion: API_VERSION })
})
}
})
describe('errorEnvelope', () => {
@@ -54,28 +54,19 @@ describe('errorEnvelope', () => {
})
describe('exitCodeForError', () => {
it('maps argument-class errors to 2', () => {
assert.equal(exitCodeForError('INVALID_ARGUMENT'), 2)
assert.equal(exitCodeForError('CURSOR_INVALID'), 2)
assert.equal(exitCodeForError('RAW_DISABLED'), 2)
assert.equal(exitCodeForError('SQL_DISABLED'), 2)
})
const cases = [
['maps argument-class errors to 2', ['INVALID_ARGUMENT', 'CURSOR_INVALID', 'RAW_DISABLED', 'SQL_DISABLED'], 2],
['maps not-found errors to 3', ['SESSION_NOT_FOUND', 'MEMBER_NOT_FOUND', 'SEGMENT_NOT_FOUND'], 3],
['maps ambiguity errors to 4', ['SESSION_AMBIGUOUS', 'MEMBER_AMBIGUOUS'], 4],
['maps SQL errors to 5', ['SQL_ERROR'], 5],
['maps unknown errors to 1', ['SOMETHING_ELSE'], 1],
] as const
it('maps not-found errors to 3', () => {
assert.equal(exitCodeForError('SESSION_NOT_FOUND'), 3)
assert.equal(exitCodeForError('MEMBER_NOT_FOUND'), 3)
assert.equal(exitCodeForError('SEGMENT_NOT_FOUND'), 3)
})
it('maps ambiguity errors to 4', () => {
assert.equal(exitCodeForError('SESSION_AMBIGUOUS'), 4)
assert.equal(exitCodeForError('MEMBER_AMBIGUOUS'), 4)
})
it('maps SQL errors to 5 and unknown codes to 1', () => {
assert.equal(exitCodeForError('SQL_ERROR'), 5)
assert.equal(exitCodeForError('SOMETHING_ELSE'), 1)
})
for (const [name, codes, expected] of cases) {
it(name, () => {
for (const code of codes) assert.equal(exitCodeForError(code), expected, code)
})
}
})
describe('QueryError', () => {
-28
View File
@@ -1,28 +0,0 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
const releaseWorkflow = readFileSync(new URL('../../../.github/workflows/release.yml', import.meta.url), 'utf-8')
function extractSection(source: string, startMarker: string, endMarker: string): string {
const start = source.indexOf(startMarker)
assert.notEqual(start, -1, `Missing workflow section: ${startMarker}`)
const end = source.indexOf(endMarker, start + startMarker.length)
assert.notEqual(end, -1, `Missing workflow section terminator: ${endMarker}`)
return source.slice(start, end)
}
test('release CLI build injects Umami configuration before tsup inlines env values', () => {
const buildCliJob = extractSection(releaseWorkflow, ' build-cli:', ' release:')
const buildCliStep = extractSection(
buildCliJob,
' - name: Build CLI (with Web UI)',
' - name: Upload CLI build output'
)
assert.match(buildCliStep, /run: pnpm --filter chatlab-cli run build:full/)
assert.match(buildCliStep, /UMAMI_ENDPOINT: \$\{\{ secrets\.UMAMI_ENDPOINT \}\}/)
assert.match(buildCliStep, /UMAMI_WEBSITE_ID: \$\{\{ secrets\.UMAMI_WEBSITE_ID \}\}/)
})