Skip to content

Fix TypeError when saving integer CPU core count in global settings#376

Draft
Copilot wants to merge 6 commits intomasterfrom
copilot/fix-max-cpu-usage-setting
Draft

Fix TypeError when saving integer CPU core count in global settings#376
Copilot wants to merge 6 commits intomasterfrom
copilot/fix-max-cpu-usage-setting

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 30, 2026

  • Previous fix: cast GlobalSettings values to string before calling setValueString
  • lib/Model/GlobalSettings.php — typed ?int properties
  • lib/Service/GlobalSettingsService.php — use getValueInt/setValueInt via reflection type detection
  • lib/Controller/GlobalSettingsController.php — cast incoming values to ?int (handle "" from empty input)
  • lib/OcrProcessors/Remote/Client/ApiClient.php — simplified getTimeout() (removed string checks)
  • lib/OcrProcessors/CommandLineUtils.php — removed intval() cast
  • tests/Unit/Service/GlobalSettingsServiceTest.php — updated mocks to use getValueInt/setValueInt
  • tests/Unit/Controller/GlobalSettingsControllerTest.php — added test for empty string → null coercion
  • tests/Unit/OcrProcessors/Remote/Client/ApiClientTest.php — use integer timeout values
  • src/test/service/globalSettingsService.spec.js — updated mock response to integer
  • src/test/components/GlobalSettings.spec.js — updated initial mock to integer

Copilot AI changed the title [WIP] Fix max CPU usage setting to accept string instead of int Fix TypeError when saving integer CPU core count in global settings Mar 30, 2026
Copilot finished work on behalf of R0Wi March 30, 2026 14:06
Copilot AI requested a review from R0Wi March 30, 2026 14:06
Copilot finished work on behalf of R0Wi March 30, 2026 20:06
@R0Wi R0Wi force-pushed the copilot/fix-max-cpu-usage-setting branch from 13d0e3f to 2f65fec Compare March 30, 2026 20:45
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TypeError when persisting integer-valued global settings (CPU core count / timeout) by moving these settings to true integers end-to-end (model → controller/service → appconfig), including a migration for existing string-typed config entries.

Changes:

  • Convert GlobalSettings numeric fields to ?int and persist them via IAppConfig::getValueInt()/setValueInt() using reflection-based type detection.
  • Add a migration to convert legacy VALUE_STRING appconfig entries for processorCount/timeout into VALUE_INT.
  • Update Vue UI + unit tests (PHP + JS) to treat these values as numbers and allow null/unset behavior.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
lib/Model/GlobalSettings.php Change global settings fields to typed ?int.
lib/Service/GlobalSettingsService.php Read/write int settings via getValueInt/setValueInt, delete keys when nullable and null.
lib/Controller/GlobalSettingsController.php Cast incoming request payload values to ints/null.
lib/Migration/Version1034Date20260331194630.php Migrate stored config keys from string type to int type.
lib/OcrProcessors/Remote/Client/ApiClient.php Simplify timeout resolution now that settings are typed ints.
lib/OcrProcessors/CommandLineUtils.php Stop string→int coercion for processor count; treat null as unset.
psalm.xml Exclude lib/Migration from Psalm analysis.
tests/psalm-baseline.xml Remove baseline entries for migration unused-class warnings.
tests/Unit/Service/GlobalSettingsServiceTest.php Update mocks/expectations for int config access + null/unset behavior.
tests/Unit/Controller/GlobalSettingsControllerTest.php Add coverage for passing null settings through the controller.
tests/Unit/OcrProcessors/Remote/Client/ApiClientTest.php Update timeout tests to use ints/null.
src/components/GlobalSettings.vue Convert numeric inputs to numbers/null before saving.
src/test/service/globalSettingsService.spec.js Update mocked payload/response types to integers.
src/test/components/GlobalSettings.spec.js Update component tests for numeric values and empty→null behavior.

Comment on lines 61 to 64
$globalSettingsObject = new GlobalSettings();
$globalSettingsObject->processorCount = $globalSettings['processorCount'];
$globalSettingsObject->timeout = $globalSettings['timeout'] ?? null;
$globalSettingsObject->processorCount = isset($globalSettings['processorCount']) ? (int)$globalSettings['processorCount'] : null;
$globalSettingsObject->timeout = isset($globalSettings['timeout']) ? (int)$globalSettings['timeout'] : null;

Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isset($globalSettings['processorCount']) will treat an empty string ("" from an empty number input) as set, and (int)'' becomes 0. That ends up persisting a 0 value even though the service treats <= 0 as “not configured”. Consider coercing "" (and other non-numeric inputs) to null, and ideally normalizing <= 0 to null before calling the service (e.g., via a small helper that returns null unless the parsed value is > 0).

Copilot uses AI. Check for mistakes.
if ($isNullable && $value === null) {
$this->config->deleteKey(Application::APP_NAME, $key);
} elseif ($type instanceof \ReflectionNamedType && $type->getName() === 'int') {
$this->config->setValueInt(Application::APP_NAME, $key, (int)$value);
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getGlobalSettings() maps int values <= 0 to null, but setGlobalSettings() will still persist 0/negative values via setValueInt() when they are provided. This creates inconsistent semantics (stored value vs returned value) and can leave invalid values in appconfig. Consider treating non-positive ints as “unset” here too (delete the key or skip writing) to keep storage consistent with the read-path logic.

Suggested change
$this->config->setValueInt(Application::APP_NAME, $key, (int)$value);
$intValue = (int)$value;
if ($intValue <= 0) {
// Keep storage consistent with read-path semantics: non-positive ints
// are treated as "not configured", so we delete the key instead of
// persisting an invalid/sentinel value.
$this->config->deleteKey(Application::APP_NAME, $key);
} else {
$this->config->setValueInt(Application::APP_NAME, $key, $intValue);
}

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +105
// requested one, so we must remove the old entry before re-inserting.
$this->appConfig->deleteKey(Application::APP_NAME, $key);
$this->appConfig->setValueInt(Application::APP_NAME, $key, $intValue);

$output->info(sprintf(
"Migrated config key '%s' for app '%s' from string to int (value: %d).",
$key,
Application::APP_NAME,
$intValue,
));
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration currently re-inserts a VALUE_INT even when the legacy string value is empty/non-numeric (mapped to 0). Since the rest of the code treats <= 0 as “not configured”, consider deleting the old key and only re-inserting when the parsed value is actually > 0; otherwise just delete and leave it unset. This avoids persisting a sentinel/invalid value in appconfig.

Suggested change
// requested one, so we must remove the old entry before re-inserting.
$this->appConfig->deleteKey(Application::APP_NAME, $key);
$this->appConfig->setValueInt(Application::APP_NAME, $key, $intValue);
$output->info(sprintf(
"Migrated config key '%s' for app '%s' from string to int (value: %d).",
$key,
Application::APP_NAME,
$intValue,
));
// requested one, so we must remove the old entry before deciding whether
// to re-insert it as an integer value.
$this->appConfig->deleteKey(Application::APP_NAME, $key);
if ($intValue > 0) {
$this->appConfig->setValueInt(Application::APP_NAME, $key, $intValue);
$output->info(sprintf(
"Migrated config key '%s' for app '%s' from string to int (value: %d).",
$key,
Application::APP_NAME,
$intValue,
));
} else {
// For empty/non-numeric or non-positive values, treat as "not configured"
// and leave the key unset instead of persisting a sentinel 0 value.
$output->info(sprintf(
"Deleted legacy string config key '%s' for app '%s' due to non-positive or invalid value.",
$key,
Application::APP_NAME,
));
}

Copilot uses AI. Check for mistakes.
if ($timeoutInt > 0) {
$timeout = $timeoutInt;
}
if ($settings->timeout !== null && $settings->timeout > 0) {
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTimeout() is typed as object but directly accesses $settings->timeout without an isset()/property_exists() guard. If anything other than GlobalSettings is ever passed, this will trigger an undefined-property warning. Consider either tightening the parameter type to GlobalSettings (preferred, since that’s what the caller passes) or reintroducing a guard before accessing the property.

Suggested change
if ($settings->timeout !== null && $settings->timeout > 0) {
if (isset($settings->timeout) && $settings->timeout > 0) {

Copilot uses AI. Check for mistakes.
Comment on lines 36 to 41
<input
v-model="settings.processorCount"
:value="settings.processorCount"
name="processorCount"
type="number"
@input="save">
@input="settings.processorCount = Number($event.target.value) || null; save()">
</div>
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The input coercion Number($event.target.value) || null only converts falsy values to null; negative numbers (e.g. “-1”) will be kept and sent to the backend, where they can be persisted even though the service treats <= 0 as unset. Consider explicitly normalizing with a > 0 check (and possibly clamping) so that any <= 0 (and NaN) becomes null.

Copilot uses AI. Check for mistakes.
Comment on lines 51 to 58
<div class="div-table-col">
<input
v-model="settings.timeout"
:value="settings.timeout"
name="timeout"
type="number"
min="1"
@input="save">
@input="settings.timeout = Number($event.target.value) || null; save()">
</div>
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as processorCount: Number($event.target.value) || null allows negative values to flow through. Even though min="1" exists, users can still type negatives manually and the handler will accept them. Consider normalizing in the handler to null unless the parsed number is > 0 to prevent persisting invalid timeouts.

Copilot uses AI. Check for mistakes.
}));

$this->controller->setGlobalSettings($settings);
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The controller now casts values to int/null, but there’s no unit test covering the API behavior when the client submits an empty string (""), which is a common payload from cleared number inputs and is called out in the PR description. Adding a test case for "" → null (or "" → delete/unset) would protect the intended behavior.

Suggested change
}
}
public function testSetSettingsConvertsEmptyStringsToNull() {
$settings = [
'processorCount' => '',
'timeout' => '',
];
$this->globalSettingsService->expects($this->once())
->method('setGlobalSettings')
->with($this->callback(function (GlobalSettings $settings) {
return $settings->processorCount === null && $settings->timeout === null;
}));
$this->controller->setGlobalSettings($settings);
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Max CPU usage cannot be set, int is not valid, string expected

3 participants