Compare commits
1 Commits
cmp-28-con
...
cmp-30-ins
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
969744733f |
16
README.md
16
README.md
@@ -37,7 +37,6 @@ pnpm run desktop:dev
|
||||
pnpm run desktop:build
|
||||
pnpm run desktop:tauri:dev
|
||||
pnpm run desktop:tauri:build
|
||||
pnpm run desktop:tauri:build:all
|
||||
```
|
||||
|
||||
说明:
|
||||
@@ -46,10 +45,9 @@ pnpm run desktop:tauri:build:all
|
||||
- 仓库通过 `.npmrc` 固定安装 devDependencies,避免宿主机的 `NODE_ENV=production` 破坏桌面构建链路。
|
||||
- `cargo test -p redis-core` 默认验证共享后端核心,包括 mock Redis 握手与命令执行测试,避免在缺少桌面系统库时阻断后端迭代。
|
||||
- `pnpm --filter @redis-gui/desktop test` 运行桌面前端的最小单测基线;当前覆盖共享错误映射层,避免后续功能迭代把后端错误码重新退回成零散文案拼接。
|
||||
- `pnpm run desktop:dev` 启动浏览器态工作台壳层,用于检查布局、交互和 demo fallback;此模式下没有 Tauri 命令桥,但会在当前会话内保留 mock string 保存和 TTL 调整结果,便于做壳层级交互验收。
|
||||
- `pnpm run desktop:dev` 启动浏览器态工作台壳层,用于检查布局、交互和 demo fallback;此模式下没有 Tauri 命令桥。
|
||||
- `pnpm run desktop:tauri:dev` 启动带 Tauri 后端桥的桌面工作台;连接测试和命令执行会调用现有 `test_redis_connection` 与 `execute_redis_command` 命令,因此需要完整的 Tauri Linux 运行依赖。
|
||||
- `pnpm run desktop:tauri:build` 是当前推荐的稳定 Linux 打包入口,只生成已验证可出的 `.deb` 与 `.rpm` 产物;产物位于仓库根 `target/release/bundle/`。
|
||||
- `pnpm run desktop:tauri:build:all` 保留完整 bundle 调查路径;当前会在 AppImage 的 `linuxdeploy` 阶段失败,因此不应作为默认 QA 打包命令。
|
||||
- `pnpm run desktop:tauri:build` 会先执行桌面前端 build,再进入 Tauri/Rust 的真实打包流程;当前 Linux `.deb` 与 `.rpm` 产物位于仓库根 `target/release/bundle/`。
|
||||
- Linux 上进入 Tauri 打包前,可先用 `pkg-config --modversion gtk+-3.0 webkit2gtk-4.1 libsoup-3.0` 做原生依赖预检查;任一缺失都会阻断桌面包产物生成。
|
||||
|
||||
## Local Redis Smoke Fixture
|
||||
@@ -81,16 +79,10 @@ pnpm run desktop:tauri:dev
|
||||
## Current Frontend Scope
|
||||
|
||||
- 首版可见界面现在已接入真实的桌面 browse / inspect / string save / TTL update 路径,但仍保持危险操作和完整产品化流程在后续阶段演进。
|
||||
- 当前前端消费 `backend_bootstrap`、`test_redis_connection`、`browse_redis_keys`、`read_redis_value`、`create_redis_value`、`write_redis_value`、`update_redis_key_ttl` 和 `execute_redis_command`;浏览器态仍保持明确的 demo fallback。
|
||||
- 当前仓库还包含一个 string-only 的 `Add key` 可见入口,走 `create_redis_value` 合约;该能力已在仓库中实现,但阶段验收仍需按 `docs/redis-gui-v1-product-requirements.md` 与 `docs/qa/acceptance-matrix.md` 单独判断,不应自动算入当前基线签收。
|
||||
- 当前前端消费 `backend_bootstrap`、`test_redis_connection`、`browse_redis_keys`、`read_redis_value`、`write_redis_value` 和 `execute_redis_command`;浏览器态仍保持明确的 demo fallback。
|
||||
- 当前前端还通过共享错误映射层统一消费 `redis-core` 的稳定错误码,使连接测试、浏览、读取、保存、TTL 和命令执行失败都落到一致的操作提示上。
|
||||
- 连接区域现在额外暴露一个可见的 session state 卡片,把 `ready / testing / demo fallback / retry needed` 边界和下一步操作提示固定在当前选中 connection / DB 语境里;如果用户在 live test 返回前切换了连接,测试结果只回写被测连接,不会偷偷改掉当前 active DB。
|
||||
- `inspect_redis_key` 与 `update_redis_key_ttl` 已在桌面桥可用;当前 UI 已暴露 TTL 设置与移除控制,但删除执行仍保持未开放状态。
|
||||
- 当前 phase 的 string 保存、TTL 设置和 TTL 移除在执行前都会进入 scoped review,重复展示 connection / DB / key 语境与 current-vs-next 变化,并阻止 no-op 提交直接触达 live bridge。
|
||||
- inspector 现在对 `hash/list/set/zset/stream` 提供结构化只读面板,不再要求操作者从原始 JSON 文本中手动解析 typed value。
|
||||
- inspector 现在还会明确显示当前 inspect 状态、value surface 语义、下一步操作路径和 key facts,降低 string / structured / missing / demo fallback 场景下的误读成本。
|
||||
- DB switcher 现在默认覆盖 `db0` 到 `db15`,并会在当前作用域落到更高 DB 时保留对应选项,避免 UI 退回到四个固定 DB 的弱验证面。
|
||||
- inspector 现在还会明确显示当前 inspect 状态、value surface 语义、下一步操作路径和 key facts,降低 string、structured、missing 和 demo fallback 场景下的误读成本。
|
||||
- 连接测试、key browse、value inspector 和命令托盘在 Tauri 桌面运行时走真实后端桥;浏览器态 `pnpm run desktop:dev` 会明确回退到 demo 数据,不伪装成真实 Redis 行为。
|
||||
- 浏览器态现在还会在单次会话内同步 mock string 保存、TTL 设置和 TTL 移除的可见状态,保证壳层演示与 UI 反馈一致,但这仍不构成任何 live Redis 证据。
|
||||
- utility rail 现在额外提供本地 smoke assist 面板,可直接切换到 `127.0.0.1:6380 / db0` 的 fixture 路径、聚焦 `smoke:*` 键,并预填安全 smoke 命令。
|
||||
- 具体 UI/UX 边界与 QA 验收入口见 `docs/frontend-workspace-baseline.md` 和 `docs/qa/acceptance-matrix.md`。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,62 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createDemoKeyRecord,
|
||||
findDuplicateKeyName,
|
||||
parseOptionalTtlMillis,
|
||||
validateAddKeyDraft,
|
||||
} from "./add-key-draft";
|
||||
|
||||
describe("add key draft helpers", () => {
|
||||
it("validates required key name and optional TTL shape", () => {
|
||||
expect(
|
||||
validateAddKeyDraft({
|
||||
name: " ",
|
||||
value: "hello world",
|
||||
ttlMillis: "0",
|
||||
}),
|
||||
).toEqual({
|
||||
name: "Redis key name must not be empty.",
|
||||
ttlMillis: "TTL must be greater than zero when provided.",
|
||||
});
|
||||
|
||||
expect(
|
||||
validateAddKeyDraft({
|
||||
name: "draft:key",
|
||||
value: "",
|
||||
ttlMillis: "soon",
|
||||
}),
|
||||
).toEqual({
|
||||
ttlMillis: "TTL must be an integer number of milliseconds when provided.",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses optional TTL input and preserves blank as persistent", () => {
|
||||
expect(parseOptionalTtlMillis("")).toBeNull();
|
||||
expect(parseOptionalTtlMillis(" 60000 ")).toBe(60000);
|
||||
});
|
||||
|
||||
it("detects duplicate key names with Redis-style case-sensitive matching", () => {
|
||||
expect(findDuplicateKeyName([{ name: "draft:key" }], "draft:key")).toBe(true);
|
||||
expect(findDuplicateKeyName([{ name: "draft:key" }], "Draft:Key")).toBe(false);
|
||||
});
|
||||
|
||||
it("builds a demo string key record with optional TTL formatting", () => {
|
||||
expect(
|
||||
createDemoKeyRecord({
|
||||
name: " draft:key ",
|
||||
value: "hello world",
|
||||
ttlMillis: "60000",
|
||||
}),
|
||||
).toEqual({
|
||||
id: "draft:key",
|
||||
name: "draft:key",
|
||||
type: "string",
|
||||
ttl: "1m",
|
||||
preview: "hello world",
|
||||
editable: true,
|
||||
value: "hello world",
|
||||
summary:
|
||||
"String-only Add Key flow stays aligned to the bounded create contract and keeps the new key visible in the current workspace scope.",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
import {
|
||||
buildDemoStringPreview,
|
||||
formatDemoDurationFromMillis,
|
||||
type DemoWorkspaceKey,
|
||||
} from "./demo-workspace";
|
||||
|
||||
export type AddKeyDraft = {
|
||||
name: string;
|
||||
value: string;
|
||||
ttlMillis: string;
|
||||
};
|
||||
|
||||
export type AddKeyDraftErrors = Partial<Record<"name" | "ttlMillis", string>>;
|
||||
|
||||
export function validateAddKeyDraft(draft: AddKeyDraft): AddKeyDraftErrors {
|
||||
const errors: AddKeyDraftErrors = {};
|
||||
|
||||
if (!draft.name.trim()) {
|
||||
errors.name = "Redis key name must not be empty.";
|
||||
}
|
||||
|
||||
const ttlValue = draft.ttlMillis.trim();
|
||||
if (ttlValue) {
|
||||
if (!/^\d+$/.test(ttlValue)) {
|
||||
errors.ttlMillis = "TTL must be an integer number of milliseconds when provided.";
|
||||
} else if (Number.parseInt(ttlValue, 10) <= 0) {
|
||||
errors.ttlMillis = "TTL must be greater than zero when provided.";
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function parseOptionalTtlMillis(value: string) {
|
||||
const normalized = value.trim();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(normalized, 10);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
export function findDuplicateKeyName<T extends { name: string }>(
|
||||
keys: T[],
|
||||
keyName: string,
|
||||
) {
|
||||
const normalized = keyName.trim();
|
||||
return keys.some((key) => key.name === normalized);
|
||||
}
|
||||
|
||||
export function createDemoKeyRecord(draft: AddKeyDraft): DemoWorkspaceKey {
|
||||
const keyName = draft.name.trim();
|
||||
const ttlMillis = parseOptionalTtlMillis(draft.ttlMillis);
|
||||
|
||||
return {
|
||||
id: keyName,
|
||||
name: keyName,
|
||||
type: "string",
|
||||
ttl: ttlMillis === null ? "persistent" : formatDemoDurationFromMillis(ttlMillis),
|
||||
preview: buildDemoStringPreview(draft.value),
|
||||
editable: true,
|
||||
value: draft.value,
|
||||
summary:
|
||||
"String-only Add Key flow stays aligned to the bounded create contract and keeps the new key visible in the current workspace scope.",
|
||||
};
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
applyConnectionTestFailure,
|
||||
applyConnectionTestSuccess,
|
||||
buildConnectionSessionSummary,
|
||||
formatConnectionCardDetailLabel,
|
||||
formatConnectionStatusLabel,
|
||||
markConnectionTesting,
|
||||
} from "./connection-session";
|
||||
|
||||
const baseConnection = {
|
||||
id: "redis-local",
|
||||
name: "redis-local",
|
||||
detail: "Draft profile",
|
||||
status: "idle" as const,
|
||||
target: {
|
||||
database: 0,
|
||||
username: null,
|
||||
},
|
||||
};
|
||||
|
||||
describe("connection session helpers", () => {
|
||||
it("keeps connection test transitions in one place", () => {
|
||||
const checking = markConnectionTesting([baseConnection], baseConnection.id);
|
||||
expect(checking[0]).toMatchObject({
|
||||
status: "checking",
|
||||
detail: "Live test in progress",
|
||||
});
|
||||
|
||||
const healthy = applyConnectionTestSuccess(checking, baseConnection.id, {
|
||||
roundTripStatus: "PONG in 11ms",
|
||||
selectedDatabase: 4,
|
||||
authenticatedAs: "readonly",
|
||||
});
|
||||
expect(healthy[0]).toMatchObject({
|
||||
status: "healthy",
|
||||
detail: "PONG in 11ms",
|
||||
target: {
|
||||
database: 4,
|
||||
username: "readonly",
|
||||
},
|
||||
});
|
||||
|
||||
const failed = applyConnectionTestFailure(healthy, baseConnection.id, "authentication_failed");
|
||||
expect(failed[0]).toMatchObject({
|
||||
status: "error",
|
||||
detail: "authentication_failed",
|
||||
});
|
||||
});
|
||||
|
||||
it("formats consistent status and card copy", () => {
|
||||
expect(formatConnectionStatusLabel(baseConnection)).toBe("Idle · Draft profile");
|
||||
expect(
|
||||
formatConnectionStatusLabel({
|
||||
...baseConnection,
|
||||
status: "healthy",
|
||||
detail: "PONG in 11ms",
|
||||
}),
|
||||
).toBe("Ready · PONG in 11ms");
|
||||
expect(
|
||||
formatConnectionCardDetailLabel({
|
||||
...baseConnection,
|
||||
status: "error",
|
||||
detail: "authentication_failed",
|
||||
}),
|
||||
).toBe("retry authentication_failed");
|
||||
});
|
||||
|
||||
it("builds session summaries for demo, testing, ready, and retry states", () => {
|
||||
expect(
|
||||
buildConnectionSessionSummary({
|
||||
connection: baseConnection,
|
||||
databaseLabel: "db0",
|
||||
tauriAvailable: false,
|
||||
activeTestRunning: false,
|
||||
otherTestingConnectionName: null,
|
||||
}),
|
||||
).toMatchObject({
|
||||
badgeLabel: "Demo fallback",
|
||||
badgeVariant: "neutral",
|
||||
});
|
||||
|
||||
expect(
|
||||
buildConnectionSessionSummary({
|
||||
connection: baseConnection,
|
||||
databaseLabel: "db0",
|
||||
tauriAvailable: true,
|
||||
activeTestRunning: true,
|
||||
otherTestingConnectionName: null,
|
||||
}),
|
||||
).toMatchObject({
|
||||
badgeLabel: "Testing",
|
||||
badgeVariant: "accent",
|
||||
});
|
||||
|
||||
expect(
|
||||
buildConnectionSessionSummary({
|
||||
connection: {
|
||||
...baseConnection,
|
||||
status: "healthy",
|
||||
detail: "PONG in 11ms",
|
||||
},
|
||||
databaseLabel: "db4",
|
||||
tauriAvailable: true,
|
||||
activeTestRunning: false,
|
||||
otherTestingConnectionName: null,
|
||||
}),
|
||||
).toMatchObject({
|
||||
badgeLabel: "Ready",
|
||||
badgeVariant: "accent",
|
||||
});
|
||||
|
||||
expect(
|
||||
buildConnectionSessionSummary({
|
||||
connection: {
|
||||
...baseConnection,
|
||||
status: "error",
|
||||
detail: "authentication_failed",
|
||||
},
|
||||
databaseLabel: "db0",
|
||||
tauriAvailable: true,
|
||||
activeTestRunning: false,
|
||||
otherTestingConnectionName: null,
|
||||
}),
|
||||
).toMatchObject({
|
||||
badgeLabel: "Retry needed",
|
||||
badgeVariant: "danger",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the active session copy clear when another connection is testing", () => {
|
||||
expect(
|
||||
buildConnectionSessionSummary({
|
||||
connection: {
|
||||
...baseConnection,
|
||||
status: "healthy",
|
||||
detail: "PONG in 11ms",
|
||||
},
|
||||
databaseLabel: "db0",
|
||||
tauriAvailable: true,
|
||||
activeTestRunning: false,
|
||||
otherTestingConnectionName: "redis-staging",
|
||||
}),
|
||||
).toMatchObject({
|
||||
badgeLabel: "Ready",
|
||||
badgeVariant: "neutral",
|
||||
summary:
|
||||
"redis-local remains selected on db0 while redis-staging finishes a separate live test.",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,175 +0,0 @@
|
||||
export type SessionConnection = {
|
||||
id: string;
|
||||
name: string;
|
||||
detail: string;
|
||||
status: "healthy" | "idle" | "checking" | "error";
|
||||
target: {
|
||||
database: number;
|
||||
username: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type ConnectionSessionSummary = {
|
||||
badgeLabel: string;
|
||||
badgeVariant: "neutral" | "accent" | "danger";
|
||||
summary: string;
|
||||
hint: string;
|
||||
};
|
||||
|
||||
export type ConnectionTestSuccessResult = {
|
||||
roundTripStatus: string;
|
||||
selectedDatabase: number;
|
||||
authenticatedAs: string | null;
|
||||
};
|
||||
|
||||
export function formatConnectionStatusLabel(connection: SessionConnection) {
|
||||
if (connection.status === "healthy") {
|
||||
return `Ready · ${connection.detail}`;
|
||||
}
|
||||
|
||||
if (connection.status === "checking") {
|
||||
return "Testing · Live contract probe";
|
||||
}
|
||||
|
||||
if (connection.status === "error") {
|
||||
return `Attention · ${connection.detail}`;
|
||||
}
|
||||
|
||||
return `Idle · ${connection.detail}`;
|
||||
}
|
||||
|
||||
export function formatConnectionCardDetailLabel(connection: SessionConnection) {
|
||||
if (connection.status === "checking") {
|
||||
return "live test in progress";
|
||||
}
|
||||
|
||||
if (connection.status === "healthy") {
|
||||
return `ready ${connection.detail}`;
|
||||
}
|
||||
|
||||
if (connection.status === "error") {
|
||||
return `retry ${connection.detail}`;
|
||||
}
|
||||
|
||||
return connection.detail;
|
||||
}
|
||||
|
||||
export function markConnectionTesting<T extends SessionConnection>(
|
||||
connections: T[],
|
||||
connectionId: string,
|
||||
) {
|
||||
return connections.map((connection) =>
|
||||
connection.id === connectionId
|
||||
? {
|
||||
...connection,
|
||||
status: "checking" as const,
|
||||
detail: "Live test in progress",
|
||||
}
|
||||
: connection,
|
||||
);
|
||||
}
|
||||
|
||||
export function applyConnectionTestSuccess<T extends SessionConnection>(
|
||||
connections: T[],
|
||||
connectionId: string,
|
||||
result: ConnectionTestSuccessResult,
|
||||
) {
|
||||
return connections.map((connection) =>
|
||||
connection.id === connectionId
|
||||
? {
|
||||
...connection,
|
||||
status: "healthy" as const,
|
||||
detail: result.roundTripStatus,
|
||||
target: {
|
||||
...connection.target,
|
||||
database: result.selectedDatabase,
|
||||
username: result.authenticatedAs ?? connection.target.username,
|
||||
},
|
||||
}
|
||||
: connection,
|
||||
);
|
||||
}
|
||||
|
||||
export function applyConnectionTestFailure<T extends SessionConnection>(
|
||||
connections: T[],
|
||||
connectionId: string,
|
||||
detail: string,
|
||||
) {
|
||||
return connections.map((connection) =>
|
||||
connection.id === connectionId
|
||||
? {
|
||||
...connection,
|
||||
status: "error" as const,
|
||||
detail,
|
||||
}
|
||||
: connection,
|
||||
);
|
||||
}
|
||||
|
||||
export function buildConnectionSessionSummary(args: {
|
||||
connection: SessionConnection;
|
||||
databaseLabel: string;
|
||||
tauriAvailable: boolean;
|
||||
activeTestRunning: boolean;
|
||||
otherTestingConnectionName: string | null;
|
||||
}): ConnectionSessionSummary {
|
||||
const {
|
||||
connection,
|
||||
databaseLabel,
|
||||
tauriAvailable,
|
||||
activeTestRunning,
|
||||
otherTestingConnectionName,
|
||||
} = args;
|
||||
|
||||
if (!tauriAvailable) {
|
||||
return {
|
||||
badgeLabel: "Demo fallback",
|
||||
badgeVariant: "neutral",
|
||||
summary: `${connection.name} stays scoped to ${databaseLabel} in browser mode without contacting Redis.`,
|
||||
hint: "Use Test demo to verify shell flow only, then switch to the Tauri desktop runtime for a live connection retry.",
|
||||
};
|
||||
}
|
||||
|
||||
if (activeTestRunning) {
|
||||
return {
|
||||
badgeLabel: "Testing",
|
||||
badgeVariant: "accent",
|
||||
summary: `Running a live connection probe for ${connection.name} on ${databaseLabel}.`,
|
||||
hint: "Keep this scope selected if you want the tested database and retry messaging to update in place.",
|
||||
};
|
||||
}
|
||||
|
||||
if (otherTestingConnectionName) {
|
||||
return {
|
||||
badgeLabel: "Ready",
|
||||
badgeVariant: "neutral",
|
||||
summary: `${connection.name} remains selected on ${databaseLabel} while ${otherTestingConnectionName} finishes a separate live test.`,
|
||||
hint: "The current workspace stays stable. Switch back to the testing connection after the probe completes if you need to review its result.",
|
||||
};
|
||||
}
|
||||
|
||||
if (connection.status === "healthy") {
|
||||
return {
|
||||
badgeLabel: "Ready",
|
||||
badgeVariant: "accent",
|
||||
summary: `${connection.name} last passed a live connection check on ${databaseLabel}.`,
|
||||
hint: "Re-run Test live after changing host, DB, credentials, or after recovering from a Redis outage.",
|
||||
};
|
||||
}
|
||||
|
||||
if (connection.status === "error") {
|
||||
return {
|
||||
badgeLabel: "Retry needed",
|
||||
badgeVariant: "danger",
|
||||
summary: `${connection.name} last failed a live connection check on ${databaseLabel}.`,
|
||||
hint: "Review the banner and connection card detail, adjust the profile if needed, then retry Test live from this same scope.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
badgeLabel: "Ready to test",
|
||||
badgeVariant: "neutral",
|
||||
summary: `${connection.name} is selected on ${databaseLabel} but has not completed a live check in the current session.`,
|
||||
hint: "Run Test live before treating browse, inspect, or command results as current Redis evidence.",
|
||||
};
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildDatabaseSwitcherLabels } from "./database-switcher";
|
||||
|
||||
describe("database switcher helper", () => {
|
||||
it("returns the default operator range from db0 through db15", () => {
|
||||
expect(buildDatabaseSwitcherLabels(0)).toEqual([
|
||||
"db0",
|
||||
"db1",
|
||||
"db2",
|
||||
"db3",
|
||||
"db4",
|
||||
"db5",
|
||||
"db6",
|
||||
"db7",
|
||||
"db8",
|
||||
"db9",
|
||||
"db10",
|
||||
"db11",
|
||||
"db12",
|
||||
"db13",
|
||||
"db14",
|
||||
"db15",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps an active out-of-range database visible and numerically ordered", () => {
|
||||
expect(buildDatabaseSwitcherLabels(23)).toEqual([
|
||||
"db0",
|
||||
"db1",
|
||||
"db2",
|
||||
"db3",
|
||||
"db4",
|
||||
"db5",
|
||||
"db6",
|
||||
"db7",
|
||||
"db8",
|
||||
"db9",
|
||||
"db10",
|
||||
"db11",
|
||||
"db12",
|
||||
"db13",
|
||||
"db14",
|
||||
"db15",
|
||||
"db23",
|
||||
]);
|
||||
});
|
||||
|
||||
it("normalizes negative or fractional inputs back to safe labels", () => {
|
||||
expect(buildDatabaseSwitcherLabels(-3)).toEqual([
|
||||
"db0",
|
||||
"db1",
|
||||
"db2",
|
||||
"db3",
|
||||
"db4",
|
||||
"db5",
|
||||
"db6",
|
||||
"db7",
|
||||
"db8",
|
||||
"db9",
|
||||
"db10",
|
||||
"db11",
|
||||
"db12",
|
||||
"db13",
|
||||
"db14",
|
||||
"db15",
|
||||
]);
|
||||
expect(buildDatabaseSwitcherLabels(4.8, 2)).toEqual([
|
||||
"db0",
|
||||
"db1",
|
||||
"db2",
|
||||
"db4",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
export function buildDatabaseSwitcherLabels(
|
||||
activeDatabase: number,
|
||||
maxDefaultDatabase = 15,
|
||||
) {
|
||||
const normalizedDefaultMax = Math.max(0, Math.floor(maxDefaultDatabase));
|
||||
const normalizedActive = Math.max(0, Math.floor(activeDatabase));
|
||||
const labels = new Set<string>();
|
||||
|
||||
for (let database = 0; database <= normalizedDefaultMax; database += 1) {
|
||||
labels.add(formatDatabaseLabel(database));
|
||||
}
|
||||
|
||||
labels.add(formatDatabaseLabel(normalizedActive));
|
||||
|
||||
return [...labels].sort(compareDatabaseLabels);
|
||||
}
|
||||
|
||||
function compareDatabaseLabels(left: string, right: string) {
|
||||
return parseDatabaseLabel(left) - parseDatabaseLabel(right);
|
||||
}
|
||||
|
||||
function parseDatabaseLabel(value: string) {
|
||||
const parsed = Number.parseInt(value.replace(/^db/i, ""), 10);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
||||
}
|
||||
|
||||
function formatDatabaseLabel(database: number) {
|
||||
return `db${database}`;
|
||||
}
|
||||
@@ -241,8 +241,12 @@ function describeWritePath(context: InspectorReadabilityContext) {
|
||||
return context.isEditable ? "session-local string save" : "inspection only";
|
||||
}
|
||||
|
||||
if (!context.record || context.isLoading) {
|
||||
return "awaiting live read";
|
||||
if (context.isLoading) {
|
||||
return "pending live capability";
|
||||
}
|
||||
|
||||
if (!context.record) {
|
||||
return "value unavailable";
|
||||
}
|
||||
|
||||
if (context.record.data.kind === "missing") {
|
||||
@@ -252,19 +256,16 @@ function describeWritePath(context: InspectorReadabilityContext) {
|
||||
return context.isEditable ? "replace full string" : "inspection only";
|
||||
}
|
||||
|
||||
function buildSurfaceTitle(keyType: string, structured: boolean) {
|
||||
if (!structured && keyType === "string") {
|
||||
return "String value";
|
||||
}
|
||||
|
||||
switch (keyType) {
|
||||
function buildSurfaceTitle(kind: string, liveStructured: boolean) {
|
||||
switch (kind) {
|
||||
case "hash":
|
||||
return "Hash fields";
|
||||
case "list":
|
||||
return "List items";
|
||||
case "set":
|
||||
return "Set members";
|
||||
return liveStructured ? "Set members" : "Set preview";
|
||||
case "sorted_set":
|
||||
case "zset":
|
||||
return "Sorted set members";
|
||||
case "stream":
|
||||
return "Stream entries";
|
||||
@@ -275,29 +276,20 @@ function buildSurfaceTitle(keyType: string, structured: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildStructuredDescription(kind: RedisValueData["kind"]) {
|
||||
function buildStructuredDescription(kind: string) {
|
||||
switch (kind) {
|
||||
case "hash":
|
||||
return "Fields and values are grouped into readable rows so operators can inspect the live hash without falling back to raw JSON.";
|
||||
return "Live HGETALL output is rendered as field/value rows so operators do not need to parse raw JSON during inspection.";
|
||||
case "list":
|
||||
return "List items stay ordered in the inspector and remain read only in the current desktop contract.";
|
||||
return "Live LRANGE output is rendered in list order so operators can scan indexed items without leaving the inspector.";
|
||||
case "set":
|
||||
return "Set members are shown as a structured read-only collection to keep support work fast without implying mutation support.";
|
||||
return "Live SMEMBERS output is rendered as a structured member list so the inspector stays readable even when the payload is dense.";
|
||||
case "sorted_set":
|
||||
return "Members and scores are grouped together for scanability while sorted-set mutation stays outside the current desktop scope.";
|
||||
return "Live ZRANGE output is rendered as member and score pairs so ordering semantics stay visible in the inspector.";
|
||||
case "stream":
|
||||
return "Stream entries remain inspectable in grouped rows so operators can confirm IDs and field payloads without CLI fallback.";
|
||||
return "Live XRANGE output is rendered as entry rows with inline field payloads so stream structure is visible at a glance.";
|
||||
default:
|
||||
return "This value remains readable in the inspector, but mutation support is outside the current desktop contract.";
|
||||
}
|
||||
}
|
||||
|
||||
function formatRedisKeyTypeLabel(type: string) {
|
||||
switch (type) {
|
||||
case "sorted_set":
|
||||
return "zset";
|
||||
default:
|
||||
return type;
|
||||
return "The live value is available on a structured read-only path in the current desktop contract.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,23 +306,35 @@ function formatRedisKeyTtl(ttl: RedisKeyTtl) {
|
||||
}
|
||||
}
|
||||
|
||||
function formatDurationFromMillis(milliseconds: number) {
|
||||
if (milliseconds < 1000) {
|
||||
return `${milliseconds}ms`;
|
||||
function formatDurationFromMillis(value: number) {
|
||||
if (value < 1_000) {
|
||||
return `${value}ms`;
|
||||
}
|
||||
|
||||
const totalSeconds = Math.round(milliseconds / 1000);
|
||||
const totalSeconds = Math.floor(value / 1_000);
|
||||
if (totalSeconds < 60) {
|
||||
return `${totalSeconds}s`;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
if (minutes < 60) {
|
||||
return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`;
|
||||
const totalMinutes = Math.floor(totalSeconds / 60);
|
||||
if (totalMinutes < 60) {
|
||||
return `${totalMinutes}m`;
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
return remainingMinutes === 0 ? `${hours}h` : `${hours}h ${remainingMinutes}m`;
|
||||
const totalHours = Math.floor(totalMinutes / 60);
|
||||
if (totalHours < 24) {
|
||||
return `${totalHours}h`;
|
||||
}
|
||||
|
||||
const totalDays = Math.floor(totalHours / 24);
|
||||
return `${totalDays}d`;
|
||||
}
|
||||
|
||||
function formatRedisKeyTypeLabel(value: string) {
|
||||
switch (value) {
|
||||
case "zset":
|
||||
return "sorted set";
|
||||
default:
|
||||
return value || "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildMutationGuardDialogContent,
|
||||
canRemoveTtl,
|
||||
hasMeaningfulStringChange,
|
||||
hasMeaningfulTtlChange,
|
||||
} from "./mutation-guardrails";
|
||||
|
||||
describe("mutation guardrails", () => {
|
||||
it("summarizes live string replacement with current and next payload previews", () => {
|
||||
expect(
|
||||
buildMutationGuardDialogContent({
|
||||
kind: "value_write",
|
||||
runtimeMode: "live",
|
||||
keyName: "cache:homepage",
|
||||
databaseLabel: "db0",
|
||||
currentValue: "hello",
|
||||
nextValue: "hello world",
|
||||
}),
|
||||
).toEqual({
|
||||
title: "Replace string value for cache:homepage?",
|
||||
description: "Review the scoped string save for db0 before continuing.",
|
||||
currentLabel: "Current value",
|
||||
currentValue: '5 chars, 1 line, preview "hello"',
|
||||
nextLabel: "Next value",
|
||||
nextValue: '11 chars, 1 line, preview "hello world"',
|
||||
warning: "This replaces the full live string value and keeps the current TTL boundary.",
|
||||
confirmLabel: "Save live string",
|
||||
});
|
||||
});
|
||||
|
||||
it("summarizes TTL update in browser demo mode", () => {
|
||||
expect(
|
||||
buildMutationGuardDialogContent({
|
||||
kind: "ttl_update",
|
||||
runtimeMode: "demo",
|
||||
keyName: "session:42",
|
||||
databaseLabel: "db5",
|
||||
currentTtl: "persistent",
|
||||
nextTtl: "1m",
|
||||
}),
|
||||
).toEqual({
|
||||
title: "Apply TTL to session:42?",
|
||||
description: "Review the scoped TTL change for db5 before continuing.",
|
||||
currentLabel: "Current TTL",
|
||||
currentValue: "persistent",
|
||||
nextLabel: "Next TTL",
|
||||
nextValue: "1m",
|
||||
warning: "This only updates the session-local browser demo state.",
|
||||
confirmLabel: "Set demo TTL",
|
||||
});
|
||||
});
|
||||
|
||||
it("summarizes live TTL removal", () => {
|
||||
expect(
|
||||
buildMutationGuardDialogContent({
|
||||
kind: "ttl_remove",
|
||||
runtimeMode: "live",
|
||||
keyName: "smoke:string:ttl",
|
||||
databaseLabel: "db0",
|
||||
currentTtl: "48s",
|
||||
}),
|
||||
).toEqual({
|
||||
title: "Remove TTL from smoke:string:ttl?",
|
||||
description: "Review the scoped TTL removal for db0 before continuing.",
|
||||
currentLabel: "Current TTL",
|
||||
currentValue: "48s",
|
||||
nextLabel: "Next TTL",
|
||||
nextValue: "persistent",
|
||||
warning: "This removes the live expiry and makes the key persistent.",
|
||||
confirmLabel: "Remove live TTL",
|
||||
});
|
||||
});
|
||||
|
||||
it("detects no-op string saves", () => {
|
||||
expect(hasMeaningfulStringChange("hello", "hello")).toBe(false);
|
||||
expect(hasMeaningfulStringChange("hello", "hello world")).toBe(true);
|
||||
});
|
||||
|
||||
it("detects exact live TTL no-ops and formatted demo TTL no-ops", () => {
|
||||
expect(
|
||||
hasMeaningfulTtlChange({ kind: "expires_in_millis", value: 60_000 }, 60_000),
|
||||
).toBe(false);
|
||||
expect(hasMeaningfulTtlChange({ kind: "display", value: "1m" }, 60_000)).toBe(false);
|
||||
expect(hasMeaningfulTtlChange({ kind: "display", value: "1m" }, 120_000)).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks TTL removal when the key is already persistent", () => {
|
||||
expect(canRemoveTtl({ kind: "persistent" })).toBe(false);
|
||||
expect(canRemoveTtl({ kind: "display", value: "persistent" })).toBe(false);
|
||||
expect(canRemoveTtl({ kind: "expires_in_millis", value: 10_000 })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,178 +0,0 @@
|
||||
export type MutationGuardRuntimeMode = "live" | "demo";
|
||||
|
||||
export type ComparableTtlState =
|
||||
| { kind: "persistent" }
|
||||
| { kind: "missing" }
|
||||
| { kind: "expires_in_millis"; value: number }
|
||||
| { kind: "display"; value: string };
|
||||
|
||||
export type MutationGuardDialogInput =
|
||||
| {
|
||||
kind: "value_write";
|
||||
runtimeMode: MutationGuardRuntimeMode;
|
||||
keyName: string;
|
||||
databaseLabel: string;
|
||||
currentValue: string;
|
||||
nextValue: string;
|
||||
}
|
||||
| {
|
||||
kind: "ttl_update";
|
||||
runtimeMode: MutationGuardRuntimeMode;
|
||||
keyName: string;
|
||||
databaseLabel: string;
|
||||
currentTtl: string;
|
||||
nextTtl: string;
|
||||
}
|
||||
| {
|
||||
kind: "ttl_remove";
|
||||
runtimeMode: MutationGuardRuntimeMode;
|
||||
keyName: string;
|
||||
databaseLabel: string;
|
||||
currentTtl: string;
|
||||
};
|
||||
|
||||
export type MutationGuardDialogContent = {
|
||||
title: string;
|
||||
description: string;
|
||||
currentLabel: string;
|
||||
currentValue: string;
|
||||
nextLabel: string;
|
||||
nextValue: string;
|
||||
warning: string;
|
||||
confirmLabel: string;
|
||||
};
|
||||
|
||||
export function buildMutationGuardDialogContent(
|
||||
input: MutationGuardDialogInput,
|
||||
): MutationGuardDialogContent {
|
||||
switch (input.kind) {
|
||||
case "value_write":
|
||||
return {
|
||||
title: `Replace string value for ${input.keyName}?`,
|
||||
description: `Review the scoped string save for ${input.databaseLabel} before continuing.`,
|
||||
currentLabel: "Current value",
|
||||
currentValue: describeStringPayload(input.currentValue),
|
||||
nextLabel: "Next value",
|
||||
nextValue: describeStringPayload(input.nextValue),
|
||||
warning:
|
||||
input.runtimeMode === "live"
|
||||
? "This replaces the full live string value and keeps the current TTL boundary."
|
||||
: "This only updates the session-local browser demo state.",
|
||||
confirmLabel:
|
||||
input.runtimeMode === "live" ? "Save live string" : "Save demo string",
|
||||
};
|
||||
case "ttl_update":
|
||||
return {
|
||||
title: `Apply TTL to ${input.keyName}?`,
|
||||
description: `Review the scoped TTL change for ${input.databaseLabel} before continuing.`,
|
||||
currentLabel: "Current TTL",
|
||||
currentValue: input.currentTtl,
|
||||
nextLabel: "Next TTL",
|
||||
nextValue: input.nextTtl,
|
||||
warning:
|
||||
input.runtimeMode === "live"
|
||||
? "This changes the live expiry for the selected key in Redis."
|
||||
: "This only updates the session-local browser demo state.",
|
||||
confirmLabel: input.runtimeMode === "live" ? "Set live TTL" : "Set demo TTL",
|
||||
};
|
||||
case "ttl_remove":
|
||||
return {
|
||||
title: `Remove TTL from ${input.keyName}?`,
|
||||
description: `Review the scoped TTL removal for ${input.databaseLabel} before continuing.`,
|
||||
currentLabel: "Current TTL",
|
||||
currentValue: input.currentTtl,
|
||||
nextLabel: "Next TTL",
|
||||
nextValue: "persistent",
|
||||
warning:
|
||||
input.runtimeMode === "live"
|
||||
? "This removes the live expiry and makes the key persistent."
|
||||
: "This only updates the session-local browser demo state.",
|
||||
confirmLabel:
|
||||
input.runtimeMode === "live" ? "Remove live TTL" : "Remove demo TTL",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: "Review mutation",
|
||||
description: "Review the scoped mutation before continuing.",
|
||||
currentLabel: "Current",
|
||||
currentValue: "n/a",
|
||||
nextLabel: "Next",
|
||||
nextValue: "n/a",
|
||||
warning: "",
|
||||
confirmLabel: "Confirm",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function hasMeaningfulStringChange(currentValue: string, nextValue: string) {
|
||||
return currentValue !== nextValue;
|
||||
}
|
||||
|
||||
export function hasMeaningfulTtlChange(
|
||||
currentTtl: ComparableTtlState,
|
||||
nextTtlMillis: number,
|
||||
) {
|
||||
switch (currentTtl.kind) {
|
||||
case "expires_in_millis":
|
||||
return currentTtl.value !== nextTtlMillis;
|
||||
case "display":
|
||||
return currentTtl.value !== formatDurationFromMillis(nextTtlMillis);
|
||||
case "persistent":
|
||||
case "missing":
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function canRemoveTtl(currentTtl: ComparableTtlState) {
|
||||
switch (currentTtl.kind) {
|
||||
case "persistent":
|
||||
return false;
|
||||
case "display":
|
||||
return currentTtl.value !== "persistent";
|
||||
case "expires_in_millis":
|
||||
case "missing":
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function describeStringPayload(value: string) {
|
||||
const lines = value.split(/\r?\n/).length;
|
||||
const compactPreview = value.replace(/\s+/g, " ").trim();
|
||||
const preview = compactPreview ? truncateText(compactPreview, 56) : "(empty string)";
|
||||
|
||||
return `${value.length} chars, ${lines} line${lines === 1 ? "" : "s"}, preview ${JSON.stringify(preview)}`;
|
||||
}
|
||||
|
||||
function truncateText(value: string, maxLength: number) {
|
||||
if (value.length <= maxLength) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return `${value.slice(0, maxLength - 3)}...`;
|
||||
}
|
||||
|
||||
function formatDurationFromMillis(value: number) {
|
||||
if (value < 1_000) {
|
||||
return `${value}ms`;
|
||||
}
|
||||
|
||||
const totalSeconds = Math.floor(value / 1_000);
|
||||
if (totalSeconds < 60) {
|
||||
return `${totalSeconds}s`;
|
||||
}
|
||||
|
||||
const totalMinutes = Math.floor(totalSeconds / 60);
|
||||
if (totalMinutes < 60) {
|
||||
return `${totalMinutes}m`;
|
||||
}
|
||||
|
||||
const totalHours = Math.floor(totalMinutes / 60);
|
||||
if (totalHours < 24) {
|
||||
return `${totalHours}h`;
|
||||
}
|
||||
|
||||
const totalDays = Math.floor(totalHours / 24);
|
||||
return `${totalDays}d`;
|
||||
}
|
||||
@@ -45,10 +45,8 @@ Use a four-zone workspace:
|
||||
|
||||
- Connection library
|
||||
- New connection form entry point
|
||||
- Active connection session summary with clear ready/testing/demo/retry states
|
||||
- Active DB switcher
|
||||
- Safety posture and backend boundary summary
|
||||
- Local smoke fixture assist for repeatable QA runs
|
||||
|
||||
### Browser pane
|
||||
|
||||
@@ -86,38 +84,24 @@ Use a four-zone workspace:
|
||||
- `apps/desktop` now runs on React and Tailwind-based styling.
|
||||
- The shell now consumes the existing Tauri bridge for `backend_bootstrap`, `test_redis_connection`, `browse_redis_keys`, `read_redis_value`, `write_redis_value`, and `execute_redis_command`.
|
||||
- The inspector now consumes `update_redis_key_ttl` for live TTL set and persist actions in desktop runtime.
|
||||
- The repository now also contains a visible string-only `Add key` dialog wired to `create_redis_value`, but that capability remains a separately tracked next-phase surface rather than part of this baseline milestone's required acceptance.
|
||||
- shadcn-style UI primitives are used locally for buttons, inputs, textareas, badges, and dialogs.
|
||||
- Browser-only dev mode uses explicit demo fallback so the visible shell can still be reviewed without a Tauri runtime.
|
||||
- Tauri desktop mode now allows live connection tests, live key browsing, live value inspection, string save for supported keys, live TTL updates, and live command execution while keeping browser dev mode on explicit demo fallback.
|
||||
- Non-string live values now render in structured read-only panels for `hash`, `list`, `set`, `zset`, and `stream`, so operators can scan typed payloads without reading raw JSON dumps.
|
||||
- The visible shell now validates draft-connection name, host, port, and DB input before creation, and prevents duplicate profile names inside the local shell state.
|
||||
- The active connection area now also exposes a dedicated session summary so operators can see whether the current scope is ready, testing, demo-only, or waiting for retry without inferring that state from scattered card copy.
|
||||
- Live connection test completion now only reapplies the returned DB scope when the same connection is still active, preventing a stale test response from silently replacing the operator's newer active workspace selection.
|
||||
- The DB switcher now exposes a predictable default operator range from `db0` through `db15` and keeps an out-of-range active DB visible instead of depending on a four-item hardcoded list.
|
||||
- Zero-result key searches now place the inspector into an explicit empty state instead of leaving a stale key visible.
|
||||
- Frontend error handling now routes stable backend error codes through a shared operator-facing notice layer so live failures render with consistent copy and banner tone across connection, browse, inspect, write, TTL, and command flows.
|
||||
- The inspector now also renders a dedicated readability layer with explicit inspect status, value-surface semantics, next-step guidance, and key facts so string, structured, missing, and demo-fallback states are easier to interpret.
|
||||
- Current-phase writable actions now require a scoped review step before string save, TTL set, or TTL removal proceeds, and no-op submissions are blocked before they reach the live bridge or demo fallback.
|
||||
- The utility rail now exposes a bounded local smoke assist surface with fixture targeting, `smoke:*` focus, safe command presets, and the seeded key list from the local smoke runbook.
|
||||
- Browser-only demo mode now keeps session-local mock string-save and TTL changes visible in the browser, inspector, and mock command responses so shell review does not claim success while leaving stale UI state behind.
|
||||
|
||||
## QA Acceptance For This Milestone
|
||||
|
||||
- The shell renders four distinct zones: rail, browser, inspector, command tray.
|
||||
- The header keeps app version, runtime mode, and build mode visible for smoke validation.
|
||||
- The utility rail exposes a copyable QA snapshot with runtime, connection, selection, and latest command context.
|
||||
- The utility rail exposes the seeded local smoke fixture path and safe smoke shortcuts without requiring manual re-entry from the runbook.
|
||||
- The active connection and active DB stay visible without opening a modal.
|
||||
- The active connection area shows a dedicated session summary with explicit ready, testing, demo fallback, or retry-needed wording plus a visible next-step hint.
|
||||
- The DB switcher exposes `db0` through `db15` by default and still keeps the current DB visible when the active scope falls outside that range.
|
||||
- Search filters the browser list and shows a no-results state.
|
||||
- Search now exposes explicit mode feedback so operators can see whether the current input is being treated as a contains search or a raw Redis pattern.
|
||||
- Search now exposes a bounded clear action in the browser header so operators can return to normal browse state without manually deleting the entire input.
|
||||
- When search returns zero keys, the inspector switches to an empty state instead of showing the last selected key.
|
||||
- Empty browser and inspector states now distinguish between an empty DB and a filtered no-match result instead of using one generic no-results sentence.
|
||||
- The connection action distinguishes desktop live testing from browser demo fallback.
|
||||
- If a live connection test finishes after the operator switches to a different connection, the tested connection card updates but the newly active DB scope remains unchanged.
|
||||
- Invalid draft connections cannot be created from the visible shell.
|
||||
- Live backend failures are shown through a consistent banner treatment instead of per-surface ad hoc copy.
|
||||
- Non-string keys are explicitly read-only.
|
||||
@@ -125,10 +109,6 @@ Use a four-zone workspace:
|
||||
- The inspector keeps current status, value semantics, action guidance, and key facts visible without requiring operators to infer meaning from badges or raw payload shape alone.
|
||||
- String keys show a save affordance near the value surface.
|
||||
- TTL controls stay beside the inspector metadata and expose both set and remove flows.
|
||||
- String save and TTL mutations now require a second review step that repeats scoped current-vs-next state before continuing.
|
||||
- Failed save and TTL attempts keep the current draft value or TTL input in place so operators can adjust and retry without rebuilding context.
|
||||
- Browser demo mode keeps mock string-save and TTL changes visible for the current session while still labeling the runtime as non-live.
|
||||
- Current baseline sign-off does not require the visible `Add key` surface to pass or fail; keep any Add Key evidence separate unless PM explicitly promotes it into the active milestone.
|
||||
- Delete requires a second confirmation step that includes key name and DB context.
|
||||
- The command tray visibly blocks dangerous administrative commands.
|
||||
- Command history marks whether each result came from the live desktop bridge or the demo shell.
|
||||
@@ -145,7 +125,7 @@ pnpm run desktop:dev
|
||||
|
||||
Notes:
|
||||
|
||||
- `pnpm run desktop:dev` is the browser shell path. It supports layout review and session-local demo fallback only; mock save and TTL actions update visible UI state, but no Tauri or Redis mutation occurs.
|
||||
- `pnpm run desktop:dev` is the browser shell path. It supports layout review and demo fallback only.
|
||||
- `pnpm run desktop:tauri:dev` is the desktop integration path. Use it when QA or backend needs to validate the existing command bridge against a live Redis target.
|
||||
- `pnpm --filter @redis-gui/desktop test` should stay green before handing frontend foundation changes to QA or other feature teams.
|
||||
- Use `pnpm run desktop:build` for a production bundle check before handing the shell to QA or integration work.
|
||||
|
||||
@@ -1,51 +1,39 @@
|
||||
# Redis GUI V1 Acceptance Matrix
|
||||
|
||||
Date: 2026-03-31
|
||||
Date: 2026-03-27
|
||||
Owner: QA Engineer
|
||||
|
||||
## Evidence Collected On 2026-03-31
|
||||
## Evidence Collected On 2026-03-27
|
||||
|
||||
- `cargo test -p redis-core` passes in this session with 20 tests green.
|
||||
- `pnpm --filter @redis-gui/desktop test` passes in this session with 12 files and 47 tests green.
|
||||
- A fresh `cargo test -p redis-core` rerun passes in this session with 16 tests green.
|
||||
- `pnpm --filter @redis-gui/desktop test` passes in this session with 8 files and 30 tests green.
|
||||
- `pnpm install --lockfile-only` passes at repository root after adding `pnpm-workspace.yaml`.
|
||||
- `pnpm run desktop:build` passes and emits `apps/desktop/dist/`.
|
||||
- `pnpm run desktop:dev` starts Vite on `http://127.0.0.1:1420/`, and the served HTML points to the desktop shell entrypoint.
|
||||
- `pnpm run desktop:tauri:build` now completes Linux `.deb` and `.rpm` artifact generation under `target/release/bundle/`.
|
||||
- `pnpm run desktop:tauri:build:all` still fails later in the AppImage stage because `linuxdeploy` aborts in the current environment.
|
||||
- `dpkg-deb -I target/release/bundle/deb/Redis GUI Foundation_0.1.0_amd64.deb` confirms Debian package metadata and runtime dependencies on `libwebkit2gtk-4.1-0` and `libgtk-3-0`.
|
||||
- Direct launch of `target/release/desktop-shell` in this heartbeat fails before window creation with `Failed to initialize GTK`, because the current session has no `DISPLAY`, `WAYLAND_DISPLAY`, or `xvfb-run`.
|
||||
- `./scripts/redis-fixture/start.sh`, `seed.sh`, and `stop.sh` pass in this session, and the seeded fixture exposes 7 `smoke:*` keys with expected string, TTL, hash, list, set, zset, and stream evidence.
|
||||
- `./scripts/redis-fixture/seed.sh` also fails correctly when the fixture container is absent, with an explicit instruction to run `start.sh` first.
|
||||
- The repository still contains no `Dockerfile`, `docker-compose.yml`, `compose.yaml`, or equivalent compose asset; Docker-backed verification currently depends on the documented fixture scripts plus a reachable local Docker daemon.
|
||||
- `pnpm run desktop:tauri:build` now completes Linux `.deb` and `.rpm` artifact generation under `target/release/bundle/`, then fails later in the AppImage stage with `io: Connection reset by peer (os error 104)`.
|
||||
- The repository now contains a Rust workspace, a React workspace UI baseline, Tauri commands for connection testing and command execution, and shared frontend scope documentation.
|
||||
- Browser dev mode still uses demo data for shell review, while Tauri desktop mode can now exercise live connection-test, key browse, value read/write, and command-execution bridges.
|
||||
- The checked-out desktop shell now also exposes a visible string-only `Add key` flow through `create_redis_value`, but PM phase notes still treat that capability as next-phase inventory rather than a required current-baseline acceptance item.
|
||||
- Browser dev mode now also keeps mock string-save and TTL changes visible for the current session, reducing shell-review drift between action banners and visible UI state without changing the live acceptance boundary.
|
||||
- The active connection area now shows an explicit session summary for ready, testing, demo fallback, and retry-needed states, and live test completion no longer rewrites the current active DB if the operator already switched to another connection while the probe was running.
|
||||
- The desktop DB switcher now exposes a default `db0` through `db15` range and keeps an active out-of-range DB visible in the selector.
|
||||
- A repeatable local Redis fixture and smoke runbook now exist in `scripts/redis-fixture/` and `docs/qa/local-desktop-smoke.md`.
|
||||
- `docs/frontend-workspace-baseline.md` and `docs/redis-gui-v1-product-requirements.md` now both exist in-repo.
|
||||
|
||||
## Matrix
|
||||
|
||||
| Scope | Happy Path Acceptance | Failure Path Acceptance | Required Evidence | Current Status |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Linux app smoke | App installs or launches from a documented command, opens main window, connects to test Redis, exits cleanly | Missing Redis, bad host, bad password, unsupported DB or network loss surface actionable error and app stays stable | Launch steps, versioned artifact or dev command, screenshots, QA run log | Partially blocked: documented fixture, package artifacts, and smoke path now exist, but this heartbeat had no GUI-capable display session, so real Tauri window launch-and-connect evidence is still pending |
|
||||
| Linux app smoke | App installs or launches from a documented command, opens main window, connects to test Redis, exits cleanly | Missing Redis, bad host, bad password, unsupported DB or network loss surface actionable error and app stays stable | Launch steps, versioned artifact or dev command, screenshots, QA run log | Partially blocked: documented fixture and smoke path now exist, but a full Linux launch-and-connect evidence set is still pending |
|
||||
| macOS app smoke | Same as Linux, using signed or documented macOS artifact | Same failure paths, plus first-run permission or gatekeeper guidance if relevant | Build artifact name, install steps, screenshots, QA run log | Blocked: no validated macOS build or artifact evidence |
|
||||
| Windows app smoke | Same as Linux, using installer or portable package | Same failure paths, plus Windows-specific install and uninstall validation | Build artifact name, install steps, screenshots, QA run log | Blocked: no validated Windows build or artifact evidence |
|
||||
| Desktop workspace shell | Rail, browser, inspector, and command tray render together with active connection, build identity, runtime mode, a copyable QA snapshot, local smoke assist, visible inspector summary/key facts surfaces, and scoped mutation review for current-phase write actions | Empty search state clears the inspector selection, search mode feedback and clear action stay visible and predictable, read-only non-string value state, inspector status/action guidance stays aligned to live vs demo vs missing cases, destructive confirmation, no-op write attempts are blocked before execution, and browser-mode demo fallback stays explicit while mock save and TTL actions update session-local UI state | `npm run desktop:dev` or `npm run desktop:build`, `docs/frontend-workspace-baseline.md`, screenshots or copied QA snapshot text | Pass for shell-level evidence; not a substitute for Redis feature acceptance |
|
||||
| Connection management | Create a validated draft profile, keep DB context visible, switch among a predictable default DB range, run the existing live connection-test bridge from Tauri desktop mode, and keep the active session state visibly scoped as ready, testing, demo fallback, or retry needed | Invalid host, bad port, bad password, duplicate name, timeout, and unreachable server are handled with consistent operator-facing messaging, and a late-arriving test result must not silently replace a newer active connection/DB selection | Tauri run log, screenshots, repeatable steps, Redis target | Partially blocked: the shell now validates draft name/host/port/DB locally, exposes `db0` through `db15` while preserving active out-of-range DB context, maps backend error codes into shared notices, keeps session-state guidance visible, and calls the real connection-test command without reapplying stale DB scope after an operator switch, but profile persistence and full evidence capture are not complete |
|
||||
| Desktop workspace shell | Rail, browser, inspector, and command tray render together with active connection, build identity, runtime mode, and a copyable QA snapshot visible; command history labels live vs demo results; inspector summary and key facts stay visible for the active key | Empty search state clears the inspector selection, read-only non-string value state, inspector status/action guidance stays aligned to live vs demo vs missing cases, destructive confirmation, and browser-mode demo fallback stay stable and explicit | `npm run desktop:dev` or `npm run desktop:build`, `docs/frontend-workspace-baseline.md`, screenshots or copied QA snapshot text | Pass for shell-level evidence; not a substitute for Redis feature acceptance |
|
||||
| Connection management | Create a validated draft profile, keep DB context visible, and run the existing live connection-test bridge from Tauri desktop mode | Invalid host, bad port, bad password, duplicate name, timeout, and unreachable server are handled with consistent operator-facing messaging | Tauri run log, screenshots, repeatable steps, Redis target | Partially blocked: the shell now validates draft name/host/port/DB locally, maps backend error codes into shared notices, and calls the real connection-test command, but profile persistence and full evidence capture are not complete |
|
||||
| Key browsing | Browse DBs and keys, paginate or virtualize large lists, view metadata | Empty DB, deleted key during browse, permission error, and connection drop do not crash UI, and live failures reuse the shared error banner treatment | Seed dataset, result screenshots, observed limits | Partially blocked: desktop UI now calls the live browse bridge with `SCAN` pagination and metadata, but fixture-backed Tauri screenshots and measured limits are still pending |
|
||||
| Search | Search by key name or pattern and return matching set within documented limits; visible shell makes it clear whether the current input is treated as a contains search or a raw Redis pattern | No matches, invalid pattern, empty DB state, and very large result sets are handled predictably, and operators can clear search back to browse state without ambiguity | Seed dataset, search queries, measured behavior | Partially blocked: desktop UI now maps search to live browse requests and exposes clearer search-state feedback, but fixture-backed evidence and measured limits are still pending |
|
||||
| Value editing | Open supported value types, review pending string replacement, confirm save, and refresh persisted value; inspector keeps current state and write semantics explicit while editing | Concurrent modification, invalid payload, missing-key transitions, and permission error preserve user context, while no-op edits are blocked before execution and live failures reuse the shared operator notice layer | Before/after values, screenshots, repeatable steps | Partially blocked: string read/save/refresh is wired through the live desktop bridge, non-string types now render in structured read-only inspector panels, inspector guidance is clearer across live and fallback states, scoped review now clarifies current-vs-next value before save, and browser demo mode keeps session-local save state coherent, but fixture-backed live evidence and broader editing scope remain pending |
|
||||
| TTL operations | View TTL, review pending TTL change or removal, confirm the mutation, and verify the resulting expiration behavior | Invalid TTL, already persistent/no-op TTL requests, already expired key, and permission error are handled safely with explicit guardrails and consistent banner feedback | Seed dataset, timestamps, verification steps | Partially blocked: desktop UI now exposes live TTL set and remove controls through the existing contract, blocks no-op TTL submissions, and keeps browser demo TTL state coherent, but seeded-fixture live validation evidence is still missing |
|
||||
| Search | Search by key name or pattern and return matching set within documented limits | No matches, invalid pattern, and very large result sets are handled predictably | Seed dataset, search queries, measured behavior | Partially blocked: desktop UI now maps search to live browse requests, but fixture-backed evidence and measured limits are still pending |
|
||||
| Value editing | Open supported value types, edit, save, and refresh persisted value; inspector keeps current state and write semantics explicit while editing | Concurrent modification, invalid payload, missing-key transitions, and permission error preserve user context and surface failure through the shared operator notice layer | Before/after values, screenshots, repeatable steps | Partially blocked: string read/save/refresh is wired through the live desktop bridge, non-string types now render in structured read-only inspector panels, and the inspector guidance is clearer across live and fallback states, but fixture-backed evidence and broader editing scope remain pending |
|
||||
| TTL operations | View TTL, set TTL, remove TTL, and verify expiration behavior | Invalid TTL, already expired key, and permission error handled safely with consistent banner feedback | Seed dataset, timestamps, verification steps | Partially blocked: desktop UI now exposes live TTL set and remove controls through the existing contract, but seeded-fixture validation evidence is still missing |
|
||||
| Command console | Execute safe Redis commands in Tauri desktop mode and show result or error output with explicit live/demo labeling | Invalid command, long-running command, and blocked dangerous command are handled predictably, and backend failures reuse the shared operator notice mapping | Command transcript, screenshots, allow or deny rules | Partially blocked: the desktop UI now calls the real command execution bridge, but it is not yet validated against a shared live Redis fixture and dangerous mutations still stay blocked at shell level |
|
||||
| Dangerous operation confirmation | Delete, flush, overwrite, or destructive commands require explicit confirmation | Cancel path does not mutate data; confirm path mutates only intended scope | Confirmation UX screenshots, before/after data | Blocked: feature not implemented |
|
||||
| Installer and packaging | Package metadata, icon, version, and uninstall behavior are correct on each target OS | Corrupt install, upgrade, downgrade, and missing dependency behavior are documented or handled | Artifact checklist, install logs, uninstall evidence | Partially blocked: Linux `.deb` metadata and contents are inspectable and the standard `.deb`/`.rpm` build path is stable, but AppImage remains blocked and install or uninstall evidence is still absent |
|
||||
| Installer and packaging | Package metadata, icon, version, and uninstall behavior are correct on each target OS | Corrupt install, upgrade, downgrade, and missing dependency behavior are documented or handled | Artifact checklist, install logs, uninstall evidence | Partially blocked: Linux `.deb` and `.rpm` artifacts now exist, but AppImage failed and install or uninstall evidence is still absent |
|
||||
|
||||
## Current Acceptance Judgment
|
||||
|
||||
- No Redis-backed Redis GUI V1 feature acceptance item can be marked passed on 2026-03-31.
|
||||
- Current repository state now supports shell-level frontend acceptance and partial backend feature review: the React workspace shell builds, live desktop bridges exist for connection test, browse, string create, value read/write, and command execution, draft-validation and empty-state behavior are explicit, and shared UI scope documentation is present in-repo.
|
||||
- The visible `Add key` surface should be tracked as implemented next-phase inventory only; its presence in the repository must not be treated as automatic current-baseline pass criteria or release scope expansion.
|
||||
- Rust source-level test evidence is current for `redis-core`, the desktop test baseline is current, the browser demo entrypoint is reproducible, Linux package artifacts exist for `.deb` and `.rpm`, Debian metadata is inspectable, and the fixture path is reproducible on both success and one key failure path, but live Redis screenshots or logs and installation evidence are still required.
|
||||
- QA can now independently execute the documented Linux baseline path, and the phase-level evidence loop is summarized in `docs/qa/current-phase-evidence-loop.md`, but feature acceptance and release sign-off remain blocked on a GUI-capable desktop session for end-to-end evidence, AppImage packaging, cross-platform evidence, and installer install or uninstall checks.
|
||||
- No Redis-backed Redis GUI V1 feature acceptance item can be marked passed on 2026-03-27.
|
||||
- Current repository state now supports shell-level frontend acceptance and partial backend feature review: the React workspace shell builds, live desktop bridges exist for connection test, browse, value read/write, and command execution, draft-validation and empty-state behavior are explicit, and shared UI scope documentation is present in-repo.
|
||||
- Rust source-level test evidence is now current for `redis-core`, Linux package artifacts exist for `.deb` and `.rpm`, but live Redis screenshots and installation evidence are still required.
|
||||
- QA can now reuse the documented fixture and smoke path, but feature acceptance and release sign-off remain blocked on end-to-end desktop evidence.
|
||||
|
||||
@@ -73,7 +73,7 @@ Seeded keys:
|
||||
4. In the desktop app, connect to host `127.0.0.1`, port `6380`, database `0`, no password, TLS disabled.
|
||||
5. Verify the desktop window opens, app version plus runtime mode are visible in the header, and the connection test succeeds.
|
||||
6. Verify the command console can run a safe read, such as `PING` or `GET smoke:string`.
|
||||
7. Verify the seeded keys appear with expected types and TTL state in the browser and inspector surfaces.
|
||||
7. Verify the seeded keys appear with expected types and TTL state in the browser and inspector surfaces, and confirm the inspector summary plus key facts stay aligned to the selected key type and current runtime.
|
||||
8. Change TTL on `smoke:string:ttl`, remove TTL from the same key, and confirm the updated TTL state is reflected in the inspector.
|
||||
9. Capture screenshots or terminal output for build, launch, connection success, key browse, TTL update, and command execution. If screenshots are unavailable, copy the in-app QA snapshot text into the smoke log.
|
||||
10. Run `./scripts/redis-fixture/stop.sh` after the session.
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
# HAC-28 Connection Context Lifecycle
|
||||
|
||||
Date: 2026-03-31
|
||||
Owner: Senior Frontend Engineer
|
||||
Issue: HAC-28
|
||||
|
||||
## Goal
|
||||
|
||||
Harden connection-context and session-state clarity without changing backend contracts or pulling next-phase scope into `desktop-v1`.
|
||||
|
||||
## Scope
|
||||
|
||||
- keep connection status copy consistent between the header, connection cards, and the active-session summary
|
||||
- make ready, checking, demo, and retry-needed states visibly distinct in the utility rail
|
||||
- prevent a finished live connection test from yanking the current active DB after the operator has already switched to another connection
|
||||
- keep retry guidance explicit when a live test fails
|
||||
- add focused frontend tests around the connection-session helper
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- no Add Key expansion
|
||||
- no delete execution
|
||||
- no non-string editing
|
||||
- no visual refresh
|
||||
- no backend contract changes
|
||||
|
||||
## Delivery Shape
|
||||
|
||||
1. Add a shared connection-session helper for:
|
||||
- connection test state transitions
|
||||
- consistent status and connection-card copy
|
||||
- visible active-session summary copy
|
||||
2. Update `apps/desktop/src/App.tsx` so:
|
||||
- the utility rail shows a clear active connection session card
|
||||
- the test button distinguishes active-scope testing from another in-flight connection test
|
||||
- live test completion only reapplies the returned DB scope when the same connection is still active
|
||||
3. Revalidate frontend tests and production build.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- connection session state is visible as ready, testing, demo fallback, or retry needed
|
||||
- connection-card detail and header status use the same wording model
|
||||
- switching to another connection during a live test does not silently replace the current active DB when the earlier test returns
|
||||
- failed live tests leave a clear retry path in visible UI copy
|
||||
- `pnpm --filter @redis-gui/desktop test` passes
|
||||
- `pnpm run desktop:build` passes
|
||||
46
plans/2026-03-31-hac-30-inspector-readability.md
Normal file
46
plans/2026-03-31-hac-30-inspector-readability.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# HAC-30 Inspector Readability
|
||||
|
||||
Date: 2026-03-31
|
||||
Owner: Senior Frontend Engineer
|
||||
Issue: HAC-30
|
||||
|
||||
## Goal
|
||||
|
||||
Improve inspector readability and operator understanding without changing backend contracts or pulling next-phase scope into the current desktop-v1 slice.
|
||||
|
||||
## Scope
|
||||
|
||||
- clarify current inspector state for live, demo, loading, and missing-key paths
|
||||
- make value-surface semantics explicit for string and structured types
|
||||
- keep next-step operator guidance visible near the inspector surface
|
||||
- expose key facts in a stable summary card instead of relying on scattered badges and support copy
|
||||
- add focused frontend tests for the readability helper
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- no Add Key expansion
|
||||
- no delete execution contract
|
||||
- no non-string edit support
|
||||
- no visual-theme refresh
|
||||
- no bilingual UI pass
|
||||
|
||||
## Delivery Shape
|
||||
|
||||
1. Add a shared inspector-readability helper that maps selected key, live record, runtime mode, and edit capability into:
|
||||
- inspect status
|
||||
- value-surface title and description
|
||||
- next-step operator guidance
|
||||
- key facts
|
||||
2. Use that helper in `apps/desktop/src/App.tsx` to:
|
||||
- show an explicit inspector status badge
|
||||
- add a semantic summary block above the value surface
|
||||
- replace the previous generic support note with summary and facts cards
|
||||
3. Revalidate frontend tests and production build.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- inspector makes live string, structured read-only, demo fallback, loading, and missing-key paths visibly distinct
|
||||
- operators can identify scope, type, TTL state, value shape, and write path without inferring them from raw payload text
|
||||
- no backend contract change is required
|
||||
- `pnpm --filter @redis-gui/desktop test` passes
|
||||
- `pnpm run desktop:build` passes
|
||||
@@ -1,37 +0,0 @@
|
||||
# HAC-31 Safe Mutation Guardrails
|
||||
|
||||
Date: 2026-03-31
|
||||
Owner: Senior Frontend Engineer
|
||||
Issue: HAC-31
|
||||
|
||||
## Goal
|
||||
|
||||
Harden current-phase writable operations without expanding product scope beyond existing string save and TTL controls.
|
||||
|
||||
## Scope
|
||||
|
||||
- add a scoped review step before string save
|
||||
- add a scoped review step before TTL set and TTL removal
|
||||
- block no-op mutation submits before they reach the desktop bridge or demo fallback
|
||||
- preserve draft value and TTL input when a live mutation fails
|
||||
|
||||
## Explicit Non-Goals
|
||||
|
||||
- Add Key flow changes
|
||||
- real delete execution
|
||||
- non-string editing
|
||||
- command-surface changes
|
||||
- visual refresh beyond mutation clarity
|
||||
|
||||
## Implementation
|
||||
|
||||
- Added `apps/desktop/src/lib/mutation-guardrails.ts` for mutation summaries and no-op detection helpers.
|
||||
- Updated `apps/desktop/src/App.tsx` so save and TTL actions open a shared review dialog with scoped current-vs-next details.
|
||||
- Disabled string save when no value change is pending.
|
||||
- Disabled TTL set when the requested TTL already matches the active TTL state.
|
||||
- Kept live failure handling on the shared operator notice layer while preserving draft state for retry.
|
||||
|
||||
## Verification
|
||||
|
||||
- `pnpm --filter @redis-gui/desktop test`
|
||||
- `pnpm run desktop:build`
|
||||
Reference in New Issue
Block a user