mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-06-18 10:19:03 -04:00
269 lines
8.6 KiB
Markdown
269 lines
8.6 KiB
Markdown
# PCD Tests
|
|
|
|
PCD tests cover the database-first behavior of the PCD system: writer output,
|
|
op replay, conflict detection, conflict resolution, and compiled cache state.
|
|
For the PCD system itself, see [pcd.md](./pcd.md). For entity behavior, see
|
|
[pcd-entities.md](./pcd-entities.md).
|
|
|
|
## Table of Contents
|
|
|
|
- [Purpose](#purpose)
|
|
- [Suite Layout](#suite-layout)
|
|
- [Harness](#harness)
|
|
- [Write Tests](#write-tests)
|
|
- [Conflict Tests](#conflict-tests)
|
|
- [Fixture Builders](#fixture-builders)
|
|
- [Browser Boundary](#browser-boundary)
|
|
|
|
## Purpose
|
|
|
|
PCD correctness is tested below the browser because the source of truth is the
|
|
compiled cache, not the UI. The suite verifies four contracts:
|
|
|
|
- Writer calls persist the expected `pcd_ops` rows.
|
|
- Compile replays schema, base, tweaks, and user layers into the expected cache
|
|
state.
|
|
- Stale user guards surface conflicts according to the database instance's
|
|
conflict strategy.
|
|
- Align and override resolution leave the compiled cache in the expected final
|
|
state.
|
|
|
|
## Suite Layout
|
|
|
|
PCD tests live in the integration test tree:
|
|
|
|
```text
|
|
tests/integration/pcd/
|
|
harness/
|
|
write/
|
|
conflicts/
|
|
```
|
|
|
|
The suite uses the shared integration harness for server lifecycle, HTTP
|
|
requests, and isolated `APP_BASE_PATH` directories. The PCD harness layers
|
|
PCD-specific setup, op seeding, writer drivers, and cache assertions on top of
|
|
that shared harness.
|
|
|
|
Tests are grouped by contract:
|
|
|
|
**Write tests** call real app entrypoints and assert the ops emitted by the
|
|
writer.
|
|
|
|
**Conflict tests** seed complete op histories and assert conflict history plus
|
|
final compiled cache state.
|
|
|
|
## Harness
|
|
|
|
The PCD harness owns the repeated setup needed by both write and conflict
|
|
tests:
|
|
|
|
- Create a minimal PCD repo on disk with `deps/schema/ops/0.schema.sql`.
|
|
- Create a `database_instances` row with configurable conflict strategy,
|
|
enabled state, and base-write access.
|
|
- Seed published base ops, draft base ops, and published user ops.
|
|
- Trigger compile through the same app path used by integration tests.
|
|
- Query `pcd_ops`, latest `pcd_op_history`, and current conflicts.
|
|
- Read compiled cache tables in deterministic order.
|
|
- Drive writer entrypoints with a `TestClient`.
|
|
- Capture emitted ops since a checkpoint.
|
|
|
|
The harness is intentionally thin. It provides setup and assertions, but it does
|
|
not duplicate writer behavior or hide that compile consumes SQL ops.
|
|
|
|
## Write Tests
|
|
|
|
Write tests verify the writer contract for one user action at a time.
|
|
|
|
Each write test follows the same shape:
|
|
|
|
1. Create an isolated database instance and seed the smallest base state
|
|
needed for the action (`seededPcd` does both, plus an initial compile).
|
|
2. Record an op checkpoint with `opCheckpoint(ctx)`.
|
|
3. Submit the real form action through `write.regex.*` (or the writer for
|
|
the entity under test).
|
|
4. Query ops emitted after the checkpoint with `userOpsSince(ctx, ...)`.
|
|
5. Assert op count, metadata, desired state, grouping, generated flags, and
|
|
SQL shape using the helpers in `harness/pcd.ts` and the colocated
|
|
`helpers.ts` for the entity.
|
|
|
|
Example, taken from `tests/integration/pcd/write/regex/update.test.ts`:
|
|
|
|
```ts
|
|
test('scalar fields split into independent grouped ops', async () => {
|
|
const ctx = await seededPcd('scalars', [
|
|
base.regex({
|
|
name: 'Scalar Regex',
|
|
pattern: '\\bold\\b',
|
|
description: 'Old',
|
|
regex101Id: 'old101'
|
|
})
|
|
]);
|
|
const checkpoint = opCheckpoint(ctx);
|
|
|
|
await write.regex.update(ctx, 1, {
|
|
name: 'Scalar Regex',
|
|
pattern: '\\bnew\\b',
|
|
description: 'New description',
|
|
regex101Id: 'new101'
|
|
});
|
|
|
|
const ops = userOpsSince(ctx, checkpoint);
|
|
assertEquals(ops.length, 3);
|
|
assertOnlyField(ops, 'pattern');
|
|
assertOnlyField(ops, 'description');
|
|
assertOnlyField(ops, 'regex101_id');
|
|
assertSameGroup(ops);
|
|
});
|
|
```
|
|
|
|
`seededPcd` and `userOpsSince` come from a per-entity `helpers.ts`
|
|
(`createScenarioFactory` binds the spec's port and counter). `base.regex`,
|
|
`opCheckpoint`, `assertOnlyField`, and `assertSameGroup` come from the
|
|
shared harness. Each spec file owns its port (one server boot per file)
|
|
and runs in parallel with siblings.
|
|
|
|
### Doc Comment Format
|
|
|
|
Each write test sits behind a JSDoc block with three labeled sections so the
|
|
inputs and expectations are visible without reading the body:
|
|
|
|
```ts
|
|
/**
|
|
* Context
|
|
* Empty PCD (only schema seeded), compiled once.
|
|
*
|
|
* Submit
|
|
* POST /regular-expressions/{ctx.dbId}/new with form fields:
|
|
* name = 'Created Regex'
|
|
* pattern = '\bcreated\b'
|
|
* description = ''
|
|
* regex101Id = ''
|
|
* tags = '[]'
|
|
* layer = 'user'
|
|
*
|
|
* Expect
|
|
* - userOpsSince(checkpoint).length === 1
|
|
* - op.metadata.operation === 'create'
|
|
* - op.metadata.entity === 'regular_expression'
|
|
* - op.metadata.name === 'Created Regex'
|
|
* - op.desired_state.name === 'Created Regex'
|
|
* - op.desired_state.pattern === '\bcreated\b'
|
|
* - op.desired_state.description === null
|
|
* - op.desired_state.regex101_id === null
|
|
* - op.desired_state.tags === []
|
|
* - op.sql matches /insert into "?regular_expressions"?/i
|
|
*/
|
|
test('minimal regex emits one create op', async () => {
|
|
// ...
|
|
});
|
|
```
|
|
|
|
Section conventions:
|
|
|
|
- **Context**: seeded entities and whether compile has run. Use `Empty PCD` if
|
|
no entities are seeded.
|
|
- **Submit**: the exact endpoint plus every form field and its value, including
|
|
fields the harness fills with defaults. The writer sees the full request, so
|
|
the doc shows it.
|
|
- **Expect**: assertions as code-shaped bullets so the test body reads as their
|
|
literal expansion. Cover op count, metadata fields, desired state values, and
|
|
SQL match patterns. Failure-path tests use `response.status` and the
|
|
SvelteKit failure payload shape.
|
|
|
|
## Conflict Tests
|
|
|
|
Conflict tests verify replay and resolution behavior from complete op histories.
|
|
Each scenario has three inputs:
|
|
|
|
- **Base**: the original published upstream state.
|
|
- **User**: local user intent written as user ops.
|
|
- **Upstream**: newer published base ops that simulate a pull.
|
|
|
|
The same scenario runs under `ask`, `align`, and `override` when all three
|
|
strategies are meaningful. The primary assertion is final compiled cache state.
|
|
Op state and history assertions explain how the system got there.
|
|
|
|
Conflict strategy expectations:
|
|
|
|
| Strategy | Conflict expectation | Final state expectation |
|
|
| ---------- | -------------------------------------------------------------------------- | ---------------------------------- |
|
|
| `ask` | conflicts remain as `conflicted_pending` | upstream state plus clean user ops |
|
|
| `align` | conflicted user ops are dropped | upstream wins |
|
|
| `override` | conflicted user ops are superseded or dropped, replacement ops are written | user intent wins |
|
|
|
|
Example:
|
|
|
|
```ts
|
|
test('regex pattern conflict resolves by strategy', async () => {
|
|
await runConflictScenario({
|
|
base: [
|
|
base.regex({
|
|
name: 'TestRegex',
|
|
pattern: '\\boriginal\\b',
|
|
description: ''
|
|
})
|
|
],
|
|
user: [
|
|
user.regex.update('TestRegex', {
|
|
pattern: { from: '\\boriginal\\b', to: '\\buser\\b' }
|
|
})
|
|
],
|
|
upstream: [
|
|
upstream.regex.update('TestRegex', {
|
|
pattern: { from: '\\boriginal\\b', to: '\\bupstream\\b' }
|
|
})
|
|
],
|
|
expect: {
|
|
ask: {
|
|
conflicts: [
|
|
{
|
|
entity: 'regular_expression',
|
|
field: 'pattern',
|
|
reason: 'guard_mismatch'
|
|
}
|
|
],
|
|
state: {
|
|
regularExpressions: [{ name: 'TestRegex', pattern: '\\bupstream\\b' }]
|
|
}
|
|
},
|
|
align: {
|
|
conflicts: [],
|
|
state: {
|
|
regularExpressions: [{ name: 'TestRegex', pattern: '\\bupstream\\b' }]
|
|
}
|
|
},
|
|
override: {
|
|
conflicts: [],
|
|
state: {
|
|
regularExpressions: [{ name: 'TestRegex', pattern: '\\buser\\b' }]
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
```
|
|
|
|
## Fixture Builders
|
|
|
|
Fixture builders remove repetitive seed SQL while preserving the op-level
|
|
contract that compile consumes. Builders emit SQL plus the metadata and desired
|
|
state needed by conflict handling.
|
|
|
|
```ts
|
|
base.regex({ name, pattern, description });
|
|
user.regex.update(name, { pattern: { from, to } });
|
|
upstream.regex.update(name, { pattern: { from, to } });
|
|
```
|
|
|
|
Raw SQL remains available for cases where a test needs exact control over the
|
|
operation. The harness does not provide a full alternate writer DSL; real writes
|
|
still go through the application writer.
|
|
|
|
## Browser Boundary
|
|
|
|
Browser tests cover browser behavior only: rendering conflict lists, selecting
|
|
resolutions, submitting forms, and displaying empty states. They do not own PCD
|
|
correctness.
|
|
|
|
Writer, compile, conflict, and final state behavior belong in integration tests.
|