fix(cli): resolve CLI-014..016 — re-triage update-command contract, doc-surface drift, table-column union
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
| Last reviewed | 2026-05-17 |
|
| Last reviewed | 2026-05-17 |
|
||||||
| Reviewer | claude-agent |
|
| Reviewer | claude-agent |
|
||||||
| Commit reviewed | `39d737e` |
|
| Commit reviewed | `39d737e` |
|
||||||
| Open findings | 3 |
|
| Open findings | 0 |
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
@@ -575,11 +575,27 @@ The CLI test suite went from 42 to 77 passing tests.
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
|--|--|
|
|--|--|
|
||||||
| Severity | Medium |
|
| Severity | Low |
|
||||||
| Category | Correctness & logic bugs |
|
| Category | Design-document adherence |
|
||||||
| Status | Open |
|
| Status | Resolved |
|
||||||
| Location | `src/ScadaLink.CLI/Commands/TemplateCommands.cs:77`, `src/ScadaLink.CLI/Commands/SiteCommands.cs:86`, `src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs:40-42`, `src/ScadaLink.CLI/Commands/DataConnectionCommands.cs:39-40`, `src/ScadaLink.CLI/Commands/NotificationCommands.cs:40-41`, `src/ScadaLink.CLI/Commands/ApiMethodCommands.cs:79` |
|
| Location | `src/ScadaLink.CLI/Commands/TemplateCommands.cs:77`, `src/ScadaLink.CLI/Commands/SiteCommands.cs:86`, `src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs:40-42`, `src/ScadaLink.CLI/Commands/DataConnectionCommands.cs:39-40`, `src/ScadaLink.CLI/Commands/NotificationCommands.cs:40-41`, `src/ScadaLink.CLI/Commands/ApiMethodCommands.cs:79` |
|
||||||
|
|
||||||
|
**Re-triage 2026-05-17:** the finding was originally filed as a Medium "Correctness &
|
||||||
|
logic bugs" issue, but verification against the Commons message contracts shows the
|
||||||
|
**code is correct** and the defect is purely documentation drift. Every `Update*Command`
|
||||||
|
record (`UpdateTemplateCommand`, `UpdateSiteCommand`, `UpdateDataConnectionCommand`,
|
||||||
|
`UpdateExternalSystemCommand`, `UpdateNotificationListCommand`, `UpdateApiMethodCommand`,
|
||||||
|
`UpdateTemplateAttribute/Alarm/ScriptCommand`, `UpdateRoleMappingCommand`,
|
||||||
|
`UpdateSharedScriptCommand`, `UpdateDatabaseConnectionDefCommand`, `UpdateAreaCommand`)
|
||||||
|
carries **non-nullable** "core" fields — they are *whole-entity replace* records, not
|
||||||
|
sparse patches. Marking the corresponding `--name` / `--protocol` / `--script` flags
|
||||||
|
`Required = true` is therefore the *correct* CLI behaviour: making them optional would
|
||||||
|
let an omitted flag send `null`/empty and silently blank the field server-side. Adopting
|
||||||
|
the sparse-patch model (recommendation option a) would require changing the Commons
|
||||||
|
records and the server-side Management handlers — both outside this module's editable
|
||||||
|
surface. Re-triaged to Low / Design-document adherence; resolved per recommendation
|
||||||
|
option (b).
|
||||||
|
|
||||||
**Description**
|
**Description**
|
||||||
|
|
||||||
The design doc presents `update` commands with all non-`--id` fields as optional, e.g.
|
The design doc presents `update` commands with all non-`--id` fields as optional, e.g.
|
||||||
@@ -618,7 +634,20 @@ entity. Option (a) matches the documented surface and the typical CLI expectatio
|
|||||||
|
|
||||||
**Resolution**
|
**Resolution**
|
||||||
|
|
||||||
_Unresolved._
|
Resolved 2026-05-17 (commit pending) per recommendation option (b). Verification of the
|
||||||
|
Commons `Update*Command` records confirmed whole-replace is the intentional contract, so
|
||||||
|
the CLI's `Required = true` flags are correct and were left unchanged. The in-repo
|
||||||
|
`src/ScadaLink.CLI/README.md` — which is authoritative and previously listed every
|
||||||
|
`update` core field as optional `[--name]` — was corrected: the core flags
|
||||||
|
(`--name`/`--protocol`/`--script`/`--code`/`--emails`/`--endpoint-url`/`--auth-type`/
|
||||||
|
`--data-type`/`--trigger-type`/`--priority`/`--connection-string`/`--ldap-group`/`--role`)
|
||||||
|
are now marked `required: yes`, the command synopses drop the `[...]`, and each `update`
|
||||||
|
section states that an update replaces the whole entity. Added
|
||||||
|
`UpdateCommandContractTests` (10 tests) pinning the whole-replace contract — every
|
||||||
|
listed `update` command's core flags must be `Required`, and the genuinely-sparse
|
||||||
|
`external-system method update` must remain optional. Note: `docs/requirements/Component-CLI.md`
|
||||||
|
also still shows these flags as optional, but it is outside this module's editable
|
||||||
|
surface — that doc-side correction is owned by the docs surface.
|
||||||
|
|
||||||
### CLI-015 — `Component-CLI.md` command surface has drifted again in two places
|
### CLI-015 — `Component-CLI.md` command surface has drifted again in two places
|
||||||
|
|
||||||
@@ -626,8 +655,16 @@ _Unresolved._
|
|||||||
|--|--|
|
|--|--|
|
||||||
| Severity | Low |
|
| Severity | Low |
|
||||||
| Category | Design-document adherence |
|
| Category | Design-document adherence |
|
||||||
| Status | Open |
|
| Status | Resolved |
|
||||||
| Location | `docs/requirements/Component-CLI.md:75`, `docs/requirements/Component-CLI.md:125-126` (vs. `src/ScadaLink.CLI/Commands/TemplateCommands.cs:404-413`, `src/ScadaLink.CLI/Commands/DataConnectionCommands.cs:41`, `:86`) |
|
| Location | `docs/requirements/Component-CLI.md:75`, `docs/requirements/Component-CLI.md:125-126`, `src/ScadaLink.CLI/README.md` (vs. `src/ScadaLink.CLI/Commands/TemplateCommands.cs:404-413`, `src/ScadaLink.CLI/Commands/DataConnectionCommands.cs:41`, `:86`) |
|
||||||
|
|
||||||
|
**Re-triage 2026-05-17:** verification found the same two drifts also present in the
|
||||||
|
in-repo `src/ScadaLink.CLI/README.md` (the authoritative reference): its
|
||||||
|
`template composition delete` section used the non-existent `--template-id` /
|
||||||
|
`--instance-name` form, and `data-connection create`/`update` documented only
|
||||||
|
`--configuration` without the canonical `--primary-config` flag (`--configuration` is in
|
||||||
|
fact an alias of `--primary-config`). The README is in this module's editable surface;
|
||||||
|
`docs/requirements/Component-CLI.md` is not.
|
||||||
|
|
||||||
**Description**
|
**Description**
|
||||||
|
|
||||||
@@ -655,7 +692,15 @@ documented elsewhere.
|
|||||||
|
|
||||||
**Resolution**
|
**Resolution**
|
||||||
|
|
||||||
_Unresolved._
|
Resolved 2026-05-17 (commit pending). Both drifts were present in the in-repo
|
||||||
|
`src/ScadaLink.CLI/README.md` and were corrected there (the README is this module's
|
||||||
|
authoritative reference): `template composition delete` now documents the real single
|
||||||
|
`--id <int>` form, and `data-connection create`/`update` now document `--primary-config`
|
||||||
|
(with the `--configuration` alias noted) alongside `--backup-config` and
|
||||||
|
`--failover-retry-count`. Added `CommandTreeTests.TemplateCompositionDelete_IsKeyedByIdOnly`
|
||||||
|
pinning the composition-delete surface (`--id` present; `--template-id`/`--instance-name`
|
||||||
|
absent). The corresponding edit to `docs/requirements/Component-CLI.md:75,125-126` is
|
||||||
|
outside this module's editable surface and remains for the docs surface to apply.
|
||||||
|
|
||||||
### CLI-016 — `WriteAsTable` derives columns from the first array element only
|
### CLI-016 — `WriteAsTable` derives columns from the first array element only
|
||||||
|
|
||||||
@@ -663,7 +708,7 @@ _Unresolved._
|
|||||||
|--|--|
|
|--|--|
|
||||||
| Severity | Low |
|
| Severity | Low |
|
||||||
| Category | Correctness & logic bugs |
|
| Category | Correctness & logic bugs |
|
||||||
| Status | Open |
|
| Status | Resolved |
|
||||||
| Location | `src/ScadaLink.CLI/Commands/CommandHelpers.cs:184-200` |
|
| Location | `src/ScadaLink.CLI/Commands/CommandHelpers.cs:184-200` |
|
||||||
|
|
||||||
**Description**
|
**Description**
|
||||||
@@ -687,4 +732,12 @@ rows, so no element's data is silently dropped.
|
|||||||
|
|
||||||
**Resolution**
|
**Resolution**
|
||||||
|
|
||||||
_Unresolved._
|
Resolved 2026-05-17 (commit pending). Root cause confirmed — `WriteAsTable` derived the
|
||||||
|
header set from `items[0].EnumerateObject()` only, dropping any property unique to a
|
||||||
|
later element. The header set is now computed as the union of property names across
|
||||||
|
*every* object element of the array, in first-seen order (a `HashSet` guards duplicates
|
||||||
|
while an ordered list preserves column order). Rows already project against the header
|
||||||
|
list and `OutputFormatter.WriteTable` pads missing cells, so heterogeneous arrays now
|
||||||
|
render every column. Regression tests added in `TableHeaderUnionTests` (3 tests:
|
||||||
|
later-element-only column included, first-seen column order preserved,
|
||||||
|
first-element-extra column still rendered).
|
||||||
|
|||||||
@@ -181,9 +181,25 @@ internal static class CommandHelpers
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var headers = items[0].ValueKind == JsonValueKind.Object
|
// Derive the header set as the union of property names across *every*
|
||||||
? items[0].EnumerateObject().Select(p => p.Name).ToArray()
|
// element, in first-seen order. Using only items[0] would silently drop
|
||||||
: new[] { "Value" };
|
// columns for any later element with a different shape (CLI-016).
|
||||||
|
var objectItems = items.Where(i => i.ValueKind == JsonValueKind.Object).ToList();
|
||||||
|
string[] headers;
|
||||||
|
if (objectItems.Count > 0)
|
||||||
|
{
|
||||||
|
var seen = new List<string>();
|
||||||
|
var known = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
foreach (var item in objectItems)
|
||||||
|
foreach (var prop in item.EnumerateObject())
|
||||||
|
if (known.Add(prop.Name))
|
||||||
|
seen.Add(prop.Name);
|
||||||
|
headers = seen.ToArray();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
headers = new[] { "Value" };
|
||||||
|
}
|
||||||
|
|
||||||
var rows = items.Select(item =>
|
var rows = items.Select(item =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -120,16 +120,18 @@ scadalink --url <url> template create --name <string> [--description <string>] [
|
|||||||
|
|
||||||
#### `template update`
|
#### `template update`
|
||||||
|
|
||||||
Update an existing template's name, description, or parent.
|
Update an existing template. An update **replaces** the whole entity — every required
|
||||||
|
field below must be supplied with the value it should have after the update, even if
|
||||||
|
it is unchanged.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scadalink --url <url> template update --id <int> [--name <string>] [--description <string>] [--parent-id <int>]
|
scadalink --url <url> template update --id <int> --name <string> [--description <string>] [--parent-id <int>]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Required | Description |
|
| Option | Required | Description |
|
||||||
|--------|----------|-------------|
|
|--------|----------|-------------|
|
||||||
| `--id` | yes | Template ID |
|
| `--id` | yes | Template ID |
|
||||||
| `--name` | no | Updated template name |
|
| `--name` | yes | Template name |
|
||||||
| `--description` | no | Updated description |
|
| `--description` | no | Updated description |
|
||||||
| `--parent-id` | no | Updated parent template ID |
|
| `--parent-id` | no | Updated parent template ID |
|
||||||
|
|
||||||
@@ -313,16 +315,15 @@ scadalink --url <url> template composition add --template-id <int> --module-temp
|
|||||||
|
|
||||||
#### `template composition delete`
|
#### `template composition delete`
|
||||||
|
|
||||||
Remove a feature module composition from a template.
|
Remove a feature module composition from a template, identified by its own composition ID.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scadalink --url <url> template composition delete --template-id <int> --instance-name <string>
|
scadalink --url <url> template composition delete --id <int>
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Required | Description |
|
| Option | Required | Description |
|
||||||
|--------|----------|-------------|
|
|--------|----------|-------------|
|
||||||
| `--template-id` | yes | Target template ID |
|
| `--id` | yes | Composition ID to remove |
|
||||||
| `--instance-name` | yes | Instance name of the composed module to remove |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -520,17 +521,17 @@ scadalink --url <url> site area create --site-id <int> --name <string> [--parent
|
|||||||
|
|
||||||
#### `site area update`
|
#### `site area update`
|
||||||
|
|
||||||
Update an area's name or parent.
|
Update an area's name. An update **replaces** the whole entity — the required field
|
||||||
|
below must be supplied, even if unchanged.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scadalink --url <url> site area update --id <int> [--name <string>] [--parent-area-id <int>]
|
scadalink --url <url> site area update --id <int> --name <string>
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Required | Description |
|
| Option | Required | Description |
|
||||||
|--------|----------|-------------|
|
|--------|----------|-------------|
|
||||||
| `--id` | yes | Area ID |
|
| `--id` | yes | Area ID |
|
||||||
| `--name` | no | Updated area name |
|
| `--name` | yes | Area name |
|
||||||
| `--parent-area-id` | no | Updated parent area ID |
|
|
||||||
|
|
||||||
#### `site area delete`
|
#### `site area delete`
|
||||||
|
|
||||||
@@ -620,7 +621,7 @@ scadalink --url <url> data-connection list [--site-id <int>]
|
|||||||
Create a new data connection belonging to a specific site.
|
Create a new data connection belonging to a specific site.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scadalink --url <url> data-connection create --site-id <int> --name <string> --protocol <string> [--configuration <json>]
|
scadalink --url <url> data-connection create --site-id <int> --name <string> --protocol <string> [--primary-config <json>] [--backup-config <json>] [--failover-retry-count <int>]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Required | Description |
|
| Option | Required | Description |
|
||||||
@@ -628,22 +629,27 @@ scadalink --url <url> data-connection create --site-id <int> --name <string> --p
|
|||||||
| `--site-id` | yes | Site ID the connection belongs to |
|
| `--site-id` | yes | Site ID the connection belongs to |
|
||||||
| `--name` | yes | Connection name |
|
| `--name` | yes | Connection name |
|
||||||
| `--protocol` | yes | Protocol identifier (e.g. `OpcUa`) |
|
| `--protocol` | yes | Protocol identifier (e.g. `OpcUa`) |
|
||||||
| `--configuration` | no | Protocol-specific configuration as a JSON string |
|
| `--primary-config` | no | Primary protocol-specific configuration as a JSON string (alias `--configuration`) |
|
||||||
|
| `--backup-config` | no | Backup protocol-specific configuration as a JSON string |
|
||||||
|
| `--failover-retry-count` | no | Retries before failover to the backup configuration (default: `3`) |
|
||||||
|
|
||||||
#### `data-connection update`
|
#### `data-connection update`
|
||||||
|
|
||||||
Update a data connection definition.
|
Update a data connection definition. An update **replaces** the whole entity — every
|
||||||
|
required field below must be supplied, even if unchanged.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scadalink --url <url> data-connection update --id <int> [--name <string>] [--protocol <string>] [--configuration <json>]
|
scadalink --url <url> data-connection update --id <int> --name <string> --protocol <string> [--primary-config <json>] [--backup-config <json>] [--failover-retry-count <int>]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Required | Description |
|
| Option | Required | Description |
|
||||||
|--------|----------|-------------|
|
|--------|----------|-------------|
|
||||||
| `--id` | yes | Data connection ID |
|
| `--id` | yes | Data connection ID |
|
||||||
| `--name` | no | Updated connection name |
|
| `--name` | yes | Connection name |
|
||||||
| `--protocol` | no | Updated protocol identifier |
|
| `--protocol` | yes | Protocol identifier |
|
||||||
| `--configuration` | no | Updated protocol-specific configuration as a JSON string |
|
| `--primary-config` | no | Primary protocol-specific configuration as a JSON string (alias `--configuration`) |
|
||||||
|
| `--backup-config` | no | Backup protocol-specific configuration as a JSON string |
|
||||||
|
| `--failover-retry-count` | no | Retries before failover to the backup configuration (default: `3`) |
|
||||||
|
|
||||||
#### `data-connection delete`
|
#### `data-connection delete`
|
||||||
|
|
||||||
@@ -698,18 +704,19 @@ scadalink --url <url> external-system create --name <string> --endpoint-url <url
|
|||||||
|
|
||||||
#### `external-system update`
|
#### `external-system update`
|
||||||
|
|
||||||
Update an external system definition.
|
Update an external system definition. An update **replaces** the whole entity — every
|
||||||
|
required field below must be supplied, even if unchanged.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scadalink --url <url> external-system update --id <int> [--name <string>] [--endpoint-url <url>] [--auth-type <string>] [--auth-config <json>]
|
scadalink --url <url> external-system update --id <int> --name <string> --endpoint-url <url> --auth-type <string> [--auth-config <json>]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Required | Description |
|
| Option | Required | Description |
|
||||||
|--------|----------|-------------|
|
|--------|----------|-------------|
|
||||||
| `--id` | yes | External system ID |
|
| `--id` | yes | External system ID |
|
||||||
| `--name` | no | Updated display name |
|
| `--name` | yes | Display name |
|
||||||
| `--endpoint-url` | no | Updated base URL |
|
| `--endpoint-url` | yes | Base URL |
|
||||||
| `--auth-type` | no | Updated authentication type |
|
| `--auth-type` | yes | Authentication type |
|
||||||
| `--auth-config` | no | Updated auth credentials as a JSON string |
|
| `--auth-config` | no | Updated auth credentials as a JSON string |
|
||||||
|
|
||||||
#### `external-system delete`
|
#### `external-system delete`
|
||||||
@@ -763,17 +770,18 @@ scadalink --url <url> notification create --name <string> --emails <email1,email
|
|||||||
|
|
||||||
#### `notification update`
|
#### `notification update`
|
||||||
|
|
||||||
Update a notification list's name or recipients.
|
Update a notification list. An update **replaces** the whole entity — every required
|
||||||
|
field below must be supplied, even if unchanged.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scadalink --url <url> notification update --id <int> [--name <string>] [--emails <email1,email2,...>]
|
scadalink --url <url> notification update --id <int> --name <string> --emails <email1,email2,...>
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Required | Description |
|
| Option | Required | Description |
|
||||||
|--------|----------|-------------|
|
|--------|----------|-------------|
|
||||||
| `--id` | yes | Notification list ID |
|
| `--id` | yes | Notification list ID |
|
||||||
| `--name` | no | Updated list name |
|
| `--name` | yes | List name |
|
||||||
| `--emails` | no | Updated comma-separated list of recipient email addresses |
|
| `--emails` | yes | Comma-separated list of recipient email addresses |
|
||||||
|
|
||||||
#### `notification delete`
|
#### `notification delete`
|
||||||
|
|
||||||
@@ -885,17 +893,18 @@ scadalink --url <url> security role-mapping create --ldap-group <string> --role
|
|||||||
|
|
||||||
#### `security role-mapping update`
|
#### `security role-mapping update`
|
||||||
|
|
||||||
Update an LDAP role mapping.
|
Update an LDAP role mapping. An update **replaces** the whole entity — every required
|
||||||
|
field below must be supplied, even if unchanged.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scadalink --url <url> security role-mapping update --id <int> [--ldap-group <string>] [--role <string>]
|
scadalink --url <url> security role-mapping update --id <int> --ldap-group <string> --role <string>
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Required | Description |
|
| Option | Required | Description |
|
||||||
|--------|----------|-------------|
|
|--------|----------|-------------|
|
||||||
| `--id` | yes | Mapping ID |
|
| `--id` | yes | Mapping ID |
|
||||||
| `--ldap-group` | no | Updated LDAP group distinguished name or CN |
|
| `--ldap-group` | yes | LDAP group distinguished name or CN |
|
||||||
| `--role` | no | Updated ScadaLink role |
|
| `--role` | yes | ScadaLink role |
|
||||||
|
|
||||||
#### `security role-mapping delete`
|
#### `security role-mapping delete`
|
||||||
|
|
||||||
@@ -1099,17 +1108,20 @@ scadalink --url <url> shared-script create --name <string> --code <string>
|
|||||||
|
|
||||||
#### `shared-script update`
|
#### `shared-script update`
|
||||||
|
|
||||||
Update a shared script's name or code.
|
Update a shared script. An update **replaces** the whole entity — every required field
|
||||||
|
below must be supplied, even if unchanged.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scadalink --url <url> shared-script update --id <int> [--name <string>] [--code <string>]
|
scadalink --url <url> shared-script update --id <int> --name <string> --code <string> [--parameters <json>] [--return-def <json>]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Required | Description |
|
| Option | Required | Description |
|
||||||
|--------|----------|-------------|
|
|--------|----------|-------------|
|
||||||
| `--id` | yes | Shared script ID |
|
| `--id` | yes | Shared script ID |
|
||||||
| `--name` | no | Updated script name |
|
| `--name` | yes | Shared script name |
|
||||||
| `--code` | no | Updated script source code (or `@filepath`) |
|
| `--code` | yes | Script source code |
|
||||||
|
| `--parameters` | no | Parameter definitions JSON |
|
||||||
|
| `--return-def` | no | Return type definition JSON |
|
||||||
|
|
||||||
#### `shared-script delete`
|
#### `shared-script delete`
|
||||||
|
|
||||||
@@ -1163,18 +1175,18 @@ scadalink --url <url> db-connection create --name <string> --connection-string <
|
|||||||
|
|
||||||
#### `db-connection update`
|
#### `db-connection update`
|
||||||
|
|
||||||
Update a database connection definition.
|
Update a database connection definition. An update **replaces** the whole entity —
|
||||||
|
every required field below must be supplied, even if unchanged.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scadalink --url <url> db-connection update --id <int> [--name <string>] [--connection-string <string>] [--provider <string>]
|
scadalink --url <url> db-connection update --id <int> --name <string> --connection-string <string>
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Required | Description |
|
| Option | Required | Description |
|
||||||
|--------|----------|-------------|
|
|--------|----------|-------------|
|
||||||
| `--id` | yes | Database connection ID |
|
| `--id` | yes | Database connection ID |
|
||||||
| `--name` | no | Updated connection name |
|
| `--name` | yes | Connection name |
|
||||||
| `--connection-string` | no | Updated connection string |
|
| `--connection-string` | yes | Database connection string |
|
||||||
| `--provider` | no | Updated database provider |
|
|
||||||
|
|
||||||
#### `db-connection delete`
|
#### `db-connection delete`
|
||||||
|
|
||||||
@@ -1228,18 +1240,20 @@ scadalink --url <url> api-method create --name <string> --code <string> [--descr
|
|||||||
|
|
||||||
#### `api-method update`
|
#### `api-method update`
|
||||||
|
|
||||||
Update an inbound API method.
|
Update an inbound API method. An update **replaces** the whole entity — every required
|
||||||
|
field below must be supplied, even if unchanged.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scadalink --url <url> api-method update --id <int> [--name <string>] [--code <string>] [--description <string>]
|
scadalink --url <url> api-method update --id <int> --script <string> [--timeout <int>] [--parameters <json>] [--return-def <json>]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Required | Description |
|
| Option | Required | Default | Description |
|
||||||
|--------|----------|-------------|
|
|--------|----------|---------|-------------|
|
||||||
| `--id` | yes | API method ID |
|
| `--id` | yes | — | API method ID |
|
||||||
| `--name` | no | Updated method name |
|
| `--script` | yes | — | Script source code implementing the method |
|
||||||
| `--code` | no | Updated script source code (or `@filepath`) |
|
| `--timeout` | no | `30` | Timeout in seconds |
|
||||||
| `--description` | no | Updated description |
|
| `--parameters` | no | — | Parameter definitions JSON |
|
||||||
|
| `--return-def` | no | — | Return type definition JSON |
|
||||||
|
|
||||||
Script changes take effect immediately — the updated code is recompiled in-memory on the active central node. No restart is required.
|
Script changes take effect immediately — the updated code is recompiled in-memory on the active central node. No restart is required.
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,22 @@ public class CommandTreeTests
|
|||||||
Assert.True(leaf.Action != null, $"Leaf command '{leaf.Name}' has no action."));
|
Assert.True(leaf.Action != null, $"Leaf command '{leaf.Name}' has no action."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TemplateCompositionDelete_IsKeyedByIdOnly()
|
||||||
|
{
|
||||||
|
// CLI-015: the in-repo README documented `template composition delete` with
|
||||||
|
// --template-id / --instance-name, but the implementation keys deletion by the
|
||||||
|
// composition's own integer ID via a single --id option. Pin the real surface.
|
||||||
|
var template = TemplateCommands.Build(Url, Format, Username, Password);
|
||||||
|
var composition = template.Subcommands.Single(c => c.Name == "composition");
|
||||||
|
var delete = composition.Subcommands.Single(c => c.Name == "delete");
|
||||||
|
|
||||||
|
var optionNames = delete.Options.Select(o => o.Name).ToList();
|
||||||
|
Assert.Contains("--id", optionNames);
|
||||||
|
Assert.DoesNotContain("--template-id", optionNames);
|
||||||
|
Assert.DoesNotContain("--instance-name", optionNames);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(typeof(GetInstanceCommand))]
|
[InlineData(typeof(GetInstanceCommand))]
|
||||||
[InlineData(typeof(ListSitesCommand))]
|
[InlineData(typeof(ListSitesCommand))]
|
||||||
|
|||||||
91
tests/ScadaLink.CLI.Tests/TableHeaderUnionTests.cs
Normal file
91
tests/ScadaLink.CLI.Tests/TableHeaderUnionTests.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using ScadaLink.CLI;
|
||||||
|
using ScadaLink.CLI.Commands;
|
||||||
|
|
||||||
|
namespace ScadaLink.CLI.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Regression tests for CLI-016 — <c>WriteAsTable</c> previously derived the table
|
||||||
|
/// header set from the first array element only, so any property unique to a later
|
||||||
|
/// element was silently dropped from the rendered table.
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Console")]
|
||||||
|
public class TableHeaderUnionTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void HandleResponse_TableFormat_HeterogeneousArray_IncludesAllColumns()
|
||||||
|
{
|
||||||
|
// The second element has a "Status" property the first lacks. The pre-fix code
|
||||||
|
// derived headers from items[0] only, so "Status" (and its value "Faulted")
|
||||||
|
// were dropped from the table entirely.
|
||||||
|
var writer = new StringWriter();
|
||||||
|
Console.SetOut(writer);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = "[{\"Id\":1,\"Name\":\"Alpha\"},{\"Id\":2,\"Name\":\"Beta\",\"Status\":\"Faulted\"}]";
|
||||||
|
var response = new ManagementResponse(200, json, null, null);
|
||||||
|
var exitCode = CommandHelpers.HandleResponse(response, "table");
|
||||||
|
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
var output = writer.ToString();
|
||||||
|
Assert.Contains("Status", output);
|
||||||
|
Assert.Contains("Faulted", output);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HandleResponse_TableFormat_HeterogeneousArray_PreservesFirstSeenColumnOrder()
|
||||||
|
{
|
||||||
|
// Column order must be the first-seen order across all elements: the first
|
||||||
|
// element contributes Id, Name; the second contributes Status after them.
|
||||||
|
var writer = new StringWriter();
|
||||||
|
Console.SetOut(writer);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = "[{\"Id\":1,\"Name\":\"Alpha\"},{\"Status\":\"Faulted\",\"Id\":2,\"Name\":\"Beta\"}]";
|
||||||
|
var response = new ManagementResponse(200, json, null, null);
|
||||||
|
var exitCode = CommandHelpers.HandleResponse(response, "table");
|
||||||
|
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
var output = writer.ToString();
|
||||||
|
var headerLine = output.Split('\n')[0];
|
||||||
|
Assert.True(headerLine.IndexOf("Id") < headerLine.IndexOf("Name"));
|
||||||
|
Assert.True(headerLine.IndexOf("Name") < headerLine.IndexOf("Status"));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HandleResponse_TableFormat_FirstElementHasExtraColumn_StillRendersAllRows()
|
||||||
|
{
|
||||||
|
// The reverse case: the first element has a property a later element lacks.
|
||||||
|
// The later row must still render (with an empty cell), and all columns kept.
|
||||||
|
var writer = new StringWriter();
|
||||||
|
Console.SetOut(writer);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = "[{\"Id\":1,\"Name\":\"Alpha\",\"Note\":\"first\"},{\"Id\":2,\"Name\":\"Beta\"}]";
|
||||||
|
var response = new ManagementResponse(200, json, null, null);
|
||||||
|
var exitCode = CommandHelpers.HandleResponse(response, "table");
|
||||||
|
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
var output = writer.ToString();
|
||||||
|
Assert.Contains("Note", output);
|
||||||
|
Assert.Contains("first", output);
|
||||||
|
Assert.Contains("Beta", output);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
tests/ScadaLink.CLI.Tests/UpdateCommandContractTests.cs
Normal file
90
tests/ScadaLink.CLI.Tests/UpdateCommandContractTests.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using ScadaLink.CLI.Commands;
|
||||||
|
|
||||||
|
namespace ScadaLink.CLI.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Regression tests for CLI-014. The <c>Update*Command</c> records in Commons carry
|
||||||
|
/// non-nullable "core" fields (e.g. <c>string Name</c>, <c>string Protocol</c>,
|
||||||
|
/// <c>string Script</c>) — an update is a <em>whole-entity replace</em>, not a sparse
|
||||||
|
/// patch. The CLI must therefore mark those core flags as <c>Required</c>: making them
|
||||||
|
/// optional would let an omitted flag send <c>null</c>/empty and silently blank the
|
||||||
|
/// field server-side. These tests pin that contract so the documented surface and the
|
||||||
|
/// implemented surface stay aligned.
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateCommandContractTests
|
||||||
|
{
|
||||||
|
private static readonly Option<string> Url = new("--url") { Recursive = true };
|
||||||
|
private static readonly Option<string> Username = new("--username") { Recursive = true };
|
||||||
|
private static readonly Option<string> Password = new("--password") { Recursive = true };
|
||||||
|
private static readonly Option<string> Format = CliOptions.CreateFormatOption();
|
||||||
|
|
||||||
|
private static Command UpdateCommand(Command group, params string[] path)
|
||||||
|
{
|
||||||
|
var current = group;
|
||||||
|
foreach (var segment in path)
|
||||||
|
current = current.Subcommands.Single(c => c.Name == segment);
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertRequired(Command command, params string[] requiredOptionNames)
|
||||||
|
{
|
||||||
|
foreach (var name in requiredOptionNames)
|
||||||
|
{
|
||||||
|
var option = command.Options.SingleOrDefault(o => o.Name == name);
|
||||||
|
Assert.True(option != null, $"'{command.Name}' is missing expected option '{name}'.");
|
||||||
|
Assert.True(option!.Required, $"'{command.Name}' option '{name}' must be Required (whole-replace contract).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TemplateUpdate_CoreFieldsRequired()
|
||||||
|
=> AssertRequired(UpdateCommand(TemplateCommands.Build(Url, Format, Username, Password), "update"), "--name");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TemplateAttributeUpdate_CoreFieldsRequired()
|
||||||
|
=> AssertRequired(UpdateCommand(TemplateCommands.Build(Url, Format, Username, Password), "attribute", "update"), "--name", "--data-type");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TemplateAlarmUpdate_CoreFieldsRequired()
|
||||||
|
=> AssertRequired(UpdateCommand(TemplateCommands.Build(Url, Format, Username, Password), "alarm", "update"), "--name", "--trigger-type", "--priority");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TemplateScriptUpdate_CoreFieldsRequired()
|
||||||
|
=> AssertRequired(UpdateCommand(TemplateCommands.Build(Url, Format, Username, Password), "script", "update"), "--name", "--code");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SiteUpdate_CoreFieldsRequired()
|
||||||
|
=> AssertRequired(UpdateCommand(SiteCommands.Build(Url, Format, Username, Password), "update"), "--name");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DataConnectionUpdate_CoreFieldsRequired()
|
||||||
|
=> AssertRequired(UpdateCommand(DataConnectionCommands.Build(Url, Format, Username, Password), "update"), "--name", "--protocol");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExternalSystemUpdate_CoreFieldsRequired()
|
||||||
|
=> AssertRequired(UpdateCommand(ExternalSystemCommands.Build(Url, Format, Username, Password), "update"), "--name", "--endpoint-url", "--auth-type");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NotificationUpdate_CoreFieldsRequired()
|
||||||
|
=> AssertRequired(UpdateCommand(NotificationCommands.Build(Url, Format, Username, Password), "update"), "--name", "--emails");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApiMethodUpdate_CoreFieldsRequired()
|
||||||
|
=> AssertRequired(UpdateCommand(ApiMethodCommands.Build(Url, Format, Username, Password), "update"), "--script");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExternalSystemMethodUpdate_IsGenuinelySparse_CoreFieldsOptional()
|
||||||
|
{
|
||||||
|
// UpdateExternalSystemMethodCommand is the one update record whose fields are
|
||||||
|
// genuinely all-nullable, so its flags are correctly optional. Pin that too so
|
||||||
|
// it is not mistakenly forced to Required.
|
||||||
|
var update = UpdateCommand(ExternalSystemCommands.Build(Url, Format, Username, Password), "method", "update");
|
||||||
|
foreach (var name in new[] { "--name", "--http-method", "--path" })
|
||||||
|
{
|
||||||
|
var option = update.Options.SingleOrDefault(o => o.Name == name);
|
||||||
|
Assert.True(option != null, $"method update is missing option '{name}'.");
|
||||||
|
Assert.False(option!.Required, $"method update option '{name}' should be optional (sparse-patch record).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user