Skip to content

Developer Tests Knowledge

Ed Mozley edited this page Sep 21, 2026 · 1 revision

πŸ§ͺ Developer Tests β€” Knowledge, search & documents

Part of Developer Tests. These are the suites where a wrong answer looks like a right one, so nearly every assertion here is paired with a positive control.

That is the theme of the whole page and worth stating once: a permission filter that quietly does nothing returns a page full of plausible results, and a query that matches nothing looks identical to an honest "not in your tickets". So every "they could not see it" is followed by "…and this person, who should, still can". Without the second half the first proves nothing.

Test Needs
knowledge-visibility/run.php Database, some parts local HTTP
knowledge-gaps/run.php Database (rolled back)
search/run.php Database
search-extract/run.php Nothing
document-permissions.php Database (rolled back)
document-search-permissions.php Database

tests/knowledge-visibility/run.php

What it tests

Whether one company's knowledge can reach another company's people. It is a runner: it executes the seven numbered harnesses beside it in order and reports which failed.

Part The question it asks
01_webchat_scope.php Can one company's anonymous website visitor be answered out of another company's knowledge base?
02_write_path.php Does KnowledgeService validate company and audience when writing?
03_readers.php Does every analyst-facing surface respect the company the analyst has switched to?
04_rest_api.php Does an API key scoped to one company see only that company's articles β€” but still the shared ones?
05_lms.php Can the LMS build a lesson from an article it should not have?
06_gap_analysis.php Does the assistant judge one company's tickets "already covered" by another's articles?
07_acl.php Folders, the access list, and the two permission models.

How the interesting ones work

03_readers.php drives the real endpoints over HTTP with a forged session, as a restricted analyst. That is the point β€” a scoped query proves nothing if the endpoint in front of it never applies the scope.

04_rest_api.php has a trap worth knowing about. The second half is the hard part: the generic apiKeyTenantFilter treats NULL as "the Default company's", which would hide every shared article from a non-Default key. So the test asserts both directions β€” not another company's, but yes to the shared ones.

05_lms.php makes the same argument the Forms audience test does: the picker hiding a title is not enough, because it posts an id, and ai_author.php is gated on LMS_MANAGE rather than on Knowledge. The real test is whether a guessed id still yields a body.

06_gap_analysis.php documents a mismatch precisely: gapWindowSql() always company-scoped the ticket side, while the article side had no tenant filter at all β€” the two halves of one comparison disagreeing about whose data was in scope.

07_acl.php runs inside one transaction that is always rolled back, so it is safe against a live database.

_bootstrap.php beside them is a shared helper, not a harness β€” it is included by each part and has no assertions of its own.

Run it

php tests/knowledge-visibility/run.php

Or one part on its own, e.g. php tests/knowledge-visibility/07_acl.php.

⚠️ Several parts look for tenants by name on your database. They are written against a specific development install, so on a different database they may not find what they expect β€” read the output rather than assuming a clean pass.

Reading the result

All Knowledge visibility harnesses passed. and exit 0. Otherwise it prints FAILED: and the harness names, and exits 1.

If it fails

Any leak here is a live cross-company data leak. Fix it at the query, not at the surface that happened to be tested β€” the reason there are seven harnesses is that there are seven ways in.


tests/knowledge-gaps/run.php

What it tests

The knowledge gap assistant's clustering. The interesting behaviour is emergent: does a pile of real tickets actually collapse into the right questions, and do the one-offs stay out? You cannot answer that by reading the code β€” only by feeding it a pile whose right answer you already know.

How it works

Everything runs inside one transaction which is always rolled back, so it is safe against a live database.

It runs in "wording" mode by default, which needs no OpenAI key and spends nothing. The clustering logic under test is identical; only the similarity function differs.

⚠️ Every negative assertion is paired with a positive control. "The one-offs did not cluster" proves nothing on its own β€” it is equally true of a harness that silently clustered nothing at all.

Run it

php tests/knowledge-gaps/run.php

If it reports the write-up schema is not ready, run System β†’ Database Verification.

If it fails

Clustering has changed shape. Check the control assertions first: if those failed, nothing clustered and the tuning is broken rather than merely different.


tests/search/run.php

What it tests

Three things, all of which can be wrong while looking right:

  1. Query translation. What a person types is not what MySQL is asked. Terms too short to be indexed must be dropped, because requiring one in boolean mode makes the whole query match nothing β€” the difference between "search works" and "search mysteriously returns nothing for that phrase".
  2. Scope goes into the query. Every "it was excluded" assertion has a twin showing the same row is returned once the predicate is relaxed. Without that twin, a filter matching nothing at all would pass.
  3. The result shape. Hits collapse to their ticket and report which parts matched, because a user thinks in tickets β€” one ticket with the term in four replies must not flood the page.

How it works

Writes a handful of rows with a reserved source_type and deletes them in a finally block. ⚠️ It cannot use a rolled-back transaction: InnoDB does not expose uncommitted rows to MATCH ... AGAINST.

Run it

php tests/search/run.php

If it fails

  • Translation β€” a search phrase now returns nothing. Look at the minimum indexed word length and what the translator does with short terms.
  • A scope twin β€” if the exclusion passes but its twin fails, the filter is excluding everything, not just what it should.

tests/search-extract/run.php

What it tests

Attachment text extraction: reading words out of an uploaded file so they can be searched.

How it works

Builds real files in a temporary directory and reads them back β€” including a minimal but genuinely valid .docx, which is a zip with word/document.xml inside. No database, no HTTP: it exercises includes/search/extract.php alone, because the risky part of attachment indexing is the file handling, not the SQL.

πŸ”‘ The hostile cases matter most. An attachment arrives from anyone who can email the service desk, so "a zip bomb is refused" is a more useful assertion than "a Word document is read".

Run it

php tests/search-extract/run.php

25 assertions, no fixtures needed β€” this is a good first test to run on a fresh checkout.

If it fails

A refused-hostile-file assertion going red means the extractor will now try to process something it should reject. That is a denial-of-service risk on a queue anyone can post to.


tests/document-permissions.php

What it tests

A document is visible if and only if you can see something it is attached to.

πŸ”‘ The same question is asked twice by the product, and the two must always agree:

  • documentVisibilityClause() β€” the set, used by search
  • documentCanView() β€” the row, used by download

A document you cannot find but can download is the whole bug this design exists to prevent.

How it works

Writes to scratch records inside a transaction and rolls back. Every "cannot see" is followed by the same analyst being granted the parent and then seeing the document.

Run it

php tests/document-permissions.php

If it fails

If the set and the row disagree, say so explicitly in your fix β€” decide which is right and change the other. Do not fix only the one the test named.


tests/document-search-permissions.php

What it tests

That search does not return a document you cannot see.

⚠️ This is the one that matters. A corpus row carries a tenant and an internal flag, which is enough to judge a ticket. It is not enough to judge a document, whose visibility lives in other tables entirely β€” so a document row has to satisfy the same at-least-one-visible-parent rule the download endpoint applies.

How it works

Searches the real corpus as a real analyst. Every denial is paired with a positive control: the same analyst, the same document, the module granted. A test that only proves absence passes just as happily when the index is empty, the word is below the full-text minimum, or the query is broken.

Run it

php tests/document-search-permissions.php

If it fails

A denial going red is a leak through search β€” someone can see the existence and title of a document they have no route to. Treat as blocking. A control going red means documents have stopped being indexed at all.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally