mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
fix: bundled plugins path for source runs and backend cleanup
This commit is contained in:
1 parent
b0a0ee3e00
commit
f803bfc140
51 files changed
+147
-163
No files matched your search
@@ -10,8 +10,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
*
|
||||
* The Drizzle schema said so, the repository was written against it, but the
|
||||
* runtime bootstrap still created `user_id TEXT NOT NULL`. A second, corrected
|
||||
* `CREATE TABLE IF NOT EXISTS` further down was a no-op — the table already
|
||||
* existed — so every fresh install got the old constraint and every user
|
||||
* `CREATE TABLE IF NOT EXISTS` further down was a no-op, the table already
|
||||
* existed, so every fresh install got the old constraint and every user
|
||||
* deletion (including the OIDC account-link cleanup) failed once the account
|
||||
* had logged in at least once.
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
/**
|
||||
* `migrateSchema()` carried a `SELECT id FROM <table> LIMIT 1` probe for a
|
||||
* number of tables that the primary bootstrap already creates. The probe never
|
||||
* threw, so the `CREATE TABLE IF NOT EXISTS` in its catch never ran — and one
|
||||
* threw, so the `CREATE TABLE IF NOT EXISTS` in its catch never ran, and one
|
||||
* of those unreachable copies had drifted away from the real definition
|
||||
* (`sessions` had lost `ON DELETE CASCADE`).
|
||||
*
|
||||
@@ -61,7 +61,7 @@ describe("bootstrap creates the tables the removed probes covered", () => {
|
||||
});
|
||||
|
||||
it("creates them on a database old enough to predate them", async () => {
|
||||
// A database with users and hosts but none of the tables above — the
|
||||
// A database with users and hosts but none of the tables above, the
|
||||
// upgrade path the deleted probes appeared to be protecting.
|
||||
const seed = new Database(":memory:");
|
||||
seed.exec(`
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("needsExplicitPersist", () => {
|
||||
|
||||
/**
|
||||
* schema.pg.ts and schema.mysql.ts are generated from schema.ts. These check the
|
||||
* generated output is usable rather than merely syntactically valid — the
|
||||
* generated output is usable rather than merely syntactically valid, the
|
||||
* repository layer's correctness rests on all three behaving the same way.
|
||||
*/
|
||||
describe("generated schemas", () => {
|
||||
@@ -133,7 +133,7 @@ describe("generated schemas", () => {
|
||||
* SQLite builds its indexes at runtime from PERFORMANCE_INDEXES; Postgres and
|
||||
* MySQL only ever get what the migrations declare, which comes from schema.ts.
|
||||
* Anything listed in one and missing from the other is an index the engines
|
||||
* chosen for scale silently do without — which is how 31 of them went missing.
|
||||
* chosen for scale silently do without, which is how 31 of them went missing.
|
||||
*/
|
||||
it("declares every performance index in schema.ts", () => {
|
||||
// Both are the leading column of an existing composite unique index, which
|
||||
|
||||
@@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
/**
|
||||
* Sharing a host used to hand the owner's SSH authentication to the recipient
|
||||
* unconditionally. 2.6.1 put that behind `ssh_data.share_ssh_auth`, added as
|
||||
* `NOT NULL DEFAULT 0` — so every host shared before the upgrade silently
|
||||
* `NOT NULL DEFAULT 0`, so every host shared before the upgrade silently
|
||||
* stopped supplying credentials, and recipients hit "No valid authentication
|
||||
* method provided" on hosts that had worked the day before.
|
||||
*
|
||||
|
||||
@@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
/**
|
||||
* `ssh_credentials.username` became nullable when key-only credentials landed,
|
||||
* and databases created before that are rebuilt on startup to drop the
|
||||
* constraint — SQLite cannot ALTER a column.
|
||||
* constraint, SQLite cannot ALTER a column.
|
||||
*
|
||||
* The rebuild restated the table's columns as a literal, then copied rows with
|
||||
* `INSERT INTO temp SELECT <every live column>`. The table has gained columns
|
||||
|
||||
@@ -17,7 +17,7 @@ describe("rowsAffected", () => {
|
||||
});
|
||||
|
||||
it("reads changes from a better-sqlite3 write result", () => {
|
||||
// The shape of a write with no .returning() attached — verified against
|
||||
// The shape of a write with no .returning() attached, verified against
|
||||
// the driver, not assumed.
|
||||
expect(rowsAffected({ changes: 1, lastInsertRowid: 7 })).toBe(1);
|
||||
expect(rowsAffected({ changes: 0, lastInsertRowid: 7 })).toBe(0);
|
||||
@@ -81,7 +81,7 @@ describe("supportsReturning", () => {
|
||||
expect(supportsReturning("sqlite")).toBe(true);
|
||||
expect(supportsReturning("postgres")).toBe(true);
|
||||
// No RETURNING clause in MySQL, and drizzle's mysql-core does not expose
|
||||
// the method — call sites that need rows back must read first.
|
||||
// the method, call sites that need rows back must read first.
|
||||
expect(supportsReturning("mysql")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@ import type { DatabaseDialect } from "../../../database/db/dialect.js";
|
||||
* The MySQL path cannot be exercised against a real engine here, and its whole
|
||||
* correctness is an ordering property: an update must be read AFTER the write,
|
||||
* a delete BEFORE it. Get either backwards and the rows describe the wrong
|
||||
* state — silently, with no error anywhere.
|
||||
* state, silently, with no error anywhere.
|
||||
*
|
||||
* So the drizzle handle is stubbed and the order of calls is recorded.
|
||||
*/
|
||||
@@ -169,7 +169,7 @@ describe("deleteReturning", () => {
|
||||
await deleteReturning(context, {} as never, where);
|
||||
|
||||
// Without this, a concurrent write between them makes the returned rows
|
||||
// describe a state that never existed — and with a pool the second
|
||||
// describe a state that never existed, and with a pool the second
|
||||
// statement need not even reach the same connection.
|
||||
expect(calls[0]).toBe("begin");
|
||||
expect(calls[calls.length - 1]).toBe("commit");
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
/**
|
||||
* The settings cache lives in one process and is updated by whichever process
|
||||
* wrote the setting. On SQLite that is the only process there is. On Postgres
|
||||
* and MySQL — the reason those exist here is to let several instances share one
|
||||
* database — a setting changed on one replica would otherwise never reach the
|
||||
* and MySQL, the reason those exist here is to let several instances share one
|
||||
* database, a setting changed on one replica would otherwise never reach the
|
||||
* others, because the synchronous read cannot go back to the database.
|
||||
*
|
||||
* Re-priming on a timer does not make settings immediately consistent. It
|
||||
@@ -57,7 +57,7 @@ describe("settings cache refresh", () => {
|
||||
|
||||
it("keeps running after a refresh throws", async () => {
|
||||
// A transient database blip must not stop the loop, or the replica is stuck
|
||||
// on stale settings until it restarts — the exact failure this prevents.
|
||||
// on stale settings until it restarts, the exact failure this prevents.
|
||||
const refresh = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("connection reset"))
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { DatabaseDialect } from "../../../database/db/dialect.js";
|
||||
*
|
||||
* Defaults to SQLite, so `npm test` behaves as it always has and needs no
|
||||
* server. Set TEST_DIALECT=postgres or mysql, plus TEST_DATABASE_URL, to run
|
||||
* the same tests against a real one — see the database-dialects CI job.
|
||||
* the same tests against a real one, see the database-dialects CI job.
|
||||
*/
|
||||
export function testDialect(env = process.env): DatabaseDialect {
|
||||
const value = env.TEST_DIALECT?.trim().toLowerCase();
|
||||
@@ -125,7 +125,7 @@ export class TestSqliteDatabase {
|
||||
* Portable write for test setup.
|
||||
*
|
||||
* Separate from query() because better-sqlite3 refuses `.all()` on a
|
||||
* statement that returns no rows — "This statement does not return data".
|
||||
* statement that returns no rows, "This statement does not return data".
|
||||
*/
|
||||
async run(statement: SQL): Promise<void> {
|
||||
const context = this.context;
|
||||
@@ -207,7 +207,7 @@ async function truncateAll(context: DatabaseContext): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits seed SQL into statements, ignoring semicolons inside string literals —
|
||||
* Splits seed SQL into statements, ignoring semicolons inside string literals,
|
||||
* JSON payloads in the fixtures contain them.
|
||||
*/
|
||||
function splitStatements(sql: string): string[] {
|
||||
@@ -274,7 +274,7 @@ function portableSql(statement: string, dialect: DatabaseDialect): string {
|
||||
if (dialect !== "mysql") return out;
|
||||
|
||||
// Only the column list, before VALUES. A blanket replace also mangles the
|
||||
// double quotes inside JSON payloads in the values — '{"slots":[]}' became
|
||||
// double quotes inside JSON payloads in the values, '{"slots":[]}' became
|
||||
// '{`slots`:[]}', which is valid SQL and silently wrong data.
|
||||
const split = /^(.*?\bVALUES\b)(.*)$/is.exec(out);
|
||||
if (!split) return out.replace(/"([a-z_]+)"/g, "`$1`");
|
||||
@@ -354,7 +354,7 @@ async function resyncAutoIncrement(
|
||||
for (const table of tables) {
|
||||
// Only tables whose id is generated. A text primary key, like users.id,
|
||||
// has no sequence and no counter to move, and a table keyed on something
|
||||
// else entirely — host_sidebar_preferences.user_id — has no id at all.
|
||||
// else entirely, host_sidebar_preferences.user_id, has no id at all.
|
||||
if (context.dialect === "postgres") {
|
||||
// pg_get_serial_sequence() raises 42703 rather than returning null when
|
||||
// the column is missing, so let information_schema decide whether there
|
||||
@@ -408,7 +408,7 @@ async function resyncAutoIncrement(
|
||||
*
|
||||
* Every test builds a fixture, and each would otherwise re-run the migrator
|
||||
* against the same shared database. drizzle's journal makes that a no-op only
|
||||
* when the first run finished — several fixtures racing inside one file hit
|
||||
* when the first run finished, several fixtures racing inside one file hit
|
||||
* "table already exists" instead.
|
||||
*/
|
||||
const migrations = new Map<string, Promise<void>>();
|
||||
@@ -436,7 +436,7 @@ let cachedSqliteSchema: string | null = null;
|
||||
* The full schema, from the generated SQLite migrations rather than
|
||||
* hand-written DDL in each test file.
|
||||
*
|
||||
* Tests used to declare a cut-down version of every table they touched — a
|
||||
* Tests used to declare a cut-down version of every table they touched, a
|
||||
* `users` with five columns where the real one has thirty. That drifts from the
|
||||
* schema silently, and it is the reason the same tests could not be pointed at
|
||||
* another engine.
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
*
|
||||
* The resolved row supplies the address, the credentials, the jump hosts and
|
||||
* the stored host key, so the session opened on whichever machine owned that
|
||||
* id on the server — the host list stayed correct the whole time, and nothing
|
||||
* id on the server, the host list stayed correct the whole time, and nothing
|
||||
* announced the substitution.
|
||||
*/
|
||||
describe("hostAddressMismatch", () => {
|
||||
|
||||
@@ -26,7 +26,7 @@ vi.mock("../../utils/shared-host-auth-resolver.js", () => ({
|
||||
/**
|
||||
* A numeric host id belongs to whichever database produced it. Resolving the
|
||||
* desktop app's id against a sync server's table lands on whatever host owns
|
||||
* that number there — a different machine, with its own address, credentials
|
||||
* that number there, a different machine, with its own address, credentials
|
||||
* and host key. `sync_id` is the same string on both sides.
|
||||
*/
|
||||
describe("resolveHostBySyncId", () => {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { getBundledPluginsDir } from "../../plugins/paths.js";
|
||||
|
||||
describe("getBundledPluginsDir", () => {
|
||||
const saved = process.env.TERMIX_BUNDLED_PLUGINS_DIR;
|
||||
|
||||
afterEach(() => {
|
||||
if (saved === undefined) delete process.env.TERMIX_BUNDLED_PLUGINS_DIR;
|
||||
else process.env.TERMIX_BUNDLED_PLUGINS_DIR = saved;
|
||||
});
|
||||
|
||||
it("reads dist/plugins when run from source", () => {
|
||||
delete process.env.TERMIX_BUNDLED_PLUGINS_DIR;
|
||||
expect(getBundledPluginsDir()).toBe(
|
||||
path.resolve(process.cwd(), "dist", "plugins"),
|
||||
);
|
||||
});
|
||||
|
||||
it("honors the override", () => {
|
||||
process.env.TERMIX_BUNDLED_PLUGINS_DIR = "/somewhere/else";
|
||||
expect(getBundledPluginsDir()).toBe("/somewhere/else");
|
||||
});
|
||||
});
|
||||
@@ -123,7 +123,7 @@ describe("forwardAuditEntry", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("swallows transport errors — a dead SIEM must not break auditing", async () => {
|
||||
it("swallows transport errors, a dead SIEM must not break auditing", async () => {
|
||||
safeFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"));
|
||||
|
||||
await expect(forwardAuditEntry(ENTRY, NOW, env)).resolves.toBe(false);
|
||||
@@ -140,7 +140,7 @@ describe("forwardAuditEntry", () => {
|
||||
await forwardAuditEntry(ENTRY, NOW, env);
|
||||
}
|
||||
|
||||
// 5 per-entry warnings, then one suppression notice — not 8.
|
||||
// 5 per-entry warnings, then one suppression notice, not 8.
|
||||
const perEntry = logs.warn.mock.calls.filter(
|
||||
(call) => call[0] === "Failed to forward audit entry",
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user