This commit is contained in:
349
packages/server/tests/tauri-config.test.ts
Normal file
349
packages/server/tests/tauri-config.test.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* 9D-1/9D-2/9D-7: Tauri configuration tests.
|
||||
*
|
||||
* Validates tauri.conf.json, Cargo.toml, lib.rs, and build scripts
|
||||
* are properly configured for production desktop builds.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dirname, '..', '..', '..');
|
||||
const TAURI_DIR = path.join(ROOT, 'app', 'src-tauri');
|
||||
const WINDOWS_1252_EXTRA_CODEPOINTS = new Set([
|
||||
0x20ac, 0x201a, 0x0192, 0x201e, 0x2026, 0x2020, 0x2021, 0x02c6, 0x2030,
|
||||
0x0160, 0x2039, 0x0152, 0x017d, 0x2018, 0x2019, 0x201c, 0x201d, 0x2022,
|
||||
0x2013, 0x2014, 0x02dc, 0x2122, 0x0161, 0x203a, 0x0153, 0x017e, 0x0178,
|
||||
]);
|
||||
const STAGED_DEPENDENCY_ALLOWLIST = new Set(['onnxruntime-web']);
|
||||
|
||||
function isWindows1252PathSafe(value: string) {
|
||||
for (const char of value) {
|
||||
const code = char.codePointAt(0) ?? 0;
|
||||
if (code <= 0x7f || (code >= 0xa0 && code <= 0xff)) continue;
|
||||
if (WINDOWS_1252_EXTRA_CODEPOINTS.has(code)) continue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function listFiles(dir: string) {
|
||||
const files: string[] = [];
|
||||
const stack = [dir];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()!;
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
const full = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) stack.push(full);
|
||||
else if (entry.isFile()) files.push(full);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function listPackageDirs(nodeModulesDir: string) {
|
||||
if (!fs.existsSync(nodeModulesDir)) return [];
|
||||
const packageDirs: string[] = [];
|
||||
const stack = [nodeModulesDir];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()!;
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const full = path.join(current, entry.name);
|
||||
if (fs.existsSync(path.join(full, 'package.json'))) {
|
||||
packageDirs.push(full);
|
||||
}
|
||||
stack.push(full);
|
||||
}
|
||||
}
|
||||
return packageDirs;
|
||||
}
|
||||
|
||||
function resolveWithinStagedResources(fromPackageDir: string, dep: string, resourcesDir: string) {
|
||||
let current = fromPackageDir;
|
||||
for (;;) {
|
||||
const candidate = path.join(current, 'node_modules', ...dep.split('/'), 'package.json');
|
||||
if (fs.existsSync(candidate)) return true;
|
||||
if (path.resolve(current) === path.resolve(resourcesDir)) return false;
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) return false;
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
describe('Tauri Production Configuration', () => {
|
||||
it('tauri.conf.json exists and has valid version', () => {
|
||||
const conf = JSON.parse(fs.readFileSync(path.join(TAURI_DIR, 'tauri.conf.json'), 'utf-8'));
|
||||
expect(conf.productName).toBe('Waggle');
|
||||
expect(conf.version).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
// Canonical bundle identifier — Egzakta-branded for code signing
|
||||
// + App Store / Mac notarization. Set in tauri.conf.json. Don't
|
||||
// change without coordinating with the signing cert subject.
|
||||
expect(conf.identifier).toBe('com.egzakta.waggle');
|
||||
});
|
||||
|
||||
it('tauri.conf.json has bundle targets configured', () => {
|
||||
const conf = JSON.parse(fs.readFileSync(path.join(TAURI_DIR, 'tauri.conf.json'), 'utf-8'));
|
||||
// targets: "all" builds for the current platform (NSIS on Windows, DMG on macOS, etc.)
|
||||
expect(conf.bundle.targets).toBe('all');
|
||||
expect(conf.bundle.active).toBe(true);
|
||||
});
|
||||
|
||||
it('tauri.conf.json bundles resources directory', () => {
|
||||
const conf = JSON.parse(fs.readFileSync(path.join(TAURI_DIR, 'tauri.conf.json'), 'utf-8'));
|
||||
expect(conf.bundle.resources).toContain('resources/*');
|
||||
});
|
||||
|
||||
it('tauri.conf.json has correct window settings', () => {
|
||||
const conf = JSON.parse(fs.readFileSync(path.join(TAURI_DIR, 'tauri.conf.json'), 'utf-8'));
|
||||
const win = conf.app.windows[0];
|
||||
expect(win.title).toBe('Waggle');
|
||||
expect(win.width).toBe(1200);
|
||||
expect(win.height).toBe(800);
|
||||
expect(win.minWidth).toBe(800);
|
||||
expect(win.minHeight).toBe(600);
|
||||
expect(win.resizable).toBe(true);
|
||||
});
|
||||
|
||||
it('tauri.conf.json has the auto-updater intentionally disabled for v1', () => {
|
||||
// The updater plugin config was removed because release.yml published
|
||||
// latest.json with EMPTY signatures — with a pubkey present, every client
|
||||
// update would fail signature verification (a broken update channel). It
|
||||
// stays disabled until updater signing is provisioned (TAURI_SIGNING_PRIVATE_KEY
|
||||
// + createUpdaterArtifacts + a real-signature manifest generator).
|
||||
const conf = JSON.parse(fs.readFileSync(path.join(TAURI_DIR, 'tauri.conf.json'), 'utf-8'));
|
||||
expect(conf.plugins?.updater).toBeUndefined();
|
||||
});
|
||||
|
||||
it('tauri.conf.json has tray icon configured', () => {
|
||||
const conf = JSON.parse(fs.readFileSync(path.join(TAURI_DIR, 'tauri.conf.json'), 'utf-8'));
|
||||
expect(conf.app.trayIcon).toBeDefined();
|
||||
expect(conf.app.trayIcon.tooltip).toBe('Waggle - AI Agent Swarm');
|
||||
});
|
||||
|
||||
it('tray menu exposes only implemented desktop actions', () => {
|
||||
const tray = fs.readFileSync(path.join(TAURI_DIR, 'src', 'tray.rs'), 'utf-8');
|
||||
expect(tray).toContain('"Open Waggle"');
|
||||
expect(tray).toContain('"Settings"');
|
||||
expect(tray).toContain('"Quit Waggle"');
|
||||
expect(tray).toContain('app.exit(0)');
|
||||
expect(tray).toContain('"waggle://navigate"');
|
||||
expect(tray).toContain('"/settings"');
|
||||
|
||||
expect(tray).not.toContain('"Pause Agents"');
|
||||
expect(tray).not.toContain('"About Waggle"');
|
||||
expect(tray).not.toContain('"waggle://pause-agents"');
|
||||
expect(tray).not.toContain('"waggle://quit"');
|
||||
expect(tray).not.toContain('"/about"');
|
||||
});
|
||||
|
||||
it('web app mounts the Tauri desktop navigation bridge', () => {
|
||||
const app = fs.readFileSync(path.join(ROOT, 'apps', 'web', 'src', 'App.tsx'), 'utf-8');
|
||||
expect(app).toContain('listenDesktopNavigation');
|
||||
expect(app).toContain('listenDesktopShellEvents');
|
||||
expect(app).toContain('<TauriDesktopEventBridge />');
|
||||
});
|
||||
|
||||
it('tauri.conf.json has CSP that allows localhost connections', () => {
|
||||
const conf = JSON.parse(fs.readFileSync(path.join(TAURI_DIR, 'tauri.conf.json'), 'utf-8'));
|
||||
const csp = conf.app.security.csp;
|
||||
expect(csp).toContain('http://localhost:*');
|
||||
expect(csp).toContain('ws://localhost:*');
|
||||
});
|
||||
|
||||
it('Cargo.toml has updater plugin dependency', () => {
|
||||
const cargo = fs.readFileSync(path.join(TAURI_DIR, 'Cargo.toml'), 'utf-8');
|
||||
expect(cargo).toContain('tauri-plugin-updater');
|
||||
expect(cargo).toContain('tauri-plugin-notification');
|
||||
expect(cargo).toContain('tauri-plugin-single-instance');
|
||||
});
|
||||
|
||||
it('capabilities do not expose updater commands while updater config is disabled', () => {
|
||||
const caps = JSON.parse(
|
||||
fs.readFileSync(path.join(TAURI_DIR, 'capabilities', 'default.json'), 'utf-8'),
|
||||
);
|
||||
expect(caps.permissions).not.toContain('updater:default');
|
||||
});
|
||||
|
||||
it('does not configure unit-only Tauri plugins as objects', () => {
|
||||
// Regression: the packaged debug exe panicked during startup when
|
||||
// `plugins.dialog` was `{ open, save }`; tauri-plugin-dialog expects no
|
||||
// config payload when initialized with `tauri_plugin_dialog::init()`.
|
||||
const conf = JSON.parse(fs.readFileSync(path.join(TAURI_DIR, 'tauri.conf.json'), 'utf-8'));
|
||||
expect(conf.plugins?.dialog).toBeUndefined();
|
||||
});
|
||||
|
||||
it('lib.rs does not initialize updater while config is disabled', () => {
|
||||
// Regression: registering tauri-plugin-updater without plugins.updater
|
||||
// config deserializes as null and panics before the desktop UI starts.
|
||||
const lib = fs.readFileSync(path.join(TAURI_DIR, 'src', 'lib.rs'), 'utf-8');
|
||||
expect(lib).not.toContain('tauri_plugin_updater::Builder::new().build()');
|
||||
expect(lib).not.toContain('.updater()');
|
||||
});
|
||||
|
||||
it('NSIS installer template exists', () => {
|
||||
const nsis = fs.readFileSync(path.join(TAURI_DIR, 'nsis', 'installer.nsi'), 'utf-8');
|
||||
expect(nsis).toContain('NSIS_HOOK_PREINSTALL');
|
||||
expect(nsis).toContain('Desktop shortcut');
|
||||
expect(nsis).toContain('Start Menu');
|
||||
});
|
||||
|
||||
it('icon.ico exists', () => {
|
||||
expect(fs.existsSync(path.join(TAURI_DIR, 'icons', 'icon.ico'))).toBe(true);
|
||||
});
|
||||
|
||||
it('build-sidecar script exists', () => {
|
||||
const script = path.join(ROOT, 'scripts', 'build-sidecar.mjs');
|
||||
expect(fs.existsSync(script)).toBe(true);
|
||||
const content = fs.readFileSync(script, 'utf-8');
|
||||
expect(content).toContain('esbuild');
|
||||
expect(content).toContain('service.ts');
|
||||
expect(content).toContain('resources/service.js');
|
||||
});
|
||||
|
||||
it('D12: the bundled sidecar is generated at build time, never tracked', () => {
|
||||
// beforeBuildCommand regenerates the bundle on EVERY build path — including
|
||||
// a raw `npx tauri build` that bypasses the npm scripts and CI steps. A
|
||||
// tracked copy goes stale silently; a binary shipping an old server is a
|
||||
// release-stopping defect class (UX-Refactor P4 / D12 ruling).
|
||||
const conf = JSON.parse(fs.readFileSync(path.join(TAURI_DIR, 'tauri.conf.json'), 'utf-8'));
|
||||
expect(conf.build.beforeBuildCommand).toContain('build-sidecar.mjs');
|
||||
|
||||
const gitignore = fs.readFileSync(path.join(ROOT, '.gitignore'), 'utf-8');
|
||||
expect(gitignore).toContain('app/src-tauri/resources/service.js');
|
||||
expect(gitignore).toContain('app/src-tauri/resources/service.js.map');
|
||||
});
|
||||
|
||||
it('bundle-node defaults to the Node version that stages native deps', () => {
|
||||
const script = fs.readFileSync(path.join(ROOT, 'scripts', 'bundle-node.mjs'), 'utf-8');
|
||||
expect(script).toContain(
|
||||
'process.env.WAGGLE_BUNDLED_NODE_VERSION ?? process.versions.node',
|
||||
);
|
||||
});
|
||||
|
||||
it('sidecar resource preflight checks bundled Node ABI compatibility', () => {
|
||||
const script = fs.readFileSync(
|
||||
path.join(ROOT, 'scripts', 'check-sidecar-resources.mjs'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(script).toContain('process.versions.modules');
|
||||
expect(script).toContain('execFileSync(nodePath');
|
||||
});
|
||||
|
||||
it('staged sidecar resources have Windows MSI codepage-safe relative paths', () => {
|
||||
// WiX 3 links the en-US MSI with codepage 1252. Dependency test fixtures
|
||||
// with paths such as "snowman" Unicode names must be pruned before MSI
|
||||
// bundling or light.exe fails after the Rust build has already succeeded.
|
||||
const resources = path.join(TAURI_DIR, 'resources');
|
||||
const unsafe = listFiles(resources)
|
||||
.map((file) => path.relative(resources, file))
|
||||
.filter((file) => !isWindows1252PathSafe(file));
|
||||
|
||||
expect(unsafe).toEqual([]);
|
||||
});
|
||||
|
||||
it('staged sidecar node_modules is self-contained after MSI extraction', () => {
|
||||
// Running from app/src-tauri/resources can accidentally resolve missing
|
||||
// packages from the repo root node_modules. The installed MSI layout cannot.
|
||||
const resources = path.join(TAURI_DIR, 'resources');
|
||||
const nodeModules = path.join(resources, 'node_modules');
|
||||
const missing: string[] = [];
|
||||
|
||||
for (const packageDir of listPackageDirs(nodeModules)) {
|
||||
const manifest = JSON.parse(
|
||||
fs.readFileSync(path.join(packageDir, 'package.json'), 'utf-8'),
|
||||
) as { name?: string; dependencies?: Record<string, string> };
|
||||
for (const dep of Object.keys(manifest.dependencies ?? {})) {
|
||||
if (STAGED_DEPENDENCY_ALLOWLIST.has(dep)) continue;
|
||||
if (!resolveWithinStagedResources(packageDir, dep, resources)) {
|
||||
missing.push(`${path.relative(nodeModules, packageDir)} -> ${dep}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CI/CD Configuration', () => {
|
||||
it('release workflow exists for Windows + macOS builds', () => {
|
||||
const workflow = path.join(ROOT, '.github', 'workflows', 'release.yml');
|
||||
expect(fs.existsSync(workflow)).toBe(true);
|
||||
const content = fs.readFileSync(workflow, 'utf-8');
|
||||
expect(content).toContain('build-windows');
|
||||
expect(content).toContain('build-macos');
|
||||
expect(content).toContain('tauri-action');
|
||||
expect(content).toContain('aarch64-apple-darwin');
|
||||
expect(content).toContain('x86_64-apple-darwin');
|
||||
});
|
||||
|
||||
it('release workflow does NOT publish a broken (empty-signature) updater manifest', () => {
|
||||
// The update-manifest job was removed with the updater config: it published
|
||||
// latest.json with empty signatures, which every client would reject. Re-add
|
||||
// it together with real updater signing (see tauri.conf updater note).
|
||||
const workflow = fs.readFileSync(
|
||||
path.join(ROOT, '.github', 'workflows', 'release.yml'),
|
||||
'utf-8',
|
||||
);
|
||||
// No active `update-manifest:` job (a re-enable note in comments is fine).
|
||||
expect(workflow).not.toMatch(/^\s*update-manifest:/m);
|
||||
});
|
||||
|
||||
it('release workflow stages sidecar dependencies before packaging', () => {
|
||||
// P0-2: the packaged sidecar require()s esbuild-externalized deps that must
|
||||
// be staged into resources/node_modules or it dies with MODULE_NOT_FOUND.
|
||||
const workflow = fs.readFileSync(
|
||||
path.join(ROOT, '.github', 'workflows', 'release.yml'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(workflow).toContain('stage-sidecar-deps');
|
||||
});
|
||||
|
||||
it('release workflow builds packages before bundling the desktop sidecar', () => {
|
||||
// Release builds must follow the same package -> sidecar ordering as the
|
||||
// PR Tauri verification lane, otherwise tag artifacts can ship stale or
|
||||
// missing workspace dist outputs even when PR verification was green.
|
||||
const workflow = fs.readFileSync(
|
||||
path.join(ROOT, '.github', 'workflows', 'release.yml'),
|
||||
'utf-8',
|
||||
);
|
||||
const buildPackageIndexes = [...workflow.matchAll(/npm run build:packages/g)].map(
|
||||
(match) => match.index ?? -1,
|
||||
);
|
||||
const sidecarIndexes = [...workflow.matchAll(/node scripts\/build-sidecar\.mjs/g)].map(
|
||||
(match) => match.index ?? -1,
|
||||
);
|
||||
|
||||
expect(buildPackageIndexes).toHaveLength(sidecarIndexes.length);
|
||||
expect(sidecarIndexes).toHaveLength(2);
|
||||
for (const [index, sidecarIndex] of sidecarIndexes.entries()) {
|
||||
expect(buildPackageIndexes[index]).toBeLessThan(sidecarIndex);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Playwright Visual Regression Setup', () => {
|
||||
it('playwright.config.ts exists', () => {
|
||||
const conf = path.join(ROOT, 'playwright.config.ts');
|
||||
expect(fs.existsSync(conf)).toBe(true);
|
||||
const content = fs.readFileSync(conf, 'utf-8');
|
||||
expect(content).toContain('maxDiffPixelRatio');
|
||||
expect(content).toContain('localhost:3333');
|
||||
});
|
||||
|
||||
it('visual test spec exists with 14 test cases (7 views x 2 themes)', () => {
|
||||
const spec = path.join(ROOT, 'tests', 'visual', 'views.spec.ts');
|
||||
expect(fs.existsSync(spec)).toBe(true);
|
||||
const content = fs.readFileSync(spec, 'utf-8');
|
||||
expect(content).toContain('Dark Mode');
|
||||
expect(content).toContain('Light Mode');
|
||||
// 7 views defined
|
||||
expect(content).toContain('chat');
|
||||
expect(content).toContain('memory');
|
||||
expect(content).toContain('events');
|
||||
expect(content).toContain('capabilities');
|
||||
expect(content).toContain('cockpit');
|
||||
expect(content).toContain('mission-control');
|
||||
expect(content).toContain('settings');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user