feat: finish Pi and workspace management updates
This commit is contained in:
@@ -238,25 +238,30 @@ test("shows a seven-step host-terminal workflow in scrollable platform tabs", as
|
||||
"Open the project root",
|
||||
"Edit the provider catalog",
|
||||
"Enable the model",
|
||||
"Set the provider credential",
|
||||
"Check the provider credential",
|
||||
"Reload Pi configuration",
|
||||
"Update the Pi version",
|
||||
"Recover a failed update",
|
||||
]);
|
||||
expect(linux).toHaveTextContent("The deploy directory is in the ThothII project root, beside compose.yaml");
|
||||
expect(linux).toHaveTextContent("The deploy directory is beside compose.yaml");
|
||||
expect(linux).toHaveTextContent("All commands below start in this project root");
|
||||
expect(linux).toHaveTextContent("mkdir -p bin");
|
||||
expect(linux).toHaveTextContent("go -C tools/thothctl build -o ../../bin/thothctl ./cmd/thothctl");
|
||||
expect(linux).toHaveTextContent("deploy/pi/models.json");
|
||||
expect(linux).toHaveTextContent("deploy/pi/settings.json");
|
||||
expect(linux).toHaveTextContent("baseUrl is the provider API endpoint");
|
||||
expect(linux).toHaveTextContent("enabledModels uses provider/model identifiers");
|
||||
expect(linux).toHaveTextContent("PI_AUTH_FILE is a setting in the installation environment file");
|
||||
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi restart --yes --drain");
|
||||
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source build --yes --drain");
|
||||
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain");
|
||||
expect(linux).toHaveTextContent("<VERSION> is a placeholder. Replace it with the Pi release/version you want to install.");
|
||||
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance status");
|
||||
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi logs");
|
||||
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi rollback --yes");
|
||||
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance recover --yes");
|
||||
expect(linux).toHaveTextContent("Pi reads the provider API key from a protected file on the host");
|
||||
expect(linux).toHaveTextContent("Do not put the key in models.json or settings.json");
|
||||
expect(linux).toHaveTextContent("./bin/thothctl pi restart --yes --drain");
|
||||
expect(linux).toHaveTextContent("./bin/thothctl pi update");
|
||||
expect(linux).toHaveTextContent("./bin/thothctl pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain");
|
||||
expect(linux).toHaveTextContent("The command installs the Pi version pinned in docker/core.Dockerfile");
|
||||
expect(linux).toHaveTextContent("./bin/thothctl pi maintenance status");
|
||||
expect(linux).toHaveTextContent("./bin/thothctl pi logs");
|
||||
expect(linux).toHaveTextContent("./bin/thothctl pi rollback --yes");
|
||||
expect(linux).toHaveTextContent("./bin/thothctl pi maintenance recover --yes");
|
||||
expect(linux).not.toHaveTextContent("~/bin/");
|
||||
expect(linux).not.toHaveTextContent("~/.pi/agent/");
|
||||
|
||||
await user.click(macosTab);
|
||||
@@ -266,22 +271,24 @@ test("shows a seven-step host-terminal workflow in scrollable platform tabs", as
|
||||
"Open the project root",
|
||||
"Edit the provider catalog",
|
||||
"Enable the model",
|
||||
"Set the provider credential",
|
||||
"Check the provider credential",
|
||||
"Reload Pi configuration",
|
||||
"Update the Pi version",
|
||||
"Recover a failed update",
|
||||
]);
|
||||
expect(macos).toHaveTextContent("The deploy directory is in the ThothII project root, beside compose.yaml");
|
||||
expect(macos).toHaveTextContent("The deploy directory is beside compose.yaml");
|
||||
expect(macos).toHaveTextContent("All commands below start in this project root");
|
||||
expect(macos).toHaveTextContent("go -C tools/thothctl build -o ../../bin/thothctl ./cmd/thothctl");
|
||||
expect(macos).toHaveTextContent("deploy/pi/models.json");
|
||||
expect(macos).toHaveTextContent("deploy/pi/settings.json");
|
||||
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi restart --yes --drain");
|
||||
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source build --yes --drain");
|
||||
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain");
|
||||
expect(macos).toHaveTextContent("<VERSION> is a placeholder. Replace it with the Pi release/version you want to install.");
|
||||
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance status");
|
||||
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi logs");
|
||||
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi rollback --yes");
|
||||
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance recover --yes");
|
||||
expect(macos).toHaveTextContent("./bin/thothctl pi restart --yes --drain");
|
||||
expect(macos).toHaveTextContent("./bin/thothctl pi update");
|
||||
expect(macos).toHaveTextContent("./bin/thothctl pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain");
|
||||
expect(macos).toHaveTextContent("The command installs the Pi version pinned in docker/core.Dockerfile");
|
||||
expect(macos).toHaveTextContent("./bin/thothctl pi maintenance status");
|
||||
expect(macos).toHaveTextContent("./bin/thothctl pi logs");
|
||||
expect(macos).toHaveTextContent("./bin/thothctl pi rollback --yes");
|
||||
expect(macos).toHaveTextContent("./bin/thothctl pi maintenance recover --yes");
|
||||
|
||||
await user.click(windowsTab);
|
||||
const windows = screen.getByRole("tabpanel", { name: "Windows" });
|
||||
@@ -290,22 +297,26 @@ test("shows a seven-step host-terminal workflow in scrollable platform tabs", as
|
||||
"Open the project root",
|
||||
"Edit the provider catalog",
|
||||
"Enable the model",
|
||||
"Set the provider credential",
|
||||
"Check the provider credential",
|
||||
"Reload Pi configuration",
|
||||
"Update the Pi version",
|
||||
"Recover a failed update",
|
||||
]);
|
||||
expect(windows).toHaveTextContent("The deploy directory is in the ThothII project root, beside compose.yaml");
|
||||
expect(windows).toHaveTextContent("The deploy directory is beside compose.yaml");
|
||||
expect(windows).toHaveTextContent("All commands below start in this project root");
|
||||
expect(windows).toHaveTextContent("deploy\\pi\\models.json");
|
||||
expect(windows).toHaveTextContent("deploy\\pi\\settings.json");
|
||||
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi restart --yes --drain');
|
||||
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi update --version <VERSION> --source build --yes --drain');
|
||||
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain');
|
||||
expect(windows).toHaveTextContent("<VERSION> is a placeholder. Replace it with the Pi release/version you want to install.");
|
||||
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi maintenance status');
|
||||
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi logs');
|
||||
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi rollback --yes');
|
||||
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi maintenance recover --yes');
|
||||
expect(windows).toHaveTextContent("New-Item -ItemType Directory -Force bin");
|
||||
expect(windows).toHaveTextContent("go -C tools/thothctl build -o ../../bin/thothctl.exe ./cmd/thothctl");
|
||||
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi restart --yes --drain');
|
||||
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi update');
|
||||
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain');
|
||||
expect(windows).toHaveTextContent("The command installs the Pi version pinned in docker/core.Dockerfile");
|
||||
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi maintenance status');
|
||||
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi logs');
|
||||
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi rollback --yes');
|
||||
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi maintenance recover --yes');
|
||||
expect(windows).not.toHaveTextContent("~\\bin\\");
|
||||
});
|
||||
|
||||
test("keeps the required PI_AUTH_FILE guidance in one static text node", async () => {
|
||||
@@ -314,13 +325,13 @@ test("keeps the required PI_AUTH_FILE guidance in one static text node", async (
|
||||
|
||||
const tablist = await screen.findByRole("tablist", { name: "Pi host platform" });
|
||||
await user.click(within(tablist).getByRole("tab", { name: "Linux" }));
|
||||
const credentialStep = screen.getByRole("heading", { name: "Set the provider credential" }).closest("li");
|
||||
const credentialStep = screen.getByRole("heading", { name: "Check the provider credential" }).closest("li");
|
||||
const guidance = credentialStep?.querySelector("p");
|
||||
|
||||
expect(
|
||||
Array.from(guidance?.childNodes ?? []).some(
|
||||
(node) => node.nodeType === Node.TEXT_NODE
|
||||
&& node.textContent?.includes("PI_AUTH_FILE is a setting in the installation environment file"),
|
||||
&& node.textContent?.includes("Pi reads the provider API key from a protected file on the host"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -108,6 +108,7 @@ type PiPlatformDetails = {
|
||||
settingsPath: string;
|
||||
terminal: string;
|
||||
credentialProtection: string;
|
||||
buildCommand: string;
|
||||
restartCommand: string;
|
||||
updateCommand: string;
|
||||
pullCommand: string;
|
||||
@@ -123,10 +124,11 @@ const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDet
|
||||
settingsPath: "deploy/pi/settings.json",
|
||||
terminal: "a terminal",
|
||||
credentialProtection: "a protected host file with mode 0600",
|
||||
restartCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi restart --yes --drain",
|
||||
updateCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source build --yes --drain",
|
||||
pullCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
|
||||
recoveryCommands: "~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance status\n~/bin/thothctl --installation ~/thothii-installation.yaml pi logs\n~/bin/thothctl --installation ~/thothii-installation.yaml pi rollback --yes\n~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance recover --yes",
|
||||
buildCommand: "mkdir -p bin\ngo -C tools/thothctl build -o ../../bin/thothctl ./cmd/thothctl",
|
||||
restartCommand: "./bin/thothctl pi restart --yes --drain",
|
||||
updateCommand: "./bin/thothctl pi update",
|
||||
pullCommand: "./bin/thothctl pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
|
||||
recoveryCommands: "./bin/thothctl pi maintenance status\n./bin/thothctl pi logs\n./bin/thothctl pi rollback --yes\n./bin/thothctl pi maintenance recover --yes",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -137,10 +139,11 @@ const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDet
|
||||
settingsPath: "deploy/pi/settings.json",
|
||||
terminal: "Terminal",
|
||||
credentialProtection: "a protected host file with mode 0600",
|
||||
restartCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi restart --yes --drain",
|
||||
updateCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source build --yes --drain",
|
||||
pullCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
|
||||
recoveryCommands: "~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance status\n~/bin/thothctl --installation ~/thothii-installation.yaml pi logs\n~/bin/thothctl --installation ~/thothii-installation.yaml pi rollback --yes\n~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance recover --yes",
|
||||
buildCommand: "mkdir -p bin\ngo -C tools/thothctl build -o ../../bin/thothctl ./cmd/thothctl",
|
||||
restartCommand: "./bin/thothctl pi restart --yes --drain",
|
||||
updateCommand: "./bin/thothctl pi update",
|
||||
pullCommand: "./bin/thothctl pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
|
||||
recoveryCommands: "./bin/thothctl pi maintenance status\n./bin/thothctl pi logs\n./bin/thothctl pi rollback --yes\n./bin/thothctl pi maintenance recover --yes",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -151,10 +154,11 @@ const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDet
|
||||
settingsPath: "deploy\\pi\\settings.json",
|
||||
terminal: "PowerShell",
|
||||
credentialProtection: "a protected host file with a user-only ACL",
|
||||
restartCommand: '& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi restart --yes --drain',
|
||||
updateCommand: '& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi update --version <VERSION> --source build --yes --drain',
|
||||
pullCommand: '& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain',
|
||||
recoveryCommands: '& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi maintenance status\n& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi logs\n& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi rollback --yes\n& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi maintenance recover --yes',
|
||||
buildCommand: "New-Item -ItemType Directory -Force bin | Out-Null\ngo -C tools/thothctl build -o ../../bin/thothctl.exe ./cmd/thothctl",
|
||||
restartCommand: ".\\bin\\thothctl.exe pi restart --yes --drain",
|
||||
updateCommand: ".\\bin\\thothctl.exe pi update",
|
||||
pullCommand: ".\\bin\\thothctl.exe pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
|
||||
recoveryCommands: ".\\bin\\thothctl.exe pi maintenance status\n.\\bin\\thothctl.exe pi logs\n.\\bin\\thothctl.exe pi rollback --yes\n.\\bin\\thothctl.exe pi maintenance recover --yes",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -167,7 +171,9 @@ function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
|
||||
return <ol className="grid gap-4 pl-5 marker:font-semibold marker:text-muted-foreground">
|
||||
<li>
|
||||
<h4 className="font-semibold text-foreground">Open the project root</h4>
|
||||
<p className="mt-1 text-muted-foreground">Open {details.terminal} in the ThothII project root. The deploy directory is in the ThothII project root, beside <code>compose.yaml</code>.</p>
|
||||
<p className="mt-1 text-muted-foreground">Using {details.terminal}, open the current ThothII checkout or worktree root. All commands below start in this project root. The deploy directory is beside <code>compose.yaml</code>. If this checkout has no <code>bin</code> directory, create it and build the local CLI once:</p>
|
||||
<PiCodeBlock className="mt-2">{details.buildCommand}</PiCodeBlock>
|
||||
<p className="mt-2 text-muted-foreground">The commands below use the executable built in this checkout, so they cannot accidentally target another worktree.</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4 className="font-semibold text-foreground">Edit the provider catalog</h4>
|
||||
@@ -188,8 +194,8 @@ function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
|
||||
</dl>
|
||||
</li>
|
||||
<li>
|
||||
<h4 className="font-semibold text-foreground">Set the provider credential</h4>
|
||||
<p className="mt-1 text-muted-foreground">PI_AUTH_FILE is a setting in the installation environment file. It selects {details.credentialProtection}; Docker mounts the selected host credential file read-only for Pi.</p>
|
||||
<h4 className="font-semibold text-foreground">Check the provider credential</h4>
|
||||
<p className="mt-1 text-muted-foreground">Pi reads the provider API key from a protected file on the host. <code>PI_AUTH_FILE</code> tells this installation which file to use; Docker mounts it read-only into the core container. Do not put the key in <code>models.json</code> or <code>settings.json</code>. Keep {details.credentialProtection}.</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4 className="font-semibold text-foreground">Reload Pi configuration</h4>
|
||||
@@ -198,8 +204,7 @@ function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
|
||||
</li>
|
||||
<li>
|
||||
<h4 className="font-semibold text-foreground">Update the Pi version</h4>
|
||||
<p className="mt-1 text-muted-foreground">Use a build update only when changing the bundled Pi version.</p>
|
||||
<p className="mt-1 text-muted-foreground">{"<VERSION> is a placeholder. Replace it with the Pi release/version you want to install."}</p>
|
||||
<p className="mt-1 text-muted-foreground">The command installs the Pi version pinned in <code>docker/core.Dockerfile</code>. Use <code>--version <VERSION></code> only when you deliberately want another version.</p>
|
||||
<PiCodeBlock className="mt-2">{details.updateCommand}</PiCodeBlock>
|
||||
<p className="mt-2 text-[11px] text-muted-foreground">Advanced: pull an immutable, digest-pinned image.</p>
|
||||
<PiCodeBlock className="mt-1 text-[11px] text-muted-foreground">{details.pullCommand}</PiCodeBlock>
|
||||
|
||||
@@ -114,7 +114,13 @@ test("level one explains the read-only Git sequence and the repository update bu
|
||||
const overview = screen.getByTestId("workspace-overview");
|
||||
expect(within(overview).getByText(/Git server such as GitHub, GitLab, or Gitea/i)).toBeVisible();
|
||||
expect(within(overview).getByText(/configured during ThothII installation/i)).toBeVisible();
|
||||
expect(within(overview).getAllByRole("listitem")[0]).toHaveTextContent(/choosing any local directory you prefer/i);
|
||||
const repositoryStep = within(overview).getAllByRole("listitem")[0];
|
||||
expect(repositoryStep).toHaveTextContent(/create a workspace repository/i);
|
||||
expect(repositoryStep).toHaveTextContent(/one directory for each workspace/i);
|
||||
expect(repositoryStep).toHaveTextContent(/thoth-workspaces\.yaml/i);
|
||||
expect(repositoryStep).toHaveTextContent(/database connection/i);
|
||||
expect(repositoryStep).toHaveTextContent(/Evidence sources/i);
|
||||
expect(repositoryStep).toHaveTextContent(/vector-database collection/i);
|
||||
expect(within(overview).getByRole("link", { name: /workspace authoring instructions on GitHub/i })).toHaveAttribute(
|
||||
"href",
|
||||
"https://github.com/mptyl/ThothII/blob/main/docs/install/local-workspace-registry.md#prepare-and-publish-a-workspace-source",
|
||||
@@ -126,7 +132,7 @@ test("level one explains the read-only Git sequence and the repository update bu
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Update workspace repository" }));
|
||||
expect(await screen.findByText("Workspace repository updated and validated.")).toBeVisible();
|
||||
expect(screen.getByText(/create a local workspace/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/create a workspace repository/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/import|export|bundle/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -141,10 +147,61 @@ test("workspace-specific commands remain isolated until a workspace is selected"
|
||||
expect(screen.getByText(/reads this revision without modifying or publishing it/i)).toBeVisible();
|
||||
expect(screen.getByText(/checks workspace.yaml and the required workspace directories/i)).toBeVisible();
|
||||
expect(screen.getByText(/temporary decrypted credentials/i)).toBeVisible();
|
||||
const databaseField = screen.getByText("Database").parentElement;
|
||||
expect(databaseField).not.toBeNull();
|
||||
expect(databaseField).toHaveTextContent("engine: postgres");
|
||||
expect(databaseField).toHaveTextContent("database: database");
|
||||
expect(databaseField).toHaveTextContent("schema: public");
|
||||
expect(screen.getByRole("button", { name: "Validate workspace source" })).toBeVisible();
|
||||
expect(screen.getByRole("button", { name: "Test workspace connections" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("keeps validation and connection results inside their respective action cards", async () => {
|
||||
const user = userEvent.setup();
|
||||
server.use(
|
||||
http.post("/api/workspaces/validate", () => HttpResponse.json({ workspace, contract: {} })),
|
||||
http.post("/api/workspaces/psd-clinical/test", () => HttpResponse.json({
|
||||
activatable: false,
|
||||
diagnostics: [{ level: "error", code: "connector_unavailable", message: "Connector diagnostic failed." }],
|
||||
})),
|
||||
);
|
||||
renderManager();
|
||||
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
|
||||
|
||||
const validationCard = screen.getByTestId("workspace-validation-card");
|
||||
const connectionCard = screen.getByTestId("workspace-connection-card");
|
||||
await user.click(within(validationCard).getByRole("button", { name: "Validate workspace source" }));
|
||||
const validationStatus = await within(validationCard).findByRole("status");
|
||||
expect(validationStatus).toHaveTextContent("Workspace source is valid.");
|
||||
expect(validationStatus).toHaveClass("text-emerald-700");
|
||||
expect(within(connectionCard).queryByText("Workspace source is valid.")).not.toBeInTheDocument();
|
||||
|
||||
await user.click(within(connectionCard).getByRole("button", { name: "Test workspace connections" }));
|
||||
expect(await within(connectionCard).findByRole("alert")).toHaveTextContent(
|
||||
"connector_unavailable: Connector diagnostic failed.",
|
||||
);
|
||||
expect(within(validationCard).queryByText("connector_unavailable: Connector diagnostic failed.")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders binding_ok as a green connection success", async () => {
|
||||
const user = userEvent.setup();
|
||||
server.use(
|
||||
http.post("/api/workspaces/psd-clinical/test", () => HttpResponse.json({
|
||||
activatable: true,
|
||||
diagnostics: [{ level: "info", code: "binding_ok", message: "Installation bindings and diagnostics succeeded." }],
|
||||
})),
|
||||
);
|
||||
renderManager();
|
||||
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
|
||||
|
||||
const connectionCard = screen.getByTestId("workspace-connection-card");
|
||||
await user.click(within(connectionCard).getByRole("button", { name: "Test workspace connections" }));
|
||||
const connectionStatus = await within(connectionCard).findByRole("status");
|
||||
expect(connectionStatus).toHaveTextContent("binding_ok: Installation bindings and diagnostics succeeded.");
|
||||
expect(connectionStatus).toHaveClass("text-emerald-700");
|
||||
expect(within(connectionCard).queryByRole("alert")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("secret fields are write-only, clear after blind save, and may be forgotten", async () => {
|
||||
const user = userEvent.setup();
|
||||
let savedBody: unknown;
|
||||
|
||||
@@ -66,6 +66,10 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
|
||||
const [secretValues, setSecretValues] = useState<Record<string, string>>({});
|
||||
const [notice, setNotice] = useState<string>();
|
||||
const [diagnostics, setDiagnostics] = useState<string[]>([]);
|
||||
const [validationNotice, setValidationNotice] = useState<string>();
|
||||
const [validationDiagnostics, setValidationDiagnostics] = useState<string[]>([]);
|
||||
const [connectionNotice, setConnectionNotice] = useState<string>();
|
||||
const [connectionDiagnostics, setConnectionDiagnostics] = useState<string[]>([]);
|
||||
const [busyAction, setBusyAction] = useState<string>();
|
||||
|
||||
const statusQuery = useQuery({
|
||||
@@ -97,6 +101,15 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
|
||||
const clearMessages = () => {
|
||||
setNotice(undefined);
|
||||
setDiagnostics([]);
|
||||
setValidationNotice(undefined);
|
||||
setValidationDiagnostics([]);
|
||||
setConnectionNotice(undefined);
|
||||
setConnectionDiagnostics([]);
|
||||
};
|
||||
|
||||
const clearGlobalMessages = () => {
|
||||
setNotice(undefined);
|
||||
setDiagnostics([]);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
@@ -139,12 +152,14 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
|
||||
async function validateSource() {
|
||||
if (!detailQuery.data) return;
|
||||
setBusyAction("validate");
|
||||
clearMessages();
|
||||
clearGlobalMessages();
|
||||
setValidationNotice(undefined);
|
||||
setValidationDiagnostics([]);
|
||||
try {
|
||||
await validateWorkspace(detailQuery.data.workspace);
|
||||
setNotice("Workspace source is valid.");
|
||||
setValidationNotice("Workspace source is valid.");
|
||||
} catch (error) {
|
||||
setDiagnostics([publicError(error, "workspace_invalid: Workspace validation could not be completed")]);
|
||||
setValidationDiagnostics([publicError(error, "workspace_invalid: Workspace validation could not be completed")]);
|
||||
} finally {
|
||||
setBusyAction(undefined);
|
||||
}
|
||||
@@ -153,17 +168,23 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
|
||||
async function testConnections() {
|
||||
if (!selectedId) return;
|
||||
setBusyAction("test");
|
||||
clearMessages();
|
||||
clearGlobalMessages();
|
||||
setConnectionNotice(undefined);
|
||||
setConnectionDiagnostics([]);
|
||||
try {
|
||||
const result = await testWorkspace(selectedId);
|
||||
setDiagnostics(result.diagnostics.map(({ code, message }) => `${code}: ${message}`));
|
||||
if (result.diagnostics.length === 0) {
|
||||
setNotice(result.activatable
|
||||
? "Workspace connections are valid."
|
||||
const issues = result.diagnostics.filter(({ level }) => level !== "info");
|
||||
const informational = result.diagnostics.find(({ level }) => level === "info");
|
||||
setConnectionDiagnostics(issues.map(({ code, message }) => `${code}: ${message}`));
|
||||
if (issues.length === 0) {
|
||||
setConnectionNotice(result.activatable
|
||||
? informational
|
||||
? `${informational.code}: ${informational.message}`
|
||||
: "Workspace connections are valid."
|
||||
: "Workspace connection test completed.");
|
||||
}
|
||||
} catch (error) {
|
||||
setDiagnostics([publicError(error, "connector_unavailable: Workspace connections could not be tested")]);
|
||||
setConnectionDiagnostics([publicError(error, "connector_unavailable: Workspace connections could not be tested")]);
|
||||
} finally {
|
||||
setBusyAction(undefined);
|
||||
}
|
||||
@@ -293,7 +314,7 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
|
||||
<h2 className="font-heading text-xl font-semibold">How workspaces reach ThothII</h2>
|
||||
</div>
|
||||
<ol className="grid list-decimal gap-3 pl-5 text-sm leading-6 text-muted-foreground">
|
||||
<li><span className="font-medium text-foreground">Create a local workspace</span> by choosing any local directory you prefer and setting up the workspace there. It must contain <code>workspace.yaml</code> and every required subdirectory, including any versioned Evidence files.</li>
|
||||
<li><span className="font-medium text-foreground">Create a workspace repository</span> in any local directory you choose. Add one directory for each workspace you want ThothII to manage. At the repository root, <code>thoth-workspaces.yaml</code> lists those workspaces; each workspace directory contains its own <code>workspace.yaml</code>, which declares the database connection, the Evidence sources, and the ThothII vector-database collection used during the process.</li>
|
||||
<li>Publish that source by committing and pushing it to a repository hosted by a Git server such as GitHub, GitLab, or Gitea.</li>
|
||||
<li>The repository address, branch, and read-only Git credentials are configured during ThothII installation. This installation reads <span className="font-medium text-foreground">{repositoryLabel}</span> on branch <span className="font-mono text-foreground">{statusQuery.data?.branch ?? "main"}</span>.</li>
|
||||
<li>ThothII fetches the configured branch into its managed read-only checkout, validates the complete candidate revision, and activates it only when validation succeeds. It never edits, commits, pushes, or publishes workspace source.</li>
|
||||
@@ -337,21 +358,56 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
|
||||
<dl className="grid gap-3 rounded-lg border border-border bg-muted/20 p-4 text-sm sm:grid-cols-2">
|
||||
<div><dt className="text-xs text-muted-foreground">Source file</dt><dd className="font-mono">{selectedSummary.file}</dd></div>
|
||||
<div><dt className="text-xs text-muted-foreground">Active revision</dt><dd className="truncate font-mono">{detailQuery.data.revision.commit}</dd></div>
|
||||
<div><dt className="text-xs text-muted-foreground">Data warehouse</dt><dd>{detailQuery.data.workspace.dwh.engine} · {detailQuery.data.workspace.dwh.database}/{detailQuery.data.workspace.dwh.schema}</dd></div>
|
||||
<div>
|
||||
<dt className="text-xs text-muted-foreground">Database</dt>
|
||||
<dd className="grid gap-0.5 font-mono text-xs">
|
||||
<span>engine: {detailQuery.data.workspace.dwh.engine}</span>
|
||||
<span>database: {detailQuery.data.workspace.dwh.database}</span>
|
||||
<span>schema: {detailQuery.data.workspace.dwh.schema}</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div><dt className="text-xs text-muted-foreground">Runtime status</dt><dd>{stateLabel(runtime.configurationState)}</dd></div>
|
||||
</dl>
|
||||
|
||||
<div className="grid gap-3 lg:grid-cols-2">
|
||||
<div className="rounded-lg border border-border p-4">
|
||||
<div data-testid="workspace-validation-card" className="rounded-lg border border-border p-4">
|
||||
<h4 className="font-heading font-semibold">Validate workspace source</h4>
|
||||
<p className="mt-1 min-h-12 text-sm leading-5 text-muted-foreground">Checks workspace.yaml and the required workspace directories against the supported workspace schema. No source file is changed.</p>
|
||||
{validationNotice && (
|
||||
<p role="status" className="mt-3 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
|
||||
<CheckCircle2 className="mr-1 inline size-4 text-emerald-600" />{validationNotice}
|
||||
</p>
|
||||
)}
|
||||
{validationDiagnostics.length > 0 && (
|
||||
<div role="alert" aria-live="polite" className="mt-3 grid gap-1 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm">
|
||||
{validationDiagnostics.map((diagnostic) => (
|
||||
<p key={diagnostic} className="flex items-start gap-2">
|
||||
<AlertCircle className="mt-0.5 size-4 shrink-0 text-amber-700" />{diagnostic}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Button className="mt-3" size="sm" variant="outline" disabled={busyAction === "validate"} onClick={() => { void validateSource(); }}>
|
||||
<ClipboardCheck />Validate workspace source
|
||||
</Button>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border p-4">
|
||||
<div data-testid="workspace-connection-card" className="rounded-lg border border-border p-4">
|
||||
<h4 className="font-heading font-semibold">Test workspace connections</h4>
|
||||
<p className="mt-1 min-h-12 text-sm leading-5 text-muted-foreground">Uses temporary decrypted credentials to verify the configured data warehouse and Evidence source. Temporary files are deleted after the test.</p>
|
||||
{connectionNotice && (
|
||||
<p role="status" className="mt-3 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
|
||||
<CheckCircle2 className="mr-1 inline size-4 text-emerald-600" />{connectionNotice}
|
||||
</p>
|
||||
)}
|
||||
{connectionDiagnostics.length > 0 && (
|
||||
<div role="alert" aria-live="polite" className="mt-3 grid gap-1 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm">
|
||||
{connectionDiagnostics.map((diagnostic) => (
|
||||
<p key={diagnostic} className="flex items-start gap-2">
|
||||
<AlertCircle className="mt-0.5 size-4 shrink-0 text-amber-700" />{diagnostic}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Button className="mt-3" size="sm" variant="outline" disabled={busyAction === "test"} onClick={() => { void testConnections(); }}>
|
||||
<FlaskConical />Test workspace connections
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user