mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
test(apps): strengthen application and build verification
This commit is contained in:
1 parent
bdfe4bbeb5
commit
2bb5eb3715
18 files changed
+465
-559
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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 ──
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 \}\}/)
|
||||
})
|
||||
Reference in new issue
Block a user